From 42a6f6e8bcf49662afb983f417b784937a5a2d9d Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Wed, 24 Jul 2024 01:56:34 +0800 Subject: [PATCH 01/41] Bot previews --- .../Sources/AccountContext.swift | 9 +- .../BotCheckoutWebInteractionController.swift | 2 +- ...CheckoutWebInteractionControllerNode.swift | 13 +- .../Sources/ChatListSearchListPaneNode.swift | 11 +- .../Display/Source/PortalSourceView.swift | 7 + submodules/Display/Source/PortalView.swift | 5 + .../Sources/SparseItemGrid.swift | 18 + submodules/TelegramApi/Sources/Api0.swift | 8 +- submodules/TelegramApi/Sources/Api14.swift | 30 +- submodules/TelegramApi/Sources/Api2.swift | 92 +- submodules/TelegramApi/Sources/Api29.swift | 110 ++- submodules/TelegramApi/Sources/Api3.swift | 50 ++ submodules/TelegramApi/Sources/Api30.swift | 58 ++ submodules/TelegramApi/Sources/Api36.swift | 62 +- .../ApiUtils/StoreMessage_Telegram.swift | 10 +- .../Sources/State/AccountStateManager.swift | 17 + .../SyncCore/SyncCore_CachedUserData.swift | 77 +- .../Messages/PendingStoryManager.swift | 13 +- .../TelegramEngine/Messages/Stories.swift | 186 ++-- .../Messages/StoryListContext.swift | 618 ++++++++++--- .../Messages/TelegramEngineMessages.swift | 8 +- .../Peers/UpdateCachedPeerData.swift | 41 +- submodules/TelegramUI/BUILD | 1 + .../ChatMessageAnimatedStickerItemNode.swift | 2 +- .../Sources/ChatMessageBubbleItemNode.swift | 2 +- .../ChatMessageInstantVideoItemNode.swift | 2 +- ...atMessageInteractiveInstantVideoNode.swift | 2 +- .../Sources/ChatMessageStickerItemNode.swift | 2 +- .../ChatRecentActionsControllerNode.swift | 2 +- .../ChatSendAudioMessageContextPreview.swift | 2 +- .../Sources/ChatControllerInteraction.swift | 4 +- .../EmptyStateIndicatorComponent.swift | 123 ++- .../Components/MiniAppListScreen/BUILD | 39 + .../Sources/MiniAppListScreen.swift | 811 ++++++++++++++++++ .../PeerInfoScreen/Sources/PeerInfoData.swift | 4 +- .../Sources/PeerInfoScreen.swift | 26 +- .../PeerInfoVisualMediaPaneNode/BUILD | 2 + .../Sources/PeerInfoStoryPaneNode.swift | 451 ++++++++-- .../Settings/LanguageSelectionScreen/BUILD | 31 + .../Sources/LanguageSelectionScreen.swift | 177 ++++ .../Sources/LanguageSelectionScreenNode.swift | 568 ++++++++++++ .../ThemeAccentColorControllerNode.swift | 2 +- .../TelegramUI/Components/SpaceWarpView/BUILD | 1 + .../SpaceWarpView/Sources/SpaceWarpView.swift | 422 +++++++++ .../Sources/StoryContainerScreen.swift | 5 +- .../Sources/TabSelectorComponent.swift | 40 +- .../TelegramUI/Sources/ChatController.swift | 4 +- .../Sources/ChatControllerNode.swift | 126 ++- .../OverlayAudioPlayerControllerNode.swift | 2 +- .../Sources/SharedAccountContext.swift | 15 +- .../Sources/TelegramRootController.swift | 2 +- .../Sources/LocalizationListItem.swift | 14 +- 52 files changed, 3806 insertions(+), 523 deletions(-) create mode 100644 submodules/TelegramUI/Components/MiniAppListScreen/BUILD create mode 100644 submodules/TelegramUI/Components/MiniAppListScreen/Sources/MiniAppListScreen.swift create mode 100644 submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/BUILD create mode 100644 submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/Sources/LanguageSelectionScreen.swift create mode 100644 submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/Sources/LanguageSelectionScreenNode.swift diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index e54fae0a28..d6b12ebca9 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -932,6 +932,9 @@ public final class BotPreviewEditorTransitionOut { } } +public protocol MiniAppListScreenInitialData: AnyObject { +} + public protocol SharedAccountContext: AnyObject { var sharedContainerPath: String { get } var basePath: String { get } @@ -999,7 +1002,7 @@ public protocol SharedAccountContext: AnyObject { selectedMessages: Signal?, NoError>, mode: ChatHistoryListMode ) -> ChatHistoryListNode - func makeChatMessagePreviewItem(context: AccountContext, messages: [Message], theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder, forcedResourceStatus: FileMediaResourceStatus?, tapMessage: ((Message) -> Void)?, clickThroughMessage: (() -> Void)?, backgroundNode: ASDisplayNode?, availableReactions: AvailableReactions?, accountPeer: Peer?, isCentered: Bool, isPreview: Bool, isStandalone: Bool) -> ListViewItem + func makeChatMessagePreviewItem(context: AccountContext, messages: [Message], theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder, forcedResourceStatus: FileMediaResourceStatus?, tapMessage: ((Message) -> Void)?, clickThroughMessage: ((UIView?, CGPoint?) -> Void)?, backgroundNode: ASDisplayNode?, availableReactions: AvailableReactions?, accountPeer: Peer?, isCentered: Bool, isPreview: Bool, isStandalone: Bool) -> ListViewItem func makeChatMessageDateHeaderItem(context: AccountContext, timestamp: Int32, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder) -> ListViewItemHeader func makeChatMessageAvatarHeaderItem(context: AccountContext, timestamp: Int32, peer: Peer, message: Message, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder) -> ListViewItemHeader func makePeerSharedMediaController(context: AccountContext, peerId: PeerId) -> ViewController? @@ -1100,6 +1103,10 @@ public protocol SharedAccountContext: AnyObject { func makeStarsAmountScreen(context: AccountContext, initialValue: Int64?, completion: @escaping (Int64) -> Void) -> ViewController func makeStarsWithdrawalScreen(context: AccountContext, stats: StarsRevenueStats, completion: @escaping (Int64) -> Void) -> ViewController func makeStarsGiftScreen(context: AccountContext, message: EngineMessage) -> ViewController + + func makeMiniAppListScreenInitialData(context: AccountContext) -> Signal + func makeMiniAppListScreen(context: AccountContext, initialData: MiniAppListScreenInitialData) -> ViewController + func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peer: EnginePeer, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool) func makeDebugSettingsController(context: AccountContext?) -> ViewController? diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutWebInteractionController.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutWebInteractionController.swift index bc03da266a..5ffc394337 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutWebInteractionController.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutWebInteractionController.swift @@ -58,7 +58,7 @@ final class BotCheckoutWebInteractionController: ViewController { } override func loadDisplayNode() { - self.displayNode = BotCheckoutWebInteractionControllerNode(presentationData: self.presentationData, url: self.url, intent: self.intent) + self.displayNode = BotCheckoutWebInteractionControllerNode(context: self.context, presentationData: self.presentationData, url: self.url, intent: self.intent) } override func viewDidAppear(_ animated: Bool) { diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutWebInteractionControllerNode.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutWebInteractionControllerNode.swift index 161bd7f296..e6d52efedd 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutWebInteractionControllerNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutWebInteractionControllerNode.swift @@ -4,6 +4,7 @@ import Display import AsyncDisplayKit import WebKit import TelegramPresentationData +import AccountContext private class WeakPaymentScriptMessageHandler: NSObject, WKScriptMessageHandler { private let f: (WKScriptMessage) -> () @@ -20,12 +21,14 @@ private class WeakPaymentScriptMessageHandler: NSObject, WKScriptMessageHandler } final class BotCheckoutWebInteractionControllerNode: ViewControllerTracingNode, WKNavigationDelegate { + private let context: AccountContext private var presentationData: PresentationData private let intent: BotCheckoutWebInteractionControllerIntent private var webView: WKWebView? - init(presentationData: PresentationData, url: String, intent: BotCheckoutWebInteractionControllerIntent) { + init(context: AccountContext, presentationData: PresentationData, url: String, intent: BotCheckoutWebInteractionControllerIntent) { + self.context = context self.presentationData = presentationData self.intent = intent @@ -146,6 +149,14 @@ final class BotCheckoutWebInteractionControllerNode: ViewControllerTracingNode, decisionHandler(.allow) } } else { + if let url = navigationAction.request.url, let scheme = url.scheme { + let defaultSchemes: [String] = ["http", "https"] + if !defaultSchemes.contains(scheme) { + decisionHandler(.cancel) + self.context.sharedContext.applicationBindings.openUrl(url.absoluteString) + return + } + } decisionHandler(.allow) } } diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 8e17694c38..55aca75b28 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -3543,9 +3543,19 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { } else if case .apps = key { if let navigationController = self.navigationController { if isRecommended { + #if DEBUG + let _ = (self.context.sharedContext.makeMiniAppListScreenInitialData(context: self.context) + |> deliverOnMainQueue).startStandalone(next: { [weak self] initialData in + guard let self, let navigationController = self.navigationController else { + return + } + navigationController.pushViewController(self.context.sharedContext.makeMiniAppListScreen(context: self.context, initialData: initialData)) + }) + #else if let peerInfoScreen = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { navigationController.pushViewController(peerInfoScreen) } + #endif } else if case let .user(user) = peer, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp), let parentController = self.parentController { self.context.sharedContext.openWebApp( context: self.context, @@ -3560,7 +3570,6 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { skipTermsOfService: true ) } else { - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams( navigationController: navigationController, context: self.context, diff --git a/submodules/Display/Source/PortalSourceView.swift b/submodules/Display/Source/PortalSourceView.swift index 337edb299f..3c932b1924 100644 --- a/submodules/Display/Source/PortalSourceView.swift +++ b/submodules/Display/Source/PortalSourceView.swift @@ -47,6 +47,13 @@ open class PortalSourceView: UIView { } } + public func removePortal(view: PortalView) { + if let index = self.portalReferences.firstIndex(where: { $0.portalView === view }) { + self.portalReferences.remove(at: index) + } + view.disablePortal() + } + func setGlobalPortal(view: GlobalPortalView?) { if let globalPortalView = self.globalPortalView { self.globalPortalView = nil diff --git a/submodules/Display/Source/PortalView.swift b/submodules/Display/Source/PortalView.swift index c26d2632fe..f5583a4cb2 100644 --- a/submodules/Display/Source/PortalView.swift +++ b/submodules/Display/Source/PortalView.swift @@ -23,6 +23,11 @@ public class PortalView { } } + func disablePortal() { + self.view.sourceView = nil + self.sourceView = nil + } + public func reloadPortal() { if let sourceView = self.sourceView as? PortalSourceView { self.reloadPortal(sourceView: sourceView) diff --git a/submodules/SparseItemGrid/Sources/SparseItemGrid.swift b/submodules/SparseItemGrid/Sources/SparseItemGrid.swift index 3820e7fd4d..de00d411bd 100644 --- a/submodules/SparseItemGrid/Sources/SparseItemGrid.swift +++ b/submodules/SparseItemGrid/Sources/SparseItemGrid.swift @@ -550,6 +550,10 @@ public final class SparseItemGrid: ASDisplayNode { var offset: CGFloat { return self.scrollView.contentOffset.y } + + var contentBottomOffset: CGFloat { + return -self.scrollView.contentOffset.y + self.scrollView.contentSize.height + } let coveringOffsetUpdated: (Viewport, ContainedViewLayoutTransition) -> Void let offsetUpdated: (Viewport, ContainedViewLayoutTransition) -> Void @@ -1442,6 +1446,10 @@ public final class SparseItemGrid: ASDisplayNode { return self.fromViewport.coveringInsetOffset * (1.0 - self.currentProgress) + self.toViewport.coveringInsetOffset * self.currentProgress } + var contentBottomOffset: CGFloat { + return self.fromViewport.contentBottomOffset * (1.0 - self.currentProgress) + self.toViewport.contentBottomOffset * self.currentProgress + } + var offset: CGFloat { return self.fromViewport.offset * (1.0 - self.currentProgress) + self.toViewport.offset * self.currentProgress } @@ -1632,6 +1640,16 @@ public final class SparseItemGrid: ASDisplayNode { } } + public var contentBottomOffset: CGFloat { + if let currentViewportTransition = self.currentViewportTransition { + return currentViewportTransition.contentBottomOffset + } else if let currentViewport = self.currentViewport { + return currentViewport.contentBottomOffset + } else { + return 0.0 + } + } + public var scrollingOffset: CGFloat { if let currentViewportTransition = self.currentViewportTransition { return currentViewportTransition.offset diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index ed28e4f1ad..ff4d7a0245 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -105,6 +105,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-944407322] = { return Api.BotMenuButton.parse_botMenuButton($0) } dict[1113113093] = { return Api.BotMenuButton.parse_botMenuButtonCommands($0) } dict[1966318984] = { return Api.BotMenuButton.parse_botMenuButtonDefault($0) } + dict[602479523] = { return Api.BotPreviewMedia.parse_botPreviewMedia($0) } dict[-2076642874] = { return Api.BroadcastRevenueBalances.parse_broadcastRevenueBalances($0) } dict[1434332356] = { return Api.BroadcastRevenueTransaction.parse_broadcastRevenueTransactionProceeds($0) } dict[1121127726] = { return Api.BroadcastRevenueTransaction.parse_broadcastRevenueTransactionRefund($0) } @@ -520,7 +521,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[340088945] = { return Api.MediaArea.parse_mediaAreaSuggestedReaction($0) } dict[926421125] = { return Api.MediaArea.parse_mediaAreaUrl($0) } dict[-1098720356] = { return Api.MediaArea.parse_mediaAreaVenue($0) } - dict[1132918857] = { return Api.MediaArea.parse_mediaAreaWeather($0) } + dict[1235637404] = { return Api.MediaArea.parse_mediaAreaWeather($0) } dict[-808853502] = { return Api.MediaAreaCoordinates.parse_mediaAreaCoordinates($0) } dict[-1808510398] = { return Api.Message.parse_message($0) } dict[-1868117372] = { return Api.Message.parse_messageEmpty($0) } @@ -1170,6 +1171,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1542017919] = { return Api.auth.SentCodeType.parse_sentCodeTypeSmsWord($0) } dict[-391678544] = { return Api.bots.BotInfo.parse_botInfo($0) } dict[428978491] = { return Api.bots.PopularAppBots.parse_popularAppBots($0) } + dict[212278628] = { return Api.bots.PreviewInfo.parse_previewInfo($0) } dict[-309659827] = { return Api.channels.AdminLogResults.parse_adminLogResults($0) } dict[-541588713] = { return Api.channels.ChannelParticipant.parse_channelParticipant($0) } dict[-1699676497] = { return Api.channels.ChannelParticipants.parse_channelParticipants($0) } @@ -1493,6 +1495,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.BotMenuButton: _1.serialize(buffer, boxed) + case let _1 as Api.BotPreviewMedia: + _1.serialize(buffer, boxed) case let _1 as Api.BroadcastRevenueBalances: _1.serialize(buffer, boxed) case let _1 as Api.BroadcastRevenueTransaction: @@ -2151,6 +2155,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.bots.PopularAppBots: _1.serialize(buffer, boxed) + case let _1 as Api.bots.PreviewInfo: + _1.serialize(buffer, boxed) case let _1 as Api.channels.AdminLogResults: _1.serialize(buffer, boxed) case let _1 as Api.channels.ChannelParticipant: diff --git a/submodules/TelegramApi/Sources/Api14.swift b/submodules/TelegramApi/Sources/Api14.swift index fdd58a93fb..d7f64bdd25 100644 --- a/submodules/TelegramApi/Sources/Api14.swift +++ b/submodules/TelegramApi/Sources/Api14.swift @@ -231,7 +231,7 @@ public extension Api { case mediaAreaSuggestedReaction(flags: Int32, coordinates: Api.MediaAreaCoordinates, reaction: Api.Reaction) case mediaAreaUrl(coordinates: Api.MediaAreaCoordinates, url: String) case mediaAreaVenue(coordinates: Api.MediaAreaCoordinates, geo: Api.GeoPoint, title: String, address: String, provider: String, venueId: String, venueType: String) - case mediaAreaWeather(flags: Int32, coordinates: Api.MediaAreaCoordinates, emoji: String, temperatureC: Double) + case mediaAreaWeather(coordinates: Api.MediaAreaCoordinates, emoji: String, temperatureC: Double, color: Int32) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -295,14 +295,14 @@ public extension Api { serializeString(venueId, buffer: buffer, boxed: false) serializeString(venueType, buffer: buffer, boxed: false) break - case .mediaAreaWeather(let flags, let coordinates, let emoji, let temperatureC): + case .mediaAreaWeather(let coordinates, let emoji, let temperatureC, let color): if boxed { - buffer.appendInt32(1132918857) + buffer.appendInt32(1235637404) } - serializeInt32(flags, buffer: buffer, boxed: false) coordinates.serialize(buffer, true) serializeString(emoji, buffer: buffer, boxed: false) serializeDouble(temperatureC, buffer: buffer, boxed: false) + serializeInt32(color, buffer: buffer, boxed: false) break } } @@ -323,8 +323,8 @@ public extension Api { return ("mediaAreaUrl", [("coordinates", coordinates as Any), ("url", url as Any)]) case .mediaAreaVenue(let coordinates, let geo, let title, let address, let provider, let venueId, let venueType): return ("mediaAreaVenue", [("coordinates", coordinates as Any), ("geo", geo as Any), ("title", title as Any), ("address", address as Any), ("provider", provider as Any), ("venueId", venueId as Any), ("venueType", venueType as Any)]) - case .mediaAreaWeather(let flags, let coordinates, let emoji, let temperatureC): - return ("mediaAreaWeather", [("flags", flags as Any), ("coordinates", coordinates as Any), ("emoji", emoji as Any), ("temperatureC", temperatureC as Any)]) + case .mediaAreaWeather(let coordinates, let emoji, let temperatureC, let color): + return ("mediaAreaWeather", [("coordinates", coordinates as Any), ("emoji", emoji as Any), ("temperatureC", temperatureC as Any), ("color", color as Any)]) } } @@ -484,22 +484,22 @@ public extension Api { } } public static func parse_mediaAreaWeather(_ reader: BufferReader) -> MediaArea? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Api.MediaAreaCoordinates? + var _1: Api.MediaAreaCoordinates? if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.MediaAreaCoordinates + _1 = Api.parse(reader, signature: signature) as? Api.MediaAreaCoordinates } - var _3: String? - _3 = parseString(reader) - var _4: Double? - _4 = reader.readDouble() + var _2: String? + _2 = parseString(reader) + var _3: Double? + _3 = reader.readDouble() + var _4: Int32? + _4 = reader.readInt32() let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil if _c1 && _c2 && _c3 && _c4 { - return Api.MediaArea.mediaAreaWeather(flags: _1!, coordinates: _2!, emoji: _3!, temperatureC: _4!) + return Api.MediaArea.mediaAreaWeather(coordinates: _1!, emoji: _2!, temperatureC: _3!, color: _4!) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api2.swift b/submodules/TelegramApi/Sources/Api2.swift index 51d1fb1d7f..8855da4143 100644 --- a/submodules/TelegramApi/Sources/Api2.swift +++ b/submodules/TelegramApi/Sources/Api2.swift @@ -724,6 +724,48 @@ public extension Api { } } +public extension Api { + indirect enum BotPreviewMedia: TypeConstructorDescription { + case botPreviewMedia(date: Int32, media: Api.MessageMedia) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .botPreviewMedia(let date, let media): + if boxed { + buffer.appendInt32(602479523) + } + serializeInt32(date, buffer: buffer, boxed: false) + media.serialize(buffer, true) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .botPreviewMedia(let date, let media): + return ("botPreviewMedia", [("date", date as Any), ("media", media as Any)]) + } + } + + public static func parse_botPreviewMedia(_ reader: BufferReader) -> BotPreviewMedia? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.MessageMedia? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.MessageMedia + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.BotPreviewMedia.botPreviewMedia(date: _1!, media: _2!) + } + else { + return nil + } + } + + } +} public extension Api { enum BroadcastRevenueBalances: TypeConstructorDescription { case broadcastRevenueBalances(currentBalance: Int64, availableBalance: Int64, overallRevenue: Int64) @@ -1160,53 +1202,3 @@ public extension Api { } } -public extension Api { - enum BusinessIntro: TypeConstructorDescription { - case businessIntro(flags: Int32, title: String, description: String, sticker: Api.Document?) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .businessIntro(let flags, let title, let description, let sticker): - if boxed { - buffer.appendInt32(1510606445) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeString(title, buffer: buffer, boxed: false) - serializeString(description, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 0) != 0 {sticker!.serialize(buffer, true)} - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .businessIntro(let flags, let title, let description, let sticker): - return ("businessIntro", [("flags", flags as Any), ("title", title as Any), ("description", description as Any), ("sticker", sticker as Any)]) - } - } - - public static func parse_businessIntro(_ reader: BufferReader) -> BusinessIntro? { - var _1: Int32? - _1 = reader.readInt32() - var _2: String? - _2 = parseString(reader) - var _3: String? - _3 = parseString(reader) - var _4: Api.Document? - if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() { - _4 = Api.parse(reader, signature: signature) as? Api.Document - } } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.BusinessIntro.businessIntro(flags: _1!, title: _2!, description: _3!, sticker: _4) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api29.swift b/submodules/TelegramApi/Sources/Api29.swift index 8af8e80f63..36dea196a1 100644 --- a/submodules/TelegramApi/Sources/Api29.swift +++ b/submodules/TelegramApi/Sources/Api29.swift @@ -1,3 +1,55 @@ +public extension Api.bots { + enum PreviewInfo: TypeConstructorDescription { + case previewInfo(media: [Api.BotPreviewMedia], langCodes: [String]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .previewInfo(let media, let langCodes): + if boxed { + buffer.appendInt32(212278628) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(media.count)) + for item in media { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(langCodes.count)) + for item in langCodes { + serializeString(item, buffer: buffer, boxed: false) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .previewInfo(let media, let langCodes): + return ("previewInfo", [("media", media as Any), ("langCodes", langCodes as Any)]) + } + } + + public static func parse_previewInfo(_ reader: BufferReader) -> PreviewInfo? { + var _1: [Api.BotPreviewMedia]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.BotPreviewMedia.self) + } + var _2: [String]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: -1255641564, elementType: String.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.bots.PreviewInfo.previewInfo(media: _1!, langCodes: _2!) + } + else { + return nil + } + } + + } +} public extension Api.channels { enum AdminLogResults: TypeConstructorDescription { case adminLogResults(events: [Api.ChannelAdminLogEvent], chats: [Api.Chat], users: [Api.User]) @@ -1404,61 +1456,3 @@ public extension Api.help { } } -public extension Api.help { - enum Country: TypeConstructorDescription { - case country(flags: Int32, iso2: String, defaultName: String, name: String?, countryCodes: [Api.help.CountryCode]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .country(let flags, let iso2, let defaultName, let name, let countryCodes): - if boxed { - buffer.appendInt32(-1014526429) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeString(iso2, buffer: buffer, boxed: false) - serializeString(defaultName, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 1) != 0 {serializeString(name!, buffer: buffer, boxed: false)} - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(countryCodes.count)) - for item in countryCodes { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .country(let flags, let iso2, let defaultName, let name, let countryCodes): - return ("country", [("flags", flags as Any), ("iso2", iso2 as Any), ("defaultName", defaultName as Any), ("name", name as Any), ("countryCodes", countryCodes as Any)]) - } - } - - public static func parse_country(_ reader: BufferReader) -> Country? { - var _1: Int32? - _1 = reader.readInt32() - var _2: String? - _2 = parseString(reader) - var _3: String? - _3 = parseString(reader) - var _4: String? - if Int(_1!) & Int(1 << 1) != 0 {_4 = parseString(reader) } - var _5: [Api.help.CountryCode]? - if let _ = reader.readInt32() { - _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.help.CountryCode.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil - let _c5 = _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return Api.help.Country.country(flags: _1!, iso2: _2!, defaultName: _3!, name: _4, countryCodes: _5!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api3.swift b/submodules/TelegramApi/Sources/Api3.swift index 331ac6b496..3ec0b4d752 100644 --- a/submodules/TelegramApi/Sources/Api3.swift +++ b/submodules/TelegramApi/Sources/Api3.swift @@ -1,3 +1,53 @@ +public extension Api { + enum BusinessIntro: TypeConstructorDescription { + case businessIntro(flags: Int32, title: String, description: String, sticker: Api.Document?) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .businessIntro(let flags, let title, let description, let sticker): + if boxed { + buffer.appendInt32(1510606445) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeString(title, buffer: buffer, boxed: false) + serializeString(description, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {sticker!.serialize(buffer, true)} + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .businessIntro(let flags, let title, let description, let sticker): + return ("businessIntro", [("flags", flags as Any), ("title", title as Any), ("description", description as Any), ("sticker", sticker as Any)]) + } + } + + public static func parse_businessIntro(_ reader: BufferReader) -> BusinessIntro? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: String? + _3 = parseString(reader) + var _4: Api.Document? + if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() { + _4 = Api.parse(reader, signature: signature) as? Api.Document + } } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.BusinessIntro.businessIntro(flags: _1!, title: _2!, description: _3!, sticker: _4) + } + else { + return nil + } + } + + } +} public extension Api { enum BusinessLocation: TypeConstructorDescription { case businessLocation(flags: Int32, geoPoint: Api.GeoPoint?, address: String) diff --git a/submodules/TelegramApi/Sources/Api30.swift b/submodules/TelegramApi/Sources/Api30.swift index 7c9e867d13..8156e1ba08 100644 --- a/submodules/TelegramApi/Sources/Api30.swift +++ b/submodules/TelegramApi/Sources/Api30.swift @@ -1,3 +1,61 @@ +public extension Api.help { + enum Country: TypeConstructorDescription { + case country(flags: Int32, iso2: String, defaultName: String, name: String?, countryCodes: [Api.help.CountryCode]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .country(let flags, let iso2, let defaultName, let name, let countryCodes): + if boxed { + buffer.appendInt32(-1014526429) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeString(iso2, buffer: buffer, boxed: false) + serializeString(defaultName, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 1) != 0 {serializeString(name!, buffer: buffer, boxed: false)} + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(countryCodes.count)) + for item in countryCodes { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .country(let flags, let iso2, let defaultName, let name, let countryCodes): + return ("country", [("flags", flags as Any), ("iso2", iso2 as Any), ("defaultName", defaultName as Any), ("name", name as Any), ("countryCodes", countryCodes as Any)]) + } + } + + public static func parse_country(_ reader: BufferReader) -> Country? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: String? + _3 = parseString(reader) + var _4: String? + if Int(_1!) & Int(1 << 1) != 0 {_4 = parseString(reader) } + var _5: [Api.help.CountryCode]? + if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.help.CountryCode.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.help.Country.country(flags: _1!, iso2: _2!, defaultName: _3!, name: _4, countryCodes: _5!) + } + else { + return nil + } + } + + } +} public extension Api.help { enum CountryCode: TypeConstructorDescription { case countryCode(flags: Int32, countryCode: String, prefixes: [String]?, patterns: [String]?) diff --git a/submodules/TelegramApi/Sources/Api36.swift b/submodules/TelegramApi/Sources/Api36.swift index 8ddd53d008..6c886c3735 100644 --- a/submodules/TelegramApi/Sources/Api36.swift +++ b/submodules/TelegramApi/Sources/Api36.swift @@ -2201,16 +2201,17 @@ public extension Api.functions.auth { } } public extension Api.functions.bots { - static func addPreviewMedia(bot: Api.InputUser, media: Api.InputMedia) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func addPreviewMedia(bot: Api.InputUser, langCode: String, media: Api.InputMedia) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(1633332331) + buffer.appendInt32(397326170) bot.serialize(buffer, true) + serializeString(langCode, buffer: buffer, boxed: false) media.serialize(buffer, true) - return (FunctionDescription(name: "bots.addPreviewMedia", parameters: [("bot", String(describing: bot)), ("media", String(describing: media))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.MessageMedia? in + return (FunctionDescription(name: "bots.addPreviewMedia", parameters: [("bot", String(describing: bot)), ("langCode", String(describing: langCode)), ("media", String(describing: media))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.BotPreviewMedia? in let reader = BufferReader(buffer) - var result: Api.MessageMedia? + var result: Api.BotPreviewMedia? if let signature = reader.readInt32() { - result = Api.parse(reader, signature: signature) as? Api.MessageMedia + result = Api.parse(reader, signature: signature) as? Api.BotPreviewMedia } return result }) @@ -2263,16 +2264,17 @@ public extension Api.functions.bots { } } public extension Api.functions.bots { - static func deletePreviewMedia(bot: Api.InputUser, media: [Api.InputMedia]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func deletePreviewMedia(bot: Api.InputUser, langCode: String, media: [Api.InputMedia]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(481471475) + buffer.appendInt32(755054003) bot.serialize(buffer, true) + serializeString(langCode, buffer: buffer, boxed: false) buffer.appendInt32(481674261) buffer.appendInt32(Int32(media.count)) for item in media { item.serialize(buffer, true) } - return (FunctionDescription(name: "bots.deletePreviewMedia", parameters: [("bot", String(describing: bot)), ("media", String(describing: media))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + return (FunctionDescription(name: "bots.deletePreviewMedia", parameters: [("bot", String(describing: bot)), ("langCode", String(describing: langCode)), ("media", String(describing: media))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in let reader = BufferReader(buffer) var result: Api.Bool? if let signature = reader.readInt32() { @@ -2283,17 +2285,18 @@ public extension Api.functions.bots { } } public extension Api.functions.bots { - static func editPreviewMedia(bot: Api.InputUser, media: Api.InputMedia, newMedia: Api.InputMedia) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func editPreviewMedia(bot: Api.InputUser, langCode: String, media: Api.InputMedia, newMedia: Api.InputMedia) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(-1436441263) + buffer.appendInt32(-2061148049) bot.serialize(buffer, true) + serializeString(langCode, buffer: buffer, boxed: false) media.serialize(buffer, true) newMedia.serialize(buffer, true) - return (FunctionDescription(name: "bots.editPreviewMedia", parameters: [("bot", String(describing: bot)), ("media", String(describing: media)), ("newMedia", String(describing: newMedia))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.MessageMedia? in + return (FunctionDescription(name: "bots.editPreviewMedia", parameters: [("bot", String(describing: bot)), ("langCode", String(describing: langCode)), ("media", String(describing: media)), ("newMedia", String(describing: newMedia))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.BotPreviewMedia? in let reader = BufferReader(buffer) - var result: Api.MessageMedia? + var result: Api.BotPreviewMedia? if let signature = reader.readInt32() { - result = Api.parse(reader, signature: signature) as? Api.MessageMedia + result = Api.parse(reader, signature: signature) as? Api.BotPreviewMedia } return result }) @@ -2364,15 +2367,31 @@ public extension Api.functions.bots { } } public extension Api.functions.bots { - static func getPreviewMedias(bot: Api.InputUser) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<[Api.MessageMedia]>) { + static func getPreviewInfo(bot: Api.InputUser, langCode: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(1720252591) + buffer.appendInt32(1111143341) bot.serialize(buffer, true) - return (FunctionDescription(name: "bots.getPreviewMedias", parameters: [("bot", String(describing: bot))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> [Api.MessageMedia]? in + serializeString(langCode, buffer: buffer, boxed: false) + return (FunctionDescription(name: "bots.getPreviewInfo", parameters: [("bot", String(describing: bot)), ("langCode", String(describing: langCode))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.bots.PreviewInfo? in let reader = BufferReader(buffer) - var result: [Api.MessageMedia]? + var result: Api.bots.PreviewInfo? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.bots.PreviewInfo + } + return result + }) + } +} +public extension Api.functions.bots { + static func getPreviewMedias(bot: Api.InputUser) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<[Api.BotPreviewMedia]>) { + let buffer = Buffer() + buffer.appendInt32(-1566222003) + bot.serialize(buffer, true) + return (FunctionDescription(name: "bots.getPreviewMedias", parameters: [("bot", String(describing: bot))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> [Api.BotPreviewMedia]? in + let reader = BufferReader(buffer) + var result: [Api.BotPreviewMedia]? if let _ = reader.readInt32() { - result = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageMedia.self) + result = Api.parseVector(reader, elementSignature: 0, elementType: Api.BotPreviewMedia.self) } return result }) @@ -2396,16 +2415,17 @@ public extension Api.functions.bots { } } public extension Api.functions.bots { - static func reorderPreviewMedias(bot: Api.InputUser, order: [Api.InputMedia]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func reorderPreviewMedias(bot: Api.InputUser, langCode: String, order: [Api.InputMedia]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(-1472444656) + buffer.appendInt32(-1238895702) bot.serialize(buffer, true) + serializeString(langCode, buffer: buffer, boxed: false) buffer.appendInt32(481674261) buffer.appendInt32(Int32(order.count)) for item in order { item.serialize(buffer, true) } - return (FunctionDescription(name: "bots.reorderPreviewMedias", parameters: [("bot", String(describing: bot)), ("order", String(describing: order))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + return (FunctionDescription(name: "bots.reorderPreviewMedias", parameters: [("bot", String(describing: bot)), ("langCode", String(describing: langCode)), ("order", String(describing: order))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in let reader = BufferReader(buffer) var result: Api.Bool? if let signature = reader.readInt32() { diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index e8d07b397e..c40ed11874 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -522,9 +522,9 @@ func mediaAreaFromApiMediaArea(_ mediaArea: Api.MediaArea) -> MediaArea? { return .link(coordinates: coodinatesFromApiMediaAreaCoordinates(coordinates), url: url) case let .mediaAreaChannelPost(coordinates, channelId, messageId): return .channelMessage(coordinates: coodinatesFromApiMediaAreaCoordinates(coordinates), messageId: EngineMessage.Id(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)), namespace: Namespaces.Message.Cloud, id: messageId)) - case let .mediaAreaWeather(flags, coordinates, emoji, temperatureC): + case let .mediaAreaWeather(coordinates, emoji, temperatureC, color): var parsedFlags = MediaArea.WeatherFlags() - if (flags & (1 << 0)) != 0 { + if color != 0 { parsedFlags.insert(.isDark) } return .weather(coordinates: coodinatesFromApiMediaAreaCoordinates(coordinates), emoji: emoji, temperature: temperatureC, flags: parsedFlags) @@ -581,11 +581,7 @@ func apiMediaAreasFromMediaAreas(_ mediaAreas: [MediaArea], transaction: Transac case let .link(_, url): apiMediaAreas.append(.mediaAreaUrl(coordinates: inputCoordinates, url: url)) case let .weather(_, emoji, temperature, flags): - var apiFlags: Int32 = 0 - if flags.contains(.isDark) { - apiFlags |= (1 << 0) - } - apiMediaAreas.append(.mediaAreaWeather(flags: apiFlags, coordinates: inputCoordinates, emoji: emoji, temperatureC: temperature)) + apiMediaAreas.append(.mediaAreaWeather(coordinates: inputCoordinates, emoji: emoji, temperatureC: temperature, color: flags.contains(.isDark) ? 1 : 0)) } } return apiMediaAreas diff --git a/submodules/TelegramCore/Sources/State/AccountStateManager.swift b/submodules/TelegramCore/Sources/State/AccountStateManager.swift index 9fa6beed64..f2f7f5321f 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManager.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManager.swift @@ -287,6 +287,11 @@ public final class AccountStateManager { return self.storyUpdatesPipe.signal() } + fileprivate let botPreviewUpdatesPipe = ValuePipe<[InternalBotPreviewUpdate]>() + public var botPreviewUpdates: Signal<[InternalBotPreviewUpdate], NoError> { + return self.botPreviewUpdatesPipe.signal() + } + private var updatedWebpageContexts: [MediaId: UpdatedWebpageSubscriberContext] = [:] private var updatedPeersNearbyContext = UpdatedPeersNearbySubscriberContext() private var updatedRevenueBalancesContext = UpdatedRevenueBalancesSubscriberContext() @@ -1856,6 +1861,18 @@ public final class AccountStateManager { } } + var botPreviewUpdates: Signal<[InternalBotPreviewUpdate], NoError> { + return self.impl.signalWith { impl, subscriber in + return impl.botPreviewUpdates.start(next: subscriber.putNext, error: subscriber.putError, completed: subscriber.putCompletion) + } + } + + func injectBotPreviewUpdates(updates: [InternalBotPreviewUpdate]) { + self.impl.with { impl in + impl.botPreviewUpdatesPipe.putNext(updates) + } + } + var updateConfigRequested: (() -> Void)? var isPremiumUpdated: (() -> Void)? diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift index 62c160c7c8..f35daeb38a 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift @@ -627,40 +627,87 @@ extension TelegramBusinessChatLinks { public final class CachedUserData: CachedPeerData { public final class BotPreview: Codable, Equatable { private enum CodingKeys: String, CodingKey { - case media + case items + case alternativeLanguageCodes } - public let media: [Media] + public final class Item: Codable, Equatable { + private enum CodingKeys: String, CodingKey { + case media = "m" + case timestamp = "t" + } + + public let media: Media + public let timestamp: Int32 + + public init(media: Media, timestamp: Int32) { + self.media = media + self.timestamp = timestamp + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let mediaData = try container.decode(Data.self, forKey: .media) + guard let media = PostboxDecoder(buffer: MemoryBuffer(data: mediaData)).decodeRootObject() as? Media else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "media")) + } + self.media = media + + self.timestamp = try container.decode(Int32.self, forKey: .timestamp) + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + let encoder = PostboxEncoder() + encoder.encodeRootObject(media) + try container.encode(encoder.makeData(), forKey: .media) + + try container.encode(self.timestamp, forKey: .timestamp) + } + + public static func ==(lhs: Item, rhs: Item) -> Bool { + if lhs === rhs { + return true + } + if !lhs.media.isEqual(to: rhs.media) { + return false + } + return true + } + } - public init(media: [Media]) { - self.media = media + public let items: [Item] + public let alternativeLanguageCodes: [String] + + public init(items: [Item], alternativeLanguageCodes: [String]) { + self.items = items + self.alternativeLanguageCodes = alternativeLanguageCodes } public init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - let mediaData = try container.decode([Data].self, forKey: .media) - self.media = mediaData.compactMap { data -> Media? in - return PostboxDecoder(buffer: MemoryBuffer(data: data)).decodeRootObject() as? Media - } + self.items = try container.decode([Item].self, forKey: .items) + self.alternativeLanguageCodes = try container.decode([String].self, forKey: .alternativeLanguageCodes) } public func encode(to encoder: any Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) - let mediaData = self.media.map { media -> Data in - let encoder = PostboxEncoder() - encoder.encodeRootObject(media) - return encoder.makeData() - } - try container.encode(mediaData, forKey: .media) + try container.encode(self.items, forKey: .items) + try container.encode(self.alternativeLanguageCodes, forKey: .alternativeLanguageCodes) } public static func ==(lhs: BotPreview, rhs: BotPreview) -> Bool { if lhs === rhs { return true } - if !areMediaArraysEqual(lhs.media, rhs.media) { + if lhs.items != rhs.items { + return false + } + if lhs.alternativeLanguageCodes != rhs.alternativeLanguageCodes { return false } return true diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/PendingStoryManager.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/PendingStoryManager.swift index 2100467d23..cf8b471f2c 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/PendingStoryManager.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/PendingStoryManager.swift @@ -8,11 +8,12 @@ public extension Stories { private enum CodingKeys: String, CodingKey { case discriminator = "tt" case peerId = "peerId" + case language = "language" } case myStories case peer(PeerId) - case botPreview(PeerId) + case botPreview(id: PeerId, language: String?) public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) @@ -23,7 +24,7 @@ public extension Stories { case 1: self = .peer(try container.decode(PeerId.self, forKey: .peerId)) case 2: - self = .botPreview(try container.decode(PeerId.self, forKey: .peerId)) + self = .botPreview(id: try container.decode(PeerId.self, forKey: .peerId), language: try container.decodeIfPresent(String.self, forKey: .language)) default: self = .myStories } @@ -38,9 +39,10 @@ public extension Stories { case let .peer(peerId): try container.encode(1 as Int32, forKey: .discriminator) try container.encode(peerId, forKey: .peerId) - case let .botPreview(peerId): + case let .botPreview(peerId, language): try container.encode(2 as Int32, forKey: .discriminator) try container.encode(peerId, forKey: .peerId) + try container.encodeIfPresent(language, forKey: .language) } } } @@ -406,13 +408,15 @@ final class PendingStoryManager { let toPeerId: PeerId var isBotPreview = false + var botPreviewLanguage: String? switch firstItem.target { case .myStories: toPeerId = self.accountPeerId case let .peer(peerId): toPeerId = peerId - case let .botPreview(peerId): + case let .botPreview(peerId, language): toPeerId = peerId + botPreviewLanguage = language isBotPreview = true } @@ -427,6 +431,7 @@ final class PendingStoryManager { revalidationContext: self.revalidationContext, auxiliaryMethods: self.auxiliaryMethods, toPeerId: toPeerId, + language: botPreviewLanguage, stableId: stableId, media: firstItem.media, mediaAreas: firstItem.mediaAreas, diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index 9840057937..ea8d77df3a 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -1270,6 +1270,7 @@ func _internal_uploadBotPreviewImpl( revalidationContext: MediaReferenceRevalidationContext, auxiliaryMethods: AccountAuxiliaryMethods, toPeerId: PeerId, + language: String?, stableId: Int32, media: Media, mediaAreas: [MediaArea], @@ -1300,46 +1301,59 @@ func _internal_uploadBotPreviewImpl( return postbox.transaction { transaction -> Signal in switch content.content { case let .media(inputMedia, _): - return network.request(Api.functions.bots.addPreviewMedia(bot: inputUser, media: inputMedia)) + return network.request(Api.functions.bots.addPreviewMedia(bot: inputUser, langCode: language ?? "", media: inputMedia)) |> map(Optional.init) - |> `catch` { _ -> Signal in + |> `catch` { _ -> Signal in return .single(nil) } - |> mapToSignal { resultMedia -> Signal in - return postbox.transaction { transaction -> StoryUploadResult in - var currentState: Stories.LocalState - if let value = transaction.getLocalStoryState()?.get(Stories.LocalState.self) { - currentState = value - } else { - currentState = Stories.LocalState(items: []) - } - if let index = currentState.items.firstIndex(where: { $0.stableId == stableId }) { - currentState.items.remove(at: index) - transaction.setLocalStoryState(state: CodableEntry(currentState)) - } - - if let resultMediaValue = textMediaAndExpirationTimerFromApiMedia(resultMedia, toPeerId).media { - applyMediaResourceChanges(from: originalMedia, to: resultMediaValue, postbox: postbox, force: originalMedia is TelegramMediaFile && resultMediaValue is TelegramMediaFile) + |> mapToSignal { resultPreviewMedia -> Signal in + guard let resultPreviewMedia else { + return .single(.completed(nil)) + } + switch resultPreviewMedia { + case let .botPreviewMedia(date, resultMedia): + return postbox.transaction { transaction -> StoryUploadResult in + var currentState: Stories.LocalState + if let value = transaction.getLocalStoryState()?.get(Stories.LocalState.self) { + currentState = value + } else { + currentState = Stories.LocalState(items: []) + } + if let index = currentState.items.firstIndex(where: { $0.stableId == stableId }) { + currentState.items.remove(at: index) + transaction.setLocalStoryState(state: CodableEntry(currentState)) + } - transaction.updatePeerCachedData(peerIds: Set([toPeerId]), update: { _, current in - guard var current = current as? CachedUserData else { - return current + if let resultMediaValue = textMediaAndExpirationTimerFromApiMedia(resultMedia, toPeerId).media { + applyMediaResourceChanges(from: originalMedia, to: resultMediaValue, postbox: postbox, force: originalMedia is TelegramMediaFile && resultMediaValue is TelegramMediaFile) + + let addedItem = CachedUserData.BotPreview.Item(media: resultMediaValue, timestamp: date) + + if language == nil { + transaction.updatePeerCachedData(peerIds: Set([toPeerId]), update: { _, current in + guard var current = current as? CachedUserData else { + return current + } + guard let currentBotPreview = current.botPreview else { + return current + } + var items = currentBotPreview.items + if let index = items.firstIndex(where: { $0.media.id == resultMediaValue.id }) { + items.remove(at: index) + } + items.insert(addedItem, at: 0) + let botPreview = CachedUserData.BotPreview(items: items, alternativeLanguageCodes: currentBotPreview.alternativeLanguageCodes) + current = current.withUpdatedBotPreview(botPreview) + return current + }) } - guard let currentBotPreview = current.botPreview else { - return current - } - var media = currentBotPreview.media - if let index = media.firstIndex(where: { $0.id == resultMediaValue.id }) { - media.remove(at: index) - } - media.insert(resultMediaValue, at: 0) - let botPreview = CachedUserData.BotPreview(media: media) - current = current.withUpdatedBotPreview(botPreview) - return current - }) + stateManager.injectBotPreviewUpdates(updates: [ + .added(peerId: toPeerId, language: language, item: addedItem) + ]) + } + + return .completed(nil) } - - return .completed(nil) } } default: @@ -1354,13 +1368,79 @@ func _internal_uploadBotPreviewImpl( } } -func _internal_deleteBotPreviews(account: Account, peerId: PeerId, ids: [MediaId]) -> Signal { +func _internal_deleteBotPreviews(account: Account, peerId: PeerId, language: String?, media: [Media]) -> Signal { return account.postbox.transaction { transaction -> (Api.InputUser?, [Api.InputMedia]) in guard let inputPeer = transaction.getPeer(peerId).flatMap(apiInputUser) else { return (nil, []) } var inputMedia: [Api.InputMedia] = [] + for item in media { + if let image = item as? TelegramMediaImage, let resource = image.representations.last?.resource as? CloudPhotoSizeMediaResource { + inputMedia.append(.inputMediaPhoto(flags: 0, id: .inputPhoto(id: resource.photoId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)), ttlSeconds: nil)) + inputMedia.append(Api.InputMedia.inputMediaPhoto(flags: 0, id: Api.InputPhoto.inputPhoto(id: resource.photoId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)), ttlSeconds: nil)) + } else if let file = item as? TelegramMediaFile, let resource = file.resource as? CloudDocumentMediaResource { + inputMedia.append(.inputMediaDocument(flags: 0, id: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())), ttlSeconds: nil, query: nil)) + } + } + if language == nil { + transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current -> CachedPeerData? in + guard var current = current as? CachedUserData else { + return current + } + guard let currentBotPreview = current.botPreview else { + return current + } + var items = currentBotPreview.items + + items = items.filter({ item in + guard let id = item.media.id else { + return false + } + return !media.contains(where: { $0.id == id }) + }) + let botPreview = CachedUserData.BotPreview(items: items, alternativeLanguageCodes: currentBotPreview.alternativeLanguageCodes) + current = current.withUpdatedBotPreview(botPreview) + return current + }) + } + + return (inputPeer, inputMedia) + } + |> mapToSignal { inputPeer, inputMedia -> Signal in + guard let inputPeer else { + return .complete() + } + + account.stateManager.injectBotPreviewUpdates(updates: [ + .deleted(peerId: peerId, language: language, ids: media.compactMap(\.id)) + ]) + + return account.network.request(Api.functions.bots.deletePreviewMedia(bot: inputPeer, langCode: language ?? "", media: inputMedia)) + |> `catch` { _ -> Signal in + return .single(.boolFalse) + } + |> mapToSignal { _ -> Signal in + return .complete() + } + } +} + +func _internal_deleteBotPreviewsLanguage(account: Account, peerId: PeerId, language: String, media: [Media]) -> Signal { + return account.postbox.transaction { transaction -> (Api.InputUser?, [Api.InputMedia]) in + guard let inputPeer = transaction.getPeer(peerId).flatMap(apiInputUser) else { + return (nil, []) + } + + var inputMedia: [Api.InputMedia] = [] + for item in media { + if let image = item as? TelegramMediaImage, let resource = image.representations.last?.resource as? CloudPhotoSizeMediaResource { + inputMedia.append(.inputMediaPhoto(flags: 0, id: .inputPhoto(id: resource.photoId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)), ttlSeconds: nil)) + inputMedia.append(Api.InputMedia.inputMediaPhoto(flags: 0, id: Api.InputPhoto.inputPhoto(id: resource.photoId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)), ttlSeconds: nil)) + } else if let file = item as? TelegramMediaFile, let resource = file.resource as? CloudDocumentMediaResource { + inputMedia.append(.inputMediaDocument(flags: 0, id: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())), ttlSeconds: nil, query: nil)) + } + } transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current -> CachedPeerData? in guard var current = current as? CachedUserData else { return current @@ -1368,29 +1448,11 @@ func _internal_deleteBotPreviews(account: Account, peerId: PeerId, ids: [MediaId guard let currentBotPreview = current.botPreview else { return current } - var media = currentBotPreview.media - - for item in media { - guard let id = item.id else { - continue - } - if ids.contains(id) { - if let image = item as? TelegramMediaImage, let resource = image.representations.last?.resource as? CloudPhotoSizeMediaResource { - inputMedia.append(.inputMediaPhoto(flags: 0, id: .inputPhoto(id: resource.photoId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)), ttlSeconds: nil)) - inputMedia.append(Api.InputMedia.inputMediaPhoto(flags: 0, id: Api.InputPhoto.inputPhoto(id: resource.photoId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)), ttlSeconds: nil)) - } else if let file = item as? TelegramMediaFile, let resource = file.resource as? CloudDocumentMediaResource { - inputMedia.append(.inputMediaDocument(flags: 0, id: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())), ttlSeconds: nil, query: nil)) - } - } + var alternativeLanguageCodes = currentBotPreview.alternativeLanguageCodes + alternativeLanguageCodes = alternativeLanguageCodes.filter { item in + return item != language } - - media = media.filter({ item in - guard let id = item.id else { - return false - } - return !ids.contains(id) - }) - let botPreview = CachedUserData.BotPreview(media: media) + let botPreview = CachedUserData.BotPreview(items: currentBotPreview.items, alternativeLanguageCodes: alternativeLanguageCodes) current = current.withUpdatedBotPreview(botPreview) return current }) @@ -1402,7 +1464,11 @@ func _internal_deleteBotPreviews(account: Account, peerId: PeerId, ids: [MediaId return .complete() } - return account.network.request(Api.functions.bots.deletePreviewMedia(bot: inputPeer, media: inputMedia)) + account.stateManager.injectBotPreviewUpdates(updates: [ + .deleted(peerId: peerId, language: language, ids: media.compactMap(\.id)) + ]) + + return account.network.request(Api.functions.bots.deletePreviewMedia(bot: inputPeer, langCode: language, media: inputMedia)) |> `catch` { _ -> Signal in return .single(.boolFalse) } @@ -1623,8 +1689,8 @@ func _internal_checkStoriesUploadAvailability(account: Account, target: Stories. return .inputPeerSelf case let .peer(peerId): return transaction.getPeer(peerId).flatMap(apiInputPeer) - case let .botPreview(peerId): - return transaction.getPeer(peerId).flatMap(apiInputPeer) + case .botPreview: + return nil } } |> mapToSignal { inputPeer -> Signal in diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift index 53a42aa58e..296abd0c85 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift @@ -12,6 +12,11 @@ enum InternalStoryUpdate { case updateMyReaction(peerId: PeerId, id: Int32, reaction: MessageReaction.Reaction?) } +enum InternalBotPreviewUpdate { + case added(peerId: PeerId, language: String?, item: CachedUserData.BotPreview.Item) + case deleted(peerId: PeerId, language: String?, ids: [MediaId]) +} + public final class EngineStoryItem: Equatable { public final class Views: Equatable { public let seenCount: Int @@ -563,8 +568,19 @@ public struct StoryListContextState: Equatable { } } + public struct Language: Equatable { + public let id: String + public let name: String + + public init(id: String, name: String) { + self.id = id + self.name = name + } + } + public var peerReference: PeerReference? public var items: [Item] + public var availableLanguages: [Language] public var pinnedIds: [Int32] public var totalCount: Int public var loadMoreToken: AnyHashable? @@ -575,6 +591,7 @@ public struct StoryListContextState: Equatable { public init( peerReference: PeerReference?, items: [Item], + availableLanguages: [Language], pinnedIds: [Int32], totalCount: Int, loadMoreToken: AnyHashable?, @@ -585,6 +602,7 @@ public struct StoryListContextState: Equatable { ) { self.peerReference = peerReference self.items = items + self.availableLanguages = availableLanguages self.pinnedIds = pinnedIds self.totalCount = totalCount self.loadMoreToken = loadMoreToken @@ -633,7 +651,7 @@ public final class PeerStoryListContext: StoryListContext { self.peerId = peerId self.isArchived = isArchived - self.stateValue = State(peerReference: nil, items: [], pinnedIds: [], totalCount: 0, loadMoreToken: AnyHashable(0 as Int), isCached: true, hasCache: false, allEntityFiles: [:], isLoading: false) + self.stateValue = State(peerReference: nil, items: [], availableLanguages: [], pinnedIds: [], totalCount: 0, loadMoreToken: AnyHashable(0 as Int), isCached: true, hasCache: false, allEntityFiles: [:], isLoading: false) let _ = (account.postbox.transaction { transaction -> (PeerReference?, [State.Item], [Int32], Int, [MediaId: TelegramMediaFile], Bool) in let key = ValueBoxKey(length: 8 + 1) @@ -723,7 +741,7 @@ public final class PeerStoryListContext: StoryListContext { return } - var updatedState = State(peerReference: peerReference, items: items, pinnedIds: pinnedIds, totalCount: totalCount, loadMoreToken: AnyHashable(0 as Int), isCached: true, hasCache: hasCache, allEntityFiles: allEntityFiles, isLoading: false) + var updatedState = State(peerReference: peerReference, items: items, availableLanguages: [], pinnedIds: pinnedIds, totalCount: totalCount, loadMoreToken: AnyHashable(0 as Int), isCached: true, hasCache: hasCache, allEntityFiles: allEntityFiles, isLoading: false) updatedState.items.sort(by: { lhs, rhs in let lhsPinned = updatedState.pinnedIds.firstIndex(of: lhs.storyItem.id) let rhsPinned = updatedState.pinnedIds.firstIndex(of: rhs.storyItem.id) @@ -746,6 +764,7 @@ public final class PeerStoryListContext: StoryListContext { deinit { self.requestDisposable?.dispose() + self.updatesDisposable?.dispose() } func loadMore(completion: (() -> Void)?) { @@ -1313,7 +1332,7 @@ public final class SearchStoryListContext: StoryListContext { self.account = account self.source = source - self.stateValue = State(peerReference: nil, items: [], pinnedIds: [], totalCount: 0, loadMoreToken: AnyHashable(""), isCached: false, hasCache: false, allEntityFiles: [:], isLoading: false) + self.stateValue = State(peerReference: nil, items: [], availableLanguages: [], pinnedIds: [], totalCount: 0, loadMoreToken: AnyHashable(""), isCached: false, hasCache: false, allEntityFiles: [:], isLoading: false) self.statePromise.set(.single(self.stateValue)) self.loadMore(completion: nil) @@ -2078,6 +2097,7 @@ public final class BotPreviewStoryListContext: StoryListContext { private let account: Account private let engine: TelegramEngine private let peerId: EnginePeer.Id + private let language: String? private let isArchived: Bool private let statePromise = Promise() @@ -2093,6 +2113,7 @@ public final class BotPreviewStoryListContext: StoryListContext { private var isLoadingMore: Bool = false private var requestDisposable: Disposable? private var updatesDisposable: Disposable? + private var eventsDisposable: Disposable? private let reorderDisposable = MetaDisposable() private var completionCallbacksByToken: [AnyHashable: [() -> Void]] = [:] @@ -2102,36 +2123,305 @@ public final class BotPreviewStoryListContext: StoryListContext { private var idMapping: [MediaId: Int32] = [:] private var reverseIdMapping: [Int32: MediaId] = [:] - init(queue: Queue, account: Account, engine: TelegramEngine, peerId: EnginePeer.Id) { + private var localItems: [State.Item] = [] + private var remoteItems: [State.Item] = [] + + init(queue: Queue, account: Account, engine: TelegramEngine, peerId: EnginePeer.Id, language: String?, assumeEmpty: Bool) { self.queue = queue self.account = account self.engine = engine self.peerId = peerId + self.language = language let isArchived = false self.isArchived = isArchived - self.stateValue = State(peerReference: nil, items: [], pinnedIds: [], totalCount: 0, loadMoreToken: AnyHashable(0 as Int), isCached: true, hasCache: false, allEntityFiles: [:], isLoading: false) + self.stateValue = State(peerReference: nil, items: [], availableLanguages: [], pinnedIds: [], totalCount: 0, loadMoreToken: AnyHashable(0 as Int), isCached: true, hasCache: false, allEntityFiles: [:], isLoading: false) let localStateKey: PostboxViewKey = .storiesState(key: .local) - self.requestDisposable = (combineLatest(queue: queue, - engine.data.subscribe( - TelegramEngine.EngineData.Item.Peer.Peer(id: peerId), - TelegramEngine.EngineData.Item.Peer.BotPreview(id: peerId) - ), - account.postbox.combinedView(keys: [localStateKey]) - ) - |> deliverOn(self.queue)).start(next: { [weak self] peerAndBotPreview, combinedView in + if let language { + let _ = (account.postbox.transaction { transaction -> Peer? in + return transaction.getPeer(peerId) + } + |> deliverOn(self.queue)).start(next: { [weak self] peer in + guard let self else { + return + } + + self.stateValue = State( + peerReference: peer.flatMap(PeerReference.init), + items: [], + availableLanguages: [], + pinnedIds: [], + totalCount: 0, + loadMoreToken: AnyHashable(0), + isCached: assumeEmpty, + hasCache: assumeEmpty, + allEntityFiles: [:], + isLoading: !assumeEmpty + ) + + self.loadLanguage(language: language, assumeEmpty: assumeEmpty) + }) + } else { + self.requestDisposable = (combineLatest(queue: queue, + engine.data.subscribe( + TelegramEngine.EngineData.Item.Peer.Peer(id: peerId), + TelegramEngine.EngineData.Item.Peer.BotPreview(id: peerId), + TelegramEngine.EngineData.Item.Configuration.LocalizationList() + ), + account.postbox.combinedView(keys: [ + localStateKey + ]) + ) + |> deliverOn(self.queue)).start(next: { [weak self] peerAndBotPreview, combinedView in + guard let self else { + return + } + + let (peer, botPreview, localizationList) = peerAndBotPreview + + var items: [State.Item] = [] + var availableLanguages: [StoryListContextState.Language] = [] + + if let stateView = combinedView.views[localStateKey] as? StoryStatesView, let localState = stateView.value?.get(Stories.LocalState.self) { + for item in localState.items.reversed() { + let mappedId: Int32 + if let current = self.pendingIdMapping[item.stableId] { + mappedId = current + } else { + mappedId = self.nextId + self.nextId += 1 + self.pendingIdMapping[item.stableId] = mappedId + } + if case let .botPreview(itemPeerId, itemLanguage) = item.target, itemPeerId == peerId, itemLanguage == language { + items.append(State.Item( + id: StoryId(peerId: peerId, id: mappedId), + storyItem: EngineStoryItem( + id: mappedId, + timestamp: 0, + expirationTimestamp: Int32.max, + media: EngineMedia(item.media), + alternativeMedia: nil, + mediaAreas: [], + text: "", + entities: [], + views: nil, + privacy: nil, + isPinned: false, + isExpired: false, + isPublic: false, + isPending: true, + isCloseFriends: false, + isContacts: false, + isSelectedContacts: false, + isForwardingDisabled: false, + isEdited: false, + isMy: false, + myReaction: nil, + forwardInfo: nil, + author: nil + ), + peer: nil + )) + } + } + } + + if let botPreview { + for item in botPreview.items { + guard let mediaId = item.media.id else { + continue + } + + let id: Int32 + if let current = self.idMapping[mediaId] { + id = current + } else { + id = self.nextId + self.nextId += 1 + self.idMapping[mediaId] = id + self.reverseIdMapping[id] = mediaId + } + + items.append(State.Item( + id: StoryId(peerId: peerId, id: id), + storyItem: EngineStoryItem( + id: id, + timestamp: item.timestamp, + expirationTimestamp: Int32.max, + media: EngineMedia(item.media), + alternativeMedia: nil, + mediaAreas: [], + text: "", + entities: [], + views: nil, + privacy: nil, + isPinned: false, + isExpired: false, + isPublic: false, + isPending: false, + isCloseFriends: false, + isContacts: false, + isSelectedContacts: false, + isForwardingDisabled: false, + isEdited: false, + isMy: false, + myReaction: nil, + forwardInfo: nil, + author: nil + ), + peer: nil + )) + } + + for id in botPreview.alternativeLanguageCodes { + inner: for localization in localizationList.availableOfficialLocalizations { + if localization.languageCode == id { + availableLanguages.append(StoryListContextState.Language( + id: localization.languageCode, + name: localization.title + )) + break inner + } + } + } + } + + self.stateValue = State( + peerReference: (peer?._asPeer()).flatMap(PeerReference.init), + items: items, + availableLanguages: availableLanguages, + pinnedIds: [], + totalCount: items.count, + loadMoreToken: nil, + isCached: botPreview != nil, + hasCache: botPreview != nil, + allEntityFiles: [:], + isLoading: botPreview == nil + ) + }) + } + } + + deinit { + self.requestDisposable?.dispose() + self.updatesDisposable?.dispose() + self.eventsDisposable?.dispose() + self.reorderDisposable.dispose() + } + + func loadMore(completion: (() -> Void)?) { + } + + private func loadLanguage(language: String, assumeEmpty: Bool) { + let account = self.account + let peerId = self.peerId + let signal: Signal<(CachedUserData.BotPreview?, Peer?), NoError> = (self.account.postbox.transaction { transaction -> Peer? in + return transaction.getPeer(peerId) + } + |> mapToSignal { peer -> Signal<(CachedUserData.BotPreview?, Peer?), NoError> in + guard let peer, let inputUser = apiInputUser(peer) else { + return .single((nil, nil)) + } + return _internal_requestBotPreview(network: account.network, peerId: peerId, inputUser: inputUser, language: language) + |> map { botPreview in + return (botPreview, peer) + } + }) + + self.requestDisposable?.dispose() + self.requestDisposable = (signal + |> deliverOn(self.queue)).startStrict(next: { [weak self] botPreview, peer in + guard let self, let peer else { + return + } + + var items: [State.Item] = [] + + if let botPreview { + for item in botPreview.items { + guard let mediaId = item.media.id else { + continue + } + + let id: Int32 + if let current = self.idMapping[mediaId] { + id = current + } else { + id = self.nextId + self.nextId += 1 + self.idMapping[mediaId] = id + self.reverseIdMapping[id] = mediaId + } + + items.append(State.Item( + id: StoryId(peerId: peerId, id: id), + storyItem: EngineStoryItem( + id: id, + timestamp: item.timestamp, + expirationTimestamp: Int32.max, + media: EngineMedia(item.media), + alternativeMedia: nil, + mediaAreas: [], + text: "", + entities: [], + views: nil, + privacy: nil, + isPinned: false, + isExpired: false, + isPublic: false, + isPending: false, + isCloseFriends: false, + isContacts: false, + isSelectedContacts: false, + isForwardingDisabled: false, + isEdited: false, + isMy: false, + myReaction: nil, + forwardInfo: nil, + author: nil + ), + peer: nil + )) + } + } + + self.remoteItems = items + self.stateValue = State( + peerReference: PeerReference(peer), + items: items, + availableLanguages: [], + pinnedIds: [], + totalCount: items.count, + loadMoreToken: nil, + isCached: botPreview != nil, + hasCache: botPreview != nil, + allEntityFiles: [:], + isLoading: botPreview == nil + ) + + if botPreview != nil { + self.beginUpdates(language: language) + } + }) + } + + private func beginUpdates(language: String) { + let localStateKey: PostboxViewKey = .storiesState(key: .local) + + self.updatesDisposable?.dispose() + self.updatesDisposable = (self.account.postbox.combinedView(keys: [ + localStateKey + ]) + |> deliverOn(self.queue)).startStrict(next: { [weak self] combinedView in guard let self else { return } - let (peer, botPreview) = peerAndBotPreview - var items: [State.Item] = [] - if let stateView = combinedView.views[localStateKey] as? StoryStatesView, let localState = stateView.value?.get(Stories.LocalState.self) { for item in localState.items.reversed() { let mappedId: Int32 @@ -2142,7 +2432,7 @@ public final class BotPreviewStoryListContext: StoryListContext { self.nextId += 1 self.pendingIdMapping[item.stableId] = mappedId } - if case .botPreview(peerId) = item.target { + if case let .botPreview(itemPeerId, itemLanguage) = item.target, itemPeerId == self.peerId, itemLanguage == language { items.append(State.Item( id: StoryId(peerId: peerId, id: mappedId), storyItem: EngineStoryItem( @@ -2176,130 +2466,160 @@ public final class BotPreviewStoryListContext: StoryListContext { } } - if let botPreview { - for media in botPreview.media { - guard let mediaId = media.id else { - continue - } - - let id: Int32 - if let current = self.idMapping[mediaId] { - id = current - } else { - id = self.nextId - self.nextId += 1 - self.idMapping[mediaId] = id - self.reverseIdMapping[id] = mediaId - } - - items.append(State.Item( - id: StoryId(peerId: peerId, id: id), - storyItem: EngineStoryItem( - id: id, - timestamp: 0, - expirationTimestamp: Int32.max, - media: EngineMedia(media), - alternativeMedia: nil, - mediaAreas: [], - text: "", - entities: [], - views: nil, - privacy: nil, - isPinned: false, - isExpired: false, - isPublic: false, - isPending: false, - isCloseFriends: false, - isContacts: false, - isSelectedContacts: false, - isForwardingDisabled: false, - isEdited: false, - isMy: false, - myReaction: nil, - forwardInfo: nil, - author: nil - ), - peer: nil - )) + if self.localItems != items { + self.localItems = items + + if self.stateValue.peerReference != nil { + self.pushLanguageItems() } } - - self.stateValue = State( - peerReference: (peer?._asPeer()).flatMap(PeerReference.init), - items: items, - pinnedIds: [], - totalCount: items.count, - loadMoreToken: nil, - isCached: botPreview != nil, - hasCache: botPreview != nil, - allEntityFiles: [:], - isLoading: botPreview == nil - ) + }) + + self.eventsDisposable?.dispose() + self.eventsDisposable = (self.account.stateManager.botPreviewUpdates + |> deliverOn(self.queue)).startStrict(next: { [weak self] events in + guard let self else { + return + } + var remoteItems = self.remoteItems + for event in events { + switch event { + case let .added(peerId, language, item): + if let mediaId = item.media.id, self.peerId == peerId, self.language == language { + let id: Int32 + if let current = self.idMapping[mediaId] { + id = current + } else { + id = self.nextId + self.nextId += 1 + self.idMapping[mediaId] = id + self.reverseIdMapping[id] = mediaId + } + + let mappedItem = State.Item( + id: StoryId(peerId: peerId, id: id), + storyItem: EngineStoryItem( + id: id, + timestamp: item.timestamp, + expirationTimestamp: Int32.max, + media: EngineMedia(item.media), + alternativeMedia: nil, + mediaAreas: [], + text: "", + entities: [], + views: nil, + privacy: nil, + isPinned: false, + isExpired: false, + isPublic: false, + isPending: false, + isCloseFriends: false, + isContacts: false, + isSelectedContacts: false, + isForwardingDisabled: false, + isEdited: false, + isMy: false, + myReaction: nil, + forwardInfo: nil, + author: nil + ), + peer: nil + ) + + if let index = remoteItems.firstIndex(where: { $0.storyItem.media.id == item.media.id }) { + remoteItems[index] = mappedItem + } else { + remoteItems.insert(mappedItem, at: 0) + } + } + case let .deleted(peerId, language, ids): + if self.peerId == peerId && self.language == language { + remoteItems = remoteItems.filter { item in + guard let id = item.storyItem.media.id else { + return false + } + return !ids.contains(id) + } + } + } + } + if self.remoteItems != remoteItems { + self.remoteItems = remoteItems + self.pushLanguageItems() + } }) } - deinit { - self.requestDisposable?.dispose() - self.updatesDisposable?.dispose() - self.reorderDisposable.dispose() + private func pushLanguageItems() { + var items = self.localItems + items.append(contentsOf: self.remoteItems) + self.stateValue = State( + peerReference: self.stateValue.peerReference, + items: items, + availableLanguages: [], + pinnedIds: [], + totalCount: items.count, + loadMoreToken: nil, + isCached: true, + hasCache: true, + allEntityFiles: [:], + isLoading: false + ) } - func loadMore(completion: (() -> Void)?) { - } - - func reorderItems(ids: [StoryId]) { + func reorderItems(media: [Media]) { let peerId = self.peerId - let idMapping = self.idMapping - let reverseIdMapping = self.reverseIdMapping + let language = self.language let _ = (self.account.postbox.transaction({ transaction -> (Api.InputUser?, [Api.InputMedia]) in let inputUser = transaction.getPeer(peerId).flatMap(apiInputUser) var inputMedia: [Api.InputMedia] = [] - transaction.updatePeerCachedData(peerIds: Set([self.peerId]), update: { _, current in - guard var current = current as? CachedUserData else { + for item in media { + if let image = item as? TelegramMediaImage, let resource = image.representations.last?.resource as? CloudPhotoSizeMediaResource { + inputMedia.append(.inputMediaPhoto(flags: 0, id: .inputPhoto(id: resource.photoId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)), ttlSeconds: nil)) + inputMedia.append(Api.InputMedia.inputMediaPhoto(flags: 0, id: Api.InputPhoto.inputPhoto(id: resource.photoId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)), ttlSeconds: nil)) + } else if let file = item as? TelegramMediaFile, let resource = file.resource as? CloudDocumentMediaResource { + inputMedia.append(.inputMediaDocument(flags: 0, id: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())), ttlSeconds: nil, query: nil)) + } + } + + if language == nil { + transaction.updatePeerCachedData(peerIds: Set([self.peerId]), update: { _, current in + guard var current = current as? CachedUserData else { + return current + } + guard let currentBotPreview = current.botPreview else { + return current + } + + var items: [CachedUserData.BotPreview.Item] = [] + + var seenIds = Set() + for item in media { + guard let mediaId = item.id else { + continue + } + if let index = currentBotPreview.items.firstIndex(where: { $0.media.id == mediaId }) { + seenIds.insert(mediaId) + items.append(currentBotPreview.items[index]) + } + } + + for item in currentBotPreview.items { + guard let id = item.media.id else { + continue + } + if !seenIds.contains(id) { + items.append(item) + } + } + + let botPreview = CachedUserData.BotPreview(items: items, alternativeLanguageCodes: currentBotPreview.alternativeLanguageCodes) + current = current.withUpdatedBotPreview(botPreview) return current - } - guard let currentBotPreview = current.botPreview else { - return current - } - - var media: [Media] = [] - media = [] - - var seenIds = Set() - for id in ids { - guard let mediaId = reverseIdMapping[id.id] else { - continue - } - if let index = currentBotPreview.media.firstIndex(where: { $0.id == mediaId }) { - seenIds.insert(id.id) - media.append(currentBotPreview.media[index]) - } - } - - for item in currentBotPreview.media { - guard let id = item.id, let storyId = idMapping[id] else { - continue - } - if !seenIds.contains(storyId) { - media.append(item) - } - } - - for item in media { - if let image = item as? TelegramMediaImage, let resource = image.representations.last?.resource as? CloudPhotoSizeMediaResource { - inputMedia.append(.inputMediaPhoto(flags: 0, id: .inputPhoto(id: resource.photoId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)), ttlSeconds: nil)) - inputMedia.append(Api.InputMedia.inputMediaPhoto(flags: 0, id: Api.InputPhoto.inputPhoto(id: resource.photoId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)), ttlSeconds: nil)) - } else if let file = item as? TelegramMediaFile, let resource = file.resource as? CloudDocumentMediaResource { - inputMedia.append(.inputMediaDocument(flags: 0, id: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())), ttlSeconds: nil, query: nil)) - } - } - - let botPreview = CachedUserData.BotPreview(media: media) - current = current.withUpdatedBotPreview(botPreview) - return current - }) + }) + } return (inputUser, inputMedia) }) @@ -2307,7 +2627,37 @@ public final class BotPreviewStoryListContext: StoryListContext { guard let self, let inputUser else { return } - let signal = self.account.network.request(Api.functions.bots.reorderPreviewMedias(bot: inputUser, order: inputMedia)) + + if language != nil { + var updatedItems: [State.Item] = [] + + var seenIds = Set() + for item in media { + guard let mediaId = item.id else { + continue + } + if let index = self.remoteItems.firstIndex(where: { $0.storyItem.media.id == mediaId }) { + seenIds.insert(mediaId) + updatedItems.append(self.remoteItems[index]) + } + } + + for item in self.remoteItems { + guard let id = item.storyItem.media.id else { + continue + } + if !seenIds.contains(id) { + updatedItems.append(item) + } + } + + if self.remoteItems != updatedItems { + self.remoteItems = updatedItems + self.pushLanguageItems() + } + } + + let signal = self.account.network.request(Api.functions.bots.reorderPreviewMedias(bot: inputUser, langCode: language ?? "", order: inputMedia)) self.reorderDisposable.set(signal.startStrict()) }) } @@ -2322,11 +2672,15 @@ public final class BotPreviewStoryListContext: StoryListContext { private let queue: Queue private let impl: QueueLocalObject - public init(account: Account, engine: TelegramEngine, peerId: EnginePeer.Id) { + public let language: String? + + public init(account: Account, engine: TelegramEngine, peerId: EnginePeer.Id, language: String?, assumeEmpty: Bool) { + self.language = language + let queue = Queue.mainQueue() self.queue = queue self.impl = QueueLocalObject(queue: queue, generate: { - return Impl(queue: queue, account: account, engine: engine, peerId: peerId) + return Impl(queue: queue, account: account, engine: engine, peerId: peerId, language: language, assumeEmpty: assumeEmpty) }) } @@ -2336,9 +2690,9 @@ public final class BotPreviewStoryListContext: StoryListContext { } } - public func reorderItems(ids: [StoryId]) { + public func reorderItems(media: [Media]) { self.impl.with { impl in - impl.reorderItems(ids: ids) + impl.reorderItems(media: media) } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index 3343528dd1..d9927f1c2e 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -1373,8 +1373,12 @@ public extension TelegramEngine { return _internal_getStoryById(accountPeerId: self.account.peerId, postbox: self.account.postbox, network: self.account.network, peerId: peerId, id: id) } - public func deleteBotPreviews(peerId: EnginePeer.Id, ids: [MediaId]) -> Signal { - return _internal_deleteBotPreviews(account: self.account, peerId: peerId, ids: ids) + public func deleteBotPreviews(peerId: EnginePeer.Id, language: String?, media: [Media]) -> Signal { + return _internal_deleteBotPreviews(account: self.account, peerId: peerId, language: language, media: media) + } + + public func deleteBotPreviewsLanguage(peerId: EnginePeer.Id, language: String, media: [Media]) -> Signal { + return _internal_deleteBotPreviewsLanguage(account: self.account, peerId: peerId, language: language, media: media) } public func synchronouslyIsMessageDeletedInteractively(ids: [EngineMessage.Id]) -> [EngineMessage.Id] { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift index 186ab62cb4..4f09b015a3 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift @@ -199,16 +199,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee let botPreview: Signal if let user = maybePeer as? TelegramUser, let _ = user.botInfo { - botPreview = network.request(Api.functions.bots.getPreviewMedias(bot: inputUser)) - |> `catch` { _ -> Signal<[Api.MessageMedia], NoError> in - return .single([]) - } - |> map { result -> CachedUserData.BotPreview? in - return CachedUserData.BotPreview(media: result.compactMap { item -> Media? in - let value = textMediaAndExpirationTimerFromApiMedia(item, user.id) - return value.media - }) - } + botPreview = _internal_requestBotPreview(network: network, peerId: user.id, inputUser: inputUser, language: nil) } else { botPreview = .single(nil) } @@ -843,3 +834,33 @@ extension CachedPeerAutoremoveTimeout.Value { } } } + +func _internal_requestBotPreview(network: Network, peerId: PeerId, inputUser: Api.InputUser, language: String?) -> Signal { + return network.request(Api.functions.bots.getPreviewInfo(bot: inputUser, langCode: language ?? "")) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> map { result -> CachedUserData.BotPreview? in + guard let result else { + return nil + } + switch result { + case let .previewInfo(media, langCodes): + return CachedUserData.BotPreview( + items: media.compactMap { item -> CachedUserData.BotPreview.Item? in + switch item { + case let .botPreviewMedia(date, media): + let value = textMediaAndExpirationTimerFromApiMedia(media, peerId) + if let media = value.media { + return CachedUserData.BotPreview.Item(media: media, timestamp: date) + } else { + return nil + } + } + }, + alternativeLanguageCodes: langCodes + ) + } + } +} diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 3d2f70b061..675a306fea 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -458,6 +458,7 @@ swift_library( "//submodules/TelegramUI/Components/Chat/ChatSendStarsScreen", "//submodules/TelegramUI/Components/MinimizedContainer", "//submodules/TelegramUI/Components/SpaceWarpView", + "//submodules/TelegramUI/Components/MiniAppListScreen", ] + select({ "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "//build-system:ios_sim_arm64": [], diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift index a84afc3bc9..30c06596e2 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -1841,7 +1841,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } } } else if case .tap = gesture { - item.controllerInteraction.clickThroughMessage() + item.controllerInteraction.clickThroughMessage(self.view, location) } else if case .doubleTap = gesture { if canAddMessageReactions(message: item.message) { item.controllerInteraction.updateMessageReaction(item.message, .default, false, nil) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index f8352540e1..afb7056b82 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -4576,7 +4576,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } } } else if case .tap = gesture { - item.controllerInteraction.clickThroughMessage() + item.controllerInteraction.clickThroughMessage(self.view, location) } else if case .doubleTap = gesture { if canAddMessageReactions(message: item.message) { item.controllerInteraction.updateMessageReaction(item.message, .default, false, nil) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift index 59b4284e66..f9c0c49728 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift @@ -961,7 +961,7 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, ASGestureReco break } } else if case .tap = gesture { - self.item?.controllerInteraction.clickThroughMessage() + self.item?.controllerInteraction.clickThroughMessage(self.view, location) } } default: diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift index d119a50ed4..5dcd3a1f81 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift @@ -1586,7 +1586,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { return } - self.item?.controllerInteraction.clickThroughMessage() + self.item?.controllerInteraction.clickThroughMessage(self.view, location) case .longTap, .doubleTap, .secondaryTap: break case .hold: diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift index 9db9e5f4ba..ae7e32a202 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift @@ -1405,7 +1405,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { } } } else if case .tap = gesture { - self.item?.controllerInteraction.clickThroughMessage() + self.item?.controllerInteraction.clickThroughMessage(self.view, location) } } default: diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift index 80da5266a9..b07d025dfc 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift @@ -306,7 +306,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { if let context = self?.context, let navigationController = self?.getNavigationController() { let _ = context.sharedContext.navigateToForumThread(context: context, peerId: peerId, threadId: threadId, messageId: nil, navigationController: navigationController, activateInput: nil, scrollToEndIfExists: false, keepStack: .always).startStandalone() } - }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _, _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _, _ in return false + }, tapMessage: nil, clickThroughMessage: { _, _ in }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _, _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _, _ in return false }, requestMessageActionCallback: { [weak self] messageId, _, _, _ in guard let self else { return diff --git a/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift b/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift index 5cd1237b3f..e07264e9c3 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift @@ -418,7 +418,7 @@ public final class ChatSendGroupMediaMessageContextPreview: UIView, ChatSendMess }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _, _ in }, navigateToMessageStandalone: { _ in }, navigateToThreadMessage: { _, _, _ in }, tapMessage: { _ in - }, clickThroughMessage: { + }, clickThroughMessage: { _, _ in }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _, _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _, _ in }, openUrl: { _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in diff --git a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift index 4e783c6ef1..3fd747901f 100644 --- a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift @@ -180,7 +180,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol public let navigateToMessageStandalone: (MessageId) -> Void public let navigateToThreadMessage: (PeerId, Int64, MessageId?) -> Void public let tapMessage: ((Message) -> Void)? - public let clickThroughMessage: () -> Void + public let clickThroughMessage: (UIView?, CGPoint?) -> Void public let toggleMessagesSelection: ([MessageId], Bool) -> Void public let sendCurrentMessage: (Bool, ChatSendMessageEffect?) -> Void public let sendMessage: (String) -> Void @@ -309,7 +309,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol navigateToMessageStandalone: @escaping (MessageId) -> Void, navigateToThreadMessage: @escaping (PeerId, Int64, MessageId?) -> Void, tapMessage: ((Message) -> Void)?, - clickThroughMessage: @escaping () -> Void, + clickThroughMessage: @escaping (UIView?, CGPoint?) -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendCurrentMessage: @escaping (Bool, ChatSendMessageEffect?) -> Void, sendMessage: @escaping (String) -> Void, diff --git a/submodules/TelegramUI/Components/EmptyStateIndicatorComponent/Sources/EmptyStateIndicatorComponent.swift b/submodules/TelegramUI/Components/EmptyStateIndicatorComponent/Sources/EmptyStateIndicatorComponent.swift index 47357705fc..38bc8689a4 100644 --- a/submodules/TelegramUI/Components/EmptyStateIndicatorComponent/Sources/EmptyStateIndicatorComponent.swift +++ b/submodules/TelegramUI/Components/EmptyStateIndicatorComponent/Sources/EmptyStateIndicatorComponent.swift @@ -13,25 +13,27 @@ public final class EmptyStateIndicatorComponent: Component { public let context: AccountContext public let theme: PresentationTheme public let animationName: String? - public let title: String + public let title: String? public let text: String public let actionTitle: String? public let fitToHeight: Bool public let action: () -> Void public let additionalActionTitle: String? public let additionalAction: () -> Void + public let additionalActionSeparator: String? public init( context: AccountContext, theme: PresentationTheme, fitToHeight: Bool, animationName: String?, - title: String, + title: String?, text: String, actionTitle: String?, action: @escaping () -> Void, additionalActionTitle: String?, - additionalAction: @escaping () -> Void + additionalAction: @escaping () -> Void, + additionalActionSeparator: String? = nil ) { self.context = context self.theme = theme @@ -43,6 +45,7 @@ public final class EmptyStateIndicatorComponent: Component { self.action = action self.additionalActionTitle = additionalActionTitle self.additionalAction = additionalAction + self.additionalActionSeparator = additionalActionSeparator } public static func ==(lhs: EmptyStateIndicatorComponent, rhs: EmptyStateIndicatorComponent) -> Bool { @@ -70,6 +73,9 @@ public final class EmptyStateIndicatorComponent: Component { if lhs.additionalActionTitle != rhs.additionalActionTitle { return false } + if lhs.additionalActionSeparator != rhs.additionalActionSeparator { + return false + } return true } @@ -82,6 +88,9 @@ public final class EmptyStateIndicatorComponent: Component { private let text = ComponentView() private var button: ComponentView? private var additionalButton: ComponentView? + private var additionalSeparatorLeft: SimpleLayer? + private var additionalSeparatorRight: SimpleLayer? + private var additionalSeparatorText: ComponentView? override public init(frame: CGRect) { super.init(frame: frame) @@ -108,16 +117,20 @@ public final class EmptyStateIndicatorComponent: Component { containerSize: CGSize(width: 120.0, height: 120.0) ) } - let titleSize = self.title.update( - transition: .immediate, - component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: component.title, font: Font.semibold(17.0), textColor: component.theme.list.itemPrimaryTextColor)), - horizontalAlignment: .center, - maximumNumberOfLines: 0 - )), - environment: {}, - containerSize: CGSize(width: min(300.0, availableSize.width - 16.0 * 2.0), height: 1000.0) - ) + + var titleSize: CGSize? + if let title = component.title { + titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: title, font: Font.semibold(17.0), textColor: component.theme.list.itemPrimaryTextColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 0 + )), + environment: {}, + containerSize: CGSize(width: min(300.0, availableSize.width - 16.0 * 2.0), height: 1000.0) + ) + } let textSize = self.text.update( transition: .immediate, component: AnyComponent(BalancedTextComponent( @@ -203,19 +216,80 @@ public final class EmptyStateIndicatorComponent: Component { } } + var additionalSeparatorTextSize: CGSize? + if let additionalActionSeparator = component.additionalActionSeparator { + let additionalSeparatorText: ComponentView + if let current = self.additionalSeparatorText { + additionalSeparatorText = current + } else { + additionalSeparatorText = ComponentView() + self.additionalSeparatorText = additionalSeparatorText + } + + let additionalSeparatorLeft: SimpleLayer + if let current = self.additionalSeparatorLeft { + additionalSeparatorLeft = current + } else { + additionalSeparatorLeft = SimpleLayer() + self.additionalSeparatorLeft = additionalSeparatorLeft + self.layer.addSublayer(additionalSeparatorLeft) + } + + let additionalSeparatorRight: SimpleLayer + if let current = self.additionalSeparatorRight { + additionalSeparatorRight = current + } else { + additionalSeparatorRight = SimpleLayer() + self.additionalSeparatorRight = additionalSeparatorRight + self.layer.addSublayer(additionalSeparatorRight) + } + + additionalSeparatorLeft.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor + additionalSeparatorRight.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor + + additionalSeparatorTextSize = additionalSeparatorText.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: additionalActionSeparator, font: Font.regular(15.0), textColor: component.theme.list.itemSecondaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: min(300.0, availableSize.width - 16.0 * 2.0), height: 100.0) + ) + } else { + if let additionalSeparatorLeft = self.additionalSeparatorLeft { + self.additionalSeparatorLeft = nil + additionalSeparatorLeft.removeFromSuperlayer() + } + if let additionalSeparatorRight = self.additionalSeparatorRight { + self.additionalSeparatorRight = nil + additionalSeparatorRight.removeFromSuperlayer() + } + if let additionalSeparatorText = self.additionalSeparatorText { + self.additionalSeparatorText = nil + additionalSeparatorText.view?.removeFromSuperview() + } + } + let animationSpacing: CGFloat = 11.0 let titleSpacing: CGFloat = 17.0 let buttonSpacing: CGFloat = 21.0 + let additionalSeparatorHeight: CGFloat = 31.0 var totalHeight: CGFloat = 0.0 if let animationSize { totalHeight += animationSize.height + animationSpacing } - totalHeight += titleSize.height + titleSpacing + textSize.height + if let titleSize { + totalHeight += titleSize.height + titleSpacing + } + totalHeight += textSize.height if let buttonSize { totalHeight += buttonSpacing + buttonSize.height } + if let _ = additionalSeparatorTextSize { + totalHeight += additionalSeparatorHeight + } if let additionalButtonSize { totalHeight += buttonSpacing + additionalButtonSize.height } @@ -234,7 +308,7 @@ public final class EmptyStateIndicatorComponent: Component { transition.setFrame(view: animationView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - animationSize.width) * 0.5), y: contentY), size: animationSize)) contentY += animationSize.height + animationSpacing } - if let titleView = self.title.view { + if let titleSize, let titleView = self.title.view { if titleView.superview == nil { self.addSubview(titleView) } @@ -255,6 +329,25 @@ public final class EmptyStateIndicatorComponent: Component { transition.setFrame(view: buttonView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - buttonSize.width) * 0.5), y: contentY), size: buttonSize)) contentY += buttonSize.height + buttonSpacing } + + if let additionalSeparatorTextSize, let additionalSeparatorText = self.additionalSeparatorText, let additionalSeparatorLeft = self.additionalSeparatorLeft, let additionalSeparatorRight = self.additionalSeparatorRight { + let additionalSeparatorTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - additionalSeparatorTextSize.width) * 0.5), y: contentY), size: additionalSeparatorTextSize) + if let additionalSeparatorTextView = additionalSeparatorText.view { + if additionalSeparatorTextView.superview == nil { + self.addSubview(additionalSeparatorTextView) + } + transition.setFrame(view: additionalSeparatorTextView, frame: additionalSeparatorTextFrame) + } + + let separatorWidth: CGFloat = 72.0 + let separatorSpacing: CGFloat = 10.0 + + transition.setFrame(layer: additionalSeparatorLeft, frame: CGRect(origin: CGPoint(x: additionalSeparatorTextFrame.minX - separatorSpacing - separatorWidth, y: additionalSeparatorTextFrame.midY + 1.0), size: CGSize(width: separatorWidth, height: UIScreenPixel))) + transition.setFrame(layer: additionalSeparatorRight, frame: CGRect(origin: CGPoint(x: additionalSeparatorTextFrame.maxX + separatorSpacing, y: additionalSeparatorTextFrame.midY + 1.0), size: CGSize(width: separatorWidth, height: UIScreenPixel))) + + contentY += additionalSeparatorHeight + } + if let additionalButtonSize, let additionalButtonView = self.additionalButton?.view { if additionalButtonView.superview == nil { self.addSubview(additionalButtonView) diff --git a/submodules/TelegramUI/Components/MiniAppListScreen/BUILD b/submodules/TelegramUI/Components/MiniAppListScreen/BUILD new file mode 100644 index 0000000000..02745a260c --- /dev/null +++ b/submodules/TelegramUI/Components/MiniAppListScreen/BUILD @@ -0,0 +1,39 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "MiniAppListScreen", + module_name = "MiniAppListScreen", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/AsyncDisplayKit", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/TelegramPresentationData", + "//submodules/TelegramUIPreferences", + "//submodules/PresentationDataUtils", + "//submodules/AccountContext", + "//submodules/ComponentFlow", + "//submodules/Components/ViewControllerComponent", + "//submodules/MergeLists", + "//submodules/Components/ComponentDisplayAdapters", + "//submodules/ItemListUI", + "//submodules/ChatListUI", + "//submodules/ItemListPeerItem", + "//submodules/TelegramUI/Components/ChatListHeaderComponent", + "//submodules/TelegramUI/Components/PlainButtonComponent", + "//submodules/Components/MultilineTextComponent", + "//submodules/SearchBarNode", + "//submodules/Components/BalancedTextComponent", + "//submodules/ChatListSearchItemHeader", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/MiniAppListScreen/Sources/MiniAppListScreen.swift b/submodules/TelegramUI/Components/MiniAppListScreen/Sources/MiniAppListScreen.swift new file mode 100644 index 0000000000..08ecaa53f2 --- /dev/null +++ b/submodules/TelegramUI/Components/MiniAppListScreen/Sources/MiniAppListScreen.swift @@ -0,0 +1,811 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import Postbox +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import PresentationDataUtils +import AccountContext +import ComponentFlow +import ViewControllerComponent +import MergeLists +import ComponentDisplayAdapters +import ItemListPeerItem +import ItemListUI +import ChatListHeaderComponent +import PlainButtonComponent +import MultilineTextComponent +import SearchBarNode +import BalancedTextComponent +import ChatListSearchItemHeader + +final class MiniAppListScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let initialData: MiniAppListScreen.InitialData + + init( + context: AccountContext, + initialData: MiniAppListScreen.InitialData + ) { + self.context = context + self.initialData = initialData + } + + static func ==(lhs: MiniAppListScreenComponent, rhs: MiniAppListScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + + return true + } + + private enum ContentEntry: Comparable, Identifiable { + enum Id: Hashable { + case item(EnginePeer.Id) + } + + var stableId: Id { + switch self { + case let .item(peer, _): + return .item(peer.id) + } + } + + case item(peer: EnginePeer, sortIndex: Int) + + static func <(lhs: ContentEntry, rhs: ContentEntry) -> Bool { + switch lhs { + case let .item(lhsPeer, lhsSortIndex): + switch rhs { + case let .item(rhsPeer, rhsSortIndex): + if lhsSortIndex != rhsSortIndex { + return lhsSortIndex < rhsSortIndex + } + return lhsPeer.id < rhsPeer.id + } + } + } + + func item(listNode: ContentListNode) -> ListViewItem { + switch self { + case let .item(peer, _): + let text: ItemListPeerItemText + if case let .user(user) = peer, let subscriberCount = user.subscriberCount { + text = .text(listNode.presentationData.strings.Conversation_StatusBotSubscribers(subscriberCount), .secondary) + } else { + text = .none + } + + return ItemListPeerItem( + presentationData: ItemListPresentationData(listNode.presentationData), + dateTimeFormat: listNode.presentationData.dateTimeFormat, + nameDisplayOrder: listNode.presentationData.nameDisplayOrder, + context: listNode.context, + peer: peer, + presence: nil, + text: text, + label: .none, + editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: nil), + enabled: true, + selectable: true, + sectionId: 0, + action: { [weak listNode] in + guard let listNode else { + return + } + if let view = listNode.parentView { + view.openItem(peer: peer) + } + }, + setPeerIdWithRevealedOptions: { _, _ in + }, + removePeer: { _ in + }, + noInsets: true, + header: nil + ) + } + } + } + + private final class ContentListNode: ListView { + weak var parentView: View? + let context: AccountContext + var presentationData: PresentationData + private var currentEntries: [ContentEntry] = [] + private var originalEntries: [ContentEntry] = [] + + init(parentView: View, context: AccountContext) { + self.parentView = parentView + self.context = context + self.presentationData = context.sharedContext.currentPresentationData.with({ $0 }) + + super.init() + } + + func update(size: CGSize, insets: UIEdgeInsets, transition: ComponentTransition) { + let (listViewDuration, listViewCurve) = listViewAnimationDurationAndCurve(transition: transition.containedViewLayoutTransition) + self.transaction( + deleteIndices: [], + insertIndicesAndItems: [], + updateIndicesAndItems: [], + options: [.Synchronous, .LowLatency, .PreferSynchronousResourceLoading], + additionalScrollDistance: 0.0, + updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: insets, duration: listViewDuration, curve: listViewCurve), + updateOpaqueState: nil + ) + } + + func setEntries(entries: [ContentEntry], animated: Bool) { + self.originalEntries = entries + + let entries = entries + + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: self.currentEntries, rightList: entries) + self.currentEntries = entries + + let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(listNode: self), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(listNode: self), directionHint: nil) } + + var options: ListViewDeleteAndInsertOptions = [.Synchronous, .LowLatency] + if animated { + options.insert(.AnimateInsertion) + } else { + options.insert(.PreferSynchronousResourceLoading) + } + + self.transaction( + deleteIndices: deletions, + insertIndicesAndItems: insertions, + updateIndicesAndItems: updates, + options: options, + scrollToItem: nil, + stationaryItemRange: nil, + updateOpaqueState: nil, + completion: { _ in + } + ) + } + } + + final class View: UIView { + private var contentListNode: ContentListNode? + private var ignoreVisibleContentOffsetChanged: Bool = false + private var emptySearchState: ComponentView? + + private let navigationBarView = ComponentView() + private var navigationHeight: CGFloat? + + private let sectionHeader = ComponentView() + + private var searchBarNode: SearchBarNode? + + private var isUpdating: Bool = false + + private var component: MiniAppListScreenComponent? + private(set) weak var state: EmptyComponentState? + private var environment: EnvironmentType? + + private var recommendedAppPeers: [EnginePeer]? + private var recommendedAppPeersDisposable: Disposable? + private var keepUpdatedDisposable: Disposable? + + private var isSearchDisplayControllerActive: Bool = false + private var searchQuery: String = "" + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.recommendedAppPeersDisposable?.dispose() + self.keepUpdatedDisposable?.dispose() + } + + func scrollToTop() { + } + + func attemptNavigation(complete: @escaping () -> Void) -> Bool { + return true + } + + func openItem(peer: EnginePeer) { + guard let component = self.component else { + return + } + guard let environment = self.environment, let controller = environment.controller() else { + return + } + + if let peerInfoScreen = component.context.sharedContext.makePeerInfoController(context: component.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + peerInfoScreen.navigationPresentation = .modal + controller.push(peerInfoScreen) + } + } + + private func updateNavigationBar( + component: MiniAppListScreenComponent, + theme: PresentationTheme, + strings: PresentationStrings, + size: CGSize, + insets: UIEdgeInsets, + statusBarHeight: CGFloat, + isModal: Bool, + transition: ComponentTransition, + deferScrollApplication: Bool + ) -> CGFloat { + let rightButtons: [AnyComponentWithIdentity] = [] + + //TODO:localize + let titleText: String = "Examples" + + let closeTitle: String = strings.Common_Close + let headerContent: ChatListHeaderComponent.Content? = ChatListHeaderComponent.Content( + title: titleText, + navigationBackTitle: nil, + titleComponent: nil, + chatListTitle: nil, + leftButton: isModal ? AnyComponentWithIdentity(id: "close", component: AnyComponent(NavigationButtonComponent( + content: .text(title: closeTitle, isBold: false), + pressed: { [weak self] _ in + guard let self else { + return + } + if self.attemptNavigation(complete: {}) { + self.environment?.controller()?.dismiss() + } + } + ))) : nil, + rightButtons: rightButtons, + backTitle: isModal ? nil : strings.Common_Back, + backPressed: { [weak self] in + guard let self else { + return + } + + if self.attemptNavigation(complete: {}) { + self.environment?.controller()?.dismiss() + } + } + ) + + let navigationBarSize = self.navigationBarView.update( + transition: transition, + component: AnyComponent(ChatListNavigationBar( + context: component.context, + theme: theme, + strings: strings, + statusBarHeight: statusBarHeight, + sideInset: insets.left, + isSearchActive: self.isSearchDisplayControllerActive, + isSearchEnabled: true, + primaryContent: headerContent, + secondaryContent: nil, + secondaryTransition: 0.0, + storySubscriptions: nil, + storiesIncludeHidden: false, + uploadProgress: [:], + tabsNode: nil, + tabsNodeIsSearch: false, + accessoryPanelContainer: nil, + accessoryPanelContainerHeight: 0.0, + activateSearch: { [weak self] _ in + guard let self else { + return + } + + self.isSearchDisplayControllerActive = true + self.state?.updated(transition: .spring(duration: 0.4)) + }, + openStatusSetup: { _ in + }, + allowAutomaticOrder: { + } + )), + environment: {}, + containerSize: size + ) + + //TODO:localize + let sectionHeaderSize = self.sectionHeader.update( + transition: transition, + component: AnyComponent(ListHeaderComponent( + theme: theme, + title: "APPS THAT ACCEPT STARS" + )), + environment: {}, + containerSize: CGSize(width: size.width, height: 1000.0) + ) + if let sectionHeaderView = self.sectionHeader.view { + if sectionHeaderView.superview == nil { + sectionHeaderView.layer.anchorPoint = CGPoint() + self.addSubview(sectionHeaderView) + } + transition.setBounds(view: sectionHeaderView, bounds: CGRect(origin: CGPoint(), size: sectionHeaderSize)) + } + + if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View { + if deferScrollApplication { + navigationBarComponentView.deferScrollApplication = true + } + + if navigationBarComponentView.superview == nil { + self.addSubview(navigationBarComponentView) + } + transition.setFrame(view: navigationBarComponentView, frame: CGRect(origin: CGPoint(), size: navigationBarSize)) + + return navigationBarSize.height + } else { + return 0.0 + } + } + + private func updateNavigationScrolling(navigationHeight: CGFloat, transition: ComponentTransition) { + var mainOffset: CGFloat + if let recommendedAppPeers = self.recommendedAppPeers, !recommendedAppPeers.isEmpty { + if let contentListNode = self.contentListNode { + switch contentListNode.visibleContentOffset() { + case .none: + mainOffset = 0.0 + case .unknown: + mainOffset = navigationHeight + case let .known(value): + mainOffset = value + } + } else { + mainOffset = navigationHeight + } + } else { + mainOffset = navigationHeight + } + + mainOffset = min(mainOffset, ChatListNavigationBar.searchScrollHeight) + if abs(mainOffset) < 0.1 { + mainOffset = 0.0 + } + + let resultingOffset = mainOffset + + var offset = resultingOffset + if self.isSearchDisplayControllerActive { + offset = 0.0 + } + + if let sectionHeaderView = self.sectionHeader.view { + transition.setPosition(view: sectionHeaderView, position: CGPoint(x: 0.0, y: navigationHeight - offset)) + } + + if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View { + navigationBarComponentView.applyScroll(offset: offset, allowAvatarsExpansion: false, forceUpdate: false, transition: transition.withUserData(ChatListNavigationBar.AnimationHint( + disableStoriesAnimations: false, + crossfadeStoryPeers: false + ))) + } + } + + func update(component: MiniAppListScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + if self.component == nil { + self.recommendedAppPeers = component.initialData.recommendedAppPeers + + /*self.shortcutMessageListDisposable = (component.context.engine.accountData.shortcutMessageList(onlyRemote: false) + |> deliverOnMainQueue).startStrict(next: { [weak self] shortcutMessageList in + guard let self else { + return + } + self.shortcutMessageList = shortcutMessageList + if !self.isUpdating { + self.state?.updated(transition: .immediate) + } + })*/ + + self.keepUpdatedDisposable = component.context.engine.peers.requestRecommendedAppsIfNeeded().startStrict() + } + + let environment = environment[EnvironmentType.self].value + let themeUpdated = self.environment?.theme !== environment.theme + self.environment = environment + + self.component = component + self.state = state + + if themeUpdated { + self.backgroundColor = environment.theme.list.plainBackgroundColor + } + + var isModal = false + if let controller = environment.controller(), controller.navigationPresentation == .modal { + isModal = true + } + + var statusBarHeight = environment.statusBarHeight + if isModal { + statusBarHeight = max(statusBarHeight, 1.0) + } + + let listBottomInset = environment.safeInsets.bottom + environment.additionalInsets.bottom + let navigationHeight = self.updateNavigationBar( + component: component, + theme: environment.theme, + strings: environment.strings, + size: availableSize, + insets: environment.safeInsets, + statusBarHeight: statusBarHeight, + isModal: isModal, + transition: transition, + deferScrollApplication: true + ) + self.navigationHeight = navigationHeight + + var removedSearchBar: SearchBarNode? + if self.isSearchDisplayControllerActive { + let searchBarNode: SearchBarNode + var searchBarTransition = transition + if let current = self.searchBarNode { + searchBarNode = current + } else { + searchBarTransition = .immediate + let searchBarTheme = SearchBarNodeTheme(theme: environment.theme, hasSeparator: false) + searchBarNode = SearchBarNode( + theme: searchBarTheme, + strings: environment.strings, + fieldStyle: .modern, + displayBackground: false + ) + searchBarNode.placeholderString = NSAttributedString(string: environment.strings.Common_Search, font: Font.regular(17.0), textColor: searchBarTheme.placeholder) + self.searchBarNode = searchBarNode + searchBarNode.cancel = { [weak self] in + guard let self else { + return + } + self.isSearchDisplayControllerActive = false + self.state?.updated(transition: .spring(duration: 0.4)) + } + searchBarNode.textUpdated = { [weak self] query, _ in + guard let self else { + return + } + if self.searchQuery != query { + self.searchQuery = query.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) + self.state?.updated(transition: .immediate) + } + } + DispatchQueue.main.async { [weak self, weak searchBarNode] in + guard let self, let searchBarNode, self.searchBarNode === searchBarNode else { + return + } + searchBarNode.activate() + } + } + + var searchBarFrame = CGRect(origin: CGPoint(x: 0.0, y: navigationHeight - 54.0 + 2.0), size: CGSize(width: availableSize.width, height: 54.0)) + if isModal { + searchBarFrame.origin.y += 2.0 + } + searchBarNode.updateLayout(boundingSize: searchBarFrame.size, leftInset: environment.safeInsets.left + 6.0, rightInset: environment.safeInsets.right, transition: searchBarTransition.containedViewLayoutTransition) + searchBarTransition.setFrame(view: searchBarNode.view, frame: searchBarFrame) + if searchBarNode.view.superview == nil { + self.addSubview(searchBarNode.view) + + if case let .curve(duration, curve) = transition.animation, let navigationBarView = self.navigationBarView.view as? ChatListNavigationBar.View, let placeholderNode = navigationBarView.searchContentNode?.placeholderNode { + let timingFunction: String + switch curve { + case .easeInOut: + timingFunction = CAMediaTimingFunctionName.easeOut.rawValue + case .linear: + timingFunction = CAMediaTimingFunctionName.linear.rawValue + case .spring: + timingFunction = kCAMediaTimingFunctionSpring + case .custom: + timingFunction = kCAMediaTimingFunctionSpring + } + + searchBarNode.animateIn(from: placeholderNode, duration: duration, timingFunction: timingFunction) + } + } + } else { + self.searchQuery = "" + if let searchBarNode = self.searchBarNode { + self.searchBarNode = nil + removedSearchBar = searchBarNode + } + } + + let contentListNode: ContentListNode + if let current = self.contentListNode { + contentListNode = current + } else { + contentListNode = ContentListNode(parentView: self, context: component.context) + self.contentListNode = contentListNode + + contentListNode.visibleContentOffsetChanged = { [weak self] offset in + guard let self else { + return + } + guard let navigationHeight = self.navigationHeight else { + return + } + if self.ignoreVisibleContentOffsetChanged { + return + } + self.updateNavigationScrolling(navigationHeight: navigationHeight, transition: .immediate) + } + + if let sectionHeaderView = self.sectionHeader.view { + self.insertSubview(contentListNode.view, belowSubview: sectionHeaderView) + } else if let navigationBarComponentView = self.navigationBarView.view { + self.insertSubview(contentListNode.view, belowSubview: navigationBarComponentView) + } else { + self.addSubview(contentListNode.view) + } + } + + var contentTopInset = navigationHeight + if let sectionHeaderView = self.sectionHeader.view { + contentTopInset += sectionHeaderView.bounds.height + } + transition.setFrame(view: contentListNode.view, frame: CGRect(origin: CGPoint(), size: availableSize)) + self.ignoreVisibleContentOffsetChanged = true + contentListNode.update(size: availableSize, insets: UIEdgeInsets(top: contentTopInset, left: environment.safeInsets.left, bottom: listBottomInset, right: environment.safeInsets.right), transition: transition) + self.ignoreVisibleContentOffsetChanged = false + + var entries: [ContentEntry] = [] + if let recommendedAppPeers = self.recommendedAppPeers { + let normalizedSearchQuery = self.searchQuery.lowercased().trimmingTrailingSpaces() + for peer in recommendedAppPeers { + if !self.searchQuery.isEmpty { + var matches = false + if peer.indexName.matchesByTokens(normalizedSearchQuery) { + matches = true + } + if !matches { + continue + } + } + entries.append(.item(peer: peer, sortIndex: entries.count)) + } + } + contentListNode.setEntries(entries: entries, animated: !transition.animation.isImmediate) + if let sectionHeaderView = self.sectionHeader.view { + sectionHeaderView.isHidden = entries.isEmpty + } + + if !self.searchQuery.isEmpty && entries.isEmpty { + var emptySearchStateTransition = transition + let emptySearchState: ComponentView + if let current = self.emptySearchState { + emptySearchState = current + } else { + emptySearchStateTransition = emptySearchStateTransition.withAnimation(.none) + emptySearchState = ComponentView() + self.emptySearchState = emptySearchState + } + let emptySearchStateSize = emptySearchState.update( + transition: .immediate, + component: AnyComponent(BalancedTextComponent( + text: .plain(NSAttributedString(string: environment.strings.Conversation_SearchNoResults, font: Font.regular(17.0), textColor: environment.theme.list.freeTextColor, paragraphAlignment: .center)), + horizontalAlignment: .center, + maximumNumberOfLines: 0 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - 16.0 * 2.0, height: availableSize.height) + ) + var emptySearchStateBottomInset = listBottomInset + emptySearchStateBottomInset = max(emptySearchStateBottomInset, environment.inputHeight) + let emptySearchStateFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - emptySearchStateSize.width) * 0.5), y: navigationHeight + floor((availableSize.height - emptySearchStateBottomInset - navigationHeight) * 0.5)), size: emptySearchStateSize) + if let emptySearchStateView = emptySearchState.view { + if emptySearchStateView.superview == nil { + if let navigationBarComponentView = self.navigationBarView.view { + self.insertSubview(emptySearchStateView, belowSubview: navigationBarComponentView) + } else { + self.addSubview(emptySearchStateView) + } + } + emptySearchStateTransition.containedViewLayoutTransition.updatePosition(layer: emptySearchStateView.layer, position: emptySearchStateFrame.center) + emptySearchStateView.bounds = CGRect(origin: CGPoint(), size: emptySearchStateFrame.size) + } + } else if let emptySearchState = self.emptySearchState { + self.emptySearchState = nil + emptySearchState.view?.removeFromSuperview() + } + + if let recommendedAppPeers = self.recommendedAppPeers, !recommendedAppPeers.isEmpty { + contentListNode.isHidden = false + } else { + contentListNode.isHidden = true + } + + self.updateNavigationScrolling(navigationHeight: navigationHeight, transition: transition) + + if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View { + navigationBarComponentView.deferScrollApplication = false + navigationBarComponentView.applyCurrentScroll(transition: transition) + } + + if let removedSearchBar { + if !transition.animation.isImmediate, let navigationBarView = self.navigationBarView.view as? ChatListNavigationBar.View, let placeholderNode = + navigationBarView.searchContentNode?.placeholderNode { + removedSearchBar.transitionOut(to: placeholderNode, transition: transition.containedViewLayoutTransition, completion: { [weak removedSearchBar] in + removedSearchBar?.view.removeFromSuperview() + }) + } else { + removedSearchBar.view.removeFromSuperview() + } + } + + return availableSize + } + } + + func makeView() -> View { + return View() + } + + 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) + } +} + +public final class MiniAppListScreen: ViewControllerComponentContainer { + public final class InitialData: MiniAppListScreenInitialData { + let recommendedAppPeers: [EnginePeer] + + init( + recommendedAppPeers: [EnginePeer] + ) { + self.recommendedAppPeers = recommendedAppPeers + } + } + + private let context: AccountContext + + public init(context: AccountContext, initialData: InitialData) { + self.context = context + + super.init(context: context, component: MiniAppListScreenComponent( + context: context, + initialData: initialData + ), navigationBarAppearance: .none, theme: .default, updatedPresentationData: nil) + + self.navigationPresentation = .modal + + self.scrollToTop = { [weak self] in + guard let self, let componentView = self.node.hostView.componentView as? MiniAppListScreenComponent.View else { + return + } + componentView.scrollToTop() + } + + self.attemptNavigation = { [weak self] complete in + guard let self, let componentView = self.node.hostView.componentView as? MiniAppListScreenComponent.View else { + return true + } + + return componentView.attemptNavigation(complete: complete) + } + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + @objc private func cancelPressed() { + self.dismiss() + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + } + + public static func initialData(context: AccountContext) -> Signal { + let recommendedAppPeers = context.engine.peers.recommendedAppPeerIds() + |> take(1) + |> mapToSignal { peerIds -> Signal<[EnginePeer], NoError> in + guard let peerIds else { + return .single([]) + } + return context.engine.data.get( + EngineDataList(peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))) + ) + |> map { peers -> [EnginePeer] in + return peers.compactMap { $0 } + } + } + + return recommendedAppPeers + |> map { recommendedAppPeers -> MiniAppListScreenInitialData in + return InitialData( + recommendedAppPeers: recommendedAppPeers + ) + } + } +} + +private final class ListHeaderComponent: Component { + let theme: PresentationTheme + let title: String + + init( + theme: PresentationTheme, + title: String + ) { + self.theme = theme + self.title = title + } + + static func ==(lhs: ListHeaderComponent, rhs: ListHeaderComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.title != rhs.title { + return false + } + return true + } + + final class View: UIView { + private let title = ComponentView() + + private var component: ListHeaderComponent? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: ListHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + if self.component?.theme !== component.theme { + self.backgroundColor = component.theme.chatList.sectionHeaderFillColor + } + + let insets = UIEdgeInsets(top: 7.0, left: 16.0, bottom: 7.0, right: 16.0) + + let titleString = component.title + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: titleString, font: Font.regular(13.0), textColor: component.theme.chatList.sectionHeaderTextColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - insets.left - insets.right, height: 100.0) + ) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + titleView.frame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: titleSize) + } + + return CGSize(width: availableSize.width, height: titleSize.height + insets.top + insets.bottom) + } + } + + 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/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift index 6dd583ca77..5ad6ea1af0 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift @@ -1148,7 +1148,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen var botPreviewStoryListContext: StoryListContext? let hasBotPreviewItems: Signal if case .bot = kind { - let botPreviewStoryListContextValue = BotPreviewStoryListContext(account: context.account, engine: context.engine, peerId: peerId) + let botPreviewStoryListContextValue = BotPreviewStoryListContext(account: context.account, engine: context.engine, peerId: peerId, language: nil, assumeEmpty: false) botPreviewStoryListContext = botPreviewStoryListContextValue hasBotPreviewItems = botPreviewStoryListContextValue.state |> map { state in @@ -1307,7 +1307,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen if let user = peerView.peers[peerView.peerId] as? TelegramUser, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp), botInfo.flags.contains(.canEdit) { availablePanes?.insert(.botPreview, at: 0) - } else if let cachedData = peerView.cachedData as? CachedUserData, let botPreview = cachedData.botPreview, !botPreview.media.isEmpty { + } else if let cachedData = peerView.cachedData as? CachedUserData, let botPreview = cachedData.botPreview, !botPreview.items.isEmpty { availablePanes?.insert(.botPreview, at: 0) } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 9e5030d259..5f768653d3 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -3287,7 +3287,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }, navigateToMessage: { _, _, _ in }, navigateToMessageStandalone: { _ in }, navigateToThreadMessage: { _, _, _ in - }, tapMessage: nil, clickThroughMessage: { + }, tapMessage: nil, clickThroughMessage: { _, _ in }, toggleMessagesSelection: { [weak self] ids, value in guard let strongSelf = self else { return @@ -9864,7 +9864,12 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro guard let self else { return } - self.openBotPreviewEditor(target: .botPreview(self.peerId), source: result, transitionIn: (transitionView, transitionRect, transitionImage)) + + guard let pane = self.paneContainerNode.currentPane?.node as? PeerInfoStoryPaneNode else { + return + } + + self.openBotPreviewEditor(target: .botPreview(id: self.peerId, language: pane.currentBotPreviewLanguage?.id), source: result, transitionIn: (transitionView, transitionRect, transitionImage)) }, dismissed: {}, groupsPresented: {} @@ -10939,6 +10944,23 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } }))) + if let language = pane.currentBotPreviewLanguage { + //TODO:localize + items.append(.action(ContextMenuActionItem(text: "Delete \(language.name)", textColor: .destructive, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) + }, action: { [weak pane] _, a in + if ignoreNextActions { + return + } + ignoreNextActions = true + a(.default) + + if let pane { + pane.presentDeleteBotPreviewLanguage() + } + }))) + } + let contextController = ContextController(presentationData: self.presentationData, source: .reference(PeerInfoContextReferenceContentSource(controller: controller, sourceNode: source)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) contextController.passthroughTouchEvent = { [weak self] sourceView, point in guard let strongSelf = self else { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/BUILD index aa61d9591a..1bef45d94e 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/BUILD @@ -46,6 +46,8 @@ swift_library( "//submodules/TelegramUI/Components/MediaEditorScreen", "//submodules/LocationUI", "//submodules/Components/MultilineTextComponent", + "//submodules/TelegramUI/Components/TabSelectorComponent", + "//submodules/TelegramUI/Components/Settings/LanguageSelectionScreen", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift index ba14a31620..4ab6017f5b 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift @@ -41,6 +41,8 @@ import Geocoding import ItemListUI import MultilineTextComponent import LocationUI +import TabSelectorComponent +import LanguageSelectionScreen private let mediaBadgeBackgroundColor = UIColor(white: 0.0, alpha: 0.6) private let mediaBadgeTextColor = UIColor.white @@ -1565,6 +1567,9 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr private var mapInfoNode: LocationInfoListItemNode? private var searchHeader: ComponentView? + private var botPreviewLanguageTab: ComponentView? + private var botPreviewFooter: ComponentView? + private var barBackgroundLayer: SimpleLayer? private let itemGrid: SparseItemGrid @@ -1638,7 +1643,11 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr public var tabBarOffsetUpdated: ((ContainedViewLayoutTransition) -> Void)? public var tabBarOffset: CGFloat { - return self.itemGrid.coveringInsetOffset + if case .botPreview = self.scope { + return 0.0 + } else { + return self.itemGrid.coveringInsetOffset + } } private var currentListState: StoryListContext.State? @@ -1646,6 +1655,9 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr private var hiddenMediaDisposable: Disposable? private let updateDisposable = MetaDisposable() + private var currentBotPreviewLanguages: [StoryListContext.State.Language] = [] + private var removedBotPreviewLanguages = Set() + private var numberOfItemsToRequest: Int = 50 private var isRequestingView: Bool = false private var isFirstHistoryView: Bool = true @@ -1656,6 +1668,24 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr public private(set) var calendarSource: SparseMessageCalendar? private var listSource: StoryListContext + + private let maxBotPreviewCount: Int + + private let defaultListSource: StoryListContext + private var cachedListSources: [String: StoryListContext] = [:] + + public var currentBotPreviewLanguage: (id: String, name: String)? { + guard let listSource = self.listSource as? BotPreviewStoryListContext else { + return nil + } + guard let id = listSource.language else { + return nil + } + guard let language = self.currentBotPreviewLanguages.first(where: { $0.id == id }) else { + return nil + } + return (language.id, language.name) + } public var openCurrentDate: (() -> Void)? public var paneDidScroll: (() -> Void)? @@ -1723,11 +1753,18 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr case let .location(coordinates, venue): self.listSource = SearchStoryListContext(account: context.account, source: .mediaArea(.venue(coordinates: coordinates, venue: venue))) case let .botPreview(id): - self.listSource = BotPreviewStoryListContext(account: context.account, engine: context.engine, peerId: id) + self.listSource = BotPreviewStoryListContext(account: context.account, engine: context.engine, peerId: id, language: nil, assumeEmpty: false) } } + self.defaultListSource = self.listSource self.calendarSource = nil + var maxBotPreviewCount = 10 + if let data = self.context.currentAppConfiguration.with({ $0 }).data, let value = data["bot_preview_medias_max"] as? Double { + maxBotPreviewCount = Int(value) + } + self.maxBotPreviewCount = maxBotPreviewCount + super.init() if case .peer = self.scope { @@ -2689,6 +2726,10 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr self.listDisposable?.dispose() self.listDisposable = nil + + if reloadAtTop { + self.didUpdateItemsOnce = false + } self.listDisposable = (state |> deliverOn(queue)).startStrict(next: { [weak self] state in @@ -2700,7 +2741,11 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr if state.totalCount == 0 { if case .botPreview = self.scope { //TODO:localize - title = "no preview added" + if state.isLoading { + title = "loading" + } else { + title = "no preview added" + } } else { title = "" } @@ -2738,9 +2783,24 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr return } + var botPreviewLanguages = self.currentBotPreviewLanguages + for language in state.availableLanguages { + if !botPreviewLanguages.contains(where: { $0.id == language.id }) && !self.removedBotPreviewLanguages.contains(language.id) { + botPreviewLanguages.append(language) + } + } + botPreviewLanguages.sort(by: { $0.name < $1.name }) self.currentListState = state self.updateItemsFromState(state: state, firstTime: firstTime, reloadAtTop: reloadAtTop, synchronous: synchronous, animated: false) + + if self.currentBotPreviewLanguages != botPreviewLanguages || reloadAtTop { + self.currentBotPreviewLanguages = botPreviewLanguages + if let (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, navigationHeight, presentationData) = self.currentParams { + self.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, navigationHeight: navigationHeight, presentationData: presentationData, synchronous: synchronous, transition: .immediate) + } + } + firstTime = false self.isRequestingView = false } @@ -2853,7 +2913,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr if let (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, navigationHeight, presentationData) = self.currentParams { var gridSnapshot: UIView? - if reloadAtTop { + if case .botPreview = scope { + } else if reloadAtTop { gridSnapshot = self.itemGrid.view.snapshotView(afterScreenUpdates: false) } self.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, navigationHeight: navigationHeight, presentationData: presentationData, synchronous: false, transition: transition, animateGridItems: animated) @@ -2971,43 +3032,6 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr public func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { return nil - - /*var foundItemLayer: SparseItemGridLayer? - self.itemGrid.forEachVisibleItem { item in - guard let itemLayer = item.layer as? ItemLayer else { - return - } - if let item = itemLayer.item, item.message.id == messageId { - foundItemLayer = itemLayer - } - } - if let itemLayer = foundItemLayer { - let itemFrame = self.view.convert(self.itemGrid.frameForItem(layer: itemLayer), from: self.itemGrid.view) - let proxyNode = ASDisplayNode() - proxyNode.frame = itemFrame - if let contents = itemLayer.getContents() { - if let image = contents as? UIImage { - proxyNode.contents = image.cgImage - } else { - proxyNode.contents = contents - } - } - proxyNode.isHidden = true - self.addSubnode(proxyNode) - - let escapeNotification = EscapeNotification { - proxyNode.removeFromSupernode() - } - - return (proxyNode, proxyNode.bounds, { - let view = UIView() - view.frame = proxyNode.frame - view.layer.contents = proxyNode.layer.contents - escapeNotification.keep() - return (view, nil) - }) - } - return nil*/ } public func extractPendingStoryTransitionView() -> UIView? { @@ -3294,29 +3318,29 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr controller?.dismissAnimated() } - var mappedItemIds: [MediaId] = [] + var mappedMedia: [Media] = [] if let items = self.items { - mappedItemIds = items.items.compactMap { item -> MediaId? in + mappedMedia = items.items.compactMap { item -> Media? in guard let item = item as? VisualMediaItem else { return nil } if ids.contains(item.story.id) { - return item.story.media.id + return item.story.media._asMedia() } else { return nil } } } - if mappedItemIds.isEmpty { + if mappedMedia.isEmpty { return } //TODO:localize let title: String - if mappedItemIds.count == 1 { + if mappedMedia.count == 1 { title = "Delete 1 Preview?" } else { - title = "Delete \(mappedItemIds.count) Previews?" + title = "Delete \(mappedMedia.count) Previews?" } controller.setItemGroups([ @@ -3328,12 +3352,15 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr guard let self else { return } + guard let listSource = self.listSource as? BotPreviewStoryListContext else { + return + } if let parentController = self.parentController as? PeerInfoScreen { parentController.cancelItemSelection() } - let _ = self.context.engine.messages.deleteBotPreviews(peerId: peerId, ids: mappedItemIds).startStandalone() + let _ = self.context.engine.messages.deleteBotPreviews(peerId: peerId, language: listSource.language, media: mappedMedia).startStandalone() }) ]), ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) @@ -3349,8 +3376,14 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } private func gridScrollingOffsetUpdated(transition: ContainedViewLayoutTransition) { - if let _ = self.mapNode, let currentParams = self.currentParams { - self.updateMapLayout(size: currentParams.size, topInset: currentParams.topInset, bottomInset: currentParams.bottomInset, deviceMetrics: currentParams.deviceMetrics, transition: transition) + if let currentParams = self.currentParams { + if let _ = self.mapNode { + self.updateMapLayout(size: currentParams.size, topInset: currentParams.topInset, bottomInset: currentParams.bottomInset, deviceMetrics: currentParams.deviceMetrics, transition: transition) + } + if case .botPreview = self.scope, self.canManageStories { + self.updateBotPreviewLanguageTab(size: currentParams.size, topInset: currentParams.topInset, transition: transition) + self.updateBotPreviewFooter(size: currentParams.size, bottomInset: currentParams.bottomInset, transition: transition) + } } } @@ -3504,6 +3537,150 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } } + private func updateBotPreviewLanguageTab(size: CGSize, topInset: CGFloat, transition: ContainedViewLayoutTransition) { + guard case .botPreview = self.scope, self.canManageStories else { + return + } + + let botPreviewLanguageTab: ComponentView + if let current = self.botPreviewLanguageTab { + botPreviewLanguageTab = current + } else { + botPreviewLanguageTab = ComponentView() + self.botPreviewLanguageTab = botPreviewLanguageTab + } + + //TODO:localize + var languageItems: [TabSelectorComponent.Item] = [] + languageItems.append(TabSelectorComponent.Item( + id: AnyHashable("_main"), + title: "Main" + )) + for language in self.currentBotPreviewLanguages { + languageItems.append(TabSelectorComponent.Item( + id: AnyHashable(language.id), + title: language.name + )) + } + languageItems.append(TabSelectorComponent.Item( + id: AnyHashable("_add"), + title: "+ Add Language" + )) + var selectedLanguageId = "_main" + if let listSource = self.listSource as? BotPreviewStoryListContext, let language = listSource.language { + selectedLanguageId = language + } + + let botPreviewLanguageTabSize = botPreviewLanguageTab.update( + transition: ComponentTransition(transition), + component: AnyComponent(TabSelectorComponent( + colors: TabSelectorComponent.Colors( + foreground: self.presentationData.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.8), + selection: self.presentationData.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.05) + ), + customLayout: TabSelectorComponent.CustomLayout( + font: Font.medium(14.0), + spacing: 9.0, + verticalInset: 11.0 + ), + items: languageItems, + selectedId: AnyHashable(selectedLanguageId), + setSelectedId: { [weak self] id in + guard let self, let id = id.base as? String else { + return + } + if id == "_add" { + self.presentAddBotPreviewLanguage() + } else if id == "_main" { + self.setBotPreviewLanguage(id: nil, assumeEmpty: false) + } else if let language = self.currentBotPreviewLanguages.first(where: { $0.id == id }) { + self.setBotPreviewLanguage(id: language.id, assumeEmpty: false) + } + } + )), + environment: {}, + containerSize: CGSize(width: size.width, height: 44.0) + ) + var botPreviewLanguageTabFrame = CGRect(origin: CGPoint(x: floor((size.width - botPreviewLanguageTabSize.width) * 0.5), y: topInset - 11.0), size: botPreviewLanguageTabSize) + + let effectiveScrollingOffset: CGFloat + effectiveScrollingOffset = self.itemGrid.scrollingOffset + botPreviewLanguageTabFrame.origin.y -= effectiveScrollingOffset + + if let botPreviewLanguageTabView = botPreviewLanguageTab.view { + if botPreviewLanguageTabView.superview == nil { + self.view.addSubview(botPreviewLanguageTabView) + } + transition.updateFrame(view: botPreviewLanguageTabView, frame: botPreviewLanguageTabFrame) + } + } + + private func updateBotPreviewFooter(size: CGSize, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) { + if let items = self.items, !items.items.isEmpty { + var botPreviewFooterTransition = ComponentTransition(transition) + let botPreviewFooter: ComponentView + if let current = self.botPreviewFooter { + botPreviewFooter = current + } else { + botPreviewFooterTransition = .immediate + botPreviewFooter = ComponentView() + self.botPreviewFooter = botPreviewFooter + } + + var isMainLanguage = true + let text: String + if let listSource = self.listSource as? BotPreviewStoryListContext, let id = listSource.language, let language = self.currentBotPreviewLanguages.first(where: { $0.id == id }) { + isMainLanguage = false + text = "This preview will be displayed for all users who have \(language.name) set as their language." + } else { + text = "This preview will be shown by default. You can also add translations into specific languages." + } + + let botPreviewFooterSize = botPreviewFooter.update( + transition: botPreviewFooterTransition, + component: AnyComponent(EmptyStateIndicatorComponent( + context: self.context, + theme: self.presentationData.theme, + fitToHeight: true, + animationName: nil, + title: nil, + text: text, + actionTitle: "Add Preview", + action: { [weak self] in + guard let self else { + return + } + self.emptyAction?() + }, + additionalActionTitle: isMainLanguage ? "Create a Translation" : nil, + additionalAction: { [weak self] in + guard let self else { + return + } + if isMainLanguage { + self.presentAddBotPreviewLanguage() + } + }, + additionalActionSeparator: isMainLanguage ? "or" : nil + )), + environment: {}, + containerSize: CGSize(width: size.width, height: 1000.0) + ) + let botPreviewFooterFrame = CGRect(origin: CGPoint(x: floor((size.width - botPreviewFooterSize.width) * 0.5), y: self.itemGrid.contentBottomOffset - botPreviewFooterSize.height - bottomInset), size: botPreviewFooterSize) + if let botPreviewFooterView = botPreviewFooter.view { + if botPreviewFooterView.superview == nil { + self.view.addSubview(botPreviewFooterView) + } + botPreviewFooterTransition.setFrame(view: botPreviewFooterView, frame: botPreviewFooterFrame) + } + } else { + if let botPreviewFooter = self.botPreviewFooter { + self.botPreviewFooter = nil + botPreviewFooter.view?.removeFromSuperview() + } + } + } + public func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, navigationHeight: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { self.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, navigationHeight: navigationHeight, presentationData: presentationData, synchronous: synchronous, transition: transition, animateGridItems: false) } @@ -3555,7 +3732,19 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr transition.updateFrame(layer: barBackgroundLayer, frame: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: gridTopInset))) } + let defaultBottomInset = bottomInset var bottomInset = bottomInset + + if case .botPreview = self.scope, self.canManageStories { + updateBotPreviewLanguageTab(size: size, topInset: topInset, transition: transition) + gridTopInset += 50.0 + + updateBotPreviewFooter(size: size, bottomInset: defaultBottomInset, transition: transition) + if let botPreviewFooterView = self.botPreviewFooter?.view { + bottomInset += 18.0 + botPreviewFooterView.bounds.height + } + } + if self.isProfileEmbedded, let selectedIds = self.itemInteraction.selectedIds, self.canManageStories, case let .peer(peerId, _, isArchived) = self.scope { let selectionPanel: ComponentView var selectionPanelTransition = ComponentTransition(transition) @@ -3816,6 +4005,12 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr self.emptyStateView = emptyStateView } //TODO:localize + + var isMainLanguage = true + if let listSource = self.listSource as? BotPreviewStoryListContext, let _ = listSource.language { + isMainLanguage = false + } + let emptyStateSize = emptyStateView.update( transition: emptyStateTransition, component: AnyComponent(EmptyStateIndicatorComponent( @@ -3824,7 +4019,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr fitToHeight: self.isProfileEmbedded, animationName: nil, title: "No Preview", - text: "Upload screenshots and video demos for your Mini App that will be visible for your users here.", + text: "Upload up to \(self.maxBotPreviewCount) screenshots and video demos for your mini app.", actionTitle: self.canManageStories ? "Add Preview" : nil, action: { [weak self] in guard let self else { @@ -3832,8 +4027,15 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } self.emptyAction?() }, - additionalActionTitle: nil, - additionalAction: {} + additionalActionTitle: self.canManageStories ? (isMainLanguage ? "Create a Translation" : "Delete this Translation") : nil, + additionalAction: { + if isMainLanguage { + self.presentAddBotPreviewLanguage() + } else { + self.presentDeleteBotPreviewLanguage() + } + }, + additionalActionSeparator: self.canManageStories ? "or" : nil )), environment: {}, containerSize: CGSize(width: size.width, height: size.height - gridTopInset - bottomInset) @@ -3860,7 +4062,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr if self.isProfileEmbedded, case .botPreview = self.scope { backgroundColor = presentationData.theme.list.blocksBackgroundColor } else if self.isProfileEmbedded { - backgroundColor = presentationData.theme.list.plainBackgroundColor + backgroundColor = presentationData.theme.list.blocksBackgroundColor } else { backgroundColor = presentationData.theme.list.blocksBackgroundColor } @@ -3876,18 +4078,28 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr self.emptyStateView = nil if let emptyStateComponentView = emptyStateView.view { - subTransition.setAlpha(view: emptyStateComponentView, alpha: 0.0, completion: { [weak emptyStateComponentView] _ in - emptyStateComponentView?.removeFromSuperview() - }) + if self.didUpdateItemsOnce { + subTransition.setAlpha(view: emptyStateComponentView, alpha: 0.0, completion: { [weak emptyStateComponentView] _ in + emptyStateComponentView?.removeFromSuperview() + }) + } else { + emptyStateComponentView.removeFromSuperview() + } } - if self.isProfileEmbedded { + if self.isProfileEmbedded, case .botPreview = self.scope { + subTransition.setBackgroundColor(view: self.view, color: presentationData.theme.list.blocksBackgroundColor) + } else if self.isProfileEmbedded { subTransition.setBackgroundColor(view: self.view, color: presentationData.theme.list.plainBackgroundColor) } else { subTransition.setBackgroundColor(view: self.view, color: presentationData.theme.list.blocksBackgroundColor) } } else { - self.view.backgroundColor = .clear + if self.isProfileEmbedded, case .botPreview = self.scope { + self.view.backgroundColor = presentationData.theme.list.blocksBackgroundColor + } else { + self.view.backgroundColor = .clear + } } } @@ -3904,6 +4116,10 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr self.itemGrid.pinchEnabled = items.count > 2 self.itemGrid.update(size: size, insets: UIEdgeInsets(top: gridTopInset, left: sideInset, bottom: bottomInset, right: sideInset), useSideInsets: !isList, scrollIndicatorInsets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), lockScrollingAtTop: isScrollingLockedAtTop, fixedItemHeight: fixedItemHeight, fixedItemAspect: fixedItemAspect, items: items, theme: self.itemGridBinding.chatPresentationData.theme.theme, synchronous: wasFirstTime ? .full : .none, transition: animateGridItems ? .spring(duration: 0.35) : .immediate) } + + if case .botPreview = self.scope, self.canManageStories { + updateBotPreviewFooter(size: size, bottomInset: defaultBottomInset, transition: transition) + } } public func currentTopTimestamp() -> Int32? { @@ -3992,12 +4208,109 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr return false } - var maxCount = 10 - if let data = self.context.currentAppConfiguration.with({ $0 }).data, let value = data["bot_preview_medias_max"] as? Double { - maxCount = Int(value) + return items.count < self.maxBotPreviewCount + } + + private func presentAddBotPreviewLanguage() { + self.parentController?.push(LanguageSelectionScreen(context: self.context, selectLocalization: { [weak self] info in + guard let self else { + return + } + self.addBotPreviewLanguage(language: StoryListContext.State.Language(id: info.languageCode, name: info.title)) + })) + } + + public func presentDeleteBotPreviewLanguage() { + //TODO:localize + self.parentController?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: "Delete Translation", text: "Are you sure you want to delete this translation?", actions: [ + TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_Cancel, action: { + }), + TextAlertAction(type: .destructiveAction, title: "OK", action: { [weak self] in + guard let self else { + return + } + if let listSource = self.listSource as? BotPreviewStoryListContext, let language = listSource.language { + self.deleteBotPreviewLanguage(id: language) + } + }) + ], parseMarkdown: true), in: .window(.root)) + } + + private func addBotPreviewLanguage(language: StoryListContext.State.Language) { + var botPreviewLanguages = self.currentBotPreviewLanguages + + var assumeEmpty = false + if !botPreviewLanguages.contains(where: { $0.id == language.id}) { + botPreviewLanguages.append(language) + assumeEmpty = true + } + botPreviewLanguages.sort(by: { $0.name < $1.name }) + self.removedBotPreviewLanguages.remove(language.id) + + if self.currentBotPreviewLanguages != botPreviewLanguages { + self.currentBotPreviewLanguages = botPreviewLanguages + if let (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, navigationHeight, presentationData) = self.currentParams { + self.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, navigationHeight: navigationHeight, presentationData: presentationData, synchronous: false, transition: .immediate) + } } - return items.count < maxCount + self.setBotPreviewLanguage(id: language.id, assumeEmpty: assumeEmpty) + } + + private func deleteBotPreviewLanguage(id: String) { + var botPreviewLanguages = self.currentBotPreviewLanguages + + if let index = botPreviewLanguages.firstIndex(where: { $0.id == id}) { + botPreviewLanguages.remove(at: index) + } + self.removedBotPreviewLanguages.insert(id) + + guard case let .botPreview(peerId) = self.scope else { + return + } + + var mappedMedia: [Media] = [] + if let items = self.items { + mappedMedia = items.items.compactMap { item -> Media? in + guard let item = item as? VisualMediaItem else { + return nil + } + return item.story.media._asMedia() + } + } + let _ = self.context.engine.messages.deleteBotPreviewsLanguage(peerId: peerId, language: id, media: mappedMedia).startStandalone() + + if self.currentBotPreviewLanguages != botPreviewLanguages { + self.currentBotPreviewLanguages = botPreviewLanguages + if let (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, navigationHeight, presentationData) = self.currentParams { + self.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, navigationHeight: navigationHeight, presentationData: presentationData, synchronous: false, transition: .immediate) + } + } + + self.setBotPreviewLanguage(id: nil, assumeEmpty: false) + } + + private func setBotPreviewLanguage(id: String?, assumeEmpty: Bool) { + guard case let .botPreview(peerId) = self.scope else { + return + } + if let listSource = self.listSource as? BotPreviewStoryListContext, listSource.language == id { + return + } + + if let id { + if let cachedListSource = self.cachedListSources[id] { + self.listSource = cachedListSource + } else { + let listSource = BotPreviewStoryListContext(account: self.context.account, engine: self.context.engine, peerId: peerId, language: id, assumeEmpty: assumeEmpty) + self.listSource = listSource + self.cachedListSources[id] = listSource + } + } else { + self.listSource = self.defaultListSource + } + + self.requestHistoryAroundVisiblePosition(synchronous: false, reloadAtTop: true) } public func beginReordering() { @@ -4027,7 +4340,17 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr if !isReordering, let reorderedIds = self.reorderedIds { self.reorderedIds = nil if case .botPreview = self.scope, let listSource = self.listSource as? BotPreviewStoryListContext { - listSource.reorderItems(ids: reorderedIds) + if let items = self.items { + var reorderedMedia: [Media] = [] + + for id in reorderedIds { + if let item = items.items.first(where: { ($0 as? VisualMediaItem)?.storyId == id }) as? VisualMediaItem { + reorderedMedia.append(item.story.media._asMedia()) + } + } + + listSource.reorderItems(media: reorderedMedia) + } } else if case let .peer(id, _, _) = self.scope, id == self.context.account.peerId, let items = self.items { var updatedPinnedIds: [Int32] = [] for id in reorderedIds { diff --git a/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/BUILD b/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/BUILD new file mode 100644 index 0000000000..802cc7a542 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/BUILD @@ -0,0 +1,31 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "LanguageSelectionScreen", + module_name = "LanguageSelectionScreen", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/AsyncDisplayKit", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramPresentationData", + "//submodules/MergeLists", + "//submodules/ItemListUI", + "//submodules/PresentationDataUtils", + "//submodules/AccountContext", + "//submodules/SearchBarNode", + "//submodules/SearchUI", + "//submodules/TelegramUIPreferences", + "//submodules/TranslateUI", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/Sources/LanguageSelectionScreen.swift b/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/Sources/LanguageSelectionScreen.swift new file mode 100644 index 0000000000..04229ce599 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/Sources/LanguageSelectionScreen.swift @@ -0,0 +1,177 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import Postbox +import SwiftSignalKit +import TelegramCore +import TelegramPresentationData +import AccountContext +import SearchUI + +public class LanguageSelectionScreen: ViewController { + private let context: AccountContext + private let selectLocalization: (LocalizationInfo) -> Void + + private var controllerNode: LanguageSelectionScreenNode { + return self.displayNode as! LanguageSelectionScreenNode + } + + private var _ready = Promise() + override public var ready: Promise { + return self._ready + } + + private var presentationData: PresentationData + private var presentationDataDisposable: Disposable? + + private var searchContentNode: NavigationBarSearchContentNode? + + private var previousContentOffset: ListViewVisibleContentOffset? + + public init(context: AccountContext, selectLocalization: @escaping (LocalizationInfo) -> Void) { + self.context = context + self.selectLocalization = selectLocalization + + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + + super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData)) + + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style + self.navigationPresentation = .modal + + //TODO:localize + self.title = "Add a Translation" + + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) + + self.scrollToTop = { [weak self] in + if let strongSelf = self { + if let searchContentNode = strongSelf.searchContentNode { + searchContentNode.updateExpansionProgress(1.0, animated: true) + } + strongSelf.controllerNode.scrollToTop() + } + } + + self.presentationDataDisposable = (context.sharedContext.presentationData + |> deliverOnMainQueue).start(next: { [weak self] presentationData in + if let strongSelf = self { + let previousTheme = strongSelf.presentationData.theme + let previousStrings = strongSelf.presentationData.strings + + strongSelf.presentationData = presentationData + + if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings { + strongSelf.updateThemeAndStrings() + } + } + }) + + self.searchContentNode = NavigationBarSearchContentNode(theme: self.presentationData.theme, placeholder: self.presentationData.strings.Common_Search, inline: true, activate: { [weak self] in + self?.activateSearch() + }) + self.navigationBar?.setContentNode(self.searchContentNode, animated: false) + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.presentationDataDisposable?.dispose() + } + + private func updateThemeAndStrings() { + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style + self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData)) + self.searchContentNode?.updateThemeAndPlaceholder(theme: self.presentationData.theme, placeholder: self.presentationData.strings.Common_Search) + self.title = self.presentationData.strings.Settings_AppLanguage + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) + self.controllerNode.updatePresentationData(self.presentationData) + } + + override public func loadDisplayNode() { + self.displayNode = LanguageSelectionScreenNode(context: self.context, presentationData: self.presentationData, navigationBar: self.navigationBar!, requestActivateSearch: { [weak self] in + self?.activateSearch() + }, requestDeactivateSearch: { [weak self] in + self?.deactivateSearch() + }, present: { [weak self] c, a in + self?.present(c, in: .window(.root), with: a) + }, push: { [weak self] c in + self?.push(c) + }, selectLocalization: { [weak self] info in + guard let self else { + return + } + self.selectLocalization(info) + self.dismiss() + }) + + self.controllerNode.listNode.visibleContentOffsetChanged = { [weak self] offset in + if let strongSelf = self { + if let searchContentNode = strongSelf.searchContentNode { + searchContentNode.updateListVisibleContentOffset(offset) + } + + var previousContentOffsetValue: CGFloat? + if let previousContentOffset = strongSelf.previousContentOffset, case let .known(value) = previousContentOffset { + previousContentOffsetValue = value + } + switch offset { + case let .known(value): + let transition: ContainedViewLayoutTransition + if let previousContentOffsetValue = previousContentOffsetValue, value <= 0.0, previousContentOffsetValue > 30.0 { + transition = .animated(duration: 0.2, curve: .easeInOut) + } else { + transition = .immediate + } + strongSelf.navigationBar?.updateBackgroundAlpha(min(30.0, max(0.0, value - 54.0)) / 30.0, transition: transition) + case .unknown, .none: + strongSelf.navigationBar?.updateBackgroundAlpha(1.0, transition: .immediate) + } + + strongSelf.previousContentOffset = offset + } + } + + self.controllerNode.listNode.didEndScrolling = { [weak self] _ in + if let strongSelf = self, let searchContentNode = strongSelf.searchContentNode { + let _ = fixNavigationSearchableListNodeScrolling(strongSelf.controllerNode.listNode, searchNode: searchContentNode) + } + } + + self._ready.set(self.controllerNode._ready.get()) + + self.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate) + + self.displayNodeDidLoad() + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.cleanNavigationHeight, transition: transition) + } + + private func activateSearch() { + if self.displayNavigationBar { + if let scrollToTop = self.scrollToTop { + scrollToTop() + } + if let searchContentNode = self.searchContentNode { + self.controllerNode.activateSearch(placeholderNode: searchContentNode.placeholderNode) + } + self.setDisplayNavigationBar(false, transition: .animated(duration: 0.5, curve: .spring)) + } + } + + private func deactivateSearch() { + if !self.displayNavigationBar { + self.setDisplayNavigationBar(true, transition: .animated(duration: 0.5, curve: .spring)) + if let searchContentNode = self.searchContentNode { + self.controllerNode.deactivateSearch(placeholderNode: searchContentNode.placeholderNode) + } + } + } +} diff --git a/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/Sources/LanguageSelectionScreenNode.swift b/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/Sources/LanguageSelectionScreenNode.swift new file mode 100644 index 0000000000..ef9438ed6e --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/Sources/LanguageSelectionScreenNode.swift @@ -0,0 +1,568 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import Postbox +import TelegramCore +import SwiftSignalKit +import TelegramPresentationData +import MergeLists +import ItemListUI +import PresentationDataUtils +import AccountContext +import SearchBarNode +import SearchUI +import TelegramUIPreferences +import TranslateUI + +private enum LanguageListSection: ItemListSectionId { + case official + case unofficial +} + +private enum LanguageListEntryId: Hashable { + case localizationTitle + case localization(String) +} + +private enum LanguageListEntryType { + case official + case unofficial +} + +private enum LanguageListEntry: Comparable, Identifiable { + case localizationTitle(text: String, section: ItemListSectionId) + case localization(index: Int, info: LocalizationInfo?, type: LanguageListEntryType) + + var stableId: LanguageListEntryId { + switch self { + case .localizationTitle: + return .localizationTitle + case let .localization(index, info, _): + return .localization(info?.languageCode ?? "\(index)") + } + } + + private func index() -> Int { + switch self { + case .localizationTitle: + return 1000 + case let .localization(index, _, _): + return 1001 + index + } + } + + static func <(lhs: LanguageListEntry, rhs: LanguageListEntry) -> Bool { + return lhs.index() < rhs.index() + } + + func item(presentationData: PresentationData, searchMode: Bool, openSearch: @escaping () -> Void, selectLocalization: @escaping (LocalizationInfo) -> Void) -> ListViewItem { + switch self { + case let .localizationTitle(text, section): + return ItemListSectionHeaderItem(presentationData: ItemListPresentationData(presentationData), text: text, sectionId: section) + case let .localization(_, info, type): + return LocalizationListItem(presentationData: ItemListPresentationData(presentationData), id: info?.languageCode ?? "", title: info?.title ?? " ", subtitle: info?.localizedTitle ?? " ", checked: false, activity: false, loading: info == nil, editing: LocalizationListItemEditing(editable: false, editing: false, revealed: false, reorderable: false), sectionId: type == .official ? LanguageListSection.official.rawValue : LanguageListSection.unofficial.rawValue, alwaysPlain: searchMode, action: { + if let info { + selectLocalization(info) + } + }, setItemWithRevealedOptions: nil, removeItem: nil) + } + } +} + +private struct LocalizationListSearchContainerTransition { + let deletions: [ListViewDeleteItem] + let insertions: [ListViewInsertItem] + let updates: [ListViewUpdateItem] + let isSearching: Bool +} + +private func preparedLanguageListSearchContainerTransition(presentationData: PresentationData, from fromEntries: [LanguageListEntry], to toEntries: [LanguageListEntry], selectLocalization: @escaping (LocalizationInfo) -> Void, isSearching: Bool, forceUpdate: Bool) -> LocalizationListSearchContainerTransition { + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries, allUpdated: forceUpdate) + + let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, searchMode: true, openSearch: {}, selectLocalization: selectLocalization), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, searchMode: true, openSearch: {}, selectLocalization: selectLocalization), directionHint: nil) } + + return LocalizationListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, isSearching: isSearching) +} + +private final class LocalizationListSearchContainerNode: SearchDisplayControllerContentNode { + private let dimNode: ASDisplayNode + private let listNode: ListView + + private var enqueuedTransitions: [LocalizationListSearchContainerTransition] = [] + private var hasValidLayout = false + + private let searchQuery = Promise() + private let searchDisposable = MetaDisposable() + + private var presentationData: PresentationData + private var presentationDataDisposable: Disposable? + + private let presentationDataPromise: Promise + + public override var hasDim: Bool { + return true + } + + init(context: AccountContext, listState: LocalizationListState, selectLocalization: @escaping (LocalizationInfo) -> Void) { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.presentationData = presentationData + + self.presentationDataPromise = Promise(self.presentationData) + + self.dimNode = ASDisplayNode() + self.dimNode.backgroundColor = UIColor.black.withAlphaComponent(0.5) + + self.listNode = ListView() + self.listNode.accessibilityPageScrolledString = { row, count in + return presentationData.strings.VoiceOver_ScrollStatus(row, count).string + } + + super.init() + + self.listNode.backgroundColor = self.presentationData.theme.chatList.backgroundColor + self.listNode.isHidden = true + + self.addSubnode(self.dimNode) + self.addSubnode(self.listNode) + + let foundItems = self.searchQuery.get() + |> mapToSignal { query -> Signal<[LocalizationInfo]?, NoError> in + if let query = query, !query.isEmpty { + let normalizedQuery = query.lowercased() + var result: [LocalizationInfo] = [] + var uniqueIds = Set() + for info in listState.availableSavedLocalizations + listState.availableOfficialLocalizations { + if info.title.lowercased().hasPrefix(normalizedQuery) || info.localizedTitle.lowercased().hasPrefix(normalizedQuery) { + if uniqueIds.contains(info.languageCode) { + continue + } + uniqueIds.insert(info.languageCode) + result.append(info) + } + } + return .single(result) + } else { + return .single(nil) + } + } + + let previousEntriesHolder = Atomic<([LanguageListEntry], PresentationTheme, PresentationStrings)?>(value: nil) + self.searchDisposable.set(combineLatest(queue: .mainQueue(), foundItems, self.presentationDataPromise.get()).start(next: { [weak self] items, presentationData in + guard let strongSelf = self else { + return + } + var entries: [LanguageListEntry] = [] + if let items = items { + for item in items { + entries.append(.localization(index: entries.count, info: item, type: .official)) + } + } + let previousEntriesAndPresentationData = previousEntriesHolder.swap((entries, presentationData.theme, presentationData.strings)) + let transition = preparedLanguageListSearchContainerTransition(presentationData: presentationData, from: previousEntriesAndPresentationData?.0 ?? [], to: entries, selectLocalization: selectLocalization, isSearching: items != nil, forceUpdate: previousEntriesAndPresentationData?.1 !== presentationData.theme || previousEntriesAndPresentationData?.2 !== presentationData.strings) + strongSelf.enqueueTransition(transition) + })) + + self.presentationDataDisposable = (context.sharedContext.presentationData + |> deliverOnMainQueue).start(next: { [weak self] presentationData in + if let strongSelf = self { + let previousTheme = strongSelf.presentationData.theme + let previousStrings = strongSelf.presentationData.strings + + strongSelf.presentationData = presentationData + + if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings { + strongSelf.updateThemeAndStrings(theme: presentationData.theme, strings: presentationData.strings) + strongSelf.presentationDataPromise.set(.single(presentationData)) + } + } + }) + + self.listNode.beganInteractiveDragging = { [weak self] _ in + self?.dismissInput?() + } + } + + deinit { + self.searchDisposable.dispose() + self.presentationDataDisposable?.dispose() + } + + override func didLoad() { + super.didLoad() + + self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) + } + + func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { + self.listNode.backgroundColor = theme.chatList.backgroundColor + } + + override func searchTextUpdated(text: String) { + if text.isEmpty { + self.searchQuery.set(.single(nil)) + } else { + self.searchQuery.set(.single(text)) + } + } + + private func enqueueTransition(_ transition: LocalizationListSearchContainerTransition) { + self.enqueuedTransitions.append(transition) + + if self.hasValidLayout { + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransitions() + } + } + } + + private func dequeueTransitions() { + if let transition = self.enqueuedTransitions.first { + self.enqueuedTransitions.remove(at: 0) + + var options = ListViewDeleteAndInsertOptions() + options.insert(.PreferSynchronousDrawing) + + let isSearching = transition.isSearching + self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in + self?.listNode.isHidden = !isSearching + self?.dimNode.isHidden = isSearching + }) + } + } + + override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) + + let topInset = navigationBarHeight + transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: layout.size.width, height: layout.size.height - topInset))) + + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) + + self.listNode.frame = CGRect(origin: CGPoint(), size: layout.size) + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: navigationBarHeight, left: layout.safeInsets.left, bottom: layout.insets(options: [.input]).bottom, right: layout.safeInsets.right), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + + if !self.hasValidLayout { + self.hasValidLayout = true + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransitions() + } + } + } + + @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.cancel?() + } + } +} + +private struct LanguageListNodeTransition { + let deletions: [ListViewDeleteItem] + let insertions: [ListViewInsertItem] + let updates: [ListViewUpdateItem] + let firstTime: Bool + let isLoading: Bool + let animated: Bool + let crossfade: Bool +} + +private func preparedLanguageListNodeTransition( + presentationData: PresentationData, + from fromEntries: [LanguageListEntry], + to toEntries: [LanguageListEntry], + openSearch: @escaping () -> Void, + selectLocalization: @escaping (LocalizationInfo) -> Void, + firstTime: Bool, + isLoading: Bool, + forceUpdate: Bool, + animated: Bool, + crossfade: Bool +) -> LanguageListNodeTransition { + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries, allUpdated: forceUpdate) + + let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, searchMode: false, openSearch: openSearch, selectLocalization: selectLocalization), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, searchMode: false, openSearch: openSearch, selectLocalization: selectLocalization), directionHint: nil) } + + return LanguageListNodeTransition(deletions: deletions, insertions: insertions, updates: updates, firstTime: firstTime, isLoading: isLoading, animated: animated, crossfade: crossfade) +} + +final class LanguageSelectionScreenNode: ViewControllerTracingNode { + private let context: AccountContext + private var presentationData: PresentationData + private weak var navigationBar: NavigationBar? + private let requestActivateSearch: () -> Void + private let requestDeactivateSearch: () -> Void + private let present: (ViewController, Any?) -> Void + private let push: (ViewController) -> Void + private let selectLocalization: (LocalizationInfo) -> Void + + private var didSetReady = false + let _ready = ValuePromise() + + private var containerLayout: (ContainerViewLayout, CGFloat)? + let listNode: ListView + private let leftOverlayNode: ASDisplayNode + private let rightOverlayNode: ASDisplayNode + private var queuedTransitions: [LanguageListNodeTransition] = [] + private var searchDisplayController: SearchDisplayController? + + private let presentationDataValue = Promise() + private var updatedDisposable: Disposable? + private var listDisposable: Disposable? + + private var currentListState: LocalizationListState? + + init(context: AccountContext, presentationData: PresentationData, navigationBar: NavigationBar, requestActivateSearch: @escaping () -> Void, requestDeactivateSearch: @escaping () -> Void, present: @escaping (ViewController, Any?) -> Void, push: @escaping (ViewController) -> Void, selectLocalization: @escaping (LocalizationInfo) -> Void) { + self.context = context + self.presentationData = presentationData + self.presentationDataValue.set(.single(presentationData)) + self.navigationBar = navigationBar + self.requestActivateSearch = requestActivateSearch + self.requestDeactivateSearch = requestDeactivateSearch + self.present = present + self.push = push + self.selectLocalization = selectLocalization + + self.listNode = ListView() + self.listNode.keepTopItemOverscrollBackground = ListViewKeepTopItemOverscrollBackground(color: presentationData.theme.list.blocksBackgroundColor, direction: true) + self.listNode.accessibilityPageScrolledString = { row, count in + return presentationData.strings.VoiceOver_ScrollStatus(row, count).string + } + self.leftOverlayNode = ASDisplayNode() + self.leftOverlayNode.backgroundColor = self.presentationData.theme.list.blocksBackgroundColor + self.rightOverlayNode = ASDisplayNode() + self.rightOverlayNode.backgroundColor = self.presentationData.theme.list.blocksBackgroundColor + + super.init() + + self.backgroundColor = presentationData.theme.list.blocksBackgroundColor + self.addSubnode(self.listNode) + + let openSearch: () -> Void = { + requestActivateSearch() + } + + let previousState = Atomic(value: nil) + let previousEntriesHolder = Atomic<([LanguageListEntry], PresentationTheme, PresentationStrings)?>(value: nil) + self.listDisposable = combineLatest( + queue: .mainQueue(), + context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.LocalizationList()), + context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)), + context.sharedContext.accountManager.sharedData(keys: [SharedDataKeys.localizationSettings, ApplicationSpecificSharedDataKeys.translationSettings]), + self.presentationDataValue.get() + ).start(next: { [weak self] localizationListState, peer, sharedData, presentationData in + guard let strongSelf = self else { + return + } + + var entries: [LanguageListEntry] = [] + var existingIds = Set() + + if !localizationListState.availableOfficialLocalizations.isEmpty { + strongSelf.currentListState = localizationListState + + let availableSavedLocalizations = localizationListState.availableSavedLocalizations.filter({ info in !localizationListState.availableOfficialLocalizations.contains(where: { $0.languageCode == info.languageCode }) }) + if !availableSavedLocalizations.isEmpty { + //entries.append(.localizationTitle(text: presentationData.strings.Localization_InterfaceLanguage.uppercased(), section: LanguageListSection.unofficial.rawValue)) + for info in availableSavedLocalizations { + if existingIds.contains(info.languageCode) { + continue + } + existingIds.insert(info.languageCode) + entries.append(.localization(index: entries.count, info: info, type: .unofficial)) + } + } else { + //entries.append(.localizationTitle(text: presentationData.strings.Localization_InterfaceLanguage.uppercased(), section: LanguageListSection.official.rawValue)) + } + for info in localizationListState.availableOfficialLocalizations { + if existingIds.contains(info.languageCode) { + continue + } + existingIds.insert(info.languageCode) + entries.append(.localization(index: entries.count, info: info, type: .official)) + } + } else { + for _ in 0 ..< 15 { + entries.append(.localization(index: entries.count, info: nil, type: .official)) + } + } + + let previousState = previousState.swap(localizationListState) + + let previousEntriesAndPresentationData = previousEntriesHolder.swap((entries, presentationData.theme, presentationData.strings)) + let transition = preparedLanguageListNodeTransition( + presentationData: presentationData, + from: previousEntriesAndPresentationData?.0 ?? [], + to: entries, + openSearch: openSearch, + selectLocalization: { [weak self] info in + self?.selectLocalization(info) + }, + firstTime: previousEntriesAndPresentationData == nil, + isLoading: entries.isEmpty, + forceUpdate: previousEntriesAndPresentationData?.1 !== presentationData.theme || previousEntriesAndPresentationData?.2 !== presentationData.strings, + animated: (previousEntriesAndPresentationData?.0.count ?? 0) != entries.count, + crossfade: (previousState == nil || previousState!.availableOfficialLocalizations.isEmpty) != localizationListState.availableOfficialLocalizations.isEmpty + ) + strongSelf.enqueueTransition(transition) + }) + self.updatedDisposable = context.engine.localization.synchronizedLocalizationListState().start() + + self.listNode.itemNodeHitTest = { [weak self] point in + if let strongSelf = self { + return point.x > strongSelf.leftOverlayNode.frame.maxX && point.x < strongSelf.rightOverlayNode.frame.minX + } else { + return true + } + } + } + + deinit { + self.listDisposable?.dispose() + self.updatedDisposable?.dispose() + } + + func updatePresentationData(_ presentationData: PresentationData) { + let stringsUpdated = self.presentationData.strings !== presentationData.strings + self.presentationData = presentationData + + if stringsUpdated { + if let snapshotView = self.view.snapshotView(afterScreenUpdates: false) { + self.view.addSubview(snapshotView) + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + } + } + + self.presentationDataValue.set(.single(presentationData)) + self.backgroundColor = presentationData.theme.list.blocksBackgroundColor + self.listNode.keepTopItemOverscrollBackground = ListViewKeepTopItemOverscrollBackground(color: presentationData.theme.list.blocksBackgroundColor, direction: true) + self.searchDisplayController?.updatePresentationData(presentationData) + self.leftOverlayNode.backgroundColor = presentationData.theme.list.blocksBackgroundColor + self.rightOverlayNode.backgroundColor = presentationData.theme.list.blocksBackgroundColor + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + let hadValidLayout = self.containerLayout != nil + self.containerLayout = (layout, navigationBarHeight) + + var listInsets = layout.insets(options: [.input]) + listInsets.top += navigationBarHeight + if layout.size.width >= 375.0 { + let inset = max(16.0, floor((layout.size.width - 674.0) / 2.0)) + listInsets.left += inset + listInsets.right += inset + } else { + listInsets.left += layout.safeInsets.left + listInsets.right += layout.safeInsets.right + } + + self.leftOverlayNode.frame = CGRect(x: 0.0, y: 0.0, width: listInsets.left, height: layout.size.height) + self.rightOverlayNode.frame = CGRect(x: layout.size.width - listInsets.right, y: 0.0, width: listInsets.right, height: layout.size.height) + + if self.leftOverlayNode.supernode == nil { + self.insertSubnode(self.leftOverlayNode, aboveSubnode: self.listNode) + } + if self.rightOverlayNode.supernode == nil { + self.insertSubnode(self.rightOverlayNode, aboveSubnode: self.listNode) + } + + if let searchDisplayController = self.searchDisplayController { + searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) + } + + self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) + self.listNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) + + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: listInsets, duration: duration, curve: curve) + + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + + if !hadValidLayout { + self.dequeueTransitions() + } + } + + private func enqueueTransition(_ transition: LanguageListNodeTransition) { + self.queuedTransitions.append(transition) + + if self.containerLayout != nil { + self.dequeueTransitions() + } + } + + private func dequeueTransitions() { + guard let _ = self.containerLayout else { + return + } + while !self.queuedTransitions.isEmpty { + let transition = self.queuedTransitions.removeFirst() + + var options = ListViewDeleteAndInsertOptions() + if transition.firstTime { + options.insert(.Synchronous) + options.insert(.LowLatency) + } else if transition.crossfade { + options.insert(.AnimateCrossfade) + } else if transition.animated { + options.insert(.AnimateInsertion) + } + self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateOpaqueState: nil, completion: { [weak self] _ in + if let strongSelf = self { + if !strongSelf.didSetReady { + strongSelf.didSetReady = true + strongSelf._ready.set(true) + } + } + }) + } + } + + func activateSearch(placeholderNode: SearchBarPlaceholderNode) { + guard let (containerLayout, navigationBarHeight) = self.containerLayout, self.searchDisplayController == nil else { + return + } + + self.searchDisplayController = SearchDisplayController( + presentationData: self.presentationData, + contentNode: LocalizationListSearchContainerNode( + context: self.context, + listState: self.currentListState ?? LocalizationListState.defaultSettings, + selectLocalization: { [weak self] info in + self?.selectLocalization(info) + }), + inline: true, + cancel: { [weak self] in + self?.requestDeactivateSearch() + } + ) + + self.searchDisplayController?.containerLayoutUpdated(containerLayout, navigationBarHeight: navigationBarHeight, transition: .immediate) + self.searchDisplayController?.activate(insertSubnode: { [weak self, weak placeholderNode] subnode, isSearchBar in + if let strongSelf = self, let strongPlaceholderNode = placeholderNode { + if isSearchBar { + strongPlaceholderNode.supernode?.insertSubnode(subnode, aboveSubnode: strongPlaceholderNode) + } else if let navigationBar = strongSelf.navigationBar { + strongSelf.insertSubnode(subnode, belowSubnode: navigationBar) + } + } + }, placeholder: placeholderNode) + } + + func deactivateSearch(placeholderNode: SearchBarPlaceholderNode) { + if let searchDisplayController = self.searchDisplayController { + searchDisplayController.deactivate(placeholder: placeholderNode) + self.searchDisplayController = nil + } + } + + func scrollToTop() { + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + } +} diff --git a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift index 6ba47f35df..0ce8b9d798 100644 --- a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift +++ b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift @@ -1093,7 +1093,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, ASScrollViewDelegate state.displayPatternPanel = false return state }, animated: true) - }, clickThroughMessage: { + }, clickThroughMessage: { _, _ in }, backgroundNode: self.backgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true, isStandalone: false) return item } diff --git a/submodules/TelegramUI/Components/SpaceWarpView/BUILD b/submodules/TelegramUI/Components/SpaceWarpView/BUILD index 44c7e34a6d..a3c6d68639 100644 --- a/submodules/TelegramUI/Components/SpaceWarpView/BUILD +++ b/submodules/TelegramUI/Components/SpaceWarpView/BUILD @@ -12,6 +12,7 @@ swift_library( deps = [ "//submodules/Display", "//submodules/AsyncDisplayKit", + "//submodules/ComponentFlow", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift b/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift index 7e4ec16830..fa27dca70d 100644 --- a/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift +++ b/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift @@ -2,7 +2,429 @@ import Foundation import UIKit import Display import AsyncDisplayKit +import ComponentFlow + +/*open class SpaceWarpView: UIView { + private final class WarpPartView: UIView { + let cloneView: PortalView + + init?(contentView: PortalSourceView) { + guard let cloneView = PortalView(matchPosition: false) else { + return nil + } + self.cloneView = cloneView + + super.init(frame: CGRect()) + + self.layer.anchorPoint = CGPoint(x: 0.5, y: 0.0) + + self.clipsToBounds = true + self.addSubview(cloneView.view) + contentView.addPortal(view: cloneView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(containerSize: CGSize, rect: CGRect, transition: ComponentTransition) { + transition.setFrame(view: self.cloneView.view, frame: CGRect(origin: CGPoint(x: -rect.minX, y: -rect.minY), size: CGSize(width: containerSize.width, height: containerSize.height))) + } + } + + public var contentView: UIView { + return self.contentViewImpl + } + + let contentViewImpl: PortalSourceView + + private var warpViews: [WarpPartView] = [] + + override public init(frame: CGRect) { + self.contentViewImpl = PortalSourceView() + + super.init(frame: frame) + + self.addSubview(self.contentView) + self.contentView.alpha = 0.1 + + for _ in 0 ..< 8 { + if let warpView = WarpPartView(contentView: self.contentViewImpl) { + self.warpViews.append(warpView) + self.addSubview(warpView) + } + } + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func update(size: CGSize, warpHeight: CGFloat, transition: ComponentTransition) { + transition.setFrame(view: self.contentView, frame: CGRect(origin: CGPoint(), size: size)) + + let allItemsHeight = warpHeight * 0.5 + for i in 0 ..< self.warpViews.count { + let itemHeight = warpHeight / CGFloat(self.warpViews.count) + let itemFraction = CGFloat(i + 1) / CGFloat(self.warpViews.count) + let _ = itemHeight + + let da = CGFloat.pi * 0.5 / CGFloat(self.warpViews.count) + let alpha = CGFloat.pi * 0.5 - itemFraction * CGFloat.pi * 0.5 + let endPoint = CGPoint(x: cos(alpha), y: sin(alpha)) + let prevAngle = alpha + da + let prevPt = CGPoint(x: cos(prevAngle), y: sin(prevAngle)) + var angle: CGFloat + angle = -atan2(endPoint.y - prevPt.y, endPoint.x - prevPt.x) + + let itemLengthVector = CGPoint(x: endPoint.x - prevPt.x, y: endPoint.y - prevPt.y) + let itemLength = sqrt(itemLengthVector.x * itemLengthVector.x + itemLengthVector.y * itemLengthVector.y) * warpHeight * 0.5 + let _ = itemLength + + var transform: CATransform3D + transform = CATransform3DIdentity + transform.m34 = 1.0 / 240.0 + + transform = CATransform3DTranslate(transform, 0.0, prevPt.x * allItemsHeight, (1.0 - prevPt.y) * allItemsHeight) + transform = CATransform3DRotate(transform, angle, 1.0, 0.0, 0.0) + + let positionY = size.height - allItemsHeight + 4.0 + CGFloat(i) * itemLength + let rect = CGRect(origin: CGPoint(x: 0.0, y: positionY), size: CGSize(width: size.width, height: itemLength)) + transition.setPosition(view: self.warpViews[i], position: CGPoint(x: rect.midX, y: 4.0)) + transition.setBounds(view: self.warpViews[i], bounds: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: itemLength))) + transition.setTransform(view: self.warpViews[i], transform: transform) + self.warpViews[i].update(containerSize: size, rect: rect, transition: transition) + } + } + + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + return self.contentView.hitTest(point, with: event) + } +}*/ + +private extension CGPoint { + static func -(lhs: CGPoint, rhs: CGPoint) -> CGPoint { + return CGPoint(x: lhs.x - rhs.x, y: lhs.y - rhs.y) + } + + static func +(lhs: CGPoint, rhs: CGPoint) -> CGPoint { + return CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y) + } + + static func *(lhs: CGPoint, rhs: CGFloat) -> CGPoint { + return CGPoint(x: lhs.x * rhs, y: lhs.y * rhs) + } +} + +private func length(_ v: CGPoint) -> CGFloat { + return sqrt(v.x * v.x + v.y * v.y) +} + +private func normalize(_ v: CGPoint) -> CGPoint { + let len = length(v) + return CGPoint(x: v.x / len, y: v.y / len) +} + +private struct RippleParams { + var amplitude: CGFloat + var frequency: CGFloat + var decay: CGFloat + var speed: CGFloat + + init(amplitude: CGFloat, frequency: CGFloat, decay: CGFloat, speed: CGFloat) { + self.amplitude = amplitude + self.frequency = frequency + self.decay = decay + self.speed = speed + } +} + +private func transformCoordinate( + position: CGPoint, + origin: CGPoint, + time: CGFloat, + params: RippleParams +) -> CGPoint { + // The distance of the current pixel position from `origin`. + let distance = length(position - origin) + + if distance < 10.0 { + return position + } + + // The amount of time it takes for the ripple to arrive at the current pixel position. + let delay = distance / params.speed + + // Adjust for delay, clamp to 0. + var time = time + time -= delay + time = max(0.0, time) + + // The ripple is a sine wave that Metal scales by an exponential decay + // function. + let rippleAmount = params.amplitude * sin(params.frequency * time) * exp(-params.decay * time) + + // A vector of length `amplitude` that points away from position. + let n = normalize(position - origin) + + // Scale `n` by the ripple amount at the current pixel position and add it + // to the current pixel position. + // + // This new position moves toward or away from `origin` based on the + // sign and magnitude of `rippleAmount`. + let newPosition = position + n * rippleAmount + return newPosition +} + +private func rectToQuad( + rect: CGRect, + quadTL: CGPoint, + quadTR: CGPoint, + quadBL: CGPoint, + quadBR: CGPoint +) -> CATransform3D { + let x1a = quadTL.x + let y1a = quadTL.y + let x2a = quadTR.x + let y2a = quadTR.y + let x3a = quadBL.x + let y3a = quadBL.y + let x4a = quadBR.x + let y4a = quadBR.y + + let X = rect.origin.x + let Y = rect.origin.y + let W = rect.size.width + let H = rect.size.height + + let y21 = y2a - y1a + let y32 = y3a - y2a + let y43 = y4a - y3a + let y14 = y1a - y4a + let y31 = y3a - y1a + let y42 = y4a - y2a + + let a = -H*(x2a*x3a*y14 + x2a*x4a*y31 - x1a*x4a*y32 + x1a*x3a*y42) + let b = W*(x2a*x3a*y14 + x3a*x4a*y21 + x1a*x4a*y32 + x1a*x2a*y43) + let c = H*X*(x2a*x3a*y14 + x2a*x4a*y31 - x1a*x4a*y32 + x1a*x3a*y42) - H*W*x1a*(x4a*y32 - x3a*y42 + x2a*y43) - W*Y*(x2a*x3a*y14 + x3a*x4a*y21 + x1a*x4a*y32 + x1a*x2a*y43) + + let d = H*(-x4a*y21*y3a + x2a*y1a*y43 - x1a*y2a*y43 - x3a*y1a*y4a + x3a*y2a*y4a) + let e = W*(x4a*y2a*y31 - x3a*y1a*y42 - x2a*y31*y4a + x1a*y3a*y42) + let f = -(W*(x4a*(Y*y2a*y31 + H*y1a*y32) - x3a*(H + Y)*y1a*y42 + H*x2a*y1a*y43 + x2a*Y*(y1a - y3a)*y4a + x1a*Y*y3a*(-y2a + y4a)) - H*X*(x4a*y21*y3a - x2a*y1a*y43 + x3a*(y1a - y2a)*y4a + x1a*y2a*(-y3a + y4a))) + + let g = H*(x3a*y21 - x4a*y21 + (-x1a + x2a)*y43) + let h = W*(-x2a*y31 + x4a*y31 + (x1a - x3a)*y42) + var i = W*Y*(x2a*y31 - x4a*y31 - x1a*y42 + x3a*y42) + H*(X*(-(x3a*y21) + x4a*y21 + x1a*y43 - x2a*y43) + W*(-(x3a*y2a) + x4a*y2a + x2a*y3a - x4a*y3a - x2a*y4a + x3a*y4a)) + + let kEpsilon = 0.0001 + + if fabs(i) < kEpsilon { + i = kEpsilon * (i > 0 ? 1.0 : -1.0) + } + + //CATransform3D transform = {a/i, d/i, 0, g/i, b/i, e/i, 0, h/i, 0, 0, 1, 0, c/i, f/i, 0, 1.0} + let transform = CATransform3D(m11: a/i, m12: d/i, m13: 0, m14: g/i, m21: b/i, m22: e/i, m23: 0, m24: h/i, m31: 0, m32: 0, m33: 1, m34: 0, m41: c/i, m42: f/i, m43: 0, m44: 1.0) + return transform +} open class SpaceWarpView: UIView { + private final class GridView: UIView { + let cloneView: PortalView + let gridPosition: CGPoint + + init?(contentView: PortalSourceView, gridPosition: CGPoint) { + self.gridPosition = gridPosition + + guard let cloneView = PortalView(matchPosition: false) else { + return nil + } + self.cloneView = cloneView + + super.init(frame: CGRect()) + + self.layer.anchorPoint = CGPoint(x: 0.0, y: 0.0) + + self.clipsToBounds = true + self.isUserInteractionEnabled = false + self.addSubview(cloneView.view) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func updateIsActive(contentView: PortalSourceView, isActive: Bool) { + if isActive { + contentView.addPortal(view: self.cloneView) + } else { + contentView.removePortal(view: self.cloneView) + } + } + + func update(containerSize: CGSize, rect: CGRect, transition: ComponentTransition) { + transition.setFrame(view: self.cloneView.view, frame: CGRect(origin: CGPoint(x: -rect.minX - containerSize.width * 0.5, y: -rect.minY - containerSize.height * 0.5), size: CGSize(width: containerSize.width, height: containerSize.height))) + } + } + private var gridViews: [GridView] = [] + + public var contentView: UIView { + return self.contentViewImpl + } + + let contentViewImpl: PortalSourceView + + private var link: SharedDisplayLinkDriver.Link? + private var startPoint: CGPoint? + + private var timeValue: CGFloat = 0.0 + private var currentActiveViews: Int = 0 + + private var resolution: (x: Int, y: Int)? + private var size: CGSize? + + override public init(frame: CGRect) { + self.contentViewImpl = PortalSourceView() + + super.init(frame: frame) + + self.addSubview(self.contentView) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func trigger(at point: CGPoint) { + self.startPoint = point + self.timeValue = 0.0 + + if self.link == nil { + self.link = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] deltaTime in + guard let self else { + return + } + self.timeValue += deltaTime * (1.0 / CGFloat(UIView.animationDurationFactor())) + + if let size = self.size { + self.update(size: size, transition: .immediate) + } + }) + } + } + + private func updateGrid(resolutionX: Int, resolutionY: Int) { + if let resolution = self.resolution, resolution.x == resolutionX, resolution.y == resolutionY { + return + } + self.resolution = (resolutionX, resolutionY) + + for gridView in self.gridViews { + gridView.removeFromSuperview() + } + + var gridViews: [GridView] = [] + for y in 0 ..< resolutionY { + for x in 0 ..< resolutionX { + if let gridView = GridView(contentView: self.contentViewImpl, gridPosition: CGPoint(x: CGFloat(x) / CGFloat(resolutionX), y: CGFloat(y) / CGFloat(resolutionY))) { + gridView.isUserInteractionEnabled = false + gridViews.append(gridView) + self.addSubview(gridView) + } + } + } + self.gridViews = gridViews + } + + public func update(size: CGSize, transition: ComponentTransition) { + self.size = size + if size.width <= 0.0 || size.height <= 0.0 { + return + } + + self.updateGrid(resolutionX: max(2, Int(size.width / 100.0)), resolutionY: max(2, Int(size.height / 100.0))) + guard let resolution = self.resolution else { + return + } + + //let pixelStep = CGPoint(x: CGFloat(resolution.x) * 0.33, y: CGFloat(resolution.y) * 0.33) + let pixelStep = CGPoint() + let itemSize = CGSize(width: size.width / CGFloat(resolution.x), height: size.height / CGFloat(resolution.y)) + + let params = RippleParams(amplitude: 22.0, frequency: 15.0, decay: 8.0, speed: 1400.0) + + var activeViews = 0 + for gridView in self.gridViews { + let sourceRect = CGRect(origin: CGPoint(x: gridView.gridPosition.x * (size.width + pixelStep.x), y: gridView.gridPosition.y * (size.height + pixelStep.y)), size: itemSize) + + gridView.bounds = CGRect(origin: CGPoint(), size: sourceRect.size) + gridView.update(containerSize: size, rect: sourceRect, transition: transition) + + let initialTopLeft = CGPoint(x: sourceRect.minX, y: sourceRect.minY) + let initialTopRight = CGPoint(x: sourceRect.maxX, y: sourceRect.minY) + let initialBottomLeft = CGPoint(x: sourceRect.minX, y: sourceRect.maxY) + let initialBottomRight = CGPoint(x: sourceRect.maxX, y: sourceRect.maxY) + + var topLeft = initialTopLeft + var topRight = initialTopRight + var bottomLeft = initialBottomLeft + var bottomRight = initialBottomRight + + if let startPoint = self.startPoint { + topLeft = transformCoordinate(position: topLeft, origin: startPoint, time: self.timeValue, params: params) + topRight = transformCoordinate(position: topRight, origin: startPoint, time: self.timeValue, params: params) + bottomLeft = transformCoordinate(position: bottomLeft, origin: startPoint, time: self.timeValue, params: params) + bottomRight = transformCoordinate(position: bottomRight, origin: startPoint, time: self.timeValue, params: params) + } + + let distanceTopLeft = length(topLeft - initialTopLeft) + let distanceTopRight = length(topRight - initialTopRight) + let distanceBottomLeft = length(bottomLeft - initialBottomLeft) + let distanceBottomRight = length(bottomRight - initialBottomRight) + var maxDistance = max(distanceTopLeft, distanceTopRight) + maxDistance = max(maxDistance, distanceBottomLeft) + maxDistance = max(maxDistance, distanceBottomRight) + + let isActive: Bool + if maxDistance <= 0.5 { + gridView.layer.transform = CATransform3DIdentity + isActive = false + } else { + let transform = rectToQuad(rect: CGRect(origin: CGPoint(), size: itemSize), quadTL: topLeft, quadTR: topRight, quadBL: bottomLeft, quadBR: bottomRight) + gridView.layer.transform = transform + isActive = true + activeViews += 1 + } + if gridView.isHidden != !isActive { + gridView.isHidden = !isActive + gridView.updateIsActive(contentView: self.contentViewImpl, isActive: isActive) + } + } + + if self.currentActiveViews != activeViews { + self.currentActiveViews = activeViews + #if DEBUG + print("SpaceWarpView: activeViews = \(activeViews)") + #endif + } + } + + + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if self.alpha.isZero || self.isHidden || !self.isUserInteractionEnabled { + return nil + } + for view in self.contentView.subviews.reversed() { + if let result = view.hitTest(self.convert(point, to: view), with: event), result.isUserInteractionEnabled { + return result + } + } + + let result = super.hitTest(point, with: event) + if result != self { + return result + } else { + return nil + } + } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift index 8e2347ed53..8ed7f10218 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift @@ -1655,9 +1655,8 @@ private final class StoryContainerScreenComponent: Component { } if case let .user(user) = slice.peer, user.botInfo != nil { - if let id = slice.item.storyItem.media.id { - let _ = component.context.engine.messages.deleteBotPreviews(peerId: slice.peer.id, ids: [id]).startStandalone() - } + //TODO:localize + let _ = component.context.engine.messages.deleteBotPreviews(peerId: slice.peer.id, language: nil, media: [slice.item.storyItem.media._asMedia()]).startStandalone() } else { let _ = component.context.engine.messages.deleteStories(peerId: slice.peer.id, ids: [slice.item.storyItem.id]).startStandalone() } diff --git a/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift b/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift index 2a2536c59c..4f47e53ca3 100644 --- a/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift +++ b/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift @@ -22,11 +22,13 @@ public final class TabSelectorComponent: Component { public var font: UIFont public var spacing: CGFloat public var lineSelection: Bool + public var verticalInset: CGFloat - public init(font: UIFont, spacing: CGFloat, lineSelection: Bool = false) { + public init(font: UIFont, spacing: CGFloat, lineSelection: Bool = false, verticalInset: CGFloat = 0.0) { self.font = font self.spacing = spacing self.lineSelection = lineSelection + self.verticalInset = verticalInset } } @@ -92,7 +94,7 @@ public final class TabSelectorComponent: Component { } } - public final class View: UIView { + public final class View: UIScrollView { private var component: TabSelectorComponent? private weak var state: EmptyComponentState? @@ -104,6 +106,14 @@ public final class TabSelectorComponent: Component { super.init(frame: frame) + self.showsVerticalScrollIndicator = false + self.showsHorizontalScrollIndicator = false + self.scrollsToTop = false + self.delaysContentTouches = false + self.canCancelContentTouches = true + self.contentInsetAdjustmentBehavior = .never + self.alwaysBounceVertical = false + self.addSubview(self.selectionView) } @@ -114,6 +124,10 @@ public final class TabSelectorComponent: Component { deinit { } + override public func touchesShouldCancel(in view: UIView) -> Bool { + return true + } + func update(component: TabSelectorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let selectionColorUpdated = component.colors.selection != self.component?.colors.selection @@ -121,6 +135,12 @@ public final class TabSelectorComponent: Component { self.state = state let baseHeight: CGFloat = 28.0 + + var verticalInset: CGFloat = 0.0 + if let customLayout = component.customLayout { + verticalInset = customLayout.verticalInset * 2.0 + } + let innerInset: CGFloat = 12.0 let spacing: CGFloat = component.customLayout?.spacing ?? 2.0 @@ -148,7 +168,7 @@ public final class TabSelectorComponent: Component { } } - var contentWidth: CGFloat = 0.0 + var contentWidth: CGFloat = spacing var previousBackgroundRect: CGRect? var selectedBackgroundRect: CGRect? var nextBackgroundRect: CGRect? @@ -213,8 +233,8 @@ public final class TabSelectorComponent: Component { if !contentWidth.isZero { contentWidth += spacing } - let itemTitleFrame = CGRect(origin: CGPoint(x: contentWidth + innerInset, y: floor((baseHeight - itemSize.height) * 0.5)), size: itemSize) - let itemBackgroundRect = CGRect(origin: CGPoint(x: contentWidth, y: 0.0), size: CGSize(width: innerInset + itemSize.width + innerInset, height: baseHeight)) + let itemTitleFrame = CGRect(origin: CGPoint(x: contentWidth + innerInset, y: verticalInset + floor((baseHeight - itemSize.height) * 0.5)), size: itemSize) + let itemBackgroundRect = CGRect(origin: CGPoint(x: contentWidth, y: verticalInset), size: CGSize(width: innerInset + itemSize.width + innerInset, height: baseHeight)) contentWidth = itemBackgroundRect.maxX if item.id == component.selectedId { @@ -237,6 +257,7 @@ public final class TabSelectorComponent: Component { } index += 1 } + contentWidth += spacing var removeIds: [AnyHashable] = [] for (id, itemView) in self.visibleItems { @@ -277,7 +298,14 @@ public final class TabSelectorComponent: Component { self.selectionView.alpha = 0.0 } - return CGSize(width: contentWidth, height: baseHeight) + self.contentSize = CGSize(width: contentWidth, height: baseHeight + verticalInset * 2.0) + self.disablesInteractiveTransitionGestureRecognizer = contentWidth > availableSize.width + + if let selectedBackgroundRect { + self.scrollRectToVisible(selectedBackgroundRect.insetBy(dx: -spacing, dy: 0.0), animated: false) + } + + return CGSize(width: min(contentWidth, availableSize.width), height: baseHeight + verticalInset * 2.0) } } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 6415b406e9..c3410e9c36 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -1816,8 +1816,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let context = self?.context, let navigationController = self?.effectiveNavigationController { let _ = context.sharedContext.navigateToForumThread(context: context, peerId: peerId, threadId: threadId, messageId: messageId, navigationController: navigationController, activateInput: nil, scrollToEndIfExists: false, keepStack: .always).startStandalone() } - }, tapMessage: nil, clickThroughMessage: { [weak self] in - self?.chatDisplayNode.dismissInput() + }, tapMessage: nil, clickThroughMessage: { [weak self] view, location in + self?.chatDisplayNode.dismissInput(view: view, location: location) }, toggleMessagesSelection: { [weak self] ids, value in guard let strongSelf = self, strongSelf.isNodeLoaded else { return diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 46432255a9..3c84f133a8 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -43,6 +43,7 @@ import ChatInlineSearchResultsListComponent import ComponentDisplayAdapters import ComponentFlow import ChatEmptyNode +import SpaceWarpView final class VideoNavigationControllerDropContentItem: NavigationControllerDropContentItem { let itemNode: OverlayMediaItemNode @@ -86,6 +87,41 @@ private struct ChatControllerNodeDerivedLayoutState { var upperInputPositionBound: CGFloat? } +class ChatNodeContainer: ASDisplayNode { + private let contentNodeImpl: ASDisplayNode + + var contentNode: ASDisplayNode { + if self.view is SpaceWarpView { + return self.contentNodeImpl + } else { + return self + } + } + + override init() { + self.contentNodeImpl = ASDisplayNode() + + super.init() + + #if DEBUG && false + self.setViewBlock({ + return SpaceWarpView(frame: CGRect()) + }) + #endif + + (self.view as? SpaceWarpView)?.contentView.addSubnode(self.contentNodeImpl) + } + + func triggerRipple(at point: CGPoint) { + (self.view as? SpaceWarpView)?.trigger(at: point) + } + + func update(size: CGSize, transition: ContainedViewLayoutTransition) { + transition.updateFrame(node: self.contentNodeImpl, frame: CGRect(origin: CGPoint(), size: size)) + (self.view as? SpaceWarpView)?.update(size: size, transition: ComponentTransition(transition)) + } +} + class HistoryNodeContainer: ASDisplayNode { var isSecret: Bool { didSet { @@ -95,7 +131,19 @@ class HistoryNodeContainer: ASDisplayNode { } } + private let contentNodeImpl: ASDisplayNode + + var contentNode: ASDisplayNode { + if self.view is SpaceWarpView { + return self.contentNodeImpl + } else { + return self + } + } + init(isSecret: Bool) { + self.contentNodeImpl = ASDisplayNode() + self.isSecret = isSecret super.init() @@ -103,6 +151,23 @@ class HistoryNodeContainer: ASDisplayNode { if self.isSecret { setLayerDisableScreenshots(self.layer, self.isSecret) } + + #if DEBUG && false + self.setViewBlock({ + return SpaceWarpView(frame: CGRect()) + }) + #endif + + (self.view as? SpaceWarpView)?.contentView.addSubnode(self.contentNodeImpl) + } + + func triggerRipple(at point: CGPoint) { + (self.view as? SpaceWarpView)?.trigger(at: point) + } + + func update(size: CGSize, transition: ContainedViewLayoutTransition) { + transition.updateFrame(node: self.contentNodeImpl, frame: CGRect(origin: CGPoint(), size: size)) + (self.view as? SpaceWarpView)?.update(size: size, transition: ComponentTransition(transition)) } } @@ -127,12 +192,12 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } } - let contentContainerNode: ASDisplayNode + let contentContainerNode: ChatNodeContainer let contentDimNode: ASDisplayNode let backgroundNode: WallpaperBackgroundNode let historyNode: ChatHistoryListNodeImpl var blurredHistoryNode: ASImageNode? - let historyNodeContainer: ASDisplayNode + let historyNodeContainer: HistoryNodeContainer let loadingNode: ChatLoadingNode private(set) var loadingPlaceholderNode: ChatLoadingPlaceholderNode? @@ -379,7 +444,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { self.backgroundNode = backgroundNode - self.contentContainerNode = ASDisplayNode() + self.contentContainerNode = ChatNodeContainer() self.contentDimNode = ASDisplayNode() self.contentDimNode.isUserInteractionEnabled = false self.contentDimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.2) @@ -633,7 +698,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { self.historyNodeContainer = HistoryNodeContainer(isSecret: chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat) - self.historyNodeContainer.addSubnode(self.historyNode) + self.historyNodeContainer.contentNode.addSubnode(self.historyNode) var getContentAreaInScreenSpaceImpl: (() -> CGRect)? var onTransitionEventImpl: ((ContainedViewLayoutTransition) -> Void)? @@ -787,11 +852,11 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { self.historyNode.enableExtractedBackgrounds = true self.addSubnode(self.contentContainerNode) - self.contentContainerNode.addSubnode(self.backgroundNode) - self.contentContainerNode.addSubnode(self.historyNodeContainer) + self.contentContainerNode.contentNode.addSubnode(self.backgroundNode) + self.contentContainerNode.contentNode.addSubnode(self.historyNodeContainer) if let navigationBar = self.navigationBar { - self.contentContainerNode.addSubnode(navigationBar) + self.contentContainerNode.contentNode.addSubnode(navigationBar) } self.inputPanelContainerNode.expansionUpdated = { [weak self] transition in @@ -817,9 +882,9 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { self.inputPanelBackgroundNode.addSubnode(self.inputPanelBottomBackgroundSeparatorNode) self.addSubnode(self.messageTransitionNode) - self.contentContainerNode.addSubnode(self.navigateButtons) + self.contentContainerNode.contentNode.addSubnode(self.navigateButtons) self.addSubnode(self.presentationContextMarker) - self.contentContainerNode.addSubnode(self.contentDimNode) + self.contentContainerNode.contentNode.addSubnode(self.contentDimNode) self.navigationBar?.additionalContentNode.addSubnode(self.titleAccessoryPanelContainer) @@ -1004,9 +1069,9 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { self.emptyNode = emptyNode if let inlineSearchResultsView = self.inlineSearchResults?.view { - self.contentContainerNode.view.insertSubview(emptyNode.view, belowSubview: inlineSearchResultsView) + self.contentContainerNode.contentNode.view.insertSubview(emptyNode.view, belowSubview: inlineSearchResultsView) } else { - self.contentContainerNode.insertSubnode(emptyNode, aboveSubnode: self.historyNodeContainer) + self.contentContainerNode.contentNode.insertSubnode(emptyNode, aboveSubnode: self.historyNodeContainer) } if let (size, insets) = self.validEmptyNodeLayout { @@ -1081,12 +1146,10 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } } - if let historyNodeContainer = self.historyNodeContainer as? HistoryNodeContainer { - let isSecret = self.chatPresentationInterfaceState.copyProtectionEnabled || self.chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat - if historyNodeContainer.isSecret != isSecret { - historyNodeContainer.isSecret = isSecret - setLayerDisableScreenshots(self.titleAccessoryPanelContainer.layer, isSecret) - } + let isSecret = self.chatPresentationInterfaceState.copyProtectionEnabled || self.chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat + if self.historyNodeContainer.isSecret != isSecret { + self.historyNodeContainer.isSecret = isSecret + setLayerDisableScreenshots(self.titleAccessoryPanelContainer.layer, isSecret) } var previousListBottomInset: CGFloat? @@ -1097,6 +1160,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { self.messageTransitionNode.frame = CGRect(origin: CGPoint(), size: layout.size) self.contentContainerNode.frame = CGRect(origin: CGPoint(), size: layout.size) + self.contentContainerNode.update(size: layout.size, transition: transition) let isOverlay: Bool switch self.chatPresentationInterfaceState.mode { @@ -1239,10 +1303,10 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { if let containerNode = self.containerNode { self.containerNode = nil containerNode.removeFromSupernode() - self.contentContainerNode.insertSubnode(self.backgroundNode, at: 0) - self.contentContainerNode.insertSubnode(self.historyNodeContainer, aboveSubnode: self.backgroundNode) + self.contentContainerNode.contentNode.insertSubnode(self.backgroundNode, at: 0) + self.contentContainerNode.contentNode.insertSubnode(self.historyNodeContainer, aboveSubnode: self.backgroundNode) if let restrictedNode = self.restrictedNode { - self.contentContainerNode.insertSubnode(restrictedNode, aboveSubnode: self.historyNodeContainer) + self.contentContainerNode.contentNode.insertSubnode(restrictedNode, aboveSubnode: self.historyNodeContainer) } self.navigationBar?.isHidden = false } @@ -1392,7 +1456,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { if self.chatImportStatusPanel != importStatusPanelNode { dismissedImportStatusPanelNode = self.chatImportStatusPanel self.chatImportStatusPanel = importStatusPanelNode - self.contentContainerNode.addSubnode(importStatusPanelNode) + self.contentContainerNode.contentNode.addSubnode(importStatusPanelNode) } importStatusPanelHeight = importStatusPanelNode.update(context: self.context, progress: CGFloat(importState.progress), presentationData: ChatPresentationData(theme: ChatPresentationThemeData(theme: self.chatPresentationInterfaceState.theme, wallpaper: self.chatPresentationInterfaceState.chatWallpaper), fontSize: self.chatPresentationInterfaceState.fontSize, strings: self.chatPresentationInterfaceState.strings, dateTimeFormat: self.chatPresentationInterfaceState.dateTimeFormat, nameDisplayOrder: self.chatPresentationInterfaceState.nameDisplayOrder, disableAnimations: false, largeEmoji: false, chatBubbleCorners: PresentationChatBubbleCorners(mainRadius: 0.0, auxiliaryRadius: 0.0, mergeBubbleCorners: false)), width: layout.size.width) @@ -1732,6 +1796,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { transition.updateBounds(node: self.historyNodeContainer, bounds: contentBounds) transition.updatePosition(node: self.historyNodeContainer, position: contentBounds.center) + self.historyNodeContainer.update(size: contentBounds.size, transition: transition) transition.updateBounds(node: self.historyNode, bounds: CGRect(origin: CGPoint(), size: contentBounds.size)) transition.updatePosition(node: self.historyNode, position: CGPoint(x: contentBounds.size.width / 2.0, y: contentBounds.size.height / 2.0)) @@ -1779,7 +1844,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { dismissedOverlayContextPanelNode = self.overlayContextPanelNode self.overlayContextPanelNode = overlayContextPanelNode - self.contentContainerNode.addSubnode(overlayContextPanelNode) + self.contentContainerNode.contentNode.addSubnode(overlayContextPanelNode) immediatelyLayoutOverlayContextPanelAndAnimateAppearance = true } } else if let overlayContextPanelNode = self.overlayContextPanelNode { @@ -1999,7 +2064,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { expandedInputDimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) expandedInputDimNode.alpha = 0.0 self.expandedInputDimNode = expandedInputDimNode - self.contentContainerNode.insertSubnode(expandedInputDimNode, aboveSubnode: self.historyNodeContainer) + self.contentContainerNode.contentNode.insertSubnode(expandedInputDimNode, aboveSubnode: self.historyNodeContainer) transition.updateAlpha(node: expandedInputDimNode, alpha: 1.0) expandedInputDimNode.frame = exandedFrame transition.animatePositionAdditive(node: expandedInputDimNode, offset: CGPoint(x: 0.0, y: previousInputPanelOrigin.y - inputPanelOrigin)) @@ -2831,9 +2896,9 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { self.skippedShowSearchResultsAsListAnimationOnce = true inlineSearchResultsView.layer.allowsGroupOpacity = true if let emptyNode = self.emptyNode { - self.contentContainerNode.view.insertSubview(inlineSearchResultsView, aboveSubview: emptyNode.view) + self.contentContainerNode.contentNode.view.insertSubview(inlineSearchResultsView, aboveSubview: emptyNode.view) } else { - self.contentContainerNode.view.insertSubview(inlineSearchResultsView, aboveSubview: self.historyNodeContainer.view) + self.contentContainerNode.contentNode.view.insertSubview(inlineSearchResultsView, aboveSubview: self.historyNodeContainer.view) } } inlineSearchResultsTransition.setFrame(view: inlineSearchResultsView, frame: CGRect(origin: CGPoint(), size: layout.size)) @@ -3269,15 +3334,20 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { if recognizer.state == .ended { - self.dismissInput() + self.dismissInput(view: self.view, location: recognizer.location(in: self.contentContainerNode.view)) } } - func dismissInput() { + func dismissInput(view: UIView? = nil, location: CGPoint? = nil) { if let _ = self.chatPresentationInterfaceState.inputTextPanelState.mediaRecordingState { return } + if let view, let location { + self.contentContainerNode.triggerRipple(at: self.contentContainerNode.view.convert(location, from: view)) + self.historyNodeContainer.triggerRipple(at: self.historyNodeContainer.view.convert(location, from: view)) + } + switch self.chatPresentationInterfaceState.inputMode { case .none: break @@ -3739,7 +3809,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { let dropDimNode = ASDisplayNode() dropDimNode.backgroundColor = self.chatPresentationInterfaceState.theme.chatList.backgroundColor.withAlphaComponent(0.35) self.dropDimNode = dropDimNode - self.contentContainerNode.addSubnode(dropDimNode) + self.contentContainerNode.contentNode.addSubnode(dropDimNode) if let (layout, _) = self.validLayout { dropDimNode.frame = CGRect(origin: CGPoint(), size: layout.size) dropDimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index 184ee1c503..c731e46943 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -84,7 +84,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu }, navigateToMessage: { _, _, _ in }, navigateToMessageStandalone: { _ in }, navigateToThreadMessage: { _, _, _ in - }, tapMessage: nil, clickThroughMessage: { + }, tapMessage: nil, clickThroughMessage: { _, _ in }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _, _ in }, sendMessage: { _ in diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 3eda83a6f9..7069a9b6f8 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -69,6 +69,7 @@ import StarsPurchaseScreen import StarsTransferScreen import StarsTransactionScreen import StarsWithdrawalScreen +import MiniAppListScreen private final class AccountUserInterfaceInUseContext { let subscribers = Bag<(Bool) -> Void>() @@ -1701,7 +1702,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { return presentAddMembersImpl(context: context, updatedPresentationData: updatedPresentationData, parentController: parentController, groupPeer: groupPeer, selectAddMemberDisposable: selectAddMemberDisposable, addMemberDisposable: addMemberDisposable) } - public func makeChatMessagePreviewItem(context: AccountContext, messages: [Message], theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder, forcedResourceStatus: FileMediaResourceStatus?, tapMessage: ((Message) -> Void)?, clickThroughMessage: (() -> Void)? = nil, backgroundNode: ASDisplayNode?, availableReactions: AvailableReactions?, accountPeer: Peer?, isCentered: Bool, isPreview: Bool, isStandalone: Bool) -> ListViewItem { + public func makeChatMessagePreviewItem(context: AccountContext, messages: [Message], theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder, forcedResourceStatus: FileMediaResourceStatus?, tapMessage: ((Message) -> Void)?, clickThroughMessage: ((UIView?, CGPoint?) -> Void)? = nil, backgroundNode: ASDisplayNode?, availableReactions: AvailableReactions?, accountPeer: Peer?, isCentered: Bool, isPreview: Bool, isStandalone: Bool) -> ListViewItem { let controllerInteraction: ChatControllerInteraction controllerInteraction = ChatControllerInteraction(openMessage: { _, _ in @@ -1711,8 +1712,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { }, navigateToThreadMessage: { _, _, _ in }, tapMessage: { message in tapMessage?(message) - }, clickThroughMessage: { - clickThroughMessage?() + }, clickThroughMessage: { view, location in + clickThroughMessage?(view, location) }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _, _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _, _ in }, openUrl: { _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in @@ -2727,6 +2728,14 @@ public final class SharedAccountContextImpl: SharedAccountContext { return StarsTransactionScreen(context: context, subject: .gift(message), action: {}) } + public func makeMiniAppListScreenInitialData(context: AccountContext) -> Signal { + return MiniAppListScreen.initialData(context: context) + } + + public func makeMiniAppListScreen(context: AccountContext, initialData: MiniAppListScreenInitialData) -> ViewController { + return MiniAppListScreen(context: context, initialData: initialData as! MiniAppListScreen.InitialData) + } + public func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peer: EnginePeer, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool) { openWebAppImpl(context: context, parentController: parentController, updatedPresentationData: updatedPresentationData, peer: peer, threadId: threadId, buttonText: buttonText, url: url, simple: simple, source: source, skipTermsOfService: skipTermsOfService) } diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index 610a6fbd86..5fffa2db5f 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -393,7 +393,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon return nil case let .peer(id): return id - case let .botPreview(id): + case let .botPreview(id, _): return id } } diff --git a/submodules/TranslateUI/Sources/LocalizationListItem.swift b/submodules/TranslateUI/Sources/LocalizationListItem.swift index 1ee42bf8e3..dc60b5f59d 100644 --- a/submodules/TranslateUI/Sources/LocalizationListItem.swift +++ b/submodules/TranslateUI/Sources/LocalizationListItem.swift @@ -37,10 +37,10 @@ public class LocalizationListItem: ListViewItem, ItemListItem { public let sectionId: ItemListSectionId let alwaysPlain: Bool let action: () -> Void - let setItemWithRevealedOptions: (String?, String?) -> Void - let removeItem: (String) -> Void + let setItemWithRevealedOptions: ((String?, String?) -> Void)? + let removeItem: ((String) -> Void)? - public init(presentationData: ItemListPresentationData, id: String, title: String, subtitle: String, checked: Bool, activity: Bool, loading: Bool, editing: LocalizationListItemEditing, enabled: Bool = true, sectionId: ItemListSectionId, alwaysPlain: Bool, action: @escaping () -> Void, setItemWithRevealedOptions: @escaping (String?, String?) -> Void, removeItem: @escaping (String) -> Void) { + public init(presentationData: ItemListPresentationData, id: String, title: String, subtitle: String, checked: Bool, activity: Bool, loading: Bool, editing: LocalizationListItemEditing, enabled: Bool = true, sectionId: ItemListSectionId, alwaysPlain: Bool, action: @escaping () -> Void, setItemWithRevealedOptions: ((String?, String?) -> Void)?, removeItem: ((String) -> Void)?) { self.presentationData = presentationData self.id = id self.title = title @@ -368,7 +368,7 @@ class LocalizationListItemNode: ItemListRevealOptionsItemNode { strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) - if item.editing.editable { + if item.editing.editable, item.removeItem != nil { strongSelf.setRevealOptions((left: [], right: [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)])) } else { strongSelf.setRevealOptions((left: [], right: [])) @@ -491,13 +491,13 @@ class LocalizationListItemNode: ItemListRevealOptionsItemNode { override func revealOptionsInteractivelyOpened() { if let item = self.item { - item.setItemWithRevealedOptions(item.id, nil) + item.setItemWithRevealedOptions?(item.id, nil) } } override func revealOptionsInteractivelyClosed() { if let item = self.item { - item.setItemWithRevealedOptions(nil, item.id) + item.setItemWithRevealedOptions?(nil, item.id) } } @@ -506,7 +506,7 @@ class LocalizationListItemNode: ItemListRevealOptionsItemNode { self.revealOptionsInteractivelyClosed() if let item = self.item { - item.removeItem(item.id) + item.removeItem?(item.id) } } } From 1c7834ad574326e1c64bdb83b4c42a05b178aa3b Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 23 Jul 2024 22:01:56 +0400 Subject: [PATCH 02/41] Various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 17 + .../Sources/AccountContext.swift | 2 + .../Sources/ChatController.swift | 2 + submodules/BrowserUI/BUILD | 7 + .../Sources/BrowserAddressBarComponent.swift | 256 ++++++--- .../Sources/BrowserAddressListComponent.swift | 466 ++++++++++++++++ .../BrowserAddressListItemComponent.swift | 251 +++++++++ .../Sources/BrowserBookmarksScreen.swift | 517 ++++++++++++++++++ .../BrowserUI/Sources/BrowserContent.swift | 31 +- .../Sources/BrowserDocumentContent.swift | 471 ++++++++++++++++ .../Sources/BrowserInstantPageContent.swift | 6 + .../BrowserNavigationBarComponent.swift | 44 +- .../BrowserUI/Sources/BrowserPdfContent.swift | 463 ++++++++++++++++ .../Sources/BrowserRecentlyVisited.swift | 88 +++ .../BrowserUI/Sources/BrowserScreen.swift | 272 ++++++--- .../Sources/BrowserSearchBarComponent.swift | 4 +- .../Sources/BrowserTitleBarComponent.swift | 85 +++ .../BrowserUI/Sources/BrowserWebContent.swift | 294 +++++++--- submodules/BrowserUI/Sources/Favicon.swift | 38 -- .../Sources/SectionHeaderComponent.swift | 168 ++++++ submodules/BrowserUI/Sources/Utils.swift | 110 ++++ .../Navigation/MinimizedContainer.swift | 12 + .../Source/Navigation/NavigationLayout.swift | 9 + .../Display/Source/ViewController.swift | 1 + .../DrawingUI/Sources/DrawingScreen.swift | 6 +- .../Sources/OpenInOptions.swift | 2 +- .../WebBrowserDomainController.swift | 35 +- .../WebBrowserDomainExceptionItem.swift | 113 +++- .../WebBrowserSettingsController.swift | 127 ++++- .../Drawing/CodableDrawingEntity.swift | 12 +- .../Sources/MediaEditorScreen.swift | 71 ++- .../Sources/MinimizedContainer.swift | 3 + .../Sources/ShareWithPeersScreen.swift | 30 +- .../Resources/WebEmbed/UIWebViewSearch.js | 46 ++ .../Chat/ChatControllerOpenStorySharing.swift | 98 ---- .../TelegramUI/Sources/OpenChatMessage.swift | 13 +- .../TelegramUI/Sources/OpenResolvedUrl.swift | 10 +- submodules/TelegramUI/Sources/OpenUrl.swift | 25 +- .../Sources/SharedAccountContext.swift | 29 + .../Sources/PostboxKeys.swift | 2 + .../Sources/WebBrowserSettings.swift | 10 +- submodules/WebUI/BUILD | 1 + .../WebUI/Sources/WebAppController.swift | 79 +++ 43 files changed, 3867 insertions(+), 459 deletions(-) create mode 100644 submodules/BrowserUI/Sources/BrowserAddressListComponent.swift create mode 100644 submodules/BrowserUI/Sources/BrowserAddressListItemComponent.swift create mode 100644 submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift create mode 100644 submodules/BrowserUI/Sources/BrowserDocumentContent.swift create mode 100644 submodules/BrowserUI/Sources/BrowserPdfContent.swift create mode 100644 submodules/BrowserUI/Sources/BrowserRecentlyVisited.swift create mode 100644 submodules/BrowserUI/Sources/BrowserTitleBarComponent.swift delete mode 100644 submodules/BrowserUI/Sources/Favicon.swift create mode 100644 submodules/BrowserUI/Sources/SectionHeaderComponent.swift create mode 100644 submodules/BrowserUI/Sources/Utils.swift diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index b181677f70..5f8a32624e 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -12581,6 +12581,23 @@ Sorry for the inconvenience."; "WebBrowser.Exceptions.Create.Text" = "Enter a domain that you don't want to be opened in the in-app browser."; "WebBrowser.Exceptions.Create.Placeholder" = "Enter URL"; +"WebBrowser.Exceptions.ClearConfirmation.Text" = "Are you sure you want to clear this list?"; +"WebBrowser.Exceptions.ClearConfirmation.Clear" = "Clear"; + "WebBrowser.Done" = "Done"; "AccessDenied.LocationWeather" = "Telegram needs access to your location so that you can add the weather widget to your stories.\n\nPlease go to Settings > Privacy > Location Services and set Telegram to ON."; + +"Story.Editor.TooltipWeatherLimitValue_1" = "**%@** weather stickers"; +"Story.Editor.TooltipWeatherLimitValue_any" = "**%@** weather stickers"; +"Story.Editor.TooltipWeatherLimitText" = "You can't add more than %@ to a story."; + +"WebBrowser.AddressPlaceholder" = "Enter URL"; + +"WebBrowser.Bookmarks.Title" = "Bookmarks"; +"WebBrowser.Bookmarks.BookmarkCurrent" = "Bookmark Current Page"; + +"Story.Privacy.ChooseCover" = "Choose Story Cover"; +"Story.Privacy.ChooseCoverInfo" = "Choose a frame from the story to show in your Profile."; +"Story.Privacy.ChooseCoverChannelInfo" = "Choose a frame from the story to show in channel profile."; +"Story.Privacy.ChooseCoverGroupInfo" = "Choose a frame from the story to show in group profile."; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 8c04d63ab5..602a697cde 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -978,6 +978,8 @@ public protocol SharedAccountContext: AnyObject { func makeMediaPickerScreen(context: AccountContext, hasSearch: Bool, completion: @escaping (Any) -> Void) -> ViewController + func makeStoryMediaEditorScreen(context: AccountContext, source: Any?, text: String?, link: String?, completion: @escaping (MediaEditorScreenResult, @escaping (@escaping () -> Void) -> Void) -> Void) -> ViewController + func makeBotPreviewEditorScreen(context: AccountContext, source: Any?, target: Stories.PendingTarget, transitionArguments: (UIView, CGRect, UIImage?)?, transitionOut: @escaping () -> BotPreviewEditorTransitionOut?, externalState: MediaEditorTransitionOutExternalState, completion: @escaping (MediaEditorScreenResult, @escaping (@escaping () -> Void) -> Void) -> Void, cancelled: @escaping () -> Void) -> ViewController func makeStickerEditorScreen(context: AccountContext, source: Any?, intro: Bool, transitionArguments: (UIView, CGRect, UIImage?)?, completion: @escaping (TelegramMediaFile, [String], @escaping () -> Void) -> Void, cancelled: @escaping () -> Void) -> ViewController diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index 6f38e5b00e..3840144291 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -1187,4 +1187,6 @@ public protocol ChatHistoryListNode: ListView { func scrollToEndOfHistory() func updateLayout(transition: ContainedViewLayoutTransition, updateSizeAndInsets: ListViewUpdateSizeAndInsets) func messageInCurrentHistoryView(_ id: MessageId) -> Message? + + var contentPositionChanged: (ListViewVisibleContentOffset) -> Void { get set } } diff --git a/submodules/BrowserUI/BUILD b/submodules/BrowserUI/BUILD index e28f412f44..d143a87202 100644 --- a/submodules/BrowserUI/BUILD +++ b/submodules/BrowserUI/BUILD @@ -39,6 +39,13 @@ swift_library( "//submodules/Svg", "//submodules/PromptUI", "//submodules/TelegramUI/Components/LottieComponent", + "//submodules/PhotoResources", + "//submodules/TelegramUI/Components/ChatControllerInteraction", + "//submodules/ChatPresentationInterfaceState", + "//submodules/UrlWhitelist", + "//submodules/TelegramUI/Components/Chat/ChatHistorySearchContainerNode", + "//submodules/SearchUI", + "//submodules/SearchBarNode", ], visibility = [ "//visibility:public", diff --git a/submodules/BrowserUI/Sources/BrowserAddressBarComponent.swift b/submodules/BrowserUI/Sources/BrowserAddressBarComponent.swift index 51618066fd..679f3ea5c3 100644 --- a/submodules/BrowserUI/Sources/BrowserAddressBarComponent.swift +++ b/submodules/BrowserUI/Sources/BrowserAddressBarComponent.swift @@ -3,25 +3,36 @@ import UIKit import AsyncDisplayKit import Display import ComponentFlow +import SwiftSignalKit import TelegramPresentationData import AccountContext import BundleIconComponent +import MultilineTextComponent +import UrlEscaping final class AddressBarContentComponent: Component { + public typealias EnvironmentType = BrowserNavigationBarEnvironment + let theme: PresentationTheme let strings: PresentationStrings let url: String + let isSecure: Bool + let isExpanded: Bool let performAction: ActionSlot init( theme: PresentationTheme, strings: PresentationStrings, url: String, + isSecure: Bool, + isExpanded: Bool, performAction: ActionSlot ) { self.theme = theme self.strings = strings self.url = url + self.isSecure = isSecure + self.isExpanded = isExpanded self.performAction = performAction } @@ -35,6 +46,12 @@ final class AddressBarContentComponent: Component { if lhs.url != rhs.url { return false } + if lhs.isSecure != rhs.isSecure { + return false + } + if lhs.isExpanded != rhs.isExpanded { + return false + } return true } @@ -43,12 +60,24 @@ final class AddressBarContentComponent: Component { override func textRect(forBounds bounds: CGRect) -> CGRect { return bounds.integral } + + override var canBecomeFirstResponder: Bool { + var canBecomeFirstResponder = super.canBecomeFirstResponder + if !canBecomeFirstResponder && self.alpha.isZero { + canBecomeFirstResponder = true + } + return canBecomeFirstResponder + } } private struct Params: Equatable { var theme: PresentationTheme var strings: PresentationStrings var size: CGSize + var isActive: Bool + var title: String + var isSecure: Bool + var collapseFraction: CGFloat static func ==(lhs: Params, rhs: Params) -> Bool { if lhs.theme !== rhs.theme { @@ -60,14 +89,25 @@ final class AddressBarContentComponent: Component { if lhs.size != rhs.size { return false } + if lhs.isActive != rhs.isActive { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.isSecure != rhs.isSecure { + return false + } + if lhs.collapseFraction != rhs.collapseFraction { + return false + } return true } } private let activated: (Bool) -> Void = { _ in } private let deactivated: (Bool) -> Void = { _ in } - private let updateQuery: (String?) -> Void = { _ in } - + private let backgroundLayer: SimpleLayer private let iconView: UIImageView @@ -79,10 +119,11 @@ final class AddressBarContentComponent: Component { private let cancelButton: HighlightTrackingButton private var placeholderContent = ComponentView() + private var titleContent = ComponentView() private var textFrame: CGRect? private var textField: TextField? - + private var tapRecognizer: UITapGestureRecognizer? private var params: Params? @@ -99,12 +140,12 @@ final class AddressBarContentComponent: Component { self.clearIconView = UIImageView() self.clearIconButton = HighlightableButton() - self.clearIconView.isHidden = true - self.clearIconButton.isHidden = true + self.clearIconView.isHidden = false + self.clearIconButton.isHidden = false self.cancelButtonTitle = ComponentView() self.cancelButton = HighlightTrackingButton() - + super.init(frame: CGRect()) self.layer.addSublayer(self.backgroundLayer) @@ -156,76 +197,49 @@ final class AddressBarContentComponent: Component { } @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - self.activateTextInput() + if case .ended = recognizer.state, let component = self.component, !component.isExpanded { + component.performAction.invoke(.openAddressBar) } } private func activateTextInput() { - if self.textField == nil, let textFrame = self.textFrame { - let backgroundFrame = self.backgroundLayer.frame - let textFieldFrame = CGRect(origin: CGPoint(x: textFrame.minX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.maxX - textFrame.minX, height: backgroundFrame.height)) - - let textField = TextField(frame: textFieldFrame) - textField.autocorrectionType = .no - textField.returnKeyType = .search - self.textField = textField - self.insertSubview(textField, belowSubview: self.clearIconView) - textField.delegate = self - textField.addTarget(self, action: #selector(self.textFieldChanged(_:)), for: .editingChanged) - } - - guard !(self.textField?.isFirstResponder ?? false) else { - return - } - self.activated(true) - - self.textField?.becomeFirstResponder() + if let textField = self.textField { + textField.becomeFirstResponder() + Queue.mainQueue().justDispatch { + textField.selectAll(nil) + } + } + } + + private func deactivateTextInput() { + self.textField?.endEditing(true) } @objc private func cancelPressed() { - self.updateQuery(nil) + self.deactivated(self.textField?.isFirstResponder ?? false) - self.clearIconView.isHidden = true - self.clearIconButton.isHidden = true - - let textField = self.textField - self.textField = nil - - self.deactivated(textField?.isFirstResponder ?? false) - - self.component?.performAction.invoke(.updateSearchActive(false)) - - if let textField { - textField.resignFirstResponder() - textField.removeFromSuperview() - } + self.component?.performAction.invoke(.closeAddressBar) } @objc private func clearPressed() { - self.updateQuery(nil) - self.textField?.text = "" - - self.clearIconView.isHidden = true - self.clearIconButton.isHidden = true - } - - func deactivate() { - if let text = self.textField?.text, !text.isEmpty { - self.textField?.endEditing(true) - } else { - self.cancelPressed() + guard let textField = self.textField else { + return } + textField.text = "" + self.textFieldChanged(textField) } - + public func textFieldDidBeginEditing(_ textField: UITextField) { } public func textFieldDidEndEditing(_ textField: UITextField) { } - + public func textFieldShouldReturn(_ textField: UITextField) -> Bool { + if let component = self.component { + component.performAction.invoke(.navigateTo(explicitUrl(textField.text ?? ""))) + } textField.endEditing(true) return false } @@ -237,38 +251,53 @@ final class AddressBarContentComponent: Component { self.clearIconButton.isHidden = text.isEmpty self.placeholderContent.view?.isHidden = !text.isEmpty - self.updateQuery(text) - - self.component?.performAction.invoke(.updateSearchQuery(text)) - if let params = self.params { - self.update(theme: params.theme, strings: params.strings, size: params.size, transition: .immediate) + self.update(theme: params.theme, strings: params.strings, size: params.size, isActive: params.isActive, title: params.title, isSecure: params.isSecure, collapseFraction: params.collapseFraction, transition: .immediate) } } - func update(component: AddressBarContentComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { + func update(component: AddressBarContentComponent, availableSize: CGSize, environment: Environment, transition: ComponentTransition) -> CGSize { + let collapseFraction = environment[BrowserNavigationBarEnvironment.self].fraction + + let wasExpanded = self.component?.isExpanded ?? false self.component = component - self.update(theme: component.theme, strings: component.strings, size: availableSize, transition: transition) + if !wasExpanded && component.isExpanded { + self.activateTextInput() + } + if wasExpanded && !component.isExpanded { + self.deactivateTextInput() + } + let isActive = self.textField?.isFirstResponder ?? false + + var title: String = "" + if let parsedUrl = URL(string: component.url) { + title = parsedUrl.host ?? component.url + } + self.update(theme: component.theme, strings: component.strings, size: availableSize, isActive: isActive, title: title.lowercased(), isSecure: component.isSecure, collapseFraction: collapseFraction, transition: transition) return availableSize } - public func update(theme: PresentationTheme, strings: PresentationStrings, size: CGSize, transition: ComponentTransition) { + public func update(theme: PresentationTheme, strings: PresentationStrings, size: CGSize, isActive: Bool, title: String, isSecure: Bool, collapseFraction: CGFloat, transition: ComponentTransition) { let params = Params( theme: theme, strings: strings, - size: size + size: size, + isActive: isActive, + title: title, + isSecure: isSecure, + collapseFraction: collapseFraction ) if self.params == params { return } - let isActiveWithText = true + let isActiveWithText = self.component?.isExpanded ?? false if self.params?.theme !== theme { - self.iconView.image = generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Loupe"), color: .white)?.withRenderingMode(.alwaysTemplate) + self.iconView.image = generateTintedImage(image: UIImage(bundleImageName: "Media Grid/Lock"), color: .white)?.withRenderingMode(.alwaysTemplate) self.iconView.tintColor = theme.rootController.navigationSearchBar.inputIconColor self.clearIconView.image = generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: .white)?.withRenderingMode(.alwaysTemplate) self.clearIconView.tintColor = theme.rootController.navigationSearchBar.inputClearButtonColor @@ -280,10 +309,9 @@ final class AddressBarContentComponent: Component { let inputHeight: CGFloat = 36.0 let topInset: CGFloat = (size.height - inputHeight) / 2.0 - let sideTextInset: CGFloat = sideInset + 4.0 + 17.0 - self.backgroundLayer.backgroundColor = theme.rootController.navigationSearchBar.inputFillColor.cgColor self.backgroundLayer.cornerRadius = 10.5 + transition.setAlpha(layer: self.backgroundLayer, alpha: max(0.0, min(1.0, 1.0 - collapseFraction * 1.5))) let cancelTextSize = self.cancelButtonTitle.update( transition: .immediate, @@ -306,35 +334,74 @@ final class AddressBarContentComponent: Component { transition.setFrame(view: self.cancelButton, frame: CGRect(origin: CGPoint(x: backgroundFrame.maxX, y: 0.0), size: CGSize(width: cancelButtonSpacing + cancelTextSize.width, height: size.height))) - let textX: CGFloat = backgroundFrame.minX + sideTextInset + let textX: CGFloat = backgroundFrame.minX + sideInset let textFrame = CGRect(origin: CGPoint(x: textX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.maxX - textX, height: backgroundFrame.height)) - self.textFrame = textFrame - - if let image = self.iconView.image { - let iconFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + 5.0, y: backgroundFrame.minY + floor((backgroundFrame.height - image.size.height) / 2.0)), size: image.size) - transition.setFrame(view: self.iconView, frame: iconFrame) - } - + let placeholderSize = self.placeholderContent.update( transition: transition, component: AnyComponent( - Text(text: strings.Common_Search, font: Font.regular(17.0), color: theme.rootController.navigationSearchBar.inputPlaceholderTextColor) + Text(text: strings.WebBrowser_AddressPlaceholder, font: Font.regular(17.0), color: theme.rootController.navigationSearchBar.inputPlaceholderTextColor) ), environment: {}, containerSize: size ) if let placeholderContentView = self.placeholderContent.view { if placeholderContentView.superview == nil { + placeholderContentView.alpha = 0.0 + placeholderContentView.isHidden = true self.addSubview(placeholderContentView) } let placeholderContentFrame = CGRect(origin: CGPoint(x: textFrame.minX, y: backgroundFrame.midY - placeholderSize.height / 2.0), size: placeholderSize) transition.setFrame(view: placeholderContentView, frame: placeholderContentFrame) + transition.setAlpha(view: placeholderContentView, alpha: isActiveWithText ? 1.0 : 0.0) + } + + let titleSize = self.titleContent.update( + transition: transition, + component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString(string: title, font: Font.regular(17.0), textColor: theme.rootController.navigationSearchBar.inputTextColor)), + horizontalAlignment: .center, + truncationType: .end, + maximumNumberOfLines: 1 + ) + ), + environment: {}, + containerSize: CGSize(width: size.width - 36.0, height: size.height) + ) + var titleContentFrame = CGRect(origin: CGPoint(x: isActiveWithText ? textFrame.minX : backgroundFrame.midX - titleSize.width / 2.0, y: backgroundFrame.midY - titleSize.height / 2.0), size: titleSize) + if isSecure && !isActiveWithText { + titleContentFrame.origin.x += 7.0 + } + var titleSizeChanged = false + if let titleContentView = self.titleContent.view { + if titleContentView.superview == nil { + self.addSubview(titleContentView) + } + if titleContentView.frame.width != titleContentFrame.size.width { + titleSizeChanged = true + } + transition.setPosition(view: titleContentView, position: titleContentFrame.center) + titleContentView.bounds = CGRect(origin: .zero, size: titleContentFrame.size) + transition.setAlpha(view: titleContentView, alpha: isActiveWithText ? 0.0 : 1.0) + } + + if let image = self.iconView.image { + let iconFrame = CGRect(origin: CGPoint(x: titleContentFrame.minX - image.size.width - 3.0, y: backgroundFrame.minY + floor((backgroundFrame.height - image.size.height) / 2.0)), size: image.size) + var iconTransition = transition + if titleSizeChanged { + iconTransition = .immediate + } + iconTransition.setFrame(view: self.iconView, frame: iconFrame) + transition.setAlpha(view: self.iconView, alpha: isActiveWithText || !isSecure ? 0.0 : 1.0) } if let image = self.clearIconView.image { let iconFrame = CGRect(origin: CGPoint(x: backgroundFrame.maxX - image.size.width - 4.0, y: backgroundFrame.minY + floor((backgroundFrame.height - image.size.height) / 2.0)), size: image.size) transition.setFrame(view: self.clearIconView, frame: iconFrame) transition.setFrame(view: self.clearIconButton, frame: iconFrame.insetBy(dx: -8.0, dy: -10.0)) + transition.setAlpha(view: self.clearIconView, alpha: isActiveWithText ? 1.0 : 0.0) + self.clearIconButton.isUserInteractionEnabled = isActiveWithText } if let cancelButtonTitleComponentView = self.cancelButtonTitle.view { @@ -343,12 +410,33 @@ final class AddressBarContentComponent: Component { cancelButtonTitleComponentView.isUserInteractionEnabled = false } transition.setFrame(view: cancelButtonTitleComponentView, frame: CGRect(origin: CGPoint(x: backgroundFrame.maxX + cancelButtonSpacing, y: floor((size.height - cancelTextSize.height) / 2.0)), size: cancelTextSize)) + transition.setAlpha(view: cancelButtonTitleComponentView, alpha: isActiveWithText ? 1.0 : 0.0) } - - if let textField = self.textField { - textField.textColor = theme.rootController.navigationSearchBar.inputTextColor - transition.setFrame(view: textField, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + sideTextInset, y: backgroundFrame.minY - UIScreenPixel), size: CGSize(width: backgroundFrame.width - sideTextInset - 32.0, height: backgroundFrame.height))) + + let textFieldFrame = CGRect(origin: CGPoint(x: textFrame.minX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.maxX - textFrame.minX, height: backgroundFrame.height)) + + let textField: TextField + if let current = self.textField { + textField = current + } else { + textField = TextField(frame: textFieldFrame) + textField.autocapitalizationType = .none + textField.autocorrectionType = .no + textField.keyboardType = .URL + textField.returnKeyType = .go + self.insertSubview(textField, belowSubview: self.clearIconView) + self.textField = textField + + textField.delegate = self + textField.addTarget(self, action: #selector(self.textFieldChanged(_:)), for: .editingChanged) } + + textField.text = self.component?.url ?? "" + + textField.textColor = theme.rootController.navigationSearchBar.inputTextColor + transition.setFrame(view: textField, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + sideInset, y: backgroundFrame.minY - UIScreenPixel), size: CGSize(width: backgroundFrame.width - sideInset - 32.0, height: backgroundFrame.height))) + transition.setAlpha(view: textField, alpha: isActiveWithText ? 1.0 : 0.0) + textField.isUserInteractionEnabled = isActiveWithText } } @@ -356,7 +444,7 @@ final class AddressBarContentComponent: Component { return View() } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - return view.update(component: self, availableSize: availableSize, transition: transition) + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition) } } diff --git a/submodules/BrowserUI/Sources/BrowserAddressListComponent.swift b/submodules/BrowserUI/Sources/BrowserAddressListComponent.swift new file mode 100644 index 0000000000..b33621c4ee --- /dev/null +++ b/submodules/BrowserUI/Sources/BrowserAddressListComponent.swift @@ -0,0 +1,466 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ComponentFlow +import SwiftSignalKit +import Postbox +import TelegramCore +import AccountContext +import TelegramPresentationData + +final class BrowserAddressListComponent: Component { + let context: AccountContext + let theme: PresentationTheme + let strings: PresentationStrings + let navigateTo: (String) -> Void + + init( + context: AccountContext, + theme: PresentationTheme, + strings: PresentationStrings, + navigateTo: @escaping (String) -> Void + ) { + self.context = context + self.theme = theme + self.strings = strings + self.navigateTo = navigateTo + } + + static func ==(lhs: BrowserAddressListComponent, rhs: BrowserAddressListComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + return true + } + + private struct ItemLayout: Equatable { + struct Section: Equatable { + var id: Int + var insets: UIEdgeInsets + var itemHeight: CGFloat + var itemCount: Int + + var totalHeight: CGFloat + + init( + id: Int, + insets: UIEdgeInsets, + itemHeight: CGFloat, + itemCount: Int + ) { + self.id = id + self.insets = insets + self.itemHeight = itemHeight + self.itemCount = itemCount + + self.totalHeight = insets.top + itemHeight * CGFloat(itemCount) + insets.bottom + } + } + + var containerSize: CGSize + var insets: UIEdgeInsets + var sections: [Section] + + var contentHeight: CGFloat + + init( + containerSize: CGSize, + insets: UIEdgeInsets, + sections: [Section] + ) { + self.containerSize = containerSize + self.insets = insets + self.sections = sections + + var contentHeight: CGFloat = 0.0 + for section in sections { + contentHeight += section.totalHeight + } + self.contentHeight = contentHeight + } + } + + private final class ScrollView: UIScrollView { + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + return super.hitTest(point, with: event) + } + + override func touchesShouldCancel(in view: UIView) -> Bool { + return true + } + } + + final class View: UIView, UIScrollViewDelegate { + struct State { + let recent: [TelegramMediaWebpage] + let bookmarks: [Message] + } + + private let backgroundView = UIView() + private let scrollView = ScrollView() + private let itemContainerView = UIView() + + private let addressTemplateItem = ComponentView() + + private var visibleSectionHeaders: [Int: ComponentView] = [:] + private var visibleItems: [AnyHashable: ComponentView] = [:] + + private var ignoreScrolling: Bool = false + + private var component: BrowserAddressListComponent? + private weak var state: EmptyComponentState? + private var itemLayout: ItemLayout? + + private var stateDisposable: Disposable? + private var stateValue: State? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.scrollView.alwaysBounceVertical = true + self.scrollView.delegate = self + self.scrollView.showsVerticalScrollIndicator = false + + self.addSubview(self.backgroundView) + self.addSubview(self.scrollView) + self.scrollView.addSubview(self.itemContainerView) + } + + required init?(coder: NSCoder) { + fatalError() + } + + deinit { + self.stateDisposable?.dispose() + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if !self.ignoreScrolling { + self.updateScrolling(transition: .immediate) + } + } + + func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + self.endEditing(true) + } + + private func updateScrolling(transition: ComponentTransition) { + guard let component = self.component, let itemLayout = self.itemLayout, let state = self.stateValue else { + return + } + + var topOffset = -self.scrollView.bounds.minY + topOffset = max(0.0, topOffset) + + let visibleBounds = self.scrollView.bounds + var visibleFrame = self.scrollView.frame + visibleFrame.origin.x = 0.0 + + var validIds: [AnyHashable] = [] + var validSectionHeaders: [AnyHashable] = [] + var sectionOffset: CGFloat = 0.0 + + let sideInset: CGFloat = 0.0 + let containerInset: CGFloat = 0.0 + + for sectionIndex in 0 ..< itemLayout.sections.count { + let section = itemLayout.sections[sectionIndex] + + do { + var sectionHeaderFrame = CGRect(origin: CGPoint(x: sideInset, y: sectionOffset - self.scrollView.bounds.minY), size: CGSize(width: itemLayout.containerSize.width, height: section.insets.top)) + + let sectionHeaderMinY = topOffset + containerInset + let sectionHeaderMaxY = containerInset + sectionOffset - self.scrollView.bounds.minY + section.totalHeight - 28.0 + + sectionHeaderFrame.origin.y = max(sectionHeaderFrame.origin.y, sectionHeaderMinY) + sectionHeaderFrame.origin.y = min(sectionHeaderFrame.origin.y, sectionHeaderMaxY) + + if visibleFrame.intersects(sectionHeaderFrame) { + validSectionHeaders.append(section.id) + let sectionHeader: ComponentView + var sectionHeaderTransition = transition + if let current = self.visibleSectionHeaders[section.id] { + sectionHeader = current + } else { + if !transition.animation.isImmediate { + sectionHeaderTransition = .immediate + } + sectionHeader = ComponentView() + self.visibleSectionHeaders[section.id] = sectionHeader + } + + let sectionTitle: String + if section.id == 0 { + sectionTitle = "RECENTLY VISITED" + } else if section.id == 1 { + sectionTitle = "BOOKMARKS" + } else { + sectionTitle = "" + } + + let _ = sectionHeader.update( + transition: sectionHeaderTransition, + component: AnyComponent(SectionHeaderComponent( + theme: component.theme, + style: .plain, + title: sectionTitle, + actionTitle: section.id == 0 ? "Clear" : nil, + action: { [weak self] in + if let self, let component = self.component { + let _ = clearRecentlyVisitedLinks(engine: component.context.engine).start() + } + } + )), + environment: {}, + containerSize: sectionHeaderFrame.size + ) + if let sectionHeaderView = sectionHeader.view { + if sectionHeaderView.superview == nil { + self.addSubview(sectionHeaderView) + + if !transition.animation.isImmediate { + sectionHeaderView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + } + let sectionXOffset = self.scrollView.frame.minX + sectionHeaderTransition.setFrame(view: sectionHeaderView, frame: sectionHeaderFrame.offsetBy(dx: sectionXOffset, dy: 0.0)) + } + } + } + + for i in 0 ..< section.itemCount { + let itemFrame = CGRect(origin: CGPoint(x: sideInset, y: sectionOffset + section.insets.top + CGFloat(i) * section.itemHeight), size: CGSize(width: itemLayout.containerSize.width, height: section.itemHeight)) + if !visibleBounds.intersects(itemFrame) { + continue + } + + var id = 0 + if section.id == 0 { + id += i + } else if section.id == 1 { + id += 1000 + i + } + + let itemId = AnyHashable(id) + validIds.append(itemId) + + var itemTransition = transition + let visibleItem: ComponentView + if let current = self.visibleItems[itemId] { + visibleItem = current + } else { + visibleItem = ComponentView() + if !transition.animation.isImmediate { + itemTransition = .immediate + } + self.visibleItems[itemId] = visibleItem + } + + var webPage: TelegramMediaWebpage? + var itemMessage: Message? + + if section.id == 0 { + webPage = state.recent[i] + } else if section.id == 1 { + let message = state.bookmarks[i] + if let primaryUrl = getPrimaryUrl(message: message) { + if let media = message.media.first(where: { $0 is TelegramMediaWebpage }) as? TelegramMediaWebpage { + webPage = media + } else { + webPage = TelegramMediaWebpage(webpageId: MediaId(namespace: 0, id: 0), content: .Loaded(TelegramMediaWebpageLoadedContent(url: primaryUrl, displayUrl: "", hash: 0, type: nil, websiteName: "", title: message.text, text: "", embedUrl: nil, embedType: nil, embedSize: nil, duration: nil, author: nil, isMediaLargeByDefault: nil, image: nil, file: nil, story: nil, attributes: [], instantPage: nil))) + } + itemMessage = message + } else { + continue + } + } + + let navigateTo = component.navigateTo + let _ = visibleItem.update( + transition: itemTransition, + component: AnyComponent( + BrowserAddressListItemComponent( + context: component.context, + theme: component.theme, + webPage: webPage!, + message: itemMessage, + hasNext: true, + action: { + if let url = webPage?.content.url { + navigateTo(url) + } + }) + ), + environment: {}, + containerSize: itemFrame.size + ) + if let itemView = visibleItem.view { + if itemView.superview == nil { + self.itemContainerView.addSubview(itemView) + } + itemTransition.setFrame(view: itemView, frame: itemFrame) + } + } + + sectionOffset += section.totalHeight + } + + var removeIds: [AnyHashable] = [] + for (id, item) in self.visibleItems { + if !validIds.contains(id) { + removeIds.append(id) + if let itemView = item.view { + if !transition.animation.isImmediate { + itemView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + itemView.removeFromSuperview() + }) + } else { + itemView.removeFromSuperview() + } + } + } + } + for id in removeIds { + self.visibleItems.removeValue(forKey: id) + } + + var removeSectionHeaderIds: [Int] = [] + for (id, item) in self.visibleSectionHeaders { + if !validSectionHeaders.contains(id) { + removeSectionHeaderIds.append(id) + if let itemView = item.view { + if !transition.animation.isImmediate { + itemView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + itemView.removeFromSuperview() + }) + } else { + itemView.removeFromSuperview() + } + } + } + } + for id in removeSectionHeaderIds { + self.visibleSectionHeaders.removeValue(forKey: id) + } + } + + func update(component: BrowserAddressListComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let themeUpdated = self.component?.theme !== component.theme + + if self.component == nil { + self.stateDisposable = combineLatest(queue: Queue.mainQueue(), + recentlyVisitedLinks(engine: component.context.engine), + component.context.account.viewTracker.aroundMessageHistoryViewForLocation(.peer(peerId: component.context.account.peerId, threadId: nil), index: .upperBound, anchorIndex: .upperBound, count: 100, fixedCombinedReadStates: nil, tag: .tag(.webPage)) + ).start(next: { [weak self] recent, view in + guard let self else { + return + } + + var bookmarks: [Message] = [] + for entry in view.0.entries.reversed() { + bookmarks.append(entry.message) + } + + self.stateValue = State( + recent: recent, + bookmarks: bookmarks + ) + self.state?.updated(transition: .immediate) + }) + } + + self.component = component + self.state = state + + let resetScrolling = self.scrollView.bounds.width != availableSize.width + if themeUpdated { + self.backgroundView.backgroundColor = component.theme.list.plainBackgroundColor + } + + let itemsContainerWidth = availableSize.width + let addressItemSize = self.addressTemplateItem.update( + transition: .immediate, + component: AnyComponent(BrowserAddressListItemComponent( + context: component.context, + theme: component.theme, + webPage: TelegramMediaWebpage(webpageId: EngineMedia.Id(namespace: 0, id: 0), content: .Loaded(TelegramMediaWebpageLoadedContent(url: "https://telegram.org", displayUrl: "https://telegram.org", hash: 0, type: nil, websiteName: "Telegram", title: "Telegram Telegram", text: "Telegram", embedUrl: nil, embedType: nil, embedSize: nil, duration: nil, author: nil, isMediaLargeByDefault: nil, image: nil, file: nil, story: nil, attributes: [], instantPage: nil))), + message: nil, + hasNext: true, + action: {} + )), + environment: {}, + containerSize: CGSize(width: itemsContainerWidth, height: 1000.0) + ) + + let _ = resetScrolling + let _ = addressItemSize + + + var sections: [ItemLayout.Section] = [] + if let state = self.stateValue { + if !state.recent.isEmpty { + sections.append(ItemLayout.Section( + id: 0, + insets: UIEdgeInsets(top: 28.0, left: 0.0, bottom: 0.0, right: 0.0), + itemHeight: addressItemSize.height, + itemCount: state.recent.count + )) + } + if !state.bookmarks.isEmpty { + sections.append(ItemLayout.Section( + id: 1, + insets: UIEdgeInsets(top: 28.0, left: 0.0, bottom: 0.0, right: 0.0), + itemHeight: addressItemSize.height, + itemCount: state.bookmarks.count + )) + } + } + + let itemLayout = ItemLayout(containerSize: availableSize, insets: .zero, sections: sections) + self.itemLayout = itemLayout + + let containerWidth = availableSize.width + let scrollContentHeight = max(itemLayout.contentHeight, availableSize.height) + + self.ignoreScrolling = true + transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: containerWidth, height: availableSize.height))) + let contentSize = CGSize(width: containerWidth, height: scrollContentHeight) + if contentSize != self.scrollView.contentSize { + self.scrollView.contentSize = contentSize + } +// let contentInset: UIEdgeInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: bottomPanelHeight + bottomPanelInset, right: 0.0) +// let indicatorInset = UIEdgeInsets(top: max(itemLayout.containerInset, environment.safeInsets.top + navigationHeight), left: 0.0, bottom: contentInset.bottom, right: 0.0) +// if indicatorInset != self.scrollView.scrollIndicatorInsets { +// self.scrollView.scrollIndicatorInsets = indicatorInset +// } +// if contentInset != self.scrollView.contentInset { +// self.scrollView.contentInset = contentInset +// } + if resetScrolling { + self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: containerWidth, height: availableSize.height)) + } + self.ignoreScrolling = false + self.updateScrolling(transition: transition) + + transition.setFrame(view: self.backgroundView, frame: CGRect(origin: .zero, size: availableSize)) + transition.setFrame(view: self.itemContainerView, frame: CGRect(origin: .zero, size: CGSize(width: containerWidth, height: scrollContentHeight))) + + 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) + } +} diff --git a/submodules/BrowserUI/Sources/BrowserAddressListItemComponent.swift b/submodules/BrowserUI/Sources/BrowserAddressListItemComponent.swift new file mode 100644 index 0000000000..49ebcb2c20 --- /dev/null +++ b/submodules/BrowserUI/Sources/BrowserAddressListItemComponent.swift @@ -0,0 +1,251 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import SwiftSignalKit +import Postbox +import TelegramCore +import MultilineTextComponent +import TelegramPresentationData +import PhotoResources +import AccountContext + +final class BrowserAddressListItemComponent: Component { + let context: AccountContext + let theme: PresentationTheme + let webPage: TelegramMediaWebpage + var message: Message? + let hasNext: Bool + let action: () -> Void + + init( + context: AccountContext, + theme: PresentationTheme, + webPage: TelegramMediaWebpage, + message: Message?, + hasNext: Bool, + action: @escaping () -> Void + ) { + self.context = context + self.theme = theme + self.webPage = webPage + self.message = message + self.hasNext = hasNext + self.action = action + } + + static func ==(lhs: BrowserAddressListItemComponent, rhs: BrowserAddressListItemComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.webPage != rhs.webPage { + return false + } + if lhs.hasNext != rhs.hasNext { + return false + } + return true + } + + final class View: UIView { + private let containerButton: HighlightTrackingButton + + private var emptyIcon: UIImageView? + private var icon = TransformImageNode() + private let title = ComponentView() + private let subtitle = ComponentView() + + private let separatorLayer: SimpleLayer + + private var component: BrowserAddressListItemComponent? + private weak var state: EmptyComponentState? + + private var currentIconImageRepresentation: TelegramMediaImageRepresentation? + + override init(frame: CGRect) { + self.separatorLayer = SimpleLayer() + + self.containerButton = HighlightTrackingButton() + + super.init(frame: frame) + + self.layer.addSublayer(self.separatorLayer) + self.addSubview(self.containerButton) + + self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func pressed() { + guard let component = self.component else { + return + } + component.action() + } + + func update(component: BrowserAddressListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let themeUpdated = self.component?.theme !== component.theme + let currentIconImageRepresentation = self.currentIconImageRepresentation + + let iconSize = CGSize(width: 40.0, height: 40.0) + let height: CGFloat = 60.0 + let leftInset: CGFloat = 11.0 + iconSize.width + 11.0 + let rightInset: CGFloat = 16.0 + let titleSpacing: CGFloat = 2.0 + + let title: String + let subtitle: String + var iconImageReferenceAndRepresentation: (AnyMediaReference, TelegramMediaImageRepresentation)? + var updateIconImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? + + if case let .Loaded(content) = component.webPage.content { + title = content.title ?? content.url + subtitle = content.url + + if let image = content.image { + if let representation = imageRepresentationLargerThan(image.representations, size: PixelDimensions(width: 80, height: 80)) { + if let message = component.message { + iconImageReferenceAndRepresentation = (.message(message: MessageReference(message), media: image), representation) + } else { + iconImageReferenceAndRepresentation = (.standalone(media: image), representation) + } + } + } else if let file = content.file { + if let representation = smallestImageRepresentation(file.previewRepresentations) { + if let message = component.message { + iconImageReferenceAndRepresentation = (.message(message: MessageReference(message), media: file), representation) + } else { + iconImageReferenceAndRepresentation = (.standalone(media: file), representation) + } + } + } + + if currentIconImageRepresentation != iconImageReferenceAndRepresentation?.1 { + if let iconImageReferenceAndRepresentation = iconImageReferenceAndRepresentation { + if let imageReference = iconImageReferenceAndRepresentation.0.concrete(TelegramMediaImage.self) { + updateIconImageSignal = chatWebpageSnippetPhoto(account: component.context.account, userLocation: (component.message?.id.peerId).flatMap(MediaResourceUserLocation.peer) ?? .other, photoReference: imageReference) + } else if let fileReference = iconImageReferenceAndRepresentation.0.concrete(TelegramMediaFile.self) { + updateIconImageSignal = chatWebpageSnippetFile(account: component.context.account, userLocation: (component.message?.id.peerId).flatMap(MediaResourceUserLocation.peer) ?? .other, mediaReference: fileReference.abstract, representation: iconImageReferenceAndRepresentation.1) + } + } else { + updateIconImageSignal = .complete() + } + } + } else { + title = "" + subtitle = "" + } + + self.component = component + self.state = state + self.currentIconImageRepresentation = iconImageReferenceAndRepresentation?.1 + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: title, font: Font.semibold(17.0), textColor: component.theme.list.itemPrimaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0) + ) + let subtitleSize = self.subtitle.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: subtitle, font: Font.regular(15.0), textColor: component.theme.list.itemAccentColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0) + ) + + let centralContentHeight = titleSize.height + subtitleSize.height + titleSpacing + + let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - 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 + } + + let subtitleFrame = CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + titleSpacing), size: subtitleSize) + if let subtitleView = self.subtitle.view { + if subtitleView.superview == nil { + subtitleView.isUserInteractionEnabled = false + self.containerButton.addSubview(subtitleView) + } + subtitleView.frame = subtitleFrame + } + + + let iconFrame = CGRect(origin: CGPoint(x: 11.0, y: floorToScreenPixels((height - iconSize.height) / 2.0)), size: iconSize) + + let iconImageLayout = self.icon.asyncLayout() + var iconImageApply: (() -> Void)? + if let iconImageReferenceAndRepresentation = iconImageReferenceAndRepresentation { + let imageCorners = ImageCorners(radius: 6.0) + let arguments = TransformImageArguments(corners: imageCorners, imageSize: iconImageReferenceAndRepresentation.1.dimensions.cgSize.aspectFilled(iconSize), boundingSize: iconSize, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor) + iconImageApply = iconImageLayout(arguments) + } + + if let iconImageApply = iconImageApply { + if let updateImageSignal = updateIconImageSignal { + self.icon.setSignal(updateImageSignal) + } + + if self.icon.supernode == nil { + self.addSubview(self.icon.view) + self.icon.frame = iconFrame + } else { + transition.setFrame(view: self.icon.view, frame: iconFrame) + } + + iconImageApply() + +// if strongSelf.iconTextBackgroundNode.supernode != nil { +// strongSelf.iconTextBackgroundNode.removeFromSupernode() +// } +// if strongSelf.iconTextNode.supernode != nil { +// strongSelf.iconTextNode.removeFromSupernode() +// } + } else { + if self.icon.supernode != nil { + self.icon.view.removeFromSuperview() + } + +// if strongSelf.iconTextBackgroundNode.supernode == nil { +// strongSelf.iconTextBackgroundNode.image = applyIconTextBackgroundImage +// strongSelf.offsetContainerNode.addSubnode(strongSelf.iconTextBackgroundNode) +// strongSelf.iconTextBackgroundNode.frame = iconFrame +// } else { +// transition.updateFrame(node: strongSelf.iconTextBackgroundNode, frame: iconFrame) +// } +// if strongSelf.iconTextNode.supernode == nil { +// strongSelf.offsetContainerNode.addSubnode(strongSelf.iconTextNode) +// } + } + + if themeUpdated { + self.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.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 containerFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: height)) + transition.setFrame(view: self.containerButton, frame: containerFrame) + + return CGSize(width: availableSize.width, height: 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/BrowserUI/Sources/BrowserBookmarksScreen.swift b/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift new file mode 100644 index 0000000000..1cf00e619e --- /dev/null +++ b/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift @@ -0,0 +1,517 @@ +import Foundation +import UIKit +import AccountContext +import AsyncDisplayKit +import Display +import SwiftSignalKit +import Postbox +import TelegramCore +import TelegramPresentationData +import PresentationDataUtils +import ChatControllerInteraction +import TelegramUIPreferences +import ChatPresentationInterfaceState +import TextFormat +import UrlWhitelist +import SearchUI +import SearchBarNode +import ChatHistorySearchContainerNode + +public final class BrowserBookmarksScreen: ViewController { + final class Node: ViewControllerTracingNode, ASScrollViewDelegate { + private let context: AccountContext + private var presentationData: PresentationData + private weak var controller: BrowserBookmarksScreen? + + private let controllerInteraction: ChatControllerInteraction + private var searchDisplayController: SearchDisplayController? + + fileprivate let historyNode: ChatHistoryListNode + private let bottomPanelNode: BottomPanelNode + + private var addedBookmark = false + + private var validLayout: (ContainerViewLayout, CGFloat, CGFloat)? + + init(context: AccountContext, controller: BrowserBookmarksScreen, presentationData: PresentationData) { + self.context = context + self.controller = controller + self.presentationData = presentationData + + var openMessageImpl: ((Message) -> Bool)? + self.controllerInteraction = ChatControllerInteraction(openMessage: { message, _ in + if let openMessageImpl = openMessageImpl { + return openMessageImpl(message) + } else { + return false + } + }, openPeer: { _, _, _, _ in + }, openPeerMention: { _, _ in + }, openMessageContextMenu: { _, _, _, _, _, _ in + }, openMessageReactionContextMenu: { _, _, _, _ in + }, updateMessageReaction: { _, _, _, _ in + }, activateMessagePinch: { _ in + }, openMessageContextActions: { _, _, _, _ in + }, navigateToMessage: { _, _, _ in + }, navigateToMessageStandalone: { _ in + }, navigateToThreadMessage: { _, _, _ in + }, tapMessage: nil, clickThroughMessage: { + }, toggleMessagesSelection: { _, _ in + }, sendCurrentMessage: { _, _ in + }, sendMessage: { _ in + }, sendSticker: { _, _, _, _, _, _, _, _, _ in + return false + }, sendEmoji: { _, _, _ in + }, sendGif: { _, _, _, _, _ in + return false + }, sendBotContextResultAsGif: { _, _, _, _, _, _ in + return false + }, requestMessageActionCallback: { _, _, _, _ in + }, requestMessageActionUrlAuth: { _, _ in + }, activateSwitchInline: { _, _, _ in + }, openUrl: { [weak controller] url in + if let controller { + controller.openUrl(url.url) + controller.dismiss() + } + }, shareCurrentLocation: { + }, shareAccountContact: { + }, sendBotCommand: { _, _ in + }, openInstantPage: { message, _ in + if let openMessageImpl = openMessageImpl { + let _ = openMessageImpl(message) + } + }, openWallpaper: { _ in + }, openTheme: {_ in + }, openHashtag: { _, _ in + }, updateInputState: { _ in + }, updateInputMode: { _ in + }, openMessageShareMenu: { _ in + }, presentController: { _, _ in + }, presentControllerInCurrent: { _, _ in + }, navigationController: { + return nil + }, chatControllerNode: { + return nil + }, presentGlobalOverlayController: { _, _ in + }, callPeer: { _, _ in + }, longTap: { _, _ in + }, openCheckoutOrReceipt: { _, _ in + }, openSearch: { + }, setupReply: { _ in + }, canSetupReply: { _ in + return .none + }, canSendMessages: { + return false + }, navigateToFirstDateMessage: { _, _ in + }, requestRedeliveryOfFailedMessages: { _ in + }, addContact: { _ in + }, rateCall: { _, _, _ in + }, requestSelectMessagePollOptions: { _, _ in + }, requestOpenMessagePollResults: { _, _ in + }, openAppStorePage: { + }, displayMessageTooltip: { _, _, _, _, _ in + }, seekToTimecode: { _, _, _ in + }, scheduleCurrentMessage: { _ in + }, sendScheduledMessagesNow: { _ in + }, editScheduledMessagesTime: { _ in + }, performTextSelectionAction: { _, _, _, _ in + }, displayImportedMessageTooltip: { _ in + }, displaySwipeToReplyHint: { + }, dismissReplyMarkupMessage: { _ in + }, openMessagePollResults: { _, _ in + }, openPollCreation: { _ in + }, displayPollSolution: { _, _ in + }, displayPsa: { _, _ in + }, displayDiceTooltip: { _ in + }, animateDiceSuccess: { _, _ in + }, displayPremiumStickerTooltip: { _, _ in + }, displayEmojiPackTooltip: { _, _ in + }, openPeerContextMenu: { _, _, _, _, _ in + }, openMessageReplies: { _, _, _ in + }, openReplyThreadOriginalMessage: { _ in + }, openMessageStats: { _ in + }, editMessageMedia: { _, _ in + }, copyText: { _ in + }, displayUndo: { _ in + }, isAnimatingMessage: { _ in + return false + }, getMessageTransitionNode: { + return nil + }, updateChoosingSticker: { _ in + }, commitEmojiInteraction: { _, _, _, _ in + }, openLargeEmojiInfo: { _, _, _ in + }, openJoinLink: { _ in + }, openWebView: { _, _, _, _ in + }, activateAdAction: { _, _ in + }, openRequestedPeerSelection: { _, _, _, _ in + }, saveMediaToFiles: { _ in + }, openNoAdsDemo: { + }, openAdsInfo: { + }, displayGiveawayParticipationStatus: { _ in + }, openPremiumStatusInfo: { _, _, _, _ in + }, openRecommendedChannelContextMenu: { _, _, _ in + }, openGroupBoostInfo: { _, _ in + }, openStickerEditor: { + }, openAgeRestrictedMessageMedia: { _, _ in + }, playMessageEffect: { _ in + }, editMessageFactCheck: { _ in + }, requestMessageUpdate: { _, _ in + }, cancelInteractiveKeyboardGestures: { + }, dismissTextInput: { + }, scrollToMessageId: { _ in + }, navigateToStory: { _, _ in + }, attemptedNavigationToPrivateQuote: { _ in + }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil)) + + + let tagMask: MessageTags = .webPage + let chatLocationContextHolder = Atomic(value: nil) + self.historyNode = context.sharedContext.makeChatHistoryListNode( + context: context, + updatedPresentationData: (context.sharedContext.currentPresentationData.with({ $0 }), context.sharedContext.presentationData), + chatLocation: .peer(id: context.account.peerId), + chatLocationContextHolder: chatLocationContextHolder, + tag: .tag(tagMask), + source: .default, + subject: nil, + controllerInteraction: self.controllerInteraction, + selectedMessages: .single(nil), + mode: .list( + search: false, + reversed: false, + reverseGroups: false, + displayHeaders: .none, + hintLinks: true, + isGlobalSearch: false + ) + ) + + var addBookmarkImpl: (() -> Void)? + self.bottomPanelNode = BottomPanelNode(theme: presentationData.theme, strings: presentationData.strings, action: { + addBookmarkImpl?() + }) + + super.init() + + self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor + + self.addSubnode(self.historyNode) + self.addSubnode(self.bottomPanelNode) + + openMessageImpl = { [weak controller] message in + guard let controller else { + return false + } + if let primaryUrl = getPrimaryUrl(message: message) { + controller.openUrl(primaryUrl) + } + controller.dismiss() + return true + } + + addBookmarkImpl = { [weak self] in + guard let self else { + return + } + self.controller?.addBookmark() + self.addedBookmark = true + if let (layout, navigationBarHeight, actualNavigationBarHeight) = self.validLayout { + self.containerLayoutUpdated(layout: layout, navigationBarHeight: navigationBarHeight, actualNavigationBarHeight: actualNavigationBarHeight, transition: .animated(duration: 0.4, curve: .spring)) + } + } + } + + func activateSearch(placeholderNode: SearchBarPlaceholderNode) { + guard let (layout, navigationBarHeight, _) = self.validLayout, let navigationBar = self.controller?.navigationBar else { + return + } + let tagMask: MessageTags = .webPage + + self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, mode: .list, placeholder: self.presentationData.strings.Common_Search, hasBackground: true, contentNode: ChatHistorySearchContainerNode(context: self.context, peerId: self.context.account.peerId, threadId: nil, tagMask: tagMask, interfaceInteraction: self.controllerInteraction), cancel: { [weak self] in + self?.controller?.deactivateSearch() + }) + + self.searchDisplayController?.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) + self.searchDisplayController?.activate(insertSubnode: { [weak self, weak placeholderNode] subnode, isSearchBar in + if let strongSelf = self, let placeholderNode { + if isSearchBar { + placeholderNode.supernode?.insertSubnode(subnode, aboveSubnode: placeholderNode) + } else { + strongSelf.insertSubnode(subnode, belowSubnode: navigationBar) + } + } + }, placeholder: placeholderNode) + } + + func deactivateSearch(placeholderNode: SearchBarPlaceholderNode) { + guard let searchDisplayController = self.searchDisplayController else { + return + } + self.searchDisplayController = nil + searchDisplayController.deactivate(placeholder: placeholderNode) + } + + func scrollToTop() { + self.historyNode.scrollToEndOfHistory() + } + + func containerLayoutUpdated(layout: ContainerViewLayout, navigationBarHeight: CGFloat, actualNavigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + self.validLayout = (layout, navigationBarHeight, actualNavigationBarHeight) + + let historyFrame = CGRect(origin: .zero, size: layout.size) + transition.updateFrame(node: self.historyNode, frame: historyFrame) + + var insets = layout.insets(options: [.input]) + insets.top += navigationBarHeight + + var headerInsets = layout.insets(options: [.input]) + headerInsets.top += actualNavigationBarHeight + + let panelHeight = self.bottomPanelNode.updateLayout(width: layout.size.width, sideInset: layout.safeInsets.left, bottomInset: insets.bottom, transition: transition) + var panelOrigin: CGFloat = layout.size.height + if !self.addedBookmark { + panelOrigin -= panelHeight + insets.bottom = panelHeight + } + let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: panelOrigin), size: CGSize(width: layout.size.width, height: panelHeight)) + transition.updateFrame(node: self.bottomPanelNode, frame: panelFrame) + + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: historyFrame.size, insets: insets, headerInsets: headerInsets, duration: duration, curve: curve) + self.historyNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets) + + if let searchDisplayController = self.searchDisplayController { + searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) + } + } + } + + private let context: AccountContext + private var presentationData: PresentationData + private var presentationDataDisposable: Disposable? + private let url: String + private let openUrl: (String) -> Void + private let addBookmark: () -> Void + + private var controllerNode: Node { + return self.displayNode as! Node + } + + private var searchContentNode: NavigationBarSearchContentNode? + + private var validLayout: ContainerViewLayout? + + private var node: Node { + return self.displayNode as! Node + } + + public init(context: AccountContext, url: String, openUrl: @escaping (String) -> Void, addBookmark: @escaping () -> Void) { + self.context = context + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.url = url + self.openUrl = openUrl + self.addBookmark = addBookmark + + super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData)) + + self.navigationPresentation = .modal + self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) + + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style + + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) + + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) + self.title = self.presentationData.strings.WebBrowser_Bookmarks_Title + + self.searchContentNode = NavigationBarSearchContentNode(theme: self.presentationData.theme, placeholder: self.presentationData.strings.Common_Search, activate: { [weak self] in + self?.activateSearch() + }) + self.navigationBar?.setContentNode(self.searchContentNode, animated: false) + + self.scrollToTop = { [weak self] in + if let self { + if let searchContentNode = self.searchContentNode { + searchContentNode.updateExpansionProgress(1.0, animated: true) + } + self.node.scrollToTop() + } + } + + self.presentationDataDisposable = (context.sharedContext.presentationData + |> deliverOnMainQueue).start(next: { [weak self] presentationData in + if let strongSelf = self { + let previousTheme = strongSelf.presentationData.theme + let previousStrings = strongSelf.presentationData.strings + + strongSelf.presentationData = presentationData + + if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings { + strongSelf.updateThemeAndStrings() + } + } + }).strict() + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.presentationDataDisposable?.dispose() + } + + override public func loadDisplayNode() { + self.displayNode = Node(context: self.context, controller: self, presentationData: self.presentationData) + + self.node.historyNode.contentPositionChanged = { [weak self] offset in + if let strongSelf = self, let searchContentNode = strongSelf.searchContentNode { + searchContentNode.updateListVisibleContentOffset(offset) + } + } +// +// self.node.historyNode.didEndScrolling = { [weak self] _ in +// if let strongSelf = self, let searchContentNode = strongSelf.searchContentNode { +// let _ = fixNavigationSearchableListNodeScrolling(strongSelf.node.historyNode, searchNode: searchContentNode) +// } +// } + + self.displayNodeDidLoad() + } + + private func updateThemeAndStrings() { + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style + self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData)) + self.searchContentNode?.updateThemeAndPlaceholder(theme: self.presentationData.theme, placeholder: self.presentationData.strings.Common_Search) + } + + fileprivate func activateSearch() { + if self.displayNavigationBar { + if let scrollToTop = self.scrollToTop { + scrollToTop() + } + if let searchContentNode = self.searchContentNode { + self.node.activateSearch(placeholderNode: searchContentNode.placeholderNode) + } + self.setDisplayNavigationBar(false, transition: .animated(duration: 0.5, curve: .spring)) + } + } + + fileprivate func deactivateSearch() { + if !self.displayNavigationBar { + self.setDisplayNavigationBar(true, transition: .animated(duration: 0.5, curve: .spring)) + if let searchContentNode = self.searchContentNode { + self.node.deactivateSearch(placeholderNode: searchContentNode.placeholderNode) + } + } + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.validLayout = layout + + self.controllerNode.containerLayoutUpdated(layout: layout, navigationBarHeight: self.cleanNavigationHeight, actualNavigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) + } + + @objc private func cancelPressed() { + self.dismiss() + } +} + +private class BottomPanelNode: ASDisplayNode { + private let theme: PresentationTheme + private let strings: PresentationStrings + private let action: () -> Void + + private let separatorNode: ASDisplayNode + private let button: HighlightTrackingButtonNode + private let iconNode: ASImageNode + private let textNode: ImmediateTextNode + + private var validLayout: (CGFloat, CGFloat, CGFloat)? + + init(theme: PresentationTheme, strings: PresentationStrings, action: @escaping () -> Void) { + self.theme = theme + self.strings = strings + self.action = action + + self.separatorNode = ASDisplayNode() + self.separatorNode.backgroundColor = theme.rootController.navigationBar.separatorColor + + self.iconNode = ASImageNode() + self.iconNode.displaysAsynchronously = false + self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat List/AddIcon"), color: theme.rootController.navigationBar.accentTextColor) + self.iconNode.isUserInteractionEnabled = false + + self.textNode = ImmediateTextNode() + self.textNode.displaysAsynchronously = false + self.textNode.attributedText = NSAttributedString(string: strings.WebBrowser_Bookmarks_BookmarkCurrent, font: Font.regular(17.0), textColor: theme.rootController.navigationBar.accentTextColor) + self.textNode.isUserInteractionEnabled = false + + self.button = HighlightTrackingButtonNode() + + super.init() + + self.backgroundColor = theme.rootController.navigationBar.opaqueBackgroundColor + + self.addSubnode(self.button) + self.addSubnode(self.separatorNode) + self.addSubnode(self.iconNode) + self.addSubnode(self.textNode) + self.addSubnode(self.button) + + self.button.highligthedChanged = { [weak self] highlighted in + if let self { + if highlighted { + self.iconNode.layer.removeAnimation(forKey: "opacity") + self.iconNode.alpha = 0.4 + + self.textNode.layer.removeAnimation(forKey: "opacity") + self.textNode.alpha = 0.4 + } else { + self.iconNode.alpha = 1.0 + self.iconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + + self.textNode.alpha = 1.0 + self.textNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + self.button.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + } + + @objc private func buttonPressed() { + self.action() + } + + func updateLayout(width: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { + self.validLayout = (width, sideInset, bottomInset) + let topInset: CGFloat = 8.0 + var bottomInset = bottomInset + bottomInset += topInset - (bottomInset.isZero ? 0.0 : 4.0) + + let buttonHeight: CGFloat = 40.0 + let textSize = self.textNode.updateLayout(CGSize(width: width, height: 44.0)) + + let spacing: CGFloat = 8.0 + var contentWidth = textSize.width + var contentOriginX = floorToScreenPixels((width - contentWidth) / 2.0) + if let icon = self.iconNode.image { + contentWidth += icon.size.width + spacing + contentOriginX = floorToScreenPixels((width - contentWidth) / 2.0) + transition.updateFrame(node: self.iconNode, frame: CGRect(origin: CGPoint(x: contentOriginX, y: 12.0 + UIScreenPixel), size: icon.size)) + contentOriginX += icon.size.width + spacing + } + let textFrame = CGRect(origin: CGPoint(x: contentOriginX, y: 17.0), size: textSize) + transition.updateFrame(node: self.textNode, frame: textFrame) + + transition.updateFrame(node: self.button, frame: textFrame.insetBy(dx: -10.0, dy: -10.0)) + + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: UIScreenPixel))) + + return topInset + buttonHeight + bottomInset + } +} + diff --git a/submodules/BrowserUI/Sources/BrowserContent.swift b/submodules/BrowserUI/Sources/BrowserContent.swift index 7a6427c01d..cf7eea0583 100644 --- a/submodules/BrowserUI/Sources/BrowserContent.swift +++ b/submodules/BrowserUI/Sources/BrowserContent.swift @@ -10,6 +10,7 @@ final class BrowserContentState: Equatable { enum ContentType: Equatable { case webPage case instantPage + case document } struct HistoryItem: Equatable { @@ -39,6 +40,7 @@ final class BrowserContentState: Equatable { let readingProgress: Double let contentType: ContentType let favicon: UIImage? + let isSecure: Bool let canGoBack: Bool let canGoForward: Bool @@ -53,6 +55,7 @@ final class BrowserContentState: Equatable { readingProgress: Double, contentType: ContentType, favicon: UIImage? = nil, + isSecure: Bool = false, canGoBack: Bool = false, canGoForward: Bool = false, backList: [HistoryItem] = [], @@ -64,6 +67,7 @@ final class BrowserContentState: Equatable { self.readingProgress = readingProgress self.contentType = contentType self.favicon = favicon + self.isSecure = isSecure self.canGoBack = canGoBack self.canGoForward = canGoForward self.backList = backList @@ -89,6 +93,9 @@ final class BrowserContentState: Equatable { if (lhs.favicon == nil) != (rhs.favicon == nil) { return false } + if lhs.isSecure != rhs.isSecure { + return false + } if lhs.canGoBack != rhs.canGoBack { return false } @@ -105,39 +112,43 @@ final class BrowserContentState: Equatable { } func withUpdatedTitle(_ title: String) -> BrowserContentState { - return BrowserContentState(title: title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList) + return BrowserContentState(title: title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, isSecure: self.isSecure, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList) } func withUpdatedUrl(_ url: String) -> BrowserContentState { - return BrowserContentState(title: self.title, url: url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList) + return BrowserContentState(title: self.title, url: url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, isSecure: self.isSecure, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList) + } + + func withUpdatedIsSecure(_ isSecure: Bool) -> BrowserContentState { + return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, isSecure: isSecure, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList) } func withUpdatedEstimatedProgress(_ estimatedProgress: Double) -> BrowserContentState { - return BrowserContentState(title: self.title, url: self.url, estimatedProgress: estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList) + return BrowserContentState(title: self.title, url: self.url, estimatedProgress: estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, isSecure: self.isSecure, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList) } func withUpdatedReadingProgress(_ readingProgress: Double) -> BrowserContentState { - return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: readingProgress, contentType: self.contentType, favicon: self.favicon, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList) + return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: readingProgress, contentType: self.contentType, favicon: self.favicon, isSecure: self.isSecure, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList) } func withUpdatedFavicon(_ favicon: UIImage?) -> BrowserContentState { - return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: favicon, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList) + return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: favicon, isSecure: self.isSecure, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList) } func withUpdatedCanGoBack(_ canGoBack: Bool) -> BrowserContentState { - return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, canGoBack: canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList) + return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, isSecure: self.isSecure, canGoBack: canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList) } func withUpdatedCanGoForward(_ canGoForward: Bool) -> BrowserContentState { - return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, canGoBack: self.canGoBack, canGoForward: canGoForward, backList: self.backList, forwardList: self.forwardList) + return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, isSecure: self.isSecure, canGoBack: self.canGoBack, canGoForward: canGoForward, backList: self.backList, forwardList: self.forwardList) } func withUpdatedBackList(_ backList: [HistoryItem]) -> BrowserContentState { - return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: backList, forwardList: self.forwardList) + return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, isSecure: self.isSecure, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: backList, forwardList: self.forwardList) } func withUpdatedForwardList(_ forwardList: [HistoryItem]) -> BrowserContentState { - return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: forwardList) + return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, isSecure: self.isSecure, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: forwardList) } } @@ -173,6 +184,8 @@ protocol BrowserContent: UIView { func scrollToTop() + func addToRecentlyVisited() + func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ComponentTransition) } diff --git a/submodules/BrowserUI/Sources/BrowserDocumentContent.swift b/submodules/BrowserUI/Sources/BrowserDocumentContent.swift new file mode 100644 index 0000000000..375979e94e --- /dev/null +++ b/submodules/BrowserUI/Sources/BrowserDocumentContent.swift @@ -0,0 +1,471 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import TelegramCore +import Postbox +import SwiftSignalKit +import TelegramPresentationData +import TelegramUIPreferences +import PresentationDataUtils +import AccountContext +import WebKit +import AppBundle +import PromptUI +import SafariServices +import ShareController +import UndoUI +import UrlEscaping + + +final class BrowserDocumentContent: UIView, BrowserContent, WKNavigationDelegate, WKUIDelegate, UIScrollViewDelegate { + private let context: AccountContext + private var presentationData: PresentationData + + private let webView: WKWebView + + let uuid: UUID + + private var _state: BrowserContentState + private let statePromise: Promise + + var currentState: BrowserContentState { + return self._state + } + var state: Signal { + return self.statePromise.get() + } + + var pushContent: (BrowserScreen.Subject) -> Void = { _ in } + var onScrollingUpdate: (ContentScrollingUpdate) -> Void = { _ in } + var minimize: () -> Void = { } + var close: () -> Void = { } + var present: (ViewController, Any?) -> Void = { _, _ in } + var presentInGlobalOverlay: (ViewController) -> Void = { _ in } + var getNavigationController: () -> NavigationController? = { return nil } + + private var tempFile: TempBoxFile? + + init(context: AccountContext, presentationData: PresentationData, file: TelegramMediaFile) { + self.context = context + self.uuid = UUID() + self.presentationData = presentationData + + let configuration = WKWebViewConfiguration() + self.webView = WKWebView(frame: CGRect(), configuration: configuration) + self.webView.allowsLinkPreview = true + + if #available(iOS 11.0, *) { + self.webView.scrollView.contentInsetAdjustmentBehavior = .never + } + + var title: String = "file" + if let path = self.context.account.postbox.mediaBox.completedResourcePath(file.resource) { + var updatedPath = path + if let fileName = file.fileName { + let tempFile = TempBox.shared.file(path: path, fileName: fileName) + updatedPath = tempFile.path + self.tempFile = tempFile + title = fileName + } + + let request = URLRequest(url: URL(fileURLWithPath: updatedPath)) + self.webView.load(request) + } + + self._state = BrowserContentState(title: title, url: "", estimatedProgress: 0.0, readingProgress: 0.0, contentType: .document) + self.statePromise = Promise(self._state) + + super.init(frame: .zero) + + self.webView.allowsBackForwardNavigationGestures = true + self.webView.scrollView.delegate = self + self.webView.scrollView.clipsToBounds = false + self.webView.navigationDelegate = self + self.webView.uiDelegate = self + if #available(iOS 15.0, *) { + self.backgroundColor = presentationData.theme.list.plainBackgroundColor + self.webView.underPageBackgroundColor = presentationData.theme.list.plainBackgroundColor + } + self.addSubview(self.webView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func updatePresentationData(_ presentationData: PresentationData) { + self.presentationData = presentationData + if #available(iOS 15.0, *) { + self.backgroundColor = presentationData.theme.list.plainBackgroundColor + self.webView.underPageBackgroundColor = presentationData.theme.list.plainBackgroundColor + } + if let (size, insets) = self.validLayout { + self.updateLayout(size: size, insets: insets, transition: .immediate) + } + } + + var currentFontState = BrowserPresentationState.FontState(size: 100, isSerif: false) + func updateFontState(_ state: BrowserPresentationState.FontState) { + self.updateFontState(state, force: false) + } + func updateFontState(_ state: BrowserPresentationState.FontState, force: Bool) { + self.currentFontState = state + + let fontFamily = state.isSerif ? "'Georgia, serif'" : "null" + let textSizeAdjust = state.size != 100 ? "'\(state.size)%'" : "null" + let js = "\(setupFontFunctions) setTelegramFontOverrides(\(fontFamily), \(textSizeAdjust))"; + self.webView.evaluateJavaScript(js) { _, _ in } + } + + private var didSetupSearch = false + private func setupSearch(completion: @escaping () -> Void) { + guard !self.didSetupSearch else { + completion() + return + } + + let bundle = getAppBundle() + guard let scriptPath = bundle.path(forResource: "UIWebViewSearch", ofType: "js") else { + return + } + guard let scriptData = try? Data(contentsOf: URL(fileURLWithPath: scriptPath)) else { + return + } + guard let script = String(data: scriptData, encoding: .utf8) else { + return + } + self.didSetupSearch = true + self.webView.evaluateJavaScript(script, completionHandler: { _, error in + if error != nil { + print() + } + completion() + }) + } + + private var previousQuery: String? + func setSearch(_ query: String?, completion: ((Int) -> Void)?) { + guard self.previousQuery != query else { + return + } + self.previousQuery = query + self.setupSearch { [weak self] in + if let query = query { + let js = "uiWebview_HighlightAllOccurencesOfString('\(query)')" + self?.webView.evaluateJavaScript(js, completionHandler: { [weak self] _, _ in + let js = "uiWebview_SearchResultCount" + self?.webView.evaluateJavaScript(js, completionHandler: { [weak self] result, _ in + if let result = result as? NSNumber { + self?.searchResultsCount = result.intValue + completion?(result.intValue) + } else { + completion?(0) + } + }) + }) + } else { + let js = "uiWebview_RemoveAllHighlights()" + self?.webView.evaluateJavaScript(js, completionHandler: nil) + + self?.currentSearchResult = 0 + self?.searchResultsCount = 0 + } + } + } + + private var currentSearchResult: Int = 0 + private var searchResultsCount: Int = 0 + + func scrollToPreviousSearchResult(completion: ((Int, Int) -> Void)?) { + let searchResultsCount = self.searchResultsCount + var index = self.currentSearchResult - 1 + if index < 0 { + index = searchResultsCount - 1 + } + self.currentSearchResult = index + + let js = "uiWebview_ScrollTo('\(searchResultsCount - index - 1)')" + self.webView.evaluateJavaScript(js, completionHandler: { _, _ in + completion?(index, searchResultsCount) + }) + } + + func scrollToNextSearchResult(completion: ((Int, Int) -> Void)?) { + let searchResultsCount = self.searchResultsCount + var index = self.currentSearchResult + 1 + if index >= searchResultsCount { + index = 0 + } + self.currentSearchResult = index + + let js = "uiWebview_ScrollTo('\(searchResultsCount - index - 1)')" + self.webView.evaluateJavaScript(js, completionHandler: { _, _ in + completion?(index, searchResultsCount) + }) + } + + func stop() { + self.webView.stopLoading() + } + + func reload() { + self.webView.reload() + } + + func navigateBack() { + self.webView.goBack() + } + + func navigateForward() { + self.webView.goForward() + } + + func navigateTo(historyItem: BrowserContentState.HistoryItem) { + if let webItem = historyItem.webItem { + self.webView.go(to: webItem) + } + } + + func navigateTo(address: String) { + let finalUrl = explicitUrl(address) + guard let url = URL(string: finalUrl) else { + return + } + self.webView.load(URLRequest(url: url)) + } + + func scrollToTop() { + self.webView.scrollView.setContentOffset(CGPoint(x: 0.0, y: -self.webView.scrollView.contentInset.top), animated: true) + } + + private var validLayout: (CGSize, UIEdgeInsets)? + func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ComponentTransition) { + self.validLayout = (size, insets) + + self.previousScrollingOffset = ScrollingOffsetState(value: self.webView.scrollView.contentOffset.y, isDraggingOrDecelerating: self.webView.scrollView.isDragging || self.webView.scrollView.isDecelerating) + + let webViewFrame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: CGSize(width: size.width - insets.left - insets.right, height: size.height - insets.top - insets.bottom)) + var refresh = false + if self.webView.frame.width > 0 && webViewFrame.width != self.webView.frame.width { + refresh = true + } + transition.setFrame(view: self.webView, frame: webViewFrame) + + if refresh { + self.webView.reloadInputViews() + } + + self.webView.scrollView.scrollIndicatorInsets = UIEdgeInsets(top: 0.0, left: -insets.left, bottom: 0.0, right: -insets.right) + self.webView.scrollView.horizontalScrollIndicatorInsets = UIEdgeInsets(top: 0.0, left: -insets.left, bottom: 0.0, right: -insets.right) + +// if let error = self.currentError { +// let errorSize = self.errorView.update( +// transition: .immediate, +// component: AnyComponent( +// ErrorComponent( +// theme: self.presentationData.theme, +// title: self.presentationData.strings.Browser_ErrorTitle, +// text: error.localizedDescription +// ) +// ), +// environment: {}, +// containerSize: CGSize(width: size.width - insets.left - insets.right - 72.0, height: size.height) +// ) +// if self.errorView.superview == nil { +// self.addSubview(self.errorView) +// self.errorView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) +// } +// self.errorView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - errorSize.width) / 2.0), y: insets.top + floorToScreenPixels((size.height - insets.top - insets.bottom - errorSize.height) / 2.0)), size: errorSize) +// } else if self.errorView.superview != nil { +// self.errorView.removeFromSuperview() +// } + } + + private func updateState(_ f: (BrowserContentState) -> BrowserContentState) { + let updated = f(self._state) + self._state = updated + self.statePromise.set(.single(self._state)) + } + + override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + if keyPath == "title" { + self.updateState { $0.withUpdatedTitle(self.webView.title ?? "") } + } else if keyPath == "URL" { + self.updateState { $0.withUpdatedUrl(self.webView.url?.absoluteString ?? "") } + self.didSetupSearch = false + } else if keyPath == "estimatedProgress" { + self.updateState { $0.withUpdatedEstimatedProgress(self.webView.estimatedProgress) } + } else if keyPath == "canGoBack" { + self.updateState { $0.withUpdatedCanGoBack(self.webView.canGoBack) } + self.webView.disablesInteractiveTransitionGestureRecognizer = self.webView.canGoBack + } else if keyPath == "canGoForward" { + self.updateState { $0.withUpdatedCanGoForward(self.webView.canGoForward) } + } + } + + private struct ScrollingOffsetState: Equatable { + var value: CGFloat + var isDraggingOrDecelerating: Bool + } + + private var previousScrollingOffset: ScrollingOffsetState? + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + self.updateScrollingOffset(isReset: false, transition: .immediate) + } + + private func snapScrollingOffsetToInsets() { + let transition = ComponentTransition(animation: .curve(duration: 0.4, curve: .spring)) + self.updateScrollingOffset(isReset: false, transition: transition) + } + + public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + if !decelerate { + self.snapScrollingOffsetToInsets() + } + } + + public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + self.snapScrollingOffsetToInsets() + } + + private func updateScrollingOffset(isReset: Bool, transition: ComponentTransition) { + let scrollView = self.webView.scrollView + let isInteracting = scrollView.isDragging || scrollView.isDecelerating + if let previousScrollingOffsetValue = self.previousScrollingOffset { + let currentBounds = scrollView.bounds + let offsetToTopEdge = max(0.0, currentBounds.minY - 0.0) + let offsetToBottomEdge = max(0.0, scrollView.contentSize.height - currentBounds.maxY) + + let relativeOffset = scrollView.contentOffset.y - previousScrollingOffsetValue.value + self.onScrollingUpdate(ContentScrollingUpdate( + relativeOffset: relativeOffset, + absoluteOffsetToTopEdge: offsetToTopEdge, + absoluteOffsetToBottomEdge: offsetToBottomEdge, + isReset: isReset, + isInteracting: isInteracting, + transition: transition + )) + } + self.previousScrollingOffset = ScrollingOffsetState(value: scrollView.contentOffset.y, isDraggingOrDecelerating: isInteracting) + + var readingProgress: CGFloat = 0.0 + if !scrollView.contentSize.height.isZero { + let value = (scrollView.contentOffset.y + scrollView.contentInset.top) / (scrollView.contentSize.height - scrollView.bounds.size.height + scrollView.contentInset.top) + readingProgress = max(0.0, min(1.0, value)) + } + self.updateState { + $0.withUpdatedReadingProgress(readingProgress) + } + } + + func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) { +// self.currentError = nil + self.updateFontState(self.currentFontState, force: true) + } + + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + self.updateState { + $0 + .withUpdatedBackList(webView.backForwardList.backList.map { BrowserContentState.HistoryItem(webItem: $0) }) + .withUpdatedForwardList(webView.backForwardList.forwardList.map { BrowserContentState.HistoryItem(webItem: $0) }) + } + } + +// func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { +// if (error as NSError).code != -999 { +// self.currentError = error +// } else { +// self.currentError = nil +// } +// if let (size, insets) = self.validLayout { +// self.updateLayout(size: size, insets: insets, transition: .immediate) +// } +// } +// +// func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { +// if (error as NSError).code != -999 { +// self.currentError = error +// } else { +// self.currentError = nil +// } +// if let (size, insets) = self.validLayout { +// self.updateLayout(size: size, insets: insets, transition: .immediate) +// } +// } + + func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? { + if navigationAction.targetFrame == nil { + if let url = navigationAction.request.url?.absoluteString { + self.open(url: url, new: true) + } + } + return nil + } + + func webViewDidClose(_ webView: WKWebView) { + self.close() + } + + @available(iOSApplicationExtension 15.0, iOS 15.0, *) + func webView(_ webView: WKWebView, requestMediaCapturePermissionFor origin: WKSecurityOrigin, initiatedByFrame frame: WKFrameInfo, type: WKMediaCaptureType, decisionHandler: @escaping (WKPermissionDecision) -> Void) { + decisionHandler(.prompt) + } + + +// @available(iOS 13.0, *) +// func webView(_ webView: WKWebView, contextMenuConfigurationForElement elementInfo: WKContextMenuElementInfo, completionHandler: @escaping (UIContextMenuConfiguration?) -> Void) { +// guard let url = elementInfo.linkURL else { +// completionHandler(nil) +// return +// } +// let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } +// let configuration = UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { [weak self] _ in +// return UIMenu(title: "", children: [ +// UIAction(title: presentationData.strings.Browser_ContextMenu_Open, image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Browser"), color: presentationData.theme.contextMenu.primaryColor), handler: { [weak self] _ in +// self?.open(url: url.absoluteString, new: false) +// }), +// UIAction(title: presentationData.strings.Browser_ContextMenu_OpenInNewTab, image: generateTintedImage(image: UIImage(bundleImageName: "Instant View/NewTab"), color: presentationData.theme.contextMenu.primaryColor), handler: { [weak self] _ in +// self?.open(url: url.absoluteString, new: true) +// }), +// UIAction(title: presentationData.strings.Browser_ContextMenu_AddToReadingList, image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReadingList"), color: presentationData.theme.contextMenu.primaryColor), handler: { _ in +// let _ = try? SSReadingList.default()?.addItem(with: url, title: nil, previewText: nil) +// }), +// UIAction(title: presentationData.strings.Browser_ContextMenu_CopyLink, image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: presentationData.theme.contextMenu.primaryColor), handler: { [weak self] _ in +// UIPasteboard.general.string = url.absoluteString +// self?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) +// }), +// UIAction(title: presentationData.strings.Browser_ContextMenu_Share, image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: presentationData.theme.contextMenu.primaryColor), handler: { [weak self] _ in +// self?.share(url: url.absoluteString) +// }) +// ]) +// } +// completionHandler(configuration) +// } + + private func open(url: String, new: Bool) { + let subject: BrowserScreen.Subject = .webPage(url: url) + if new, let navigationController = self.getNavigationController() { + navigationController._keepModalDismissProgress = true + self.minimize() + let controller = BrowserScreen(context: self.context, subject: subject) + navigationController._keepModalDismissProgress = true + navigationController.pushViewController(controller) + } else { + self.pushContent(subject) + } + } + + private func share(url: String) { + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + let shareController = ShareController(context: self.context, subject: .url(url)) + shareController.actionCompleted = { [weak self] in + self?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) + } + self.present(shareController, nil) + } + + func addToRecentlyVisited() { + } +} diff --git a/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift b/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift index f9e9e721ef..b8da562cf6 100644 --- a/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift +++ b/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift @@ -1378,4 +1378,10 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg } self.updateVisibleItems(visibleBounds: self.scrollNode.view.bounds, animated: animated) } + + func addToRecentlyVisited() { + if let webPage = self.webPage { + let _ = addRecentlyVisitedLink(engine: self.context.engine, webPage: webPage).startStandalone() + } + } } diff --git a/submodules/BrowserUI/Sources/BrowserNavigationBarComponent.swift b/submodules/BrowserUI/Sources/BrowserNavigationBarComponent.swift index c873ddbaef..d31d7fd6a1 100644 --- a/submodules/BrowserUI/Sources/BrowserNavigationBarComponent.swift +++ b/submodules/BrowserUI/Sources/BrowserNavigationBarComponent.swift @@ -5,6 +5,21 @@ import ComponentFlow import BlurredBackgroundComponent import ContextUI +final class BrowserNavigationBarEnvironment: Equatable { + public let fraction: CGFloat + + public init(fraction: CGFloat) { + self.fraction = fraction + } + + public static func ==(lhs: BrowserNavigationBarEnvironment, rhs: BrowserNavigationBarEnvironment) -> Bool { + if lhs.fraction != rhs.fraction { + return false + } + return true + } +} + final class BrowserNavigationBarComponent: CombinedComponent { let backgroundColor: UIColor let separatorColor: UIColor @@ -16,7 +31,7 @@ final class BrowserNavigationBarComponent: CombinedComponent { let sideInset: CGFloat let leftItems: [AnyComponentWithIdentity] let rightItems: [AnyComponentWithIdentity] - let centerItem: AnyComponentWithIdentity? + let centerItem: AnyComponentWithIdentity? let readingProgress: CGFloat let loadingProgress: Double? let collapseFraction: CGFloat @@ -32,7 +47,7 @@ final class BrowserNavigationBarComponent: CombinedComponent { sideInset: CGFloat, leftItems: [AnyComponentWithIdentity], rightItems: [AnyComponentWithIdentity], - centerItem: AnyComponentWithIdentity?, + centerItem: AnyComponentWithIdentity?, readingProgress: CGFloat, loadingProgress: Double?, collapseFraction: CGFloat @@ -106,7 +121,7 @@ final class BrowserNavigationBarComponent: CombinedComponent { let loadingProgress = Child(LoadingProgressComponent.self) let leftItems = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self) let rightItems = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self) - let centerItems = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self) + let centerItems = ChildMap(environment: BrowserNavigationBarEnvironment.self, keyedBy: AnyHashable.self) return { context in var availableWidth = context.availableSize.width @@ -169,16 +184,20 @@ final class BrowserNavigationBarComponent: CombinedComponent { } if !leftItemList.isEmpty || !rightItemList.isEmpty { - availableWidth -= 32.0 + availableWidth -= 14.0 } context.add(background .position(CGPoint(x: size.width / 2.0, y: size.height / 2.0)) ) + var readingProgressAlpha = context.component.collapseFraction + if leftItemList.isEmpty && rightItemList.isEmpty { + readingProgressAlpha = 0.0 + } context.add(readingProgress .position(CGPoint(x: readingProgress.size.width / 2.0, y: size.height / 2.0)) - .opacity(context.component.centerItem?.id == AnyHashable("search") ? 0.0 : 1.0) + .opacity(readingProgressAlpha) ) context.add(separator @@ -199,7 +218,7 @@ final class BrowserNavigationBarComponent: CombinedComponent { .appear(.default(scale: true, alpha: true)) .disappear(.default(scale: true, alpha: true)) ) - leftItemX -= item.size.width + 8.0 + leftItemX += item.size.width + 8.0 centerLeftInset += item.size.width + 8.0 } @@ -220,20 +239,27 @@ final class BrowserNavigationBarComponent: CombinedComponent { let maxCenterInset = max(centerLeftInset, centerRightInset) if !leftItemList.isEmpty || !rightItemList.isEmpty { - availableWidth -= 28.0 + availableWidth -= 20.0 } + let environment = BrowserNavigationBarEnvironment(fraction: context.component.collapseFraction) + let centerItem = context.component.centerItem.flatMap { item in centerItems[item.id].update( component: item.component, + environment: { environment }, availableSize: CGSize(width: availableWidth, height: expandedHeight), transition: context.transition ) } - + + var centerX = maxCenterInset + (context.availableSize.width - maxCenterInset * 2.0) / 2.0 + if "".isEmpty { + centerX = centerLeftInset + (context.availableSize.width - centerLeftInset - centerRightInset) / 2.0 + } if let centerItem = centerItem { context.add(centerItem - .position(CGPoint(x: maxCenterInset + (context.availableSize.width - maxCenterInset * 2.0) / 2.0, y: context.component.topInset + contentHeight / 2.0)) + .position(CGPoint(x: centerX, y: context.component.topInset + contentHeight / 2.0)) .scale(1.0 - 0.35 * context.component.collapseFraction) .appear(.default(scale: false, alpha: true)) .disappear(.default(scale: false, alpha: true)) diff --git a/submodules/BrowserUI/Sources/BrowserPdfContent.swift b/submodules/BrowserUI/Sources/BrowserPdfContent.swift new file mode 100644 index 0000000000..470b350ae9 --- /dev/null +++ b/submodules/BrowserUI/Sources/BrowserPdfContent.swift @@ -0,0 +1,463 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import TelegramCore +import Postbox +import SwiftSignalKit +import TelegramPresentationData +import TelegramUIPreferences +import PresentationDataUtils +import AccountContext +import WebKit +import AppBundle +import PromptUI +import SafariServices +import ShareController +import UndoUI +import UrlEscaping +import PDFKit + +final class BrowserPdfContent: UIView, BrowserContent, WKNavigationDelegate, WKUIDelegate, UIScrollViewDelegate { + private let context: AccountContext + private var presentationData: PresentationData + + private let webView: PDFView + private let scrollView: UIScrollView! + + let uuid: UUID + + private var _state: BrowserContentState + private let statePromise: Promise + + var currentState: BrowserContentState { + return self._state + } + var state: Signal { + return self.statePromise.get() + } + + var pushContent: (BrowserScreen.Subject) -> Void = { _ in } + var onScrollingUpdate: (ContentScrollingUpdate) -> Void = { _ in } + var minimize: () -> Void = { } + var close: () -> Void = { } + var present: (ViewController, Any?) -> Void = { _, _ in } + var presentInGlobalOverlay: (ViewController) -> Void = { _ in } + var getNavigationController: () -> NavigationController? = { return nil } + + private var tempFile: TempBoxFile? + + init(context: AccountContext, presentationData: PresentationData, file: TelegramMediaFile) { + self.context = context + self.uuid = UUID() + self.presentationData = presentationData + + self.webView = PDFView() + self.webView.maxScaleFactor = 4.0; + self.webView.minScaleFactor = self.webView.scaleFactorForSizeToFit + self.webView.autoScales = true + + var scrollView: UIScrollView? + for view in self.webView.subviews { + if let view = view as? UIScrollView { + scrollView = view + } else { + for subview in view.subviews { + if let subview = subview as? UIScrollView { + scrollView = subview + } + } + } + } + self.scrollView = scrollView + + var title: String = "file" + if let path = self.context.account.postbox.mediaBox.completedResourcePath(file.resource), let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe) { +// var updatedPath = path +// if let fileName = file.fileName { +// let tempFile = TempBox.shared.file(path: path, fileName: fileName) +// updatedPath = tempFile.path +// self.tempFile = tempFile +// title = fileName +// } + + self.webView.document = PDFDocument(data: data) + title = file.fileName ?? "file" + } + + self._state = BrowserContentState(title: title, url: "", estimatedProgress: 0.0, readingProgress: 0.0, contentType: .document) + self.statePromise = Promise(self._state) + + super.init(frame: .zero) + + if #available(iOS 15.0, *) { + self.backgroundColor = presentationData.theme.list.plainBackgroundColor + } + self.addSubview(self.webView) + + Queue.mainQueue().after(1.0) { + scrollView?.delegate = self + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func updatePresentationData(_ presentationData: PresentationData) { + self.presentationData = presentationData + if #available(iOS 15.0, *) { + self.backgroundColor = presentationData.theme.list.plainBackgroundColor + } + if let (size, insets) = self.validLayout { + self.updateLayout(size: size, insets: insets, transition: .immediate) + } + } + + var currentFontState = BrowserPresentationState.FontState(size: 100, isSerif: false) + func updateFontState(_ state: BrowserPresentationState.FontState) { + self.updateFontState(state, force: false) + } + func updateFontState(_ state: BrowserPresentationState.FontState, force: Bool) { + self.currentFontState = state + +// let fontFamily = state.isSerif ? "'Georgia, serif'" : "null" +// let textSizeAdjust = state.size != 100 ? "'\(state.size)%'" : "null" +// let js = "\(setupFontFunctions) setTelegramFontOverrides(\(fontFamily), \(textSizeAdjust))"; +// self.webView.evaluateJavaScript(js) { _, _ in } + } + + private var didSetupSearch = false + private func setupSearch(completion: @escaping () -> Void) { +// guard !self.didSetupSearch else { +// completion() +// return +// } +// +// let bundle = getAppBundle() +// guard let scriptPath = bundle.path(forResource: "UIWebViewSearch", ofType: "js") else { +// return +// } +// guard let scriptData = try? Data(contentsOf: URL(fileURLWithPath: scriptPath)) else { +// return +// } +// guard let script = String(data: scriptData, encoding: .utf8) else { +// return +// } +// self.didSetupSearch = true +// self.webView.evaluateJavaScript(script, completionHandler: { _, error in +// if error != nil { +// print() +// } +// completion() +// }) + } + + private var previousQuery: String? + func setSearch(_ query: String?, completion: ((Int) -> Void)?) { +// guard self.previousQuery != query else { +// return +// } +// self.previousQuery = query +// self.setupSearch { [weak self] in +// if let query = query { +// let js = "uiWebview_HighlightAllOccurencesOfString('\(query)')" +// self?.webView.evaluateJavaScript(js, completionHandler: { [weak self] _, _ in +// let js = "uiWebview_SearchResultCount" +// self?.webView.evaluateJavaScript(js, completionHandler: { [weak self] result, _ in +// if let result = result as? NSNumber { +// self?.searchResultsCount = result.intValue +// completion?(result.intValue) +// } else { +// completion?(0) +// } +// }) +// }) +// } else { +// let js = "uiWebview_RemoveAllHighlights()" +// self?.webView.evaluateJavaScript(js, completionHandler: nil) +// +// self?.currentSearchResult = 0 +// self?.searchResultsCount = 0 +// } +// } + } + + private var currentSearchResult: Int = 0 + private var searchResultsCount: Int = 0 + + func scrollToPreviousSearchResult(completion: ((Int, Int) -> Void)?) { +// let searchResultsCount = self.searchResultsCount +// var index = self.currentSearchResult - 1 +// if index < 0 { +// index = searchResultsCount - 1 +// } +// self.currentSearchResult = index +// +// let js = "uiWebview_ScrollTo('\(searchResultsCount - index - 1)')" +// self.webView.evaluateJavaScript(js, completionHandler: { _, _ in +// completion?(index, searchResultsCount) +// }) + } + + func scrollToNextSearchResult(completion: ((Int, Int) -> Void)?) { +// let searchResultsCount = self.searchResultsCount +// var index = self.currentSearchResult + 1 +// if index >= searchResultsCount { +// index = 0 +// } +// self.currentSearchResult = index +// +// let js = "uiWebview_ScrollTo('\(searchResultsCount - index - 1)')" +// self.webView.evaluateJavaScript(js, completionHandler: { _, _ in +// completion?(index, searchResultsCount) +// }) + } + + func stop() { +// self.webView.stopLoading() + } + + func reload() { +// self.webView.reload() + } + + func navigateBack() { +// self.webView.goBack() + } + + func navigateForward() { +// self.webView.goForward() + } + + func navigateTo(historyItem: BrowserContentState.HistoryItem) { +// if let webItem = historyItem.webItem { +// self.webView.go(to: webItem) +// } + } + + func navigateTo(address: String) { +// let finalUrl = explicitUrl(address) +// guard let url = URL(string: finalUrl) else { +// return +// } +// self.webView.load(URLRequest(url: url)) + } + + func scrollToTop() { + self.scrollView.setContentOffset(CGPoint(x: 0.0, y: -self.scrollView.contentInset.top), animated: true) + } + + private var validLayout: (CGSize, UIEdgeInsets)? + func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ComponentTransition) { + self.validLayout = (size, insets) + + self.previousScrollingOffset = ScrollingOffsetState(value: self.scrollView.contentOffset.y, isDraggingOrDecelerating: self.scrollView.isDragging || self.scrollView.isDecelerating) + + let webViewFrame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: CGSize(width: size.width - insets.left - insets.right, height: size.height - insets.top - insets.bottom)) + var refresh = false + if self.webView.frame.width > 0 && webViewFrame.width != self.webView.frame.width { + refresh = true + } + transition.setFrame(view: self.webView, frame: webViewFrame) + + if refresh { + self.webView.reloadInputViews() + } + +// if let error = self.currentError { +// let errorSize = self.errorView.update( +// transition: .immediate, +// component: AnyComponent( +// ErrorComponent( +// theme: self.presentationData.theme, +// title: self.presentationData.strings.Browser_ErrorTitle, +// text: error.localizedDescription +// ) +// ), +// environment: {}, +// containerSize: CGSize(width: size.width - insets.left - insets.right - 72.0, height: size.height) +// ) +// if self.errorView.superview == nil { +// self.addSubview(self.errorView) +// self.errorView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) +// } +// self.errorView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - errorSize.width) / 2.0), y: insets.top + floorToScreenPixels((size.height - insets.top - insets.bottom - errorSize.height) / 2.0)), size: errorSize) +// } else if self.errorView.superview != nil { +// self.errorView.removeFromSuperview() +// } + } + + private func updateState(_ f: (BrowserContentState) -> BrowserContentState) { + let updated = f(self._state) + self._state = updated + self.statePromise.set(.single(self._state)) + } + + private struct ScrollingOffsetState: Equatable { + var value: CGFloat + var isDraggingOrDecelerating: Bool + } + + private var previousScrollingOffset: ScrollingOffsetState? + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + self.updateScrollingOffset(isReset: false, transition: .immediate) + } + + private func snapScrollingOffsetToInsets() { + let transition = ComponentTransition(animation: .curve(duration: 0.4, curve: .spring)) + self.updateScrollingOffset(isReset: false, transition: transition) + } + + public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + if !decelerate { + self.snapScrollingOffsetToInsets() + } + } + + public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + self.snapScrollingOffsetToInsets() + } + + private func updateScrollingOffset(isReset: Bool, transition: ComponentTransition) { + guard let scrollView = self.scrollView else { + return + } + let isInteracting = scrollView.isDragging || scrollView.isDecelerating + if let previousScrollingOffsetValue = self.previousScrollingOffset { + let currentBounds = scrollView.bounds + let offsetToTopEdge = max(0.0, currentBounds.minY - 0.0) + let offsetToBottomEdge = max(0.0, scrollView.contentSize.height - currentBounds.maxY) + + let relativeOffset = scrollView.contentOffset.y - previousScrollingOffsetValue.value + self.onScrollingUpdate(ContentScrollingUpdate( + relativeOffset: relativeOffset, + absoluteOffsetToTopEdge: offsetToTopEdge, + absoluteOffsetToBottomEdge: offsetToBottomEdge, + isReset: isReset, + isInteracting: isInteracting, + transition: transition + )) + } + self.previousScrollingOffset = ScrollingOffsetState(value: scrollView.contentOffset.y, isDraggingOrDecelerating: isInteracting) + + var readingProgress: CGFloat = 0.0 + if !scrollView.contentSize.height.isZero { + let value = (scrollView.contentOffset.y + scrollView.contentInset.top) / (scrollView.contentSize.height - scrollView.bounds.size.height + scrollView.contentInset.top) + readingProgress = max(0.0, min(1.0, value)) + } + self.updateState { + $0.withUpdatedReadingProgress(readingProgress) + } + } + + func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) { +// self.currentError = nil + self.updateFontState(self.currentFontState, force: true) + } + + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + self.updateState { + $0 + .withUpdatedBackList(webView.backForwardList.backList.map { BrowserContentState.HistoryItem(webItem: $0) }) + .withUpdatedForwardList(webView.backForwardList.forwardList.map { BrowserContentState.HistoryItem(webItem: $0) }) + } + } + +// func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { +// if (error as NSError).code != -999 { +// self.currentError = error +// } else { +// self.currentError = nil +// } +// if let (size, insets) = self.validLayout { +// self.updateLayout(size: size, insets: insets, transition: .immediate) +// } +// } +// +// func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { +// if (error as NSError).code != -999 { +// self.currentError = error +// } else { +// self.currentError = nil +// } +// if let (size, insets) = self.validLayout { +// self.updateLayout(size: size, insets: insets, transition: .immediate) +// } +// } + + func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? { + if navigationAction.targetFrame == nil { + if let url = navigationAction.request.url?.absoluteString { + self.open(url: url, new: true) + } + } + return nil + } + + func webViewDidClose(_ webView: WKWebView) { + self.close() + } + + @available(iOSApplicationExtension 15.0, iOS 15.0, *) + func webView(_ webView: WKWebView, requestMediaCapturePermissionFor origin: WKSecurityOrigin, initiatedByFrame frame: WKFrameInfo, type: WKMediaCaptureType, decisionHandler: @escaping (WKPermissionDecision) -> Void) { + decisionHandler(.prompt) + } + + +// @available(iOS 13.0, *) +// func webView(_ webView: WKWebView, contextMenuConfigurationForElement elementInfo: WKContextMenuElementInfo, completionHandler: @escaping (UIContextMenuConfiguration?) -> Void) { +// guard let url = elementInfo.linkURL else { +// completionHandler(nil) +// return +// } +// let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } +// let configuration = UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { [weak self] _ in +// return UIMenu(title: "", children: [ +// UIAction(title: presentationData.strings.Browser_ContextMenu_Open, image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Browser"), color: presentationData.theme.contextMenu.primaryColor), handler: { [weak self] _ in +// self?.open(url: url.absoluteString, new: false) +// }), +// UIAction(title: presentationData.strings.Browser_ContextMenu_OpenInNewTab, image: generateTintedImage(image: UIImage(bundleImageName: "Instant View/NewTab"), color: presentationData.theme.contextMenu.primaryColor), handler: { [weak self] _ in +// self?.open(url: url.absoluteString, new: true) +// }), +// UIAction(title: presentationData.strings.Browser_ContextMenu_AddToReadingList, image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReadingList"), color: presentationData.theme.contextMenu.primaryColor), handler: { _ in +// let _ = try? SSReadingList.default()?.addItem(with: url, title: nil, previewText: nil) +// }), +// UIAction(title: presentationData.strings.Browser_ContextMenu_CopyLink, image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: presentationData.theme.contextMenu.primaryColor), handler: { [weak self] _ in +// UIPasteboard.general.string = url.absoluteString +// self?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) +// }), +// UIAction(title: presentationData.strings.Browser_ContextMenu_Share, image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: presentationData.theme.contextMenu.primaryColor), handler: { [weak self] _ in +// self?.share(url: url.absoluteString) +// }) +// ]) +// } +// completionHandler(configuration) +// } + + private func open(url: String, new: Bool) { + let subject: BrowserScreen.Subject = .webPage(url: url) + if new, let navigationController = self.getNavigationController() { + navigationController._keepModalDismissProgress = true + self.minimize() + let controller = BrowserScreen(context: self.context, subject: subject) + navigationController._keepModalDismissProgress = true + navigationController.pushViewController(controller) + } else { + self.pushContent(subject) + } + } + + private func share(url: String) { + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + let shareController = ShareController(context: self.context, subject: .url(url)) + shareController.actionCompleted = { [weak self] in + self?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) + } + self.present(shareController, nil) + } + + func addToRecentlyVisited() { + } +} diff --git a/submodules/BrowserUI/Sources/BrowserRecentlyVisited.swift b/submodules/BrowserUI/Sources/BrowserRecentlyVisited.swift new file mode 100644 index 0000000000..fc8bb321f9 --- /dev/null +++ b/submodules/BrowserUI/Sources/BrowserRecentlyVisited.swift @@ -0,0 +1,88 @@ +import Foundation +import Postbox +import TelegramCore +import SwiftSignalKit +import TelegramUIPreferences + +private struct RecentlyVisitedLinkItemId { + public let rawValue: MemoryBuffer + + var value: String { + return String(data: self.rawValue.makeData(), encoding: .utf8) ?? "" + } + + init(_ rawValue: MemoryBuffer) { + self.rawValue = rawValue + } + + init?(_ value: String) { + if let data = value.data(using: .utf8) { + self.rawValue = MemoryBuffer(data: data) + } else { + return nil + } + } +} + +public final class RecentVisitedLinkItem: Codable { + private enum CodingKeys: String, CodingKey { + case webPage + } + + public let webPage: TelegramMediaWebpage + + public init(webPage: TelegramMediaWebpage) { + self.webPage = webPage + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + if let webPageData = try container.decodeIfPresent(Data.self, forKey: .webPage) { + self.webPage = PostboxDecoder(buffer: MemoryBuffer(data: webPageData)).decodeRootObject() as! TelegramMediaWebpage + } else { + fatalError() + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + let encoder = PostboxEncoder() + encoder.encodeRootObject(self.webPage) + let webPageData = encoder.makeData() + try container.encode(webPageData, forKey: .webPage) + } +} + +func addRecentlyVisitedLink(engine: TelegramEngine, webPage: TelegramMediaWebpage) -> Signal { + if let url = webPage.content.url, let itemId = RecentlyVisitedLinkItemId(url) { + return engine.orderedLists.addOrMoveToFirstPosition(collectionId: ApplicationSpecificOrderedItemListCollectionId.browserRecentlyVisited, id: itemId.rawValue, item: RecentVisitedLinkItem(webPage: webPage), removeTailIfCountExceeds: 10) + } else { + return .complete() + } +} + +func removeRecentlyVisitedLink(engine: TelegramEngine, url: String) -> Signal { + if let itemId = RecentlyVisitedLinkItemId(url) { + return engine.orderedLists.removeItem(collectionId: ApplicationSpecificOrderedItemListCollectionId.browserRecentlyVisited, id: itemId.rawValue) + } else { + return .complete() + } +} + +func clearRecentlyVisitedLinks(engine: TelegramEngine) -> Signal { + return engine.orderedLists.clear(collectionId: ApplicationSpecificOrderedItemListCollectionId.browserRecentlyVisited) +} + +func recentlyVisitedLinks(engine: TelegramEngine) -> Signal<[TelegramMediaWebpage], NoError> { + return engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: ApplicationSpecificOrderedItemListCollectionId.browserRecentlyVisited)) + |> map { items -> [TelegramMediaWebpage] in + var result: [TelegramMediaWebpage] = [] + for item in items { + if let link = item.contents.get(RecentVisitedLinkItem.self) { + result.append(link.webPage) + } + } + return result + } +} diff --git a/submodules/BrowserUI/Sources/BrowserScreen.swift b/submodules/BrowserUI/Sources/BrowserScreen.swift index f555d1a821..1899dcc14f 100644 --- a/submodules/BrowserUI/Sources/BrowserScreen.swift +++ b/submodules/BrowserUI/Sources/BrowserScreen.swift @@ -73,13 +73,14 @@ private final class BrowserScreenComponent: CombinedComponent { static var body: Body { let navigationBar = Child(BrowserNavigationBarComponent.self) let toolbar = Child(BrowserToolbarComponent.self) + let addressList = Child(BrowserAddressListComponent.self) return { context in let environment = context.environment[ViewControllerComponentContainer.Environment.self].value let performAction = context.component.performAction let performHoldAction = context.component.performHoldAction - let navigationContent: AnyComponentWithIdentity? + let navigationContent: AnyComponentWithIdentity? var navigationLeftItems: [AnyComponentWithIdentity] var navigationRightItems: [AnyComponentWithIdentity] if context.component.presentationState.isSearching { @@ -96,51 +97,78 @@ private final class BrowserScreenComponent: CombinedComponent { navigationLeftItems = [] navigationRightItems = [] } else { - let title = context.component.contentState?.title ?? "" - navigationContent = AnyComponentWithIdentity( - id: "title_\(title)", - component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: title, font: Font.bold(17.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor, paragraphAlignment: .center)), horizontalAlignment: .center, maximumNumberOfLines: 1) - ) - ) - navigationLeftItems = [ - AnyComponentWithIdentity( - id: "close", + let contentType = context.component.contentState?.contentType ?? .instantPage + switch contentType { + case .webPage: + navigationContent = AnyComponentWithIdentity( + id: "addressBar", component: AnyComponent( - Button( - content: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.WebBrowser_Done, font: Font.regular(17.0), textColor: environment.theme.rootController.navigationBar.accentTextColor, paragraphAlignment: .center)), horizontalAlignment: .left, maximumNumberOfLines: 1) - ), - action: { - performAction.invoke(.close) - } + AddressBarContentComponent( + theme: environment.theme, + strings: environment.strings, + url: context.component.contentState?.url ?? "", + isSecure: context.component.contentState?.isSecure ?? false, + isExpanded: context.component.presentationState.addressFocused, + performAction: performAction ) ) ) - ] - - navigationRightItems = [ - AnyComponentWithIdentity( - id: "settings", + case .instantPage, .document: + let title = context.component.contentState?.title ?? "" + navigationContent = AnyComponentWithIdentity( + id: "titleBar_\(title)", component: AnyComponent( - ReferenceButtonComponent( - content: AnyComponent( - LottieComponent( - content: LottieComponent.AppBundleContent( - name: "anim_moredots" - ), - color: environment.theme.rootController.navigationBar.accentTextColor, - size: CGSize(width: 30.0, height: 30.0) - ) - ), - tag: settingsTag, - action: { - performAction.invoke(.openSettings) - } + TitleBarContentComponent( + theme: environment.theme, + title: title ) ) ) - ] + } + + if context.component.presentationState.addressFocused { + navigationLeftItems = [] + navigationRightItems = [] + } else { + navigationLeftItems = [ + AnyComponentWithIdentity( + id: "close", + component: AnyComponent( + Button( + content: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.WebBrowser_Done, font: Font.semibold(17.0), textColor: environment.theme.rootController.navigationBar.accentTextColor, paragraphAlignment: .center)), horizontalAlignment: .left, maximumNumberOfLines: 1) + ), + action: { + performAction.invoke(.close) + } + ) + ) + ) + ] + + navigationRightItems = [ + AnyComponentWithIdentity( + id: "settings", + component: AnyComponent( + ReferenceButtonComponent( + content: AnyComponent( + LottieComponent( + content: LottieComponent.AppBundleContent( + name: "anim_moredots" + ), + color: environment.theme.rootController.navigationBar.accentTextColor, + size: CGSize(width: 30.0, height: 30.0) + ) + ), + tag: settingsTag, + action: { + performAction.invoke(.openSettings) + } + ) + ) + ) + ] + } } let collapseFraction = context.component.presentationState.isSearching ? 0.0 : context.component.panelCollapseFraction @@ -224,6 +252,26 @@ private final class BrowserScreenComponent: CombinedComponent { .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - toolbar.size.height / 2.0)) ) + if context.component.presentationState.addressFocused { + let addressList = addressList.update( + component: BrowserAddressListComponent( + context: context.component.context, + theme: environment.theme, + strings: environment.strings, + navigateTo: { url in + performAction.invoke(.navigateTo(url)) + } + ), + availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height - navigationBar.size.height - toolbar.size.height), + transition: context.transition + ) + context.add(addressList + .position(CGPoint(x: context.availableSize.width / 2.0, y: navigationBar.size.height + addressList.size.height / 2.0)) + .appear(.default(alpha: true)) + .disappear(.default(alpha: true)) + ) + } + return context.availableSize } } @@ -239,6 +287,7 @@ struct BrowserPresentationState: Equatable { var searchResultIndex: Int var searchResultCount: Int var searchQueryIsEmpty: Bool + var addressFocused: Bool } public class BrowserScreen: ViewController, MinimizableController { @@ -262,6 +311,9 @@ public class BrowserScreen: ViewController, MinimizableController { case updateFontIsSerif(Bool) case addBookmark case openBookmarks + case openAddressBar + case closeAddressBar + case navigateTo(String) } fileprivate final class Node: ViewControllerTracingNode { @@ -271,7 +323,6 @@ public class BrowserScreen: ViewController, MinimizableController { private let contentContainerView = UIView() fileprivate let contentNavigationContainer = ComponentView() fileprivate var content: [BrowserContent] = [] - fileprivate var contentState: BrowserContentState? private var contentStateDisposable = MetaDisposable() @@ -292,13 +343,20 @@ public class BrowserScreen: ViewController, MinimizableController { self.presentationState = BrowserPresentationState( fontState: BrowserPresentationState.FontState(size: 100, isSerif: false), - isSearching: false, searchResultIndex: 0, searchResultCount: 0, searchQueryIsEmpty: true + isSearching: false, + searchResultIndex: 0, + searchResultCount: 0, + searchQueryIsEmpty: true, + addressFocused: false ) super.init() self.pushContent(controller.subject, transition: .immediate) - + if let content = self.content.last { + content.addToRecentlyVisited() + } + self.performAction.connect { [weak self] action in guard let self, let content = self.content.last, let url = self.contentState?.url else { return @@ -341,7 +399,7 @@ public class BrowserScreen: ViewController, MinimizableController { let text: String var savedMessages = false if peerIds.count == 1, let peerId = peerIds.first, peerId == strongSelf.context.account.peerId { - text = presentationData.strings.WebBrowser_LinkForwardTooltip_SavedMessages_One + text = presentationData.strings.WebBrowser_LinkAddedToBookmarks savedMessages = true } else { if peers.count == 1, let peer = peers.first { @@ -388,7 +446,7 @@ public class BrowserScreen: ViewController, MinimizableController { case .openSettings: self.openSettings() case let .updateSearchActive(active): - self.updatePresentationState(animated: true, { state in + self.updatePresentationState(transition: .easeInOut(duration: 0.2), { state in var updatedState = state updatedState.isSearching = active updatedState.searchQueryIsEmpty = true @@ -485,10 +543,31 @@ public class BrowserScreen: ViewController, MinimizableController { content.updateFontState(self.presentationState.fontState) case .addBookmark: if let content = self.content.last { - self.addBookmark(content.currentState.url) + self.addBookmark(content.currentState.url, showArrow: true) } case .openBookmarks: - break + self.openBookmarks() + case .openAddressBar: + self.updatePresentationState(transition: .spring(duration: 0.4), { state in + var updatedState = state + updatedState.addressFocused = true + return updatedState + }) + case .closeAddressBar: + self.updatePresentationState(transition: .spring(duration: 0.4), { state in + var updatedState = state + updatedState.addressFocused = false + return updatedState + }) + case let .navigateTo(address): + if let content = self.content.last as? BrowserWebContent { + content.navigateTo(address: address) + } + self.updatePresentationState(transition: .spring(duration: 0.4), { state in + var updatedState = state + updatedState.addressFocused = false + return updatedState + }) } } @@ -517,9 +596,9 @@ public class BrowserScreen: ViewController, MinimizableController { self.view.addSubview(self.contentContainerView) } - func updatePresentationState(animated: Bool = false, _ f: (BrowserPresentationState) -> BrowserPresentationState) { + func updatePresentationState(transition: ComponentTransition = .immediate, _ f: (BrowserPresentationState) -> BrowserPresentationState) { self.presentationState = f(self.presentationState) - self.requestLayout(transition: animated ? .easeInOut(duration: 0.2) : .immediate) + self.requestLayout(transition: transition) } func pushContent(_ content: BrowserScreen.Subject, transition: ComponentTransition) { @@ -536,6 +615,10 @@ public class BrowserScreen: ViewController, MinimizableController { self.openPeer(peer) } browserContent = instantPageContent + case let .document(file): + browserContent = BrowserDocumentContent(context: self.context, presentationData: self.presentationData, file: file) + case let .pdfDocument(file): + browserContent = BrowserPdfContent(context: self.context, presentationData: self.presentationData, file: file) } browserContent.pushContent = { [weak self] content in guard let self else { @@ -596,7 +679,7 @@ public class BrowserScreen: ViewController, MinimizableController { self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), animated: true)) } - func addBookmark(_ url: String) { + func addBookmark(_ url: String, showArrow: Bool) { let _ = enqueueMessages( account: self.context.account, peerId: self.context.account.peerId, @@ -615,7 +698,9 @@ public class BrowserScreen: ViewController, MinimizableController { ).start() let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } - self.controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: true, text: presentationData.strings.WebBrowser_LinkAddedToBookmarks), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] action in + + let lastController = self.controller?.navigationController?.viewControllers.last as? ViewController + lastController?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: true, text: presentationData.strings.WebBrowser_LinkAddedToBookmarks), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] action in if 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 @@ -708,6 +793,20 @@ public class BrowserScreen: ViewController, MinimizableController { }, animated: true) } + func openBookmarks() { + guard let url = self.contentState?.url else { + return + } + let controller = BrowserBookmarksScreen(context: self.context, url: url, openUrl: { [weak self] url in + if let self { + self.performAction.invoke(.navigateTo(url)) + } + }, addBookmark: { [weak self] in + self?.addBookmark(url, showArrow: false) + }) + self.controller?.push(controller) + } + func openSettings() { guard let referenceView = self.componentHost.findTaggedView(tag: settingsTag) as? ReferenceButtonComponent.View else { return @@ -736,7 +835,7 @@ public class BrowserScreen: ViewController, MinimizableController { let _ = (settings |> deliverOnMainQueue).start(next: { [weak self] settings in - guard let self, let controller = self.controller else { + guard let self, let controller = self.controller, let contentState = self.contentState else { return } @@ -771,7 +870,7 @@ public class BrowserScreen: ViewController, MinimizableController { defaultWebBrowser = "safari" } - let url = self.contentState?.url ?? "" + let url = contentState.url let openInOptions = availableOpenInOptions(context: self.context, item: .url(url: url)) let openInTitle: String let openInUrl: String @@ -787,40 +886,52 @@ public class BrowserScreen: ViewController, MinimizableController { openInUrl = url } - let items: [ContextMenuItem] = [ - .custom(fontItem, false), - .action(ContextMenuActionItem(text: self.presentationData.strings.InstantPage_FontSanFrancisco, icon: forceIsSerif ? emptyIcon : checkIcon, action: { (controller, action) in + var items: [ContextMenuItem] = [] + items.append(.custom(fontItem, false)) + + + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.InstantPage_FontSanFrancisco, icon: forceIsSerif ? emptyIcon : checkIcon, action: { (controller, action) in performAction.invoke(.updateFontIsSerif(false)) action(.default) - })), - .action(ContextMenuActionItem(text: self.presentationData.strings.InstantPage_FontNewYork, textFont: .custom(font: Font.with(size: 17.0, design: .serif, traits: []), height: nil, verticalOffset: nil), icon: forceIsSerif ? checkIcon : emptyIcon, action: { (controller, action) in + }))) + + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.InstantPage_FontNewYork, textFont: .custom(font: Font.with(size: 17.0, design: .serif, traits: []), height: nil, verticalOffset: nil), icon: forceIsSerif ? checkIcon : emptyIcon, action: { (controller, action) in performAction.invoke(.updateFontIsSerif(true)) action(.default) - })), - .separator, - .action(ContextMenuActionItem(text: self.presentationData.strings.WebBrowser_Reload, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Instant View/Settings/Reload"), color: theme.contextMenu.primaryColor) }, action: { (controller, action) in + }))) + + items.append(.separator) + + if case .webPage = contentState.contentType { + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.WebBrowser_Reload, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Instant View/Settings/Reload"), color: theme.contextMenu.primaryColor) }, action: { (controller, action) in performAction.invoke(.reload) action(.default) - })), - .action(ContextMenuActionItem(text: self.presentationData.strings.InstantPage_Search, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Instant View/Settings/Search"), color: theme.contextMenu.primaryColor) }, action: { (controller, action) in + }))) + } + if [.webPage, .instantPage].contains(contentState.contentType) { + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.InstantPage_Search, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Instant View/Settings/Search"), color: theme.contextMenu.primaryColor) }, action: { (controller, action) in performAction.invoke(.updateSearchActive(true)) action(.default) - })), - .action(ContextMenuActionItem(text: self.presentationData.strings.WebBrowser_Share, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Share"), color: theme.contextMenu.primaryColor) }, action: { (controller, action) in - performAction.invoke(.share) - action(.default) - })), - .action(ContextMenuActionItem(text: self.presentationData.strings.WebBrowser_AddBookmark, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Fave"), color: theme.contextMenu.primaryColor) }, action: { (controller, action) in + }))) + } + + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.WebBrowser_Share, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Share"), color: theme.contextMenu.primaryColor) }, action: { (controller, action) in + performAction.invoke(.share) + action(.default) + }))) + + if [.webPage, .instantPage].contains(contentState.contentType) { + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.WebBrowser_AddBookmark, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Fave"), color: theme.contextMenu.primaryColor) }, action: { (controller, action) in performAction.invoke(.addBookmark) action(.default) - })), - .action(ContextMenuActionItem(text: self.presentationData.strings.InstantPage_OpenInBrowser(openInTitle).string, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Browser"), color: theme.contextMenu.primaryColor) }, action: { [weak self] (controller, action) in + }))) + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.InstantPage_OpenInBrowser(openInTitle).string, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Browser"), color: theme.contextMenu.primaryColor) }, action: { [weak self] (controller, action) in if let self { self.context.sharedContext.applicationBindings.openUrl(openInUrl) } action(.default) - })) - ] + }))) + } let contextController = ContextController(presentationData: self.presentationData, source: source, items: .single(ContextController.Items(content: .list(items)))) self.controller?.present(contextController, in: .window(.root)) @@ -1050,21 +1161,34 @@ public class BrowserScreen: ViewController, MinimizableController { public enum Subject { case webPage(url: String) case instantPage(webPage: TelegramMediaWebpage, anchor: String?, sourceLocation: InstantPageSourceLocation) + case document(file: TelegramMediaFile) + case pdfDocument(file: TelegramMediaFile) } private let context: AccountContext private let subject: Subject - var openPreviousOnClose = false + public static let supportedDocumentMimeTypes: [String] = [ + "text/plain", + "text/rtf", + "application/pdf", + "application/msword", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/vnd.ms-excel", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "application/vnd.openxmlformats-officedocument.spreadsheetml.template", + "application/vnd.openxmlformats-officedocument.presentationml.presentation" + ] + public init(context: AccountContext, subject: Subject) { self.context = context self.subject = subject super.init(navigationBarPresentationData: nil) - self.navigationPresentation = .modal + self.navigationPresentation = .modalInCompactLayout self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .allButUpsideDown) @@ -1107,6 +1231,8 @@ public class BrowserScreen: ViewController, MinimizableController { return contentState.favicon case .instantPage: return UIImage(bundleImageName: "Chat/Message/AttachedContentInstantIcon")?.withRenderingMode(.alwaysTemplate) + case .document: + return nil } } return nil diff --git a/submodules/BrowserUI/Sources/BrowserSearchBarComponent.swift b/submodules/BrowserUI/Sources/BrowserSearchBarComponent.swift index d588b744f2..6e8974667f 100644 --- a/submodules/BrowserUI/Sources/BrowserSearchBarComponent.swift +++ b/submodules/BrowserUI/Sources/BrowserSearchBarComponent.swift @@ -8,6 +8,8 @@ import AccountContext import BundleIconComponent final class SearchBarContentComponent: Component { + public typealias EnvironmentType = BrowserNavigationBarEnvironment + let theme: PresentationTheme let strings: PresentationStrings let performAction: ActionSlot @@ -351,7 +353,7 @@ final class SearchBarContentComponent: Component { return View() } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } diff --git a/submodules/BrowserUI/Sources/BrowserTitleBarComponent.swift b/submodules/BrowserUI/Sources/BrowserTitleBarComponent.swift new file mode 100644 index 0000000000..a362da7d61 --- /dev/null +++ b/submodules/BrowserUI/Sources/BrowserTitleBarComponent.swift @@ -0,0 +1,85 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import ComponentFlow +import SwiftSignalKit +import TelegramPresentationData +import AccountContext +import BundleIconComponent +import MultilineTextComponent +import UrlEscaping + +final class TitleBarContentComponent: Component { + public typealias EnvironmentType = BrowserNavigationBarEnvironment + + let theme: PresentationTheme + let title: String + + init( + theme: PresentationTheme, + title: String + ) { + self.theme = theme + self.title = title + } + + static func ==(lhs: TitleBarContentComponent, rhs: TitleBarContentComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.title != rhs.title { + return false + } + return true + } + + final class View: UIView { + private var titleContent = ComponentView() + private var component: TitleBarContentComponent? + + init() { + super.init(frame: CGRect()) + } + + required public init?(coder: NSCoder) { + fatalError() + } + + func update(component: TitleBarContentComponent, availableSize: CGSize, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + + let titleSize = self.titleContent.update( + transition: transition, + component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString(string: component.title, font: Font.semibold(17.0), textColor: component.theme.rootController.navigationBar.primaryTextColor)), + horizontalAlignment: .center, + truncationType: .end, + maximumNumberOfLines: 1 + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - 36.0, height: availableSize.height) + ) + let titleContentFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - titleSize.width) / 2.0), y: floorToScreenPixels((availableSize.height - titleSize.height) / 2.0)), size: titleSize) + if let titleContentView = self.titleContent.view { + if titleContentView.superview == nil { + self.addSubview(titleContentView) + } + transition.setPosition(view: titleContentView, position: titleContentFrame.center) + titleContentView.bounds = CGRect(origin: .zero, size: titleContentFrame.size) + } + + return availableSize + } + } + + func makeView() -> View { + return View() + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition) + } +} diff --git a/submodules/BrowserUI/Sources/BrowserWebContent.swift b/submodules/BrowserUI/Sources/BrowserWebContent.swift index 1da55837da..e99d97666f 100644 --- a/submodules/BrowserUI/Sources/BrowserWebContent.swift +++ b/submodules/BrowserUI/Sources/BrowserWebContent.swift @@ -17,6 +17,7 @@ import ShareController import UndoUI import LottieComponent import MultilineTextComponent +import UrlEscaping private final class TonSchemeHandler: NSObject, WKURLSchemeHandler { private final class PendingTask { @@ -145,6 +146,8 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU var presentInGlobalOverlay: (ViewController) -> Void = { _ in } var getNavigationController: () -> NavigationController? = { return nil } + private var tempFile: TempBoxFile? + init(context: AccountContext, presentationData: PresentationData, url: String) { self.context = context self.uuid = UUID() @@ -176,7 +179,15 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU } var title: String = "" - if let parsedUrl = URL(string: url) { + if url.hasPrefix("file://") { + var updatedPath = url + let tempFile = TempBox.shared.file(path: url.replacingOccurrences(of: "file://", with: ""), fileName: "file.xlsx") + updatedPath = tempFile.path + self.tempFile = tempFile + + let request = URLRequest(url: URL(fileURLWithPath: updatedPath)) + self.webView.load(request) + } else if let parsedUrl = URL(string: url) { let request = URLRequest(url: parsedUrl) self.webView.load(request) @@ -201,6 +212,7 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress), options: [], context: nil) self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.canGoBack), options: [], context: nil) self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.canGoForward), options: [], context: nil) + self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.hasOnlySecureContent), options: [], context: nil) if #available(iOS 15.0, *) { self.backgroundColor = presentationData.theme.list.plainBackgroundColor self.webView.underPageBackgroundColor = presentationData.theme.list.plainBackgroundColor @@ -221,6 +233,7 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU self.webView.removeObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress)) self.webView.removeObserver(self, forKeyPath: #keyPath(WKWebView.canGoBack)) self.webView.removeObserver(self, forKeyPath: #keyPath(WKWebView.canGoForward)) + self.webView.removeObserver(self, forKeyPath: #keyPath(WKWebView.hasOnlySecureContent)) self.faviconDisposable.dispose() } @@ -236,41 +249,6 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU } } - private let setupFontFunctions = """ - (function() { - const styleId = 'telegram-font-overrides'; - - function setTelegramFontOverrides(font, textSizeAdjust) { - let style = document.getElementById(styleId); - - if (!style) { - style = document.createElement('style'); - style.id = styleId; - document.head.appendChild(style); - } - - let cssRules = '* {'; - if (font !== null) { - cssRules += ` - font-family: ${font} !important; - `; - } - if (textSizeAdjust !== null) { - cssRules += ` - -webkit-text-size-adjust: ${textSizeAdjust} !important; - `; - } - cssRules += '}'; - - style.innerHTML = cssRules; - - if (font === null && textSizeAdjust === null) { - style.parentNode.removeChild(style); - } - } - window.setTelegramFontOverrides = setTelegramFontOverrides; - })(); - """ var currentFontState = BrowserPresentationState.FontState(size: 100, isSerif: false) func updateFontState(_ state: BrowserPresentationState.FontState) { @@ -311,65 +289,113 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU }) } + private var findSession: Any? private var previousQuery: String? func setSearch(_ query: String?, completion: ((Int) -> Void)?) { guard self.previousQuery != query else { return } - self.previousQuery = query - self.setupSearch { [weak self] in - if let query = query { - let js = "uiWebview_HighlightAllOccurencesOfString('\(query)')" - self?.webView.evaluateJavaScript(js, completionHandler: { [weak self] _, _ in - let js = "uiWebview_SearchResultCount" - self?.webView.evaluateJavaScript(js, completionHandler: { [weak self] result, _ in - if let result = result as? NSNumber { - self?.searchResultsCount = result.intValue - completion?(result.intValue) - } else { - completion?(0) - } - }) - }) + + if #available(iOS 16.0, *), !"".isEmpty { + if let query { + var findSession: UIFindSession? + if let current = self.findSession as? UIFindSession { + findSession = current + } else { + self.webView.isFindInteractionEnabled = true + + if let findInteraction = self.webView.findInteraction, let webView = self.webView as? UIFindInteractionDelegate, let session = webView.findInteraction(findInteraction, sessionFor: self.webView) { +// session.setValue(findInteraction, forKey: "_parentInteraction") +// findInteraction.setValue(session, forKey: "_activeFindSession") + findSession = session + self.findSession = session + + webView.findInteraction?(findInteraction, didBegin: session) + } + } + if let findSession { + findSession.performSearch(query: query, options: BrowserSearchOptions()) + self.webView.findInteraction?.updateResultCount() + completion?(findSession.resultCount) + } } else { - let js = "uiWebview_RemoveAllHighlights()" - self?.webView.evaluateJavaScript(js, completionHandler: nil) - - self?.currentSearchResult = 0 - self?.searchResultsCount = 0 + if let findInteraction = self.webView.findInteraction, let webView = self.webView as? UIFindInteractionDelegate, let session = self.findSession as? UIFindSession { + webView.findInteraction?(findInteraction, didEnd: session) + self.findSession = nil + self.webView.isFindInteractionEnabled = false + } + } + } else { + self.setupSearch { [weak self] in + if let query, !query.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + let js = "uiWebview_HighlightAllOccurencesOfString('\(query)')" + self?.webView.evaluateJavaScript(js, completionHandler: { [weak self] _, _ in + let js = "uiWebview_SearchResultCount" + self?.webView.evaluateJavaScript(js, completionHandler: { [weak self] result, _ in + if let result = result as? NSNumber { + self?.searchResultsCount = result.intValue + completion?(result.intValue) + } else { + completion?(0) + } + }) + }) + } else { + let js = "uiWebview_RemoveAllHighlights()" + self?.webView.evaluateJavaScript(js, completionHandler: nil) + + self?.currentSearchResult = 0 + self?.searchResultsCount = 0 + } } } + + self.previousQuery = query } private var currentSearchResult: Int = 0 private var searchResultsCount: Int = 0 func scrollToPreviousSearchResult(completion: ((Int, Int) -> Void)?) { - let searchResultsCount = self.searchResultsCount - var index = self.currentSearchResult - 1 - if index < 0 { - index = searchResultsCount - 1 + if #available(iOS 16.0, *), !"".isEmpty { + if let session = self.findSession as? UIFindSession { + session.highlightNextResult(in: .backward) + completion?(session.highlightedResultIndex, session.resultCount) + } + } else { + let searchResultsCount = self.searchResultsCount + var index = self.currentSearchResult - 1 + if index < 0 { + index = searchResultsCount - 1 + } + self.currentSearchResult = index + + let js = "uiWebview_ScrollTo('\(searchResultsCount - index - 1)')" + self.webView.evaluateJavaScript(js, completionHandler: { _, _ in + completion?(index, searchResultsCount) + }) } - self.currentSearchResult = index - - let js = "uiWebview_ScrollTo('\(searchResultsCount - index - 1)')" - self.webView.evaluateJavaScript(js, completionHandler: { _, _ in - completion?(index, searchResultsCount) - }) } func scrollToNextSearchResult(completion: ((Int, Int) -> Void)?) { - let searchResultsCount = self.searchResultsCount - var index = self.currentSearchResult + 1 - if index >= searchResultsCount { - index = 0 + if #available(iOS 16.0, *), !"".isEmpty { + if let session = self.findSession as? UIFindSession { + session.highlightNextResult(in: .forward) + completion?(session.highlightedResultIndex, session.resultCount) + } + } else { + let searchResultsCount = self.searchResultsCount + var index = self.currentSearchResult + 1 + if index >= searchResultsCount { + index = 0 + } + self.currentSearchResult = index + + let js = "uiWebview_ScrollTo('\(searchResultsCount - index - 1)')" + self.webView.evaluateJavaScript(js, completionHandler: { _, _ in + completion?(index, searchResultsCount) + }) } - self.currentSearchResult = index - - let js = "uiWebview_ScrollTo('\(searchResultsCount - index - 1)')" - self.webView.evaluateJavaScript(js, completionHandler: { _, _ in - completion?(index, searchResultsCount) - }) } func stop() { @@ -394,6 +420,14 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU } } + func navigateTo(address: String) { + let finalUrl = explicitUrl(address) + guard let url = URL(string: finalUrl) else { + return + } + self.webView.load(URLRequest(url: url)) + } + func scrollToTop() { self.webView.scrollView.setContentOffset(CGPoint(x: 0.0, y: -self.webView.scrollView.contentInset.top), animated: true) } @@ -458,8 +492,10 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU } else if keyPath == "canGoBack" { self.updateState { $0.withUpdatedCanGoBack(self.webView.canGoBack) } self.webView.disablesInteractiveTransitionGestureRecognizer = self.webView.canGoBack - } else if keyPath == "canGoForward" { + } else if keyPath == "canGoForward" { self.updateState { $0.withUpdatedCanGoForward(self.webView.canGoForward) } + } else if keyPath == "hasOnlySecureContent" { + self.updateState { $0.withUpdatedIsSecure(self.webView.hasOnlySecureContent) } } } @@ -694,6 +730,9 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU } private func parseFavicon() { + let addToRecentsWhenReady = self.addToRecentsWhenReady + self.addToRecentsWhenReady = false + struct Favicon: Equatable, Hashable { let url: String let dimensions: PixelDimensions? @@ -774,10 +813,64 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU return } self.updateState { $0.withUpdatedFavicon(favicon) } + + if addToRecentsWhenReady { + var image: TelegramMediaImage? + + if let favicon, let imageData = favicon.pngData() { + let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) + self.context.account.postbox.mediaBox.storeResourceData(resource.id, data: imageData) + image = TelegramMediaImage( + imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: Int64.random(in: Int64.min ... Int64.max)), + representations: [ + TelegramMediaImageRepresentation( + dimensions: PixelDimensions(width: Int32(favicon.size.width), height: Int32(favicon.size.height)), + resource: resource, + progressiveSizes: [], + immediateThumbnailData: nil, + hasVideo: false, + isPersonal: false + ) + ], + immediateThumbnailData: nil, + reference: nil, + partialReference: nil, + flags: [] + ) + } + + let webPage = TelegramMediaWebpage(webpageId: MediaId(namespace: 0, id: 0), content: .Loaded(TelegramMediaWebpageLoadedContent( + url: self._state.url, + displayUrl: self._state.url, + hash: 0, + type: "", + websiteName: self._state.title, + title: self._state.title, + text: nil, + embedUrl: nil, + embedType: nil, + embedSize: nil, + duration: nil, + author: nil, + isMediaLargeByDefault: nil, + image: image, + file: nil, + story: nil, + attributes: [], + instantPage: nil)) + ) + + let _ = addRecentlyVisitedLink(engine: self.context.engine, webPage: webPage).startStandalone() + } })) } }) } + + private var addToRecentsWhenReady = false + func addToRecentlyVisited() { + self.addToRecentsWhenReady = true + } } private final class ErrorComponent: CombinedComponent { @@ -873,3 +966,50 @@ private final class ErrorComponent: CombinedComponent { } } } + +let setupFontFunctions = """ +(function() { + const styleId = 'telegram-font-overrides'; + + function setTelegramFontOverrides(font, textSizeAdjust) { + let style = document.getElementById(styleId); + + if (!style) { + style = document.createElement('style'); + style.id = styleId; + document.head.appendChild(style); + } + + let cssRules = '* {'; + if (font !== null) { + cssRules += ` + font-family: ${font} !important; + `; + } + if (textSizeAdjust !== null) { + cssRules += ` + -webkit-text-size-adjust: ${textSizeAdjust} !important; + `; + } + cssRules += '}'; + + style.innerHTML = cssRules; + + if (font === null && textSizeAdjust === null) { + style.parentNode.removeChild(style); + } + } + window.setTelegramFontOverrides = setTelegramFontOverrides; +})(); +""" + +@available(iOS 16.0, *) +final class BrowserSearchOptions: UITextSearchOptions { + override var wordMatchMethod: UITextSearchOptions.WordMatchMethod { + return .contains + } + + override var stringCompareOptions: NSString.CompareOptions { + return .caseInsensitive + } +} diff --git a/submodules/BrowserUI/Sources/Favicon.swift b/submodules/BrowserUI/Sources/Favicon.swift deleted file mode 100644 index fe0e8ae84a..0000000000 --- a/submodules/BrowserUI/Sources/Favicon.swift +++ /dev/null @@ -1,38 +0,0 @@ -import Foundation -import UIKit -import Display -import SwiftSignalKit -import TelegramCore -import AccountContext -import Svg - -private var faviconCache: [String: UIImage] = [:] -func fetchFavicon(context: AccountContext, url: String, size: CGSize) -> Signal { - if let icon = faviconCache[url] { - return .single(icon) - } - return context.engine.resources.httpData(url: url) - |> map(Optional.init) - |> `catch` { _ -> Signal in - return .single(nil) - } - |> map { data in - if let data { - if let image = UIImage(data: data) { - return image - } else if url.lowercased().contains(".svg"), let preparedData = prepareSvgImage(data, false), let image = renderPreparedImage(preparedData, size, .clear, UIScreenScale, false) { - return image - } - return nil - } else { - return nil - } - } - |> beforeNext { image in - if let image { - Queue.mainQueue().async { - faviconCache[url] = image - } - } - } -} diff --git a/submodules/BrowserUI/Sources/SectionHeaderComponent.swift b/submodules/BrowserUI/Sources/SectionHeaderComponent.swift new file mode 100644 index 0000000000..6ebe3e80c9 --- /dev/null +++ b/submodules/BrowserUI/Sources/SectionHeaderComponent.swift @@ -0,0 +1,168 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import TelegramPresentationData +import MultilineTextComponent + +final class SectionHeaderComponent: Component { + enum Style { + case blocks + case plain + } + let theme: PresentationTheme + let style: Style + let title: String + let actionTitle: String? + let action: (() -> Void)? + + init( + theme: PresentationTheme, + style: Style, + title: String, + actionTitle: String?, + action: (() -> Void)? + ) { + self.theme = theme + self.style = style + self.title = title + self.actionTitle = actionTitle + self.action = action + } + + static func ==(lhs: SectionHeaderComponent, rhs: SectionHeaderComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.style != rhs.style { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.actionTitle != rhs.actionTitle { + return false + } + return true + } + + final class View: UIView { + private let title = ComponentView() + private let backgroundView: BlurredBackgroundView + private let action = ComponentView() + + private var component: SectionHeaderComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true) + + super.init(frame: frame) + + self.addSubview(self.backgroundView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: SectionHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let themeUpdated = self.component?.theme !== component.theme + + self.component = component + self.state = state + + let height: CGFloat = 28.0 + let leftInset: CGFloat = 16.0 + let rightInset: CGFloat = 0.0 + + let previousTitleFrame = self.title.view?.frame + + if themeUpdated { + switch component.style { + case .plain: + self.backgroundView.isHidden = false + self.backgroundView.updateColor(color: component.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) + case .blocks: + self.backgroundView.isHidden = true + } + } + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.title, font: Font.regular(13.0), textColor: component.theme.list.itemSecondaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0) + ) + + let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - titleSize.height) / 2.0)), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + titleView.isUserInteractionEnabled = false + self.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 actionTitle = component.actionTitle { + let actionSize = self.action.update( + transition: .immediate, + component: AnyComponent( + Button(content: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: actionTitle, font: Font.regular(13.0), textColor: component.theme.list.itemSecondaryTextColor)) + )), action: { [weak self] in + if let self, let component = self.component { + component.action?() + } + }) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0) + ) + if let view = self.action.view { + if view.superview == nil { + self.addSubview(view) + if !transition.animation.isImmediate { + view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + view.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2) + } + } + let actionFrame = CGRect(origin: CGPoint(x: availableSize.width - leftInset - actionSize.width, y: floor((height - titleSize.height) / 2.0)), size: actionSize) + view.frame = actionFrame + } + } else if let view = self.action.view, view.superview != nil { + if !transition.animation.isImmediate { + view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { finished in + if finished { + view.removeFromSuperview() + view.layer.removeAllAnimations() + } + }) + view.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + } else { + view.removeFromSuperview() + } + } + + let size = CGSize(width: availableSize.width, height: height) + + transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size)) + self.backgroundView.update(size: size, transition: transition.containedViewLayoutTransition) + + 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/BrowserUI/Sources/Utils.swift b/submodules/BrowserUI/Sources/Utils.swift new file mode 100644 index 0000000000..7caff6dd87 --- /dev/null +++ b/submodules/BrowserUI/Sources/Utils.swift @@ -0,0 +1,110 @@ +import Foundation +import UIKit +import Display +import SwiftSignalKit +import Postbox +import TelegramCore +import AccountContext +import TextFormat +import UrlWhitelist +import Svg + +private var faviconCache: [String: UIImage] = [:] +func fetchFavicon(context: AccountContext, url: String, size: CGSize) -> Signal { + if let icon = faviconCache[url] { + return .single(icon) + } + return context.engine.resources.httpData(url: url) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> map { data in + if let data { + if let image = UIImage(data: data) { + return image + } else if url.lowercased().contains(".svg"), let preparedData = prepareSvgImage(data, false), let image = renderPreparedImage(preparedData, size, .clear, UIScreenScale, false) { + return image + } + return nil + } else { + return nil + } + } + |> beforeNext { image in + if let image { + Queue.mainQueue().async { + faviconCache[url] = image + } + } + } +} + +func getPrimaryUrl(message: Message) -> String? { + var primaryUrl: String? + if let webPage = message.media.first(where: { $0 is TelegramMediaWebpage }) as? TelegramMediaWebpage, let url = webPage.content.url { + primaryUrl = url + } else { + var entities = message.textEntitiesAttribute?.entities + if entities == nil { + let parsedEntities = generateTextEntities(message.text, enabledTypes: .all) + if !parsedEntities.isEmpty { + entities = parsedEntities + } + } + + if let entities { + loop: for entity in entities { + switch entity.type { + case .Url, .Email: + var range = NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound) + let nsString = message.text as NSString + if range.location + range.length > nsString.length { + range.location = max(0, nsString.length - range.length) + range.length = nsString.length - range.location + } + let tempUrlString = nsString.substring(with: range) + + var (urlString, concealed) = parseUrl(url: tempUrlString, wasConcealed: false) + var parsedUrl = URL(string: urlString) + if (parsedUrl == nil || parsedUrl!.host == nil || parsedUrl!.host!.isEmpty) && !urlString.contains("@") { + urlString = "http://" + urlString + parsedUrl = URL(string: urlString) + } + var host: String? = concealed ? urlString : parsedUrl?.host + if host == nil { + host = urlString + } + if let _ = parsedUrl, let _ = host { + primaryUrl = urlString + } + break loop + case let .TextUrl(url): + let messageText = message.text + + var range = NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound) + let nsString = messageText as NSString + if range.location + range.length > nsString.length { + range.location = max(0, nsString.length - range.length) + range.length = nsString.length - range.location + } + + var (urlString, concealed) = parseUrl(url: url, wasConcealed: false) + var parsedUrl = URL(string: urlString) + if (parsedUrl == nil || parsedUrl!.host == nil || parsedUrl!.host!.isEmpty) && !urlString.contains("@") { + urlString = "http://" + urlString + parsedUrl = URL(string: urlString) + } + let host: String? = concealed ? urlString : parsedUrl?.host + if let _ = parsedUrl, let _ = host { + primaryUrl = urlString + } + break loop + default: + break + } + } + } + } + return primaryUrl +} diff --git a/submodules/Display/Source/Navigation/MinimizedContainer.swift b/submodules/Display/Source/Navigation/MinimizedContainer.swift index bc5cb3941b..58201ab9bd 100644 --- a/submodules/Display/Source/Navigation/MinimizedContainer.swift +++ b/submodules/Display/Source/Navigation/MinimizedContainer.swift @@ -30,6 +30,10 @@ public protocol MinimizableController: ViewController { func requestMinimize(topEdgeOffset: CGFloat?, initialVelocity: CGFloat?) func makeContentSnapshotView() -> UIView? + + func prepareContentSnapshotView() + func resetContentSnapshotView() + func shouldDismissImmediately() -> Bool } @@ -66,6 +70,14 @@ public extension MinimizableController { return self.displayNode.view.snapshotView(afterScreenUpdates: false) } + func prepareContentSnapshotView() { + + } + + func resetContentSnapshotView() { + + } + func shouldDismissImmediately() -> Bool { return true } diff --git a/submodules/Display/Source/Navigation/NavigationLayout.swift b/submodules/Display/Source/Navigation/NavigationLayout.swift index 839c4f41ea..61cfc02b45 100644 --- a/submodules/Display/Source/Navigation/NavigationLayout.swift +++ b/submodules/Display/Source/Navigation/NavigationLayout.swift @@ -50,6 +50,15 @@ func makeNavigationLayout(mode: NavigationControllerMode, layout: ContainerViewL case .regular: requiresModal = true } + case .modalInCompactLayout: + switch layout.metrics.widthClass { + case .compact: + requiresModal = true + case .regular: + requiresModal = true + beginsModal = true + isFlat = true + } } if requiresModal { controller._presentedInModal = true diff --git a/submodules/Display/Source/ViewController.swift b/submodules/Display/Source/ViewController.swift index 347ef7df37..0c3d1a7e92 100644 --- a/submodules/Display/Source/ViewController.swift +++ b/submodules/Display/Source/ViewController.swift @@ -65,6 +65,7 @@ public enum ViewControllerNavigationPresentation { case flatModal case standaloneModal case modalInLargeLayout + case modalInCompactLayout } public enum TabBarItemContextActionType { diff --git a/submodules/DrawingUI/Sources/DrawingScreen.swift b/submodules/DrawingUI/Sources/DrawingScreen.swift index dd4c749639..ca595aadc5 100644 --- a/submodules/DrawingUI/Sources/DrawingScreen.swift +++ b/submodules/DrawingUI/Sources/DrawingScreen.swift @@ -3212,10 +3212,12 @@ public final class DrawingToolsInteraction { self.isActive = false } - public func insertEntity(_ entity: DrawingEntity, scale: CGFloat? = nil, position: CGPoint? = nil) { + public func insertEntity(_ entity: DrawingEntity, scale: CGFloat? = nil, position: CGPoint? = nil, select: Bool = true) { self.entitiesView.prepareNewEntity(entity, scale: scale, position: position) self.entitiesView.add(entity) - self.entitiesView.selectEntity(entity, animate: !(entity is DrawingTextEntity)) + if select { + self.entitiesView.selectEntity(entity, animate: !(entity is DrawingTextEntity)) + } if let entityView = self.entitiesView.getView(for: entity.uuid) { if let textEntityView = entityView as? DrawingTextEntityView { diff --git a/submodules/OpenInExternalAppUI/Sources/OpenInOptions.swift b/submodules/OpenInExternalAppUI/Sources/OpenInOptions.swift index e359d7895b..cdc9c00032 100644 --- a/submodules/OpenInExternalAppUI/Sources/OpenInOptions.swift +++ b/submodules/OpenInExternalAppUI/Sources/OpenInOptions.swift @@ -286,7 +286,7 @@ private func allOpenInOptions(context: AccountContext, item: OpenInItem) -> [Ope })) } - options.append(OpenInOption(identifier: "2gis", application: .other(title: "2GIS", identifier: 481627348, scheme: "dgis", store: nil), action: { + options.append(OpenInOption(identifier: "2gis", application: .other(title: "2GIS", identifier: 481627348, scheme: "dgis", store: "ru"), action: { let coordinates = "\(lon),\(lat)" if let _ = directions { return .openUrl(url: "dgis://2gis.ru/routeSearch/to/\(coordinates)/go") diff --git a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserDomainController.swift b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserDomainController.swift index 2d439c746a..4d62514a07 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserDomainController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserDomainController.swift @@ -7,6 +7,7 @@ import TelegramCore import TelegramPresentationData import AccountContext import UrlEscaping +import ActivityIndicator private final class WebBrowserDomainInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegate { private var theme: PresentationTheme @@ -116,7 +117,12 @@ private final class WebBrowserDomainInputFieldNode: ASDisplayNode, ASEditableTex private let domainRegex = try? NSRegularExpression(pattern: "^(https?://)?([a-zA-Z0-9-]+\\.?)*([a-zA-Z]*)?(:)?(/)?$", options: []) private let pathRegex = try? NSRegularExpression(pattern: "^(https?://)?([a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,6}/", options: []) + var inProgress = false + func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + if self.inProgress { + return false + } if text == "\n" { self.complete?() return false @@ -168,6 +174,7 @@ private final class WebBrowserDomainAlertContentNode: AlertContentNode { private let titleNode: ASTextNode private let textNode: ASTextNode + let activityIndicator: ActivityIndicator let inputFieldNode: WebBrowserDomainInputFieldNode private let actionNodesSeparator: ASDisplayNode @@ -198,6 +205,9 @@ private final class WebBrowserDomainAlertContentNode: AlertContentNode { self.textNode = ASTextNode() self.textNode.maximumNumberOfLines = 2 + self.activityIndicator = ActivityIndicator(type: .custom(ptheme.rootController.navigationBar.secondaryTextColor, 20.0, 1.5, false), speed: .slow) + self.activityIndicator.isHidden = true + self.inputFieldNode = WebBrowserDomainInputFieldNode(theme: ptheme, placeholder: strings.WebBrowser_Exceptions_Create_Placeholder) self.inputFieldNode.text = "" @@ -224,7 +234,8 @@ private final class WebBrowserDomainAlertContentNode: AlertContentNode { self.addSubnode(self.textNode) self.addSubnode(self.inputFieldNode) - + self.addSubnode(self.activityIndicator) + self.addSubnode(self.actionNodesSeparator) for actionNode in self.actionNodes { @@ -335,9 +346,13 @@ private final class WebBrowserDomainAlertContentNode: AlertContentNode { let inputFieldWidth = resultWidth let inputFieldHeight = self.inputFieldNode.updateLayout(width: inputFieldWidth, transition: transition) let inputHeight = inputFieldHeight - transition.updateFrame(node: self.inputFieldNode, frame: CGRect(x: 0.0, y: origin.y, width: resultWidth, height: inputFieldHeight)) + let inputFrame = CGRect(x: 0.0, y: origin.y, width: resultWidth, height: inputFieldHeight) + transition.updateFrame(node: self.inputFieldNode, frame: inputFrame) transition.updateAlpha(node: self.inputFieldNode, alpha: inputHeight > 0.0 ? 1.0 : 0.0) + let activitySize = CGSize(width: 20.0, height: 20.0) + transition.updateFrame(node: self.activityIndicator, frame: CGRect(origin: CGPoint(x: inputFrame.maxX - activitySize.width - 23.0, y: inputFrame.midY - activitySize.height / 2.0 - 3.0), size: activitySize)) + let resultSize = CGSize(width: resultWidth, height: titleSize.height + textSize.height + spacing + inputHeight + actionsHeight + insets.top + insets.bottom) transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) @@ -404,11 +419,16 @@ public func webBrowserDomainController(context: AccountContext, updatedPresentat var dismissImpl: ((Bool) -> Void)? var applyImpl: (() -> Void)? + var inProgress = false let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - dismissImpl?(true) - apply(nil) + if !inProgress { + dismissImpl?(true) + apply(nil) + } }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Done, action: { - applyImpl?() + if !inProgress { + applyImpl?() + } })] let contentNode = WebBrowserDomainAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions) @@ -419,9 +439,12 @@ public func webBrowserDomainController(context: AccountContext, updatedPresentat guard let contentNode = contentNode else { return } + inProgress = true + contentNode.inputFieldNode.inProgress = true + contentNode.activityIndicator.isHidden = false + let updatedLink = explicitUrl(contentNode.link) if !updatedLink.isEmpty && isValidUrl(updatedLink, validSchemes: ["http": true, "https": true]) { - dismissImpl?(true) apply(updatedLink) } else { contentNode.animateError() diff --git a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserDomainExceptionItem.swift b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserDomainExceptionItem.swift index 572e65ed9f..3e66f1ff29 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserDomainExceptionItem.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserDomainExceptionItem.swift @@ -7,32 +7,43 @@ import TelegramPresentationData import TelegramCore import AccountContext import ItemListUI +import PhotoResources -public class WebBrowserDomainExceptionItem: ListViewItem, ItemListItem { +private enum RevealOptionKey: Int32 { + case delete +} + +final class WebBrowserDomainExceptionItem: ListViewItem, ItemListItem { let presentationData: ItemListPresentationData - let context: AccountContext? + let context: AccountContext let title: String let label: String - public let sectionId: ItemListSectionId + let icon: TelegramMediaImage? + let sectionId: ItemListSectionId let style: ItemListStyle + let deleted: (() -> Void)? - public init( + init( presentationData: ItemListPresentationData, - context: AccountContext? = nil, + context: AccountContext, title: String, label: String, + icon: TelegramMediaImage?, sectionId: ItemListSectionId, - style: ItemListStyle + style: ItemListStyle, + deleted: (() -> Void)? ) { self.presentationData = presentationData self.context = context self.title = title self.label = label + self.icon = icon self.sectionId = sectionId self.style = style + self.deleted = deleted } - public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { async { let node = WebBrowserDomainExceptionItemNode() let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) @@ -48,7 +59,7 @@ public class WebBrowserDomainExceptionItem: ListViewItem, ItemListItem { } } - public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { Queue.mainQueue().async { if let nodeValue = node() as? WebBrowserDomainExceptionItemNode { let makeLayout = nodeValue.asyncLayout() @@ -65,33 +76,34 @@ public class WebBrowserDomainExceptionItem: ListViewItem, ItemListItem { } } - public var selectable: Bool = false + var selectable: Bool = false - public func selected(listView: ListView){ + func selected(listView: ListView){ } } -public class WebBrowserDomainExceptionItemNode: ListViewItemNode, ItemListItemNode { +final class WebBrowserDomainExceptionItemNode: ItemListRevealOptionsItemNode, ItemListItemNode { private let backgroundNode: ASDisplayNode private let topStripeNode: ASDisplayNode private let bottomStripeNode: ASDisplayNode private let maskNode: ASImageNode - let iconNode: ASImageNode + let iconNode: TransformImageNode let titleNode: TextNode let labelNode: TextNode private let activateArea: AccessibilityAreaNode private var item: WebBrowserDomainExceptionItem? + private var layoutParams: ListViewItemLayoutParams? override public var canBeSelected: Bool { return false } - public var tag: ItemListItemTag? = nil + var tag: ItemListItemTag? = nil - public init() { + init() { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true self.backgroundNode.backgroundColor = .white @@ -105,7 +117,7 @@ public class WebBrowserDomainExceptionItemNode: ListViewItemNode, ItemListItemNo self.bottomStripeNode = ASDisplayNode() self.bottomStripeNode.isLayerBacked = true - self.iconNode = ASImageNode() + self.iconNode = TransformImageNode() self.iconNode.isLayerBacked = true self.iconNode.displaysAsynchronously = false @@ -117,15 +129,16 @@ public class WebBrowserDomainExceptionItemNode: ListViewItemNode, ItemListItemNo self.activateArea = AccessibilityAreaNode() - super.init(layerBacked: false, dynamicBounce: false) + super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) + self.addSubnode(self.iconNode) self.addSubnode(self.titleNode) self.addSubnode(self.labelNode) self.addSubnode(self.activateArea) } - public func asyncLayout() -> (_ item: WebBrowserDomainExceptionItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + func asyncLayout() -> (_ item: WebBrowserDomainExceptionItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeLabelLayout = TextNode.asyncLayout(self.labelNode) @@ -143,7 +156,7 @@ public class WebBrowserDomainExceptionItemNode: ListViewItemNode, ItemListItemNo let itemBackgroundColor: UIColor let itemSeparatorColor: UIColor - let leftInset = 16.0 + params.leftInset + 43.0 + let leftInset = 16.0 + params.leftInset + 46.0 let titleColor: UIColor = item.presentationData.theme.list.itemPrimaryTextColor let labelColor: UIColor = item.presentationData.theme.list.itemAccentColor @@ -180,6 +193,7 @@ public class WebBrowserDomainExceptionItemNode: ListViewItemNode, ItemListItemNo return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in if let strongSelf = self { strongSelf.item = item + strongSelf.layoutParams = params strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height)) strongSelf.activateArea.accessibilityLabel = item.title @@ -191,6 +205,15 @@ public class WebBrowserDomainExceptionItemNode: ListViewItemNode, ItemListItemNo strongSelf.backgroundNode.backgroundColor = itemBackgroundColor } + let iconSize = CGSize(width: 40.0, height: 40.0) + var imageSize = iconSize + if currentItem?.icon?.id != item.icon?.id, let icon = item.icon { + strongSelf.iconNode.setSignal(chatMessagePhoto(postbox: item.context.account.postbox, userLocation: .other, photoReference: .standalone(media: icon))) + } + if let icon = item.icon, let dimensions = largestImageRepresentation(icon.representations)?.dimensions.cgSize { + imageSize = dimensions.aspectFilled(imageSize) + } + let _ = titleApply() let _ = labelApply() @@ -256,25 +279,69 @@ public class WebBrowserDomainExceptionItemNode: ListViewItemNode, ItemListItemNo centralContentHeight += titleSpacing centralContentHeight += labelLayout.size.height - let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - centralContentHeight) / 2.0)), size: titleLayout.size) + let titleFrame = CGRect(origin: CGPoint(x: leftInset + strongSelf.revealOffset, y: floor((height - centralContentHeight) / 2.0)), size: titleLayout.size) strongSelf.titleNode.frame = titleFrame - let labelFrame = CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + titleSpacing), size: labelLayout.size) + let labelFrame = CGRect(origin: CGPoint(x: leftInset + strongSelf.revealOffset, y: titleFrame.maxY + titleSpacing), size: labelLayout.size) strongSelf.labelNode.frame = labelFrame + + let iconFrame = CGRect(origin: CGPoint(x: params.leftInset + 11.0 + strongSelf.revealOffset, y: floorToScreenPixels((contentSize.height - iconSize.height) / 2.0)), size: iconSize) + strongSelf.iconNode.frame = iconFrame + + strongSelf.iconNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: 7.0), imageSize: imageSize, boundingSize: iconSize, intrinsicInsets: .zero))() + + strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) + + var revealOptions: [ItemListRevealOption] = [] + revealOptions.append(ItemListRevealOption(key: RevealOptionKey.delete.rawValue, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)) + strongSelf.setRevealOptions((left: [], right: revealOptions)) } }) } } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } - override public func animateAdded(_ currentTimestamp: Double, duration: Double) { + override func animateAdded(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } - override public func animateRemoved(_ currentTimestamp: Double, duration: Double) { + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) } + + override func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { + super.updateRevealOffset(offset: offset, transition: transition) + + if let params = self.layoutParams { + let leftInset: CGFloat = 16.0 + params.leftInset + 46.0 + + var iconFrame = self.iconNode.frame + iconFrame.origin.x = params.leftInset + 11.0 + offset + transition.updateFrame(node: self.iconNode, frame: iconFrame) + + var titleFrame = self.titleNode.frame + titleFrame.origin.x = leftInset + offset + transition.updateFrame(node: self.titleNode, frame: titleFrame) + + var subtitleFrame = self.labelNode.frame + subtitleFrame.origin.x = leftInset + offset + transition.updateFrame(node: self.labelNode, frame: subtitleFrame) + } + } + + override func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) { + if let item = self.item { + switch option.key { + case RevealOptionKey.delete.rawValue: + item.deleted?() + default: + break + } + } + self.setRevealOptionsOpened(false, animated: true) + self.revealOptionsInteractivelyClosed() + } } diff --git a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift index eda596b35c..29a4b1b14e 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift @@ -6,6 +6,7 @@ import Postbox import TelegramCore import TelegramPresentationData import TelegramUIPreferences +import PresentationDataUtils import ItemListUI import AccountContext import OpenInExternalAppUI @@ -13,12 +14,15 @@ import ItemListPeerActionItem import UndoUI import WebKit import LinkPresentation +import CoreServices +import PersistentStringHash private final class WebBrowserSettingsControllerArguments { let context: AccountContext let updateDefaultBrowser: (String?) -> Void let clearCookies: () -> Void let addException: () -> Void + let removeException: (String) -> Void let clearExceptions: () -> Void init( @@ -26,12 +30,14 @@ private final class WebBrowserSettingsControllerArguments { updateDefaultBrowser: @escaping (String?) -> Void, clearCookies: @escaping () -> Void, addException: @escaping () -> Void, + removeException: @escaping (String) -> Void, clearExceptions: @escaping () -> Void ) { self.context = context self.updateDefaultBrowser = updateDefaultBrowser self.clearCookies = clearCookies self.addException = addException + self.removeException = removeException self.clearExceptions = clearExceptions } } @@ -66,7 +72,30 @@ private enum WebBrowserSettingsControllerEntry: ItemListNodeEntry { } } - var stableId: Int32 { + var stableId: UInt64 { + switch self { + case .browserHeader: + return 0 + case let .browser(_, _, _, _, _, index): + return UInt64(1 + index) + case .clearCookies: + return 102 + case .clearCookiesInfo: + return 103 + case .exceptionsHeader: + return 104 + case .exceptionsAdd: + return 105 + case let .exception(_, _, exception): + return 2000 + exception.domain.persistentHashValue + case .exceptionsClear: + return 1000 + case .exceptionsInfo: + return 1001 + } + } + + var sortId: Int32 { switch self { case .browserHeader: return 0 @@ -149,7 +178,7 @@ private enum WebBrowserSettingsControllerEntry: ItemListNodeEntry { } static func <(lhs: WebBrowserSettingsControllerEntry, rhs: WebBrowserSettingsControllerEntry) -> Bool { - return lhs.stableId < rhs.stableId + return lhs.sortId < rhs.sortId } func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { @@ -170,7 +199,9 @@ private enum WebBrowserSettingsControllerEntry: ItemListNodeEntry { case let .exceptionsHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .exception(_, _, exception): - return WebBrowserDomainExceptionItem(presentationData: presentationData, context: arguments.context, title: exception.title, label: exception.domain, sectionId: self.section, style: .blocks) + return WebBrowserDomainExceptionItem(presentationData: presentationData, context: arguments.context, title: exception.title, label: exception.domain, icon: exception.icon, sectionId: self.section, style: .blocks, deleted: { + arguments.removeException(exception.domain) + }) case let .exceptionsAdd(_, text): return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.plusIconImage(presentationData.theme), title: text, sectionId: self.section, height: .generic, color: .accent, editing: false, action: { arguments.addException() @@ -207,7 +238,7 @@ private func webBrowserSettingsControllerEntries(context: AccountContext, presen entries.append(.exceptionsAdd(presentationData.theme, presentationData.strings.WebBrowser_Exceptions_AddException)) var exceptionIndex: Int32 = 0 - for exception in settings.exceptions { + for exception in settings.exceptions.reversed() { entries.append(.exception(exceptionIndex, presentationData.theme, exception)) exceptionIndex += 1 } @@ -225,6 +256,7 @@ private func webBrowserSettingsControllerEntries(context: AccountContext, presen public func webBrowserSettingsController(context: AccountContext) -> ViewController { var clearCookiesImpl: (() -> Void)? var addExceptionImpl: (() -> Void)? + var removeExceptionImpl: ((String) -> Void)? var clearExceptionsImpl: (() -> Void)? let arguments = WebBrowserSettingsControllerArguments( @@ -240,6 +272,9 @@ public func webBrowserSettingsController(context: AccountContext) -> ViewControl addException: { addExceptionImpl?() }, + removeException: { domain in + removeExceptionImpl?(domain) + }, clearExceptions: { clearExceptionsImpl?() } @@ -261,6 +296,9 @@ public func webBrowserSettingsController(context: AccountContext) -> ViewControl if previousSettings.defaultWebBrowser != settings.defaultWebBrowser { animateChanges = true } + if previousSettings.exceptions.count != settings.exceptions.count { + animateChanges = true + } } let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.WebBrowser_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) @@ -290,9 +328,11 @@ public func webBrowserSettingsController(context: AccountContext) -> ViewControl } addExceptionImpl = { [weak controller] in + var dismissImpl: (() -> Void)? let linkController = webBrowserDomainController(context: context, apply: { url in if let url { - let _ = fetchDomainExceptionInfo(url: url).startStandalone(next: { newException in + let _ = (fetchDomainExceptionInfo(context: context, url: url) + |> deliverOnMainQueue).startStandalone(next: { newException in let _ = updateWebBrowserSettingsInteractively(accountManager: context.sharedContext.accountManager, { currentSettings in var currentExceptions = currentSettings.exceptions for exception in currentExceptions { @@ -303,18 +343,44 @@ public func webBrowserSettingsController(context: AccountContext) -> ViewControl currentExceptions.append(newException) return currentSettings.withUpdatedExceptions(currentExceptions) }).start() + dismissImpl?() }) } }) + dismissImpl = { [weak linkController] in + linkController?.view.endEditing(true) + linkController?.dismissAnimated() + } controller?.present(linkController, in: .window(.root)) } - clearExceptionsImpl = { + removeExceptionImpl = { domain in let _ = updateWebBrowserSettingsInteractively(accountManager: context.sharedContext.accountManager, { currentSettings in - return currentSettings.withUpdatedExceptions([]) + let updatedExceptions = currentSettings.exceptions.filter { $0.domain != domain } + return currentSettings.withUpdatedExceptions(updatedExceptions) }).start() } + clearExceptionsImpl = { [weak controller] in + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + let alertController = textAlertController( + context: context, + updatedPresentationData: nil, + title: nil, + text: presentationData.strings.WebBrowser_Exceptions_ClearConfirmation_Text, + actions: [ + TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), + TextAlertAction(type: .defaultAction, title: presentationData.strings.WebBrowser_Exceptions_ClearConfirmation_Clear, action: { + let _ = updateWebBrowserSettingsInteractively(accountManager: context.sharedContext.accountManager, { currentSettings in + return currentSettings.withUpdatedExceptions([]) + }).start() + }) + ] + ) + controller?.present(alertController, in: .window(.root)) + } + return controller } @@ -333,22 +399,59 @@ private func cleanDomain(url: String) -> (domain: String, fullUrl: String) { } } -private func fetchDomainExceptionInfo(url: String) -> Signal { +private func fetchDomainExceptionInfo(context: AccountContext, url: String) -> Signal { let (domain, domainUrl) = cleanDomain(url: url) if #available(iOS 13.0, *), let url = URL(string: domainUrl) { return Signal { subscriber in let metadataProvider = LPMetadataProvider() metadataProvider.shouldFetchSubresources = true metadataProvider.startFetchingMetadata(for: url, completionHandler: { metadata, _ in - let title = metadata?.value(forKey: "_siteName") as? String ?? metadata?.title - subscriber.putNext(WebBrowserException(domain: domain, title: title ?? domain)) - subscriber.putCompletion() + let completeWithImage: (Data?) -> Void = { imageData in + var image: TelegramMediaImage? + if let imageData, let parsedImage = UIImage(data: imageData) { + let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) + context.account.postbox.mediaBox.storeResourceData(resource.id, data: imageData) + image = TelegramMediaImage( + imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: Int64.random(in: Int64.min ... Int64.max)), + representations: [ + TelegramMediaImageRepresentation( + dimensions: PixelDimensions(width: Int32(parsedImage.size.width), height: Int32(parsedImage.size.height)), + resource: resource, + progressiveSizes: [], + immediateThumbnailData: nil, + hasVideo: false, + isPersonal: false + ) + ], + immediateThumbnailData: nil, + reference: nil, + partialReference: nil, + flags: [] + ) + } + + let title = metadata?.value(forKey: "_siteName") as? String ?? metadata?.title + subscriber.putNext(WebBrowserException(domain: domain, title: title ?? domain, icon: image)) + subscriber.putCompletion() + } + + if let imageProvider = metadata?.iconProvider { + imageProvider.loadFileRepresentation(forTypeIdentifier: kUTTypeImage as String, completionHandler: { imageUrl, _ in + guard let imageUrl, let imageData = try? Data(contentsOf: imageUrl) else { + completeWithImage(nil) + return + } + completeWithImage(imageData) + }) + } else { + completeWithImage(nil) + } }) return ActionDisposable { metadataProvider.cancel() } } } else { - return .single(WebBrowserException(domain: domain, title: domain)) + return .single(WebBrowserException(domain: domain, title: domain, icon: nil)) } } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift index c1aa31fd74..b930925be4 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift @@ -192,7 +192,17 @@ public enum CodableDrawingEntity: Equatable { url: url ) case let .weather(entity): - let color: UInt32 = 0xffffffff + let color: UInt32 + switch entity.style { + case .white: + color = 0xffffffff + case .black: + color = 0xff000000 + case .transparent: + color = 0x51000000 + case .custom: + color = entity.color.toUIColor().argb + } return .weather( coordinates: coordinates, emoji: entity.emoji, diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 944830b8bf..27a8139900 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -3155,6 +3155,10 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } } } + + if let initialLink = controller.initialLink { + self.addInitialLink(initialLink) + } } private var initialMaskScale: CGFloat = .zero @@ -4559,6 +4563,45 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate controller.push(linkController) } + func addInitialLink(_ link: String) { + guard self.context.isPremium else { + return + } + let text = link + + var attributes: [MessageAttribute] = [] + attributes.append(TextEntitiesMessageAttribute(entities: [.init(range: 0 ..< (text as NSString).length, type: .Url)])) + +// attributes.append(WebpagePreviewMessageAttribute(leadingPreview: !self.positionBelowText, forceLargeMedia: self.largeMedia, isManuallyAdded: false, isSafe: true)) + + let effectiveMedia: TelegramMediaWebpage? = nil +// if let webpage = self.webpage, case .Loaded = webpage.content { +// effectiveMedia = webpage +// } + + let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(1)) + let message = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: text, attributes: attributes, media: effectiveMedia.flatMap { [$0] } ?? [], peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + + let renderer = DrawingMessageRenderer(context: self.context, messages: [message], parentView: self.view, isLink: true) + renderer.render(completion: { [weak self] renderResult in + guard let self else { + return + } + let result = CreateLinkScreen.Result( + url: link, + name: "", + webpage: effectiveMedia, + positionBelowText: false, + largeMedia: nil, + image: effectiveMedia != nil ? renderResult.dayImage : nil, + nightImage: effectiveMedia != nil ? renderResult.nightImage : nil + ) + + let entity = DrawingLinkEntity(url: result.url, name: result.name, webpage: result.webpage, positionBelowText: result.positionBelowText, largeMedia: result.largeMedia, style: .white) + self.interaction?.insertEntity(entity, position: CGPoint(x: storyDimensions.width / 2.0, y: storyDimensions.width / 3.0 * 4.0), select: false) + }) + } + func addReaction() { guard let controller = self.controller else { return @@ -4625,7 +4668,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } } if currentWeatherCount >= maxWeatherCount { - self.controller?.hapticFeedback.error() + self.controller?.presentWeatherLimitTooltip() return } @@ -5535,6 +5578,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate fileprivate let initialPrivacy: EngineStoryPrivacy? fileprivate let initialMediaAreas: [MediaArea]? fileprivate let initialVideoPosition: Double? + fileprivate let initialLink: String? fileprivate let transitionIn: TransitionIn? fileprivate let transitionOut: (Bool, Bool?) -> TransitionOut? @@ -5569,6 +5613,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate initialPrivacy: EngineStoryPrivacy? = nil, initialMediaAreas: [MediaArea]? = nil, initialVideoPosition: Double? = nil, + initialLink: String? = nil, transitionIn: TransitionIn?, transitionOut: @escaping (Bool, Bool?) -> TransitionOut?, completion: @escaping (MediaEditorScreen.Result, @escaping (@escaping () -> Void) -> Void) -> Void @@ -5583,6 +5628,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.initialPrivacy = initialPrivacy self.initialMediaAreas = initialMediaAreas self.initialVideoPosition = initialVideoPosition + self.initialLink = initialLink self.transitionIn = transitionIn self.transitionOut = transitionOut self.completion = completion @@ -6216,6 +6262,29 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate }) self.present(controller, in: .window(.root)) } + + fileprivate func presentWeatherLimitTooltip() { + self.hapticFeedback.impact(.light) + + self.dismissAllTooltips() + + let context = self.context + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let limit: Int32 = 3 + + let value = presentationData.strings.Story_Editor_TooltipWeatherLimitValue(limit) + let content: UndoOverlayContent = .info( + title: nil, + text: presentationData.strings.Story_Editor_TooltipWeatherLimitText(value).string, + timeout: nil, + customUndoText: nil + ) + + let controller = UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: true, position: .top, animateInAsReplacement: false, action: { _ in + return true + }) + self.present(controller, in: .window(.root)) + } func maybePresentDiscardAlert() { self.hapticFeedback.impact(.light) diff --git a/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedContainer.swift b/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedContainer.swift index 2bcb788957..8cb8ed781a 100644 --- a/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedContainer.swift +++ b/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedContainer.swift @@ -615,6 +615,9 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll guard !self.items.isEmpty && !self.isExpanded && self.currentTransition == nil else { return } + +// self.scrollView.contentOffset = CGPoint(x: 0.0, y: max(0.0, self.scrollView.contentSize.height - self.scrollView.bounds.height)) + if self.items.count == 1, let item = self.items.first { if let navigationController = self.navigationController { item.beforeMaximize(navigationController, { [weak self] in diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift index b16bd59352..1fe793e04b 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift @@ -992,7 +992,7 @@ final class ShareWithPeersScreenComponent: Component { let sectionTitle: String if section.id == 0, case .stories = component.stateContext.subject { - sectionTitle = environment.strings.Story_Privacy_PostStoryAsHeader + sectionTitle = component.coverItem == nil ? environment.strings.Story_Privacy_PostStoryAsHeader : "" } else if section.id == 2 { sectionTitle = environment.strings.Story_Privacy_WhoCanViewHeader } else if section.id == 1 { @@ -1721,10 +1721,9 @@ final class ShareWithPeersScreenComponent: Component { self.visibleSectionFooters[section.id] = sectionFooter } - var footerText = "Choose a frame from the story to show in your Profile." - + var footerText = environment.strings.Story_Privacy_ChooseCoverInfo if let sendAsPeerId = self.sendAsPeerId, sendAsPeerId.isGroupOrChannel == true { - footerText = isSendAsGroup ? "Choose a frame from the story to show in group profile.": "Choose a frame from the story to show in channel profile." + footerText = isSendAsGroup ? environment.strings.Story_Privacy_ChooseCoverGroupInfo : environment.strings.Story_Privacy_ChooseCoverChannelInfo } let footerSize = sectionFooter.update( @@ -2043,8 +2042,13 @@ final class ShareWithPeersScreenComponent: Component { var sideInset: CGFloat = 0.0 if case .stories = component.stateContext.subject { sideInset = 16.0 - self.scrollView.isScrollEnabled = false - self.dismissPanGesture?.isEnabled = true + if availableSize.width < 393.0 && hasCover { + self.scrollView.isScrollEnabled = true + self.dismissPanGesture?.isEnabled = false + } else { + self.scrollView.isScrollEnabled = false + self.dismissPanGesture?.isEnabled = true + } } else if case .peers = component.stateContext.subject { sideInset = 16.0 self.dismissPanGesture?.isEnabled = false @@ -2417,7 +2421,7 @@ final class ShareWithPeersScreenComponent: Component { if !editing && hasChannels { sections.append(ItemLayout.Section( id: 0, - insets: UIEdgeInsets(top: 28.0, left: 0.0, bottom: 0.0, right: 0.0), + insets: UIEdgeInsets(top: component.coverItem == nil ? 28.0 : 12.0, left: 0.0, bottom: 0.0, right: 0.0), itemHeight: peerItemSize.height, itemCount: 1 )) @@ -2461,7 +2465,7 @@ final class ShareWithPeersScreenComponent: Component { } else { containerInset += 10.0 } - + var navigationHeight: CGFloat = 56.0 let navigationSideInset: CGFloat = 16.0 var navigationButtonsWidth: CGFloat = 0.0 @@ -2599,9 +2603,9 @@ final class ShareWithPeersScreenComponent: Component { } navigationHeight += navigationTextFieldFrame.height - if case .stories = component.stateContext.subject { - navigationHeight += 16.0 - } +// if case .stories = component.stateContext.subject { +// navigationHeight += 16.0 +// } let topInset: CGFloat if environment.inputHeight != 0.0 || !self.navigationTextFieldState.text.isEmpty { @@ -2905,7 +2909,7 @@ final class ShareWithPeersScreenComponent: Component { bottomPanelInset = 8.0 transition.setFrame(view: self.bottomBackgroundView, frame: CGRect(origin: CGPoint(x: containerSideInset, y: availableSize.height - bottomPanelHeight - 8.0), size: CGSize(width: containerWidth, height: bottomPanelHeight + bottomPanelInset))) self.bottomBackgroundView.update(size: self.bottomBackgroundView.bounds.size, transition: transition.containedViewLayoutTransition) - transition.setFrame(layer: self.bottomSeparatorLayer, frame: CGRect(origin: CGPoint(x: containerSideInset + sideInset, y: availableSize.height - bottomPanelHeight - bottomPanelInset - UIScreenPixel), size: CGSize(width: containerWidth, height: UIScreenPixel))) + transition.setFrame(layer: self.bottomSeparatorLayer, frame: CGRect(origin: CGPoint(x: containerSideInset, y: availableSize.height - bottomPanelHeight - bottomPanelInset - UIScreenPixel), size: CGSize(width: containerWidth, height: UIScreenPixel))) } let itemContainerSize = CGSize(width: itemsContainerWidth, height: availableSize.height) @@ -3146,7 +3150,7 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { } if !editing || pin, coverImage != nil { - coverItem = ShareWithPeersScreenComponent.CoverItem(id: .choose, title: "Choose Story Cover", image: coverImage) + coverItem = ShareWithPeersScreenComponent.CoverItem(id: .choose, title: presentationData.strings.Story_Privacy_ChooseCover, image: coverImage) } } diff --git a/submodules/TelegramUI/Resources/WebEmbed/UIWebViewSearch.js b/submodules/TelegramUI/Resources/WebEmbed/UIWebViewSearch.js index 20b3fe89e7..fcb419a5bd 100644 --- a/submodules/TelegramUI/Resources/WebEmbed/UIWebViewSearch.js +++ b/submodules/TelegramUI/Resources/WebEmbed/UIWebViewSearch.js @@ -38,12 +38,42 @@ function uiWebview_HighlightAllOccurencesOfStringForElement(element,keyword) { span.appendChild(text); span.setAttribute("class","uiWebviewHighlight"); + span.style.position = "relative"; + span.style.display = "inline-block"; span.style.backgroundColor="#ffe438"; span.style.color="black"; span.style.borderRadius="3px"; + span.style.scrollMargin="44px"; + span.style.zIndex = "1001"; // Ensure highlights are above the overlay index--; span.setAttribute("id", "SEARCH WORD"+(index)); + + var beforeStyle = document.createElement('style'); + beforeStyle.innerHTML = ` + .uiWebviewHighlight::before { + content: ''; + position: absolute; + top: 0px; + bottom: 0px; + left: -2px; + right: -2px; + background-color: #ffe438; + z-index: -1; + border-radius: 3px; + } + .dark-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.22); + z-index: 1000; + pointer-events: none; + } + `; + document.head.appendChild(beforeStyle); text = document.createTextNode(value.substr(idx+keyword.length)); element.deleteData(idx, value.length - idx); @@ -68,6 +98,7 @@ function uiWebview_HighlightAllOccurencesOfStringForElement(element,keyword) { // the main entry point to start the search function uiWebview_HighlightAllOccurencesOfString(keyword) { uiWebview_RemoveAllHighlights(); + uiWebview_AddDarkOverlay(); uiWebview_HighlightAllOccurencesOfStringForElement(document.body, keyword.toLowerCase()); } @@ -100,9 +131,24 @@ function uiWebview_RemoveAllHighlightsForElement(element) { function uiWebview_RemoveAllHighlights() { uiWebview_SearchResultCount = 0; uiWebview_RemoveAllHighlightsForElement(document.body); + uiWebview_RemoveDarkOverlay(); } function uiWebview_ScrollTo(idx) { var scrollTo = document.getElementById("SEARCH WORD" + idx); if (scrollTo) scrollTo.scrollIntoView(); } + +function uiWebview_AddDarkOverlay() { + var overlay = document.createElement('div'); + overlay.classList.add('dark-overlay'); + overlay.setAttribute('id', 'dark-overlay'); + document.body.appendChild(overlay); +} + +function uiWebview_RemoveDarkOverlay() { + var overlay = document.getElementById('dark-overlay'); + if (overlay) { + document.body.removeChild(overlay); + } +} diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenStorySharing.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenStorySharing.swift index 77353f1fad..20b2ba01a1 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenStorySharing.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenStorySharing.swift @@ -16,113 +16,16 @@ import TextFormat import TelegramBaseController import AccountContext import TelegramStringFormatting -import OverlayStatusController -import DeviceLocationManager -import ShareController -import UrlEscaping -import ContextUI -import ComposePollUI -import AlertUI import PresentationDataUtils import UndoUI -import TelegramCallsUI -import TelegramNotices -import GameUI -import ScreenCaptureDetection -import GalleryUI -import OpenInExternalAppUI -import LegacyUI -import InstantPageUI -import LocationUI -import BotPaymentsUI -import DeleteChatPeerActionSheetItem -import HashtagSearchUI -import LegacyMediaPickerUI -import Emoji -import PeerAvatarGalleryUI import PeerInfoUI -import RaiseToListen -import UrlHandling -import AvatarNode import AppBundle import LocalizedPeerData -import PhoneNumberFormat -import SettingsUI -import UrlWhitelist -import TelegramIntents -import TooltipUI -import StatisticsUI -import MediaResources -import GalleryData import ChatInterfaceState -import InviteLinksUI -import Markdown -import TelegramPermissionsUI -import Speak -import TranslateUI -import UniversalMediaPlayer -import WallpaperBackgroundNode -import ChatListUI -import CalendarMessageScreen -import ReactionSelectionNode -import ReactionListContextMenuContent -import AttachmentUI -import AttachmentTextInputPanelNode -import MediaPickerUI -import ChatPresentationInterfaceState -import Pasteboard -import ChatSendMessageActionUI -import ChatTextLinkEditUI -import WebUI -import PremiumUI -import ImageTransparency -import StickerPackPreviewUI -import TextNodeWithEntities -import EntityKeyboard -import ChatTitleView -import EmojiStatusComponent -import ChatTimerScreen -import MediaPasteboardUI -import ChatListHeaderComponent import ChatControllerInteraction -import FeaturedStickersScreen -import ChatEntityKeyboardInputNode -import StorageUsageScreen -import AvatarEditorScreen -import ChatScheduleTimeController -import ICloudResources import StoryContainerScreen -import MoreHeaderButton -import VolumeButtons -import ChatAvatarNavigationNode -import ChatContextQuery -import PeerReportScreen -import PeerSelectionController import SaveToCameraRoll -import ChatMessageDateAndStatusNode -import ReplyAccessoryPanelNode -import TextSelectionNode -import ChatMessagePollBubbleContentNode -import ChatMessageItem -import ChatMessageItemImpl -import ChatMessageItemView -import ChatMessageItemCommon -import ChatMessageAnimatedStickerItemNode -import ChatMessageBubbleItemNode -import ChatNavigationButton -import WebsiteType -import ChatQrCodeScreen -import PeerInfoScreen import MediaEditorScreen -import WallpaperGalleryScreen -import WallpaperGridScreen -import VideoMessageCameraScreen -import TopMessageReactions -import AudioWaveform -import PeerNameColorScreen -import ChatEmptyNode -import ChatMediaInputStickerGridItem -import AdsInfoScreen extension ChatControllerImpl { func openStorySharing(messages: [Message]) { @@ -187,7 +90,6 @@ extension ChatControllerImpl { } } }) - } ) self.push(controller) diff --git a/submodules/TelegramUI/Sources/OpenChatMessage.swift b/submodules/TelegramUI/Sources/OpenChatMessage.swift index 1422973870..c0e002509a 100644 --- a/submodules/TelegramUI/Sources/OpenChatMessage.swift +++ b/submodules/TelegramUI/Sources/OpenChatMessage.swift @@ -231,7 +231,18 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool { params.present(controller, nil) } else if let rootController = params.navigationController?.view.window?.rootViewController { let proceed = { - presentDocumentPreviewController(rootController: rootController, theme: presentationData.theme, strings: presentationData.strings, postbox: params.context.account.postbox, file: file, canShare: !params.message.isCopyProtected()) + if params.context.sharedContext.immediateExperimentalUISettings.browserExperiment && BrowserScreen.supportedDocumentMimeTypes.contains(file.mimeType) { + let subject: BrowserScreen.Subject + if file.mimeType == "application/pdf" { + subject = .pdfDocument(file: file) + } else { + subject = .document(file: file) + } + let controller = BrowserScreen(context: params.context, subject: subject) + params.navigationController?.pushViewController(controller) + } else { + presentDocumentPreviewController(rootController: rootController, theme: presentationData.theme, strings: presentationData.strings, postbox: params.context.account.postbox, file: file, canShare: !params.message.isCopyProtected()) + } } if file.mimeType.contains("image/svg") { let presentationData = params.context.sharedContext.currentPresentationData.with { $0 } diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index 6625d8b06f..167e85acec 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -34,6 +34,7 @@ import StoryContainerScreen import WallpaperGalleryScreen import TelegramStringFormatting import TextFormat +import BrowserUI private func defaultNavigationForPeerId(_ peerId: PeerId?, navigation: ChatControllerInteractionNavigateToPeer) -> ChatControllerInteractionNavigateToPeer { if case .default = navigation { @@ -248,7 +249,14 @@ func openResolvedUrlImpl( }) present(controller, nil) case let .instantView(webpage, anchor): - navigationController?.pushViewController(InstantPageController(context: context, webPage: webpage, sourceLocation: InstantPageSourceLocation(userLocation: .other, peerType: .channel), anchor: anchor)) + let sourceLocation = InstantPageSourceLocation(userLocation: .other, peerType: .channel) + let pageController: ViewController + if context.sharedContext.immediateExperimentalUISettings.browserExperiment { + pageController = BrowserScreen(context: context, subject: .instantPage(webPage: webpage, anchor: anchor, sourceLocation: sourceLocation)) + } else { + pageController = InstantPageController(context: context, webPage: webpage, sourceLocation: sourceLocation, anchor: anchor) + } + navigationController?.pushViewController(pageController) case let .join(link): dismissInput() diff --git a/submodules/TelegramUI/Sources/OpenUrl.swift b/submodules/TelegramUI/Sources/OpenUrl.swift index e232ff65cc..c25368c74b 100644 --- a/submodules/TelegramUI/Sources/OpenUrl.swift +++ b/submodules/TelegramUI/Sources/OpenUrl.swift @@ -1039,18 +1039,18 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur return settings } - var isCompact = false - if let metrics = navigationController?.validLayout?.metrics, case .compact = metrics.widthClass { - isCompact = true - } +// var isCompact = false +// if let metrics = navigationController?.validLayout?.metrics, case .compact = metrics.widthClass { +// isCompact = true +// } let _ = (settings |> deliverOnMainQueue).startStandalone(next: { settings in if let defaultWebBrowser = settings.defaultWebBrowser, defaultWebBrowser != "inApp" { let openInOptions = availableOpenInOptions(context: context, item: .url(url: url)) if let option = openInOptions.first(where: { $0.identifier == settings.defaultWebBrowser }) { - if case let .openUrl(url) = option.action() { - context.sharedContext.applicationBindings.openUrl(url) + if case let .openUrl(openInUrl) = option.action() { + context.sharedContext.applicationBindings.openUrl(openInUrl) } else { context.sharedContext.applicationBindings.openUrl(url) } @@ -1058,11 +1058,20 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur context.sharedContext.applicationBindings.openUrl(url) } } else { - if settings.defaultWebBrowser == nil && isCompact { + var isExceptedDomain = false + let host = ".\((parsedUrl.host ?? "").lowercased())" + for exception in settings.exceptions { + if host.hasSuffix(".\(exception.domain)") { + isExceptedDomain = true + break + } + } + + if settings.defaultWebBrowser == nil && !isExceptedDomain { let controller = BrowserScreen(context: context, subject: .webPage(url: parsedUrl.absoluteString)) navigationController?.pushViewController(controller) } else { - if let window = navigationController?.view.window { + if let window = navigationController?.view.window, !isExceptedDomain { let controller = SFSafariViewController(url: parsedUrl) controller.preferredBarTintColor = presentationData.theme.rootController.navigationBar.opaqueBackgroundColor controller.preferredControlTintColor = presentationData.theme.rootController.navigationBar.accentTextColor diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 1afcc0ab6a..abc647f4f6 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -2652,6 +2652,35 @@ public final class SharedAccountContextImpl: SharedAccountContext { } return editorController } + + public func makeStoryMediaEditorScreen(context: AccountContext, source: Any?, text: String?, link: String?, completion: @escaping (MediaEditorScreenResult, @escaping (@escaping () -> Void) -> Void) -> Void) -> ViewController { + let subject: Signal + if let image = source as? UIImage { + subject = .single(.image(image, PixelDimensions(image.size), nil, .bottomRight)) + } else if let path = source as? String { + subject = .single(.video(path, nil, false, nil, nil, PixelDimensions(width: 1080, height: 1920), 0.0, [], .bottomRight)) + } else { + subject = .single(.empty(PixelDimensions(width: 1080, height: 1920))) + } + let editorController = MediaEditorScreen( + context: context, + mode: .storyEditor, + subject: subject, + customTarget: nil, + initialCaption: text.flatMap { NSAttributedString(string: $0) }, + initialLink: link, + transitionIn: nil, + transitionOut: { finished, isNew in + return nil + }, completion: { result, commit in + completion(result, commit) + } as (MediaEditorScreen.Result, @escaping (@escaping () -> Void) -> Void) -> Void + ) +// editorController.cancelled = { _ in +// cancelled() +// } + return editorController + } public func makeMediaPickerScreen(context: AccountContext, hasSearch: Bool, completion: @escaping (Any) -> Void) -> ViewController { return mediaPickerController(context: context, hasSearch: hasSearch, completion: completion) diff --git a/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift b/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift index cba2c06b8c..ebefd6a4f9 100644 --- a/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift +++ b/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift @@ -104,6 +104,7 @@ private enum ApplicationSpecificOrderedItemListCollectionIdValues: Int32 { case storyDrafts = 4 case storySources = 5 case hashtagSearchRecentQueries = 6 + case browserRecentlyVisited = 7 } public struct ApplicationSpecificOrderedItemListCollectionId { @@ -114,4 +115,5 @@ public struct ApplicationSpecificOrderedItemListCollectionId { public static let storyDrafts = applicationSpecificOrderedItemListCollectionId(ApplicationSpecificOrderedItemListCollectionIdValues.storyDrafts.rawValue) public static let storySources = applicationSpecificOrderedItemListCollectionId(ApplicationSpecificOrderedItemListCollectionIdValues.storySources.rawValue) public static let hashtagSearchRecentQueries = applicationSpecificOrderedItemListCollectionId(ApplicationSpecificOrderedItemListCollectionIdValues.hashtagSearchRecentQueries.rawValue) + public static let browserRecentlyVisited = applicationSpecificOrderedItemListCollectionId(ApplicationSpecificOrderedItemListCollectionIdValues.browserRecentlyVisited.rawValue) } diff --git a/submodules/TelegramUIPreferences/Sources/WebBrowserSettings.swift b/submodules/TelegramUIPreferences/Sources/WebBrowserSettings.swift index 9c45fb6edd..68a6760183 100644 --- a/submodules/TelegramUIPreferences/Sources/WebBrowserSettings.swift +++ b/submodules/TelegramUIPreferences/Sources/WebBrowserSettings.swift @@ -6,10 +6,12 @@ import SwiftSignalKit public struct WebBrowserException: Codable, Equatable { public let domain: String public let title: String + public let icon: TelegramMediaImage? - public init(domain: String, title: String) { + public init(domain: String, title: String, icon: TelegramMediaImage?) { self.domain = domain self.title = title + self.icon = icon } public init(from decoder: Decoder) throws { @@ -17,6 +19,7 @@ public struct WebBrowserException: Codable, Equatable { self.domain = try container.decode(String.self, forKey: "domain") self.title = try container.decode(String.self, forKey: "title") + self.icon = try container.decodeIfPresent(TelegramMediaImage.self, forKey: "icon") } public func encode(to encoder: Encoder) throws { @@ -24,6 +27,11 @@ public struct WebBrowserException: Codable, Equatable { try container.encode(self.domain, forKey: "domain") try container.encode(self.title, forKey: "title") + if let icon = self.icon { + try container.encode(icon, forKey: "icon") + } else { + try container.encodeNil(forKey: "icon") + } } } diff --git a/submodules/WebUI/BUILD b/submodules/WebUI/BUILD index a0017b0e91..70f24be4cf 100644 --- a/submodules/WebUI/BUILD +++ b/submodules/WebUI/BUILD @@ -39,6 +39,7 @@ swift_library( "//submodules/TelegramUI/Components/EmojiStatusComponent", "//submodules/ShareController", "//submodules/UndoUI", + "//submodules/OverlayStatusController", ], visibility = [ "//visibility:public", diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index 08f7b3bec2..1191fa54c3 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -28,6 +28,7 @@ import OpenInExternalAppUI import ShareController import UndoUI import AvatarNode +import OverlayStatusController private let durgerKingBotIds: [Int64] = [5104055776, 2200339955] @@ -1100,6 +1101,84 @@ public final class WebAppController: ViewController, AttachmentContainable { if let json = json, let isPanGestureEnabled = json["allow_vertical_swipe"] as? Bool { self.controller?._isPanGestureEnabled = isPanGestureEnabled } + case "web_app_share_to_story": + if let json = json, let mediaUrl = json["media_url"] as? String { + let text = json["text"] as? String + let link = json["widget_link"] as? String + + enum FetchResult { + case result(Data) + case progress(Float) + } + + let controller = OverlayStatusController(theme: self.presentationData.theme, type: .loading(cancelled: { + + })) + self.controller?.present(controller, in: .window(.root)) + + let _ = (fetchHttpResource(url: mediaUrl) + |> map(Optional.init) + |> `catch` { error in + return .single(nil) + } + |> mapToSignal { value -> Signal in + if case let .dataPart(_, data, _, complete) = value, complete { + return .single(.result(data)) + } else if case let .progressUpdated(progress) = value { + return .single(.progress(progress)) + } else { + return .complete() + } + } + |> deliverOnMainQueue).start(next: { [weak self, weak controller] next in + guard let self else { + return + } + controller?.dismiss() + + switch next { + case let .result(data): + var source: Any? + if let image = UIImage(data: data) { + source = image + } else { + let tempFile = TempBox.shared.tempFile(fileName: "image.mp4") + if let _ = try? data.write(to: URL(fileURLWithPath: tempFile.path), options: .atomic) { + source = tempFile.path + } + } + if let source { + let externalState = MediaEditorTransitionOutExternalState( + storyTarget: nil, + isForcedTarget: false, + isPeerArchived: false, + transitionOut: nil + ) + let controller = self.context.sharedContext.makeStoryMediaEditorScreen(context: self.context, source: source, text: text, link: link, completion: { result, commit in +// let targetPeerId: EnginePeer.Id + let target: Stories.PendingTarget +// if let sendAsPeerId = result.options.sendAsPeerId { +// target = .peer(sendAsPeerId) +// targetPeerId = sendAsPeerId +// } else { + target = .myStories +// targetPeerId = self.context.account.peerId +// } + externalState.storyTarget = target + + if let rootController = self.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface { + rootController.proceedWithStoryUpload(target: target, result: result, existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit) + } + }) + if let navigationController = self.controller?.getNavigationController() { + navigationController.pushViewController(controller) + } + } + default: + break + } + }) + } default: break } From 9ba07435539f0aced741dd62fbf54bc54c893b7b Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Wed, 24 Jul 2024 09:25:48 +0400 Subject: [PATCH 03/41] Various improvements --- .../Sources/BrowserBookmarksScreen.swift | 2 +- submodules/DrawingUI/BUILD | 1 + .../Sources/DrawingWeatherEntityView.swift | 94 +++++++------------ .../Sources/StarsPurchaseScreen.swift | 38 ++++++-- .../Stars/StarsTransactionScreen/BUILD | 1 + .../Sources/StarsTransactionScreen.swift | 41 +++++++- 6 files changed, 110 insertions(+), 67 deletions(-) diff --git a/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift b/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift index 1cf00e619e..a3bcafe42c 100644 --- a/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift +++ b/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift @@ -55,7 +55,7 @@ public final class BrowserBookmarksScreen: ViewController { }, navigateToMessage: { _, _, _ in }, navigateToMessageStandalone: { _ in }, navigateToThreadMessage: { _, _, _ in - }, tapMessage: nil, clickThroughMessage: { + }, tapMessage: nil, clickThroughMessage: { _, _ in }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _, _ in }, sendMessage: { _ in diff --git a/submodules/DrawingUI/BUILD b/submodules/DrawingUI/BUILD index 5d8642145c..68824329eb 100644 --- a/submodules/DrawingUI/BUILD +++ b/submodules/DrawingUI/BUILD @@ -97,6 +97,7 @@ swift_library( "//submodules/ChatPresentationInterfaceState:ChatPresentationInterfaceState", "//submodules/StickerPackPreviewUI:StickerPackPreviewUI", "//submodules/TelegramUI/Components/LottieComponent", + "//submodules/TelegramUI/Components/LottieComponentResourceContent", "//submodules/ImageTransparency", "//submodules/GalleryUI", "//submodules/MediaPlayer:UniversalMediaPlayer", diff --git a/submodules/DrawingUI/Sources/DrawingWeatherEntityView.swift b/submodules/DrawingUI/Sources/DrawingWeatherEntityView.swift index 1012eb1c77..5815a10143 100644 --- a/submodules/DrawingUI/Sources/DrawingWeatherEntityView.swift +++ b/submodules/DrawingUI/Sources/DrawingWeatherEntityView.swift @@ -1,6 +1,7 @@ import Foundation import UIKit import Display +import ComponentFlow import SwiftSignalKit import AccountContext import TelegramCore @@ -9,6 +10,8 @@ import TelegramAnimatedStickerNode import StickerResources import MediaEditor import TelegramStringFormatting +import LottieComponent +import LottieComponentResourceContent private func generateIcon(style: DrawingWeatherEntity.Style) -> UIImage? { guard let image = UIImage(bundleImageName: "Chat/Attach Menu/Location") else { @@ -53,9 +56,8 @@ public final class DrawingWeatherEntityView: DrawingEntityView, UITextViewDelega let backgroundView: UIView let textView: DrawingTextView - let iconView: UIImageView - private let imageNode: TransformImageNode - private var animationNode: AnimatedStickerNode? + + private var animation = ComponentView() private var didSetUpAnimationNode = false private let stickerFetchedDisposable = MetaDisposable() @@ -88,20 +90,14 @@ public final class DrawingWeatherEntityView: DrawingEntityView, UITextViewDelega self.textView.spellCheckingType = .no self.textView.textContainer.maximumNumberOfLines = 2 self.textView.textContainer.lineBreakMode = .byTruncatingTail - - self.iconView = UIImageView() - self.imageNode = TransformImageNode() - + super.init(context: context, entity: entity) self.textView.delegate = self self.addSubview(self.backgroundView) self.addSubview(self.textView) - self.addSubview(self.iconView) self.update(animated: false) - - self.setup() } required init?(coder: NSCoder) { @@ -134,17 +130,35 @@ public final class DrawingWeatherEntityView: DrawingEntityView, UITextViewDelega let iconSize = min(80.0, floor(self.bounds.height * 0.7)) let iconOffset: CGFloat = 0.3 - self.iconView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels(iconSize * iconOffset), y: floorToScreenPixels((self.bounds.height - iconSize) / 2.0)), size: CGSize(width: iconSize, height: iconSize)) - self.imageNode.frame = self.iconView.frame.offsetBy(dx: 0.0, dy: 2.0) + let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(iconSize * iconOffset), y: floorToScreenPixels((self.bounds.height - iconSize) / 2.0)), size: CGSize(width: iconSize, height: iconSize)) - if let animationNode = self.animationNode { - animationNode.frame = self.iconView.frame - animationNode.updateLayout(size: self.iconView.frame.size) + if let icon = self.weatherEntity.icon { + let _ = self.animation.update( + transition: .immediate, + component: AnyComponent( + LottieComponent( + content: LottieComponent.ResourceContent( + context: self.context, + file: icon, + attemptSynchronously: true, + providesPlaceholder: true + ), + color: nil, + placeholderColor: UIColor(rgb: 0x000000, alpha: 0.1), + loop: !["🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘"].contains(self.weatherEntity.emoji) + ) + ), + environment: {}, + containerSize: iconFrame.size + ) + if let animationView = self.animation.view { + if animationView.superview == nil { + self.addSubview(animationView) + } + animationView.frame = iconFrame + } } - - let imageSize = CGSize(width: iconSize, height: iconSize) - self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))() - + self.textView.frame = CGRect(origin: CGPoint(x: self.bounds.width - self.textSize.width - 6.0, y: floorToScreenPixels((self.bounds.height - self.textSize.height) / 2.0)), size: self.textSize) self.backgroundView.frame = self.bounds } @@ -263,10 +277,8 @@ public final class DrawingWeatherEntityView: DrawingEntityView, UITextViewDelega self.sizeToFit() - if self.currentStyle != self.weatherEntity.style { - self.currentStyle = self.weatherEntity.style - self.iconView.image = generateIcon(style: self.weatherEntity.style) - } + + self.currentStyle = self.weatherEntity.style self.backgroundView.layer.cornerRadius = self.textSize.height * 0.2 if #available(iOS 13.0, *) { @@ -276,42 +288,6 @@ public final class DrawingWeatherEntityView: DrawingEntityView, UITextViewDelega super.update(animated: animated) } - private func setup() { - if let file = self.weatherEntity.icon { - self.iconView.isHidden = true - self.addSubnode(self.imageNode) - if let dimensions = file.dimensions { - if file.isAnimatedSticker || file.isVideoSticker || file.mimeType == "video/webm" { - let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 256.0, height: 256.0)) - if self.animationNode == nil { - let animationNode = DefaultAnimatedStickerNodeImpl() - animationNode.autoplay = true - self.animationNode = animationNode - animationNode.started = { [weak self] in - self?.imageNode.isHidden = true - } - animationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: file.resource, isVideo: file.isVideoSticker), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), playbackMode: .loop, mode: .direct(cachePathPrefix: nil)) - - self.addSubnode(animationNode) - } - self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: self.context.account.postbox, userLocation: .other, file: file, small: false, size: fittedDimensions)) - self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: self.context.account, userLocation: .other, fileReference: stickerPackFileReference(file), resource: file.resource).start()) - } else { - if let animationNode = self.animationNode { - animationNode.visibility = false - self.animationNode = nil - animationNode.removeFromSupernode() - self.imageNode.isHidden = false - self.didSetUpAnimationNode = false - } - self.imageNode.setSignal(chatMessageSticker(account: self.context.account, userLocation: .other, file: file, small: false, synchronousLoad: false)) - self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: self.context.account, userLocation: .other, fileReference: stickerPackFileReference(file), resource: chatMessageStickerResource(file: file, small: false)).start()) - } - self.setNeedsLayout() - } - } - } - override func updateSelectionView() { guard let selectionView = self.selectionView as? DrawingWeatherEntitySelectionView else { return diff --git a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift index 843c567bcc..74529be6e7 100644 --- a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift @@ -86,6 +86,7 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { let peers: [EnginePeer.Id: EnginePeer] let stateUpdated: (ComponentTransition) -> Void let buy: (StarsProduct) -> Void + let openAppExamples: () -> Void init( context: AccountContext, @@ -100,7 +101,8 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { expanded: Bool, peers: [EnginePeer.Id: EnginePeer], stateUpdated: @escaping (ComponentTransition) -> Void, - buy: @escaping (StarsProduct) -> Void + buy: @escaping (StarsProduct) -> Void, + openAppExamples: @escaping () -> Void ) { self.context = context self.externalState = externalState @@ -115,6 +117,7 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { self.peers = peers self.stateUpdated = stateUpdated self.buy = buy + self.openAppExamples = openAppExamples } static func ==(lhs: StarsPurchaseScreenContentComponent, rhs: StarsPurchaseScreenContentComponent) -> Bool { @@ -225,15 +228,16 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: accentColor)!, theme) } - let titleAttributedString = parseMarkdownIntoAttributedString(textString, attributes: markdownAttributes).mutableCopy() as! NSMutableAttributedString + let textAttributedString = parseMarkdownIntoAttributedString(textString, attributes: markdownAttributes).mutableCopy() as! NSMutableAttributedString - if let range = titleAttributedString.string.range(of: ">"), let chevronImage = state.cachedChevronImage?.0 { - titleAttributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: titleAttributedString.string)) + if let range = textAttributedString.string.range(of: ">"), let chevronImage = state.cachedChevronImage?.0 { + textAttributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: textAttributedString.string)) } + let openAppExamples = component.openAppExamples let text = text.update( component: BalancedTextComponent( - text: .plain(titleAttributedString), + text: .plain(textAttributedString), horizontalAlignment: .center, maximumNumberOfLines: 0, lineSpacing: 0.2, @@ -246,6 +250,7 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { } }, tapAction: { _, _ in + openAppExamples() } ), environment: {}, @@ -473,6 +478,7 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { let options: [Any] let purpose: StarsPurchasePurpose let forceDark: Bool + let openAppExamples: () -> Void let updateInProgress: (Bool) -> Void let present: (ViewController) -> Void let completion: (Int64) -> Void @@ -483,6 +489,7 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { options: [Any], purpose: StarsPurchasePurpose, forceDark: Bool, + openAppExamples: @escaping () -> Void, updateInProgress: @escaping (Bool) -> Void, present: @escaping (ViewController) -> Void, completion: @escaping (Int64) -> Void @@ -492,6 +499,7 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { self.options = options self.purpose = purpose self.forceDark = forceDark + self.openAppExamples = openAppExamples self.updateInProgress = updateInProgress self.present = present self.completion = completion @@ -872,7 +880,8 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { }, buy: { [weak state] product in state?.buy(product: product) - } + }, + openAppExamples: context.component.openAppExamples )), contentInsets: UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: environment.safeInsets.bottom, right: 0.0), contentOffsetUpdated: { [weak state] topContentOffset, bottomContentOffset in @@ -985,6 +994,7 @@ public final class StarsPurchaseScreen: ViewControllerComponentContainer { self.context = context self.starsContext = starsContext + var openAppExamplesImpl: (() -> Void)? var updateInProgressImpl: ((Bool) -> Void)? var presentImpl: ((ViewController) -> Void)? var completionImpl: ((Int64) -> Void)? @@ -994,6 +1004,9 @@ public final class StarsPurchaseScreen: ViewControllerComponentContainer { options: options, purpose: purpose, forceDark: false, + openAppExamples: { + openAppExamplesImpl?() + }, updateInProgress: { inProgress in updateInProgressImpl?(inProgress) }, @@ -1011,6 +1024,19 @@ public final class StarsPurchaseScreen: ViewControllerComponentContainer { self.navigationItem.setLeftBarButton(cancelItem, animated: false) self.navigationPresentation = .modal + openAppExamplesImpl = { [weak self] in + guard let self else { + return + } + let _ = (context.sharedContext.makeMiniAppListScreenInitialData(context: context) + |> deliverOnMainQueue).startStandalone(next: { [weak self] initialData in + guard let self, let navigationController = self.navigationController as? NavigationController else { + return + } + navigationController.pushViewController(context.sharedContext.makeMiniAppListScreen(context: context, initialData: initialData)) + }) + } + updateInProgressImpl = { [weak self] inProgress in if let strongSelf = self { strongSelf.navigationItem.leftBarButtonItem?.isEnabled = !inProgress diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/BUILD b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/BUILD index 66f7f31423..62a8ef68a8 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/BUILD +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/BUILD @@ -34,6 +34,7 @@ swift_library( "//submodules/TelegramUI/Components/Stars/StarsImageComponent", "//submodules/TelegramUI/Components/Stars/StarsAvatarComponent", "//submodules/GalleryUI", + "//submodules/TelegramUI/Components/MiniAppListScreen", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift index 21f05af42e..ee53a897b4 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift @@ -23,6 +23,7 @@ import UndoUI import StarsImageComponent import GalleryUI import StarsAvatarComponent +import MiniAppListScreen private final class StarsTransactionSheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -34,6 +35,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { let openPeer: (EnginePeer) -> Void let openMessage: (EngineMessage.Id) -> Void let openMedia: ([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void + let openAppExamples: () -> Void let copyTransactionId: (String) -> Void init( @@ -44,6 +46,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { openPeer: @escaping (EnginePeer) -> Void, openMessage: @escaping (EngineMessage.Id) -> Void, openMedia: @escaping ([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void, + openAppExamples: @escaping () -> Void, copyTransactionId: @escaping (String) -> Void ) { self.context = context @@ -53,6 +56,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { self.openPeer = openPeer self.openMessage = openMessage self.openMedia = openMedia + self.openAppExamples = openAppExamples self.copyTransactionId = copyTransactionId } @@ -345,6 +349,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { photo = nil isRefund = false isGift = true + delayedCloseOnOpenPeer = false } if let spaceRegex { let nsRange = NSRange(descriptionText.startIndex..., in: descriptionText) @@ -647,6 +652,8 @@ private final class StarsTransactionSheetContent: CombinedComponent { var descriptionSize: CGSize = .zero if !descriptionText.isEmpty { + let openAppExamples = component.openAppExamples + if state.cachedChevronImage == nil || state.cachedChevronImage?.1 !== environment.theme { state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: linkColor)!, theme) } @@ -665,7 +672,18 @@ private final class StarsTransactionSheetContent: CombinedComponent { text: .plain(attributedString), horizontalAlignment: .center, maximumNumberOfLines: 5, - lineSpacing: 0.2 + lineSpacing: 0.2, + highlightColor: linkColor.withAlphaComponent(0.2), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + } else { + return nil + } + }, + tapAction: { _, _ in + openAppExamples() + } ), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude), transition: .immediate @@ -765,6 +783,7 @@ private final class StarsTransactionSheetComponent: CombinedComponent { let openPeer: (EnginePeer) -> Void let openMessage: (EngineMessage.Id) -> Void let openMedia: ([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void + let openAppExamples: () -> Void let copyTransactionId: (String) -> Void init( @@ -774,6 +793,7 @@ private final class StarsTransactionSheetComponent: CombinedComponent { openPeer: @escaping (EnginePeer) -> Void, openMessage: @escaping (EngineMessage.Id) -> Void, openMedia: @escaping ([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void, + openAppExamples: @escaping () -> Void, copyTransactionId: @escaping (String) -> Void ) { self.context = context @@ -782,6 +802,7 @@ private final class StarsTransactionSheetComponent: CombinedComponent { self.openPeer = openPeer self.openMessage = openMessage self.openMedia = openMedia + self.openAppExamples = openAppExamples self.copyTransactionId = copyTransactionId } @@ -826,6 +847,7 @@ private final class StarsTransactionSheetComponent: CombinedComponent { openPeer: context.component.openPeer, openMessage: context.component.openMessage, openMedia: context.component.openMedia, + openAppExamples: context.component.openAppExamples, copyTransactionId: context.component.copyTransactionId )), backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), @@ -915,6 +937,7 @@ public class StarsTransactionScreen: ViewControllerComponentContainer { var openPeerImpl: ((EnginePeer) -> Void)? var openMessageImpl: ((EngineMessage.Id) -> Void)? var openMediaImpl: (([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void)? + var openAppExamplesImpl: (() -> Void)? var copyTransactionIdImpl: ((String) -> Void)? super.init( context: context, @@ -931,6 +954,9 @@ public class StarsTransactionScreen: ViewControllerComponentContainer { openMedia: { media, transitionNode, addToTransitionSurface in openMediaImpl?(media, transitionNode, addToTransitionSurface) }, + openAppExamples: { + openAppExamplesImpl?() + }, copyTransactionId: { transactionId in copyTransactionIdImpl?(transactionId) } @@ -1018,6 +1044,19 @@ public class StarsTransactionScreen: ViewControllerComponentContainer { })) } + openAppExamplesImpl = { [weak self] in + guard let self else { + return + } + let _ = (context.sharedContext.makeMiniAppListScreenInitialData(context: context) + |> deliverOnMainQueue).startStandalone(next: { [weak self] initialData in + guard let self, let navigationController = self.navigationController as? NavigationController else { + return + } + navigationController.pushViewController(context.sharedContext.makeMiniAppListScreen(context: context, initialData: initialData)) + }) + } + copyTransactionIdImpl = { [weak self] transactionId in guard let self else { return From b006c36c59ad4b5a387a664283724b0bfa4c788b Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Wed, 24 Jul 2024 10:28:56 +0400 Subject: [PATCH 04/41] Various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 1 + .../BrowserUI/Sources/BrowserContent.swift | 2 +- .../Sources/BrowserDocumentContent.swift | 10 ++-- .../Sources/BrowserInstantPageContent.swift | 52 +++++++++++++++---- .../BrowserUI/Sources/BrowserPdfContent.swift | 10 ++-- .../BrowserUI/Sources/BrowserScreen.swift | 7 ++- .../BrowserUI/Sources/BrowserWebContent.swift | 21 ++++---- .../Sources/InstantPageTheme.swift | 6 ++- .../MediaEditor/Sources/MediaEditor.swift | 10 +++- .../Sources/MediaCoverScreen.swift | 2 +- .../Sources/MediaEditorScreen.swift | 7 ++- .../Sources/MediaScrubberComponent.swift | 6 ++- .../InstantPagePresentationSettings.swift | 2 + 13 files changed, 95 insertions(+), 41 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 5f8a32624e..2993d91058 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -9587,6 +9587,7 @@ Sorry for the inconvenience."; "Story.ContextPrivacy.LabelEveryone" = "Everyone"; "Story.Context.Privacy" = "Who Can See"; "Story.Context.Edit" = "Edit Story"; +"Story.Context.EditCover" = "Edit Cover"; "Story.Context.SaveToProfile" = "Save to Profile"; "Story.Context.RemoveFromProfile" = "Remove from Profile"; "Story.ToastRemovedFromProfileText" = "Story removed from your profile"; diff --git a/submodules/BrowserUI/Sources/BrowserContent.swift b/submodules/BrowserUI/Sources/BrowserContent.swift index cf7eea0583..6b4c94a940 100644 --- a/submodules/BrowserUI/Sources/BrowserContent.swift +++ b/submodules/BrowserUI/Sources/BrowserContent.swift @@ -186,7 +186,7 @@ protocol BrowserContent: UIView { func addToRecentlyVisited() - func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ComponentTransition) + func updateLayout(size: CGSize, insets: UIEdgeInsets, fullInsets: UIEdgeInsets, transition: ComponentTransition) } struct ContentScrollingUpdate { diff --git a/submodules/BrowserUI/Sources/BrowserDocumentContent.swift b/submodules/BrowserUI/Sources/BrowserDocumentContent.swift index 375979e94e..488b3cec8a 100644 --- a/submodules/BrowserUI/Sources/BrowserDocumentContent.swift +++ b/submodules/BrowserUI/Sources/BrowserDocumentContent.swift @@ -100,8 +100,8 @@ final class BrowserDocumentContent: UIView, BrowserContent, WKNavigationDelegate self.backgroundColor = presentationData.theme.list.plainBackgroundColor self.webView.underPageBackgroundColor = presentationData.theme.list.plainBackgroundColor } - if let (size, insets) = self.validLayout { - self.updateLayout(size: size, insets: insets, transition: .immediate) + if let (size, insets, fullInsets) = self.validLayout { + self.updateLayout(size: size, insets: insets, fullInsets: fullInsets, transition: .immediate) } } @@ -239,9 +239,9 @@ final class BrowserDocumentContent: UIView, BrowserContent, WKNavigationDelegate self.webView.scrollView.setContentOffset(CGPoint(x: 0.0, y: -self.webView.scrollView.contentInset.top), animated: true) } - private var validLayout: (CGSize, UIEdgeInsets)? - func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ComponentTransition) { - self.validLayout = (size, insets) + private var validLayout: (CGSize, UIEdgeInsets, UIEdgeInsets)? + func updateLayout(size: CGSize, insets: UIEdgeInsets, fullInsets: UIEdgeInsets, transition: ComponentTransition) { + self.validLayout = (size, insets, fullInsets) self.previousScrollingOffset = ScrollingOffsetState(value: self.webView.scrollView.contentOffset.y, isDraggingOrDecelerating: self.webView.scrollView.isDragging || self.webView.scrollView.isDecelerating) diff --git a/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift b/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift index b8da562cf6..497c6b074c 100644 --- a/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift +++ b/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift @@ -26,6 +26,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg private let context: AccountContext private var presentationData: PresentationData private var theme: InstantPageTheme + private var settings: InstantPagePresentationSettings = .defaultSettings private let sourceLocation: InstantPageSourceLocation private var webPage: TelegramMediaWebpage? @@ -85,7 +86,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg private let loadProgress = ValuePromise(1.0, ignoreRepeated: true) private let readingProgress = ValuePromise(1.0, ignoreRepeated: true) - private var containerLayout: (size: CGSize, insets: UIEdgeInsets)? + private var containerLayout: (size: CGSize, insets: UIEdgeInsets, fullInsets: UIEdgeInsets)? private var setupScrollOffsetOnLayout = false init(context: AccountContext, presentationData: PresentationData, webPage: TelegramMediaWebpage, anchor: String?, url: String, sourceLocation: InstantPageSourceLocation) { @@ -175,8 +176,9 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg func updatePresentationData(_ presentationData: PresentationData) { self.presentationData = presentationData - self.theme = instantPageThemeForType(presentationData.theme.overallDarkAppearance ? .dark : .light, settings: .defaultSettings) + self.theme = instantPageThemeForType(presentationData.theme.overallDarkAppearance ? .dark : .light, settings: self.settings) self.updatePageLayout() + self.updateVisibleItems(visibleBounds: self.scrollNode.view.bounds) } func tapActionAtPoint(_ point: CGPoint) -> TapLongTapOrDoubleTapGestureRecognizerAction { @@ -295,10 +297,10 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg } private func requestLayout(transition: ContainedViewLayoutTransition) { - guard let (size, insets) = self.containerLayout else { + guard let (size, insets, fullInsets) = self.containerLayout else { return } - self.updateLayout(size: size, insets: insets, transition: transition) + self.updateLayout(size: size, insets: insets, fullInsets: fullInsets, transition: transition) } func reload() { @@ -319,8 +321,40 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg } + var currentFontState = BrowserPresentationState.FontState(size: 100, isSerif: false) func updateFontState(_ state: BrowserPresentationState.FontState) { + self.currentFontState = state + let fontSize: InstantPagePresentationFontSize + switch state.size { + case 50: + fontSize = .xxsmall + case 75: + fontSize = .xsmall + case 85: + fontSize = .small + case 100: + fontSize = .standard + case 115: + fontSize = .large + case 125: + fontSize = .xlarge + case 150: + fontSize = .xxlarge + default: + fontSize = .standard + } + + self.settings = InstantPagePresentationSettings( + themeType: self.presentationData.theme.overallDarkAppearance ? .dark : .light, + fontSize: fontSize, + forceSerif: state.isSerif, + autoNightMode: false, + ignoreAutoNightModeUntil: 0 + ) + self.theme = instantPageThemeForType(self.presentationData.theme.overallDarkAppearance ? .dark : .light, settings: self.settings) + self.updatePageLayout() + self.updateVisibleItems(visibleBounds: self.scrollNode.view.bounds) } func setSearch(_ query: String?, completion: ((Int) -> Void)?) { @@ -340,12 +374,12 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: true) } - func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ComponentTransition) { - self.updateLayout(size: size, insets: insets, transition: transition.containedViewLayoutTransition) + func updateLayout(size: CGSize, insets: UIEdgeInsets, fullInsets: UIEdgeInsets, transition: ComponentTransition) { + self.updateLayout(size: size, insets: insets, fullInsets: fullInsets, transition: transition.containedViewLayoutTransition) } - func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ContainedViewLayoutTransition) { - self.containerLayout = (size, insets) + func updateLayout(size: CGSize, insets: UIEdgeInsets, fullInsets: UIEdgeInsets, transition: ContainedViewLayoutTransition) { + self.containerLayout = (size, insets, fullInsets) var updateVisibleItems = false let resetContentOffset = self.scrollNode.bounds.size.width.isZero || self.setupScrollOffsetOnLayout || !(self.initialAnchor ?? "").isEmpty @@ -401,7 +435,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg } private func updatePageLayout() { - guard let (size, insets) = self.containerLayout, let webPage = self.webPage else { + guard let (size, insets, _) = self.containerLayout, let webPage = self.webPage else { return } diff --git a/submodules/BrowserUI/Sources/BrowserPdfContent.swift b/submodules/BrowserUI/Sources/BrowserPdfContent.swift index 470b350ae9..4e03d954ac 100644 --- a/submodules/BrowserUI/Sources/BrowserPdfContent.swift +++ b/submodules/BrowserUI/Sources/BrowserPdfContent.swift @@ -109,8 +109,8 @@ final class BrowserPdfContent: UIView, BrowserContent, WKNavigationDelegate, WKU if #available(iOS 15.0, *) { self.backgroundColor = presentationData.theme.list.plainBackgroundColor } - if let (size, insets) = self.validLayout { - self.updateLayout(size: size, insets: insets, transition: .immediate) + if let (size, insets, fullInsets) = self.validLayout { + self.updateLayout(size: size, insets: insets, fullInsets: fullInsets, transition: .immediate) } } @@ -248,9 +248,9 @@ final class BrowserPdfContent: UIView, BrowserContent, WKNavigationDelegate, WKU self.scrollView.setContentOffset(CGPoint(x: 0.0, y: -self.scrollView.contentInset.top), animated: true) } - private var validLayout: (CGSize, UIEdgeInsets)? - func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ComponentTransition) { - self.validLayout = (size, insets) + private var validLayout: (CGSize, UIEdgeInsets, UIEdgeInsets)? + func updateLayout(size: CGSize, insets: UIEdgeInsets, fullInsets: UIEdgeInsets, transition: ComponentTransition) { + self.validLayout = (size, insets, fullInsets) self.previousScrollingOffset = ScrollingOffsetState(value: self.scrollView.contentOffset.y, isDraggingOrDecelerating: self.scrollView.isDragging || self.scrollView.isDecelerating) diff --git a/submodules/BrowserUI/Sources/BrowserScreen.swift b/submodules/BrowserUI/Sources/BrowserScreen.swift index 1899dcc14f..39dff6a3f4 100644 --- a/submodules/BrowserUI/Sources/BrowserScreen.swift +++ b/submodules/BrowserUI/Sources/BrowserScreen.swift @@ -908,7 +908,7 @@ public class BrowserScreen: ViewController, MinimizableController { action(.default) }))) } - if [.webPage, .instantPage].contains(contentState.contentType) { + if [.webPage].contains(contentState.contentType) { items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.InstantPage_Search, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Instant View/Settings/Search"), color: theme.contextMenu.primaryColor) }, action: { (controller, action) in performAction.invoke(.updateSearchActive(true)) action(.default) @@ -1311,7 +1311,10 @@ private final class BrowserContentComponent: Component { let collapsedHeight: CGFloat = 24.0 let topInset: CGFloat = component.insets.top + component.navigationBarHeight * (1.0 - component.scrollingPanelOffsetFraction) + collapsedHeight * component.scrollingPanelOffsetFraction let bottomInset = (49.0 + component.insets.bottom) * (1.0 - component.scrollingPanelOffsetFraction) - component.content.updateLayout(size: availableSize, insets: UIEdgeInsets(top: topInset, left: component.insets.left, bottom: bottomInset, right: component.insets.right), transition: transition) + let insets = UIEdgeInsets(top: topInset, left: component.insets.left, bottom: bottomInset, right: component.insets.right) + let fullInsets = UIEdgeInsets(top: component.insets.top + component.navigationBarHeight, left: component.insets.left, bottom: 49.0 + component.insets.bottom, right: component.insets.right) + + component.content.updateLayout(size: availableSize, insets: insets, fullInsets: fullInsets, transition: transition) transition.setFrame(view: component.content, frame: CGRect(origin: .zero, size: availableSize)) return availableSize diff --git a/submodules/BrowserUI/Sources/BrowserWebContent.swift b/submodules/BrowserUI/Sources/BrowserWebContent.swift index e99d97666f..82ff887339 100644 --- a/submodules/BrowserUI/Sources/BrowserWebContent.swift +++ b/submodules/BrowserUI/Sources/BrowserWebContent.swift @@ -244,8 +244,8 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU self.backgroundColor = presentationData.theme.list.plainBackgroundColor self.webView.underPageBackgroundColor = presentationData.theme.list.plainBackgroundColor } - if let (size, insets) = self.validLayout { - self.updateLayout(size: size, insets: insets, transition: .immediate) + if let (size, insets, fullInsets) = self.validLayout { + self.updateLayout(size: size, insets: insets, fullInsets: fullInsets, transition: .immediate) } } @@ -254,6 +254,7 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU func updateFontState(_ state: BrowserPresentationState.FontState) { self.updateFontState(state, force: false) } + func updateFontState(_ state: BrowserPresentationState.FontState, force: Bool) { self.currentFontState = state @@ -432,13 +433,13 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU self.webView.scrollView.setContentOffset(CGPoint(x: 0.0, y: -self.webView.scrollView.contentInset.top), animated: true) } - private var validLayout: (CGSize, UIEdgeInsets)? - func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ComponentTransition) { - self.validLayout = (size, insets) + private var validLayout: (CGSize, UIEdgeInsets, UIEdgeInsets)? + func updateLayout(size: CGSize, insets: UIEdgeInsets, fullInsets: UIEdgeInsets, transition: ComponentTransition) { + self.validLayout = (size, insets, fullInsets) self.previousScrollingOffset = ScrollingOffsetState(value: self.webView.scrollView.contentOffset.y, isDraggingOrDecelerating: self.webView.scrollView.isDragging || self.webView.scrollView.isDecelerating) - let webViewFrame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: CGSize(width: size.width - insets.left - insets.right, height: size.height - insets.top - insets.bottom)) + let webViewFrame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: CGSize(width: size.width - insets.left - insets.right, height: size.height - insets.top - fullInsets.bottom)) var refresh = false if self.webView.frame.width > 0 && webViewFrame.width != self.webView.frame.width { refresh = true @@ -575,8 +576,8 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU } else { self.currentError = nil } - if let (size, insets) = self.validLayout { - self.updateLayout(size: size, insets: insets, transition: .immediate) + if let (size, insets, fullInsets) = self.validLayout { + self.updateLayout(size: size, insets: insets, fullInsets: fullInsets, transition: .immediate) } } @@ -586,8 +587,8 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU } else { self.currentError = nil } - if let (size, insets) = self.validLayout { - self.updateLayout(size: size, insets: insets, transition: .immediate) + if let (size, insets, fullInsets) = self.validLayout { + self.updateLayout(size: size, insets: insets, fullInsets: fullInsets, transition: .immediate) } } diff --git a/submodules/InstantPageUI/Sources/InstantPageTheme.swift b/submodules/InstantPageUI/Sources/InstantPageTheme.swift index 6a3e013311..3706d23bc5 100644 --- a/submodules/InstantPageUI/Sources/InstantPageTheme.swift +++ b/submodules/InstantPageUI/Sources/InstantPageTheme.swift @@ -264,6 +264,10 @@ private let darkTheme = InstantPageTheme( private func fontSizeMultiplierForVariant(_ variant: InstantPagePresentationFontSize) -> CGFloat { switch variant { + case .xxsmall: + return 0.5 + case .xsmall: + return 0.75 case .small: return 0.85 case .standard: @@ -271,7 +275,7 @@ private func fontSizeMultiplierForVariant(_ variant: InstantPagePresentationFont case .large: return 1.15 case .xlarge: - return 1.3 + return 1.25 case .xxlarge: return 1.5 } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift index 0f4bc63cf2..7480b59332 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift @@ -1508,7 +1508,15 @@ public final class MediaEditor { public func setVideoTrimRange(_ trimRange: Range, apply: Bool) { self.updateValues(mode: .skipRendering) { values in - return values.withUpdatedVideoTrimRange(trimRange) + var updatedValues = values.withUpdatedVideoTrimRange(trimRange) + if let coverImageTimestamp = updatedValues.coverImageTimestamp { + if coverImageTimestamp < trimRange.lowerBound { + updatedValues = updatedValues.withUpdatedCoverImageTimestamp(trimRange.lowerBound) + } else if coverImageTimestamp > trimRange.upperBound { + updatedValues = updatedValues.withUpdatedCoverImageTimestamp(trimRange.upperBound) + } + } + return updatedValues } if apply { diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCoverScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCoverScreen.swift index ac4ccd2601..70f32bbce4 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCoverScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCoverScreen.swift @@ -565,7 +565,7 @@ final class MediaCoverScreen: ViewController { if let coverImageTimestamp = mediaEditor.values.coverImageTimestamp { mediaEditor.seek(coverImageTimestamp, andPlay: false) } else { - mediaEditor.seek(0.0, andPlay: false) + mediaEditor.seek(mediaEditor.values.videoTrimRange?.lowerBound ?? 0.0, andPlay: false) } } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 27a8139900..be8a51cecc 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -4747,7 +4747,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.controller?.currentCoverImage = image } } - self.controller?.present(coverController, in: .window(.root)) + self.controller?.present(coverController, in: .current) self.coverScreen = coverController self.animateOutToTool(tool: .cover) } @@ -5808,6 +5808,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate return } mediaEditor.maybePauseVideo() + mediaEditor.seek(mediaEditor.values.videoTrimRange?.lowerBound ?? 0.0, andPlay: false) let privacy = privacy ?? self.state.privacy @@ -5916,9 +5917,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate editCoverImpl = { [weak self, weak controller] in if let self { - Queue.mainQueue().after(0.25, { - self.node.openCoverSelection() - }) + self.node.openCoverSelection() } if let controller { controller.dismiss() diff --git a/submodules/TelegramUI/Components/MediaScrubberComponent/Sources/MediaScrubberComponent.swift b/submodules/TelegramUI/Components/MediaScrubberComponent/Sources/MediaScrubberComponent.swift index aa47178efe..7c955ae8a1 100644 --- a/submodules/TelegramUI/Components/MediaScrubberComponent/Sources/MediaScrubberComponent.swift +++ b/submodules/TelegramUI/Components/MediaScrubberComponent/Sources/MediaScrubberComponent.swift @@ -665,7 +665,7 @@ public final class MediaScrubberComponent: Component { transition.setFrame(view: self.ghostTrimView, frame: ghostTrimViewFrame) transition.setAlpha(view: self.ghostTrimView, alpha: ghostTrimVisible ? 0.75 : 0.0) - if case .videoMessage = component.style { + if [.videoMessage, .cover].contains(component.style) { for (_ , trackView) in self.trackViews { trackView.updateOpaqueEdges( left: leftHandleFrame.minX, @@ -803,7 +803,6 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega self.scrollView.delegate = self - self.videoTransparentFramesContainer.alpha = 0.5 self.videoTransparentFramesContainer.clipsToBounds = true self.videoTransparentFramesContainer.isUserInteractionEnabled = false @@ -935,13 +934,16 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega case .editor, .cover: fullTrackHeight = trackHeight framesCornerRadius = 9.0 + self.videoTransparentFramesContainer.alpha = 0.35 case .videoMessage: fullTrackHeight = 33.0 framesCornerRadius = fullTrackHeight / 2.0 + self.videoTransparentFramesContainer.alpha = 0.5 } self.videoTransparentFramesContainer.layer.cornerRadius = framesCornerRadius self.videoOpaqueFramesContainer.layer.cornerRadius = framesCornerRadius + let scrubberSize = CGSize(width: availableSize.width, height: isSelected ? fullTrackHeight : collapsedTrackHeight) var screenSpanDuration = duration diff --git a/submodules/TelegramUIPreferences/Sources/InstantPagePresentationSettings.swift b/submodules/TelegramUIPreferences/Sources/InstantPagePresentationSettings.swift index 26a1ddba44..420dca7e11 100644 --- a/submodules/TelegramUIPreferences/Sources/InstantPagePresentationSettings.swift +++ b/submodules/TelegramUIPreferences/Sources/InstantPagePresentationSettings.swift @@ -11,6 +11,8 @@ public enum InstantPageThemeType: Int32 { } public enum InstantPagePresentationFontSize: Int32 { + case xxsmall = -2 + case xsmall = -1 case small = 0 case standard = 1 case large = 2 From 8120dde68c27f009bb2e8e25d47aa5d0b810a0da Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 25 Jul 2024 00:54:36 +0200 Subject: [PATCH 05/41] Various improvements --- .../Sources/AccountContext.swift | 2 +- .../Sources/AttachmentController.swift | 9 ++ .../BrowserUI/Sources/BrowserContent.swift | 2 + .../Sources/BrowserDocumentContent.swift | 4 + .../Sources/BrowserInstantPageContent.swift | 4 + .../BrowserUI/Sources/BrowserPdfContent.swift | 4 + .../BrowserUI/Sources/BrowserScreen.swift | 46 ++++-- .../BrowserUI/Sources/BrowserWebContent.swift | 35 ++--- .../Sources/EditStories.swift | 2 + .../Sources/MediaCoverScreen.swift | 72 ++++++---- .../Sources/MediaEditorScreen.swift | 132 +++++++++++------- .../MediaEditorScreen/Sources/Weather.swift | 48 ++++--- .../Sources/MinimizedContainer.swift | 70 ++++++---- .../Sources/MinimizedHeaderNode.swift | 6 +- .../Sources/PeerInfoStoryPaneNode.swift | 1 + .../Sources/StickerPickerScreen.swift | 3 +- .../StoryItemSetContainerComponent.swift | 41 +++++- .../TelegramUI/Sources/ChatController.swift | 36 ++++- .../Sources/SharedAccountContext.swift | 2 +- .../Sources/TelegramRootController.swift | 12 +- .../WebUI/Sources/WebAppController.swift | 37 ++++- 21 files changed, 400 insertions(+), 168 deletions(-) diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index f9485be0eb..e8a808b9a0 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -981,7 +981,7 @@ public protocol SharedAccountContext: AnyObject { func makeMediaPickerScreen(context: AccountContext, hasSearch: Bool, completion: @escaping (Any) -> Void) -> ViewController - func makeStoryMediaEditorScreen(context: AccountContext, source: Any?, text: String?, link: String?, completion: @escaping (MediaEditorScreenResult, @escaping (@escaping () -> Void) -> Void) -> Void) -> ViewController + func makeStoryMediaEditorScreen(context: AccountContext, source: Any?, text: String?, link: (url: String, name: String?)?, completion: @escaping (MediaEditorScreenResult, @escaping (@escaping () -> Void) -> Void) -> Void) -> ViewController func makeBotPreviewEditorScreen(context: AccountContext, source: Any?, target: Stories.PendingTarget, transitionArguments: (UIView, CGRect, UIImage?)?, transitionOut: @escaping () -> BotPreviewEditorTransitionOut?, externalState: MediaEditorTransitionOutExternalState, completion: @escaping (MediaEditorScreenResult, @escaping (@escaping () -> Void) -> Void) -> Void, cancelled: @escaping () -> Void) -> ViewController diff --git a/submodules/AttachmentUI/Sources/AttachmentController.swift b/submodules/AttachmentUI/Sources/AttachmentController.swift index 68179ddca0..9841bfd570 100644 --- a/submodules/AttachmentUI/Sources/AttachmentController.swift +++ b/submodules/AttachmentUI/Sources/AttachmentController.swift @@ -1365,4 +1365,13 @@ public class AttachmentController: ViewController, MinimizableController { }) return disposableSet } + + public func makeContentSnapshotView() -> UIView? { + let snapshotView = self.view.snapshotView(afterScreenUpdates: false) + if let contentSnapshotView = self.mainController.makeContentSnapshotView() { + contentSnapshotView.frame = contentSnapshotView.frame.offsetBy(dx: 0.0, dy: 64.0 + 56.0) + snapshotView?.addSubview(contentSnapshotView) + } + return snapshotView + } } diff --git a/submodules/BrowserUI/Sources/BrowserContent.swift b/submodules/BrowserUI/Sources/BrowserContent.swift index 6b4c94a940..7096b83042 100644 --- a/submodules/BrowserUI/Sources/BrowserContent.swift +++ b/submodules/BrowserUI/Sources/BrowserContent.swift @@ -187,6 +187,8 @@ protocol BrowserContent: UIView { func addToRecentlyVisited() func updateLayout(size: CGSize, insets: UIEdgeInsets, fullInsets: UIEdgeInsets, transition: ComponentTransition) + + func makeContentSnapshotView() -> UIView? } struct ContentScrollingUpdate { diff --git a/submodules/BrowserUI/Sources/BrowserDocumentContent.swift b/submodules/BrowserUI/Sources/BrowserDocumentContent.swift index 488b3cec8a..26b1d11e7f 100644 --- a/submodules/BrowserUI/Sources/BrowserDocumentContent.swift +++ b/submodules/BrowserUI/Sources/BrowserDocumentContent.swift @@ -468,4 +468,8 @@ final class BrowserDocumentContent: UIView, BrowserContent, WKNavigationDelegate func addToRecentlyVisited() { } + + func makeContentSnapshotView() -> UIView? { + return nil + } } diff --git a/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift b/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift index 497c6b074c..29bed73a96 100644 --- a/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift +++ b/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift @@ -1418,4 +1418,8 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg let _ = addRecentlyVisitedLink(engine: self.context.engine, webPage: webPage).startStandalone() } } + + func makeContentSnapshotView() -> UIView? { + return nil + } } diff --git a/submodules/BrowserUI/Sources/BrowserPdfContent.swift b/submodules/BrowserUI/Sources/BrowserPdfContent.swift index 4e03d954ac..3bd36c1cf6 100644 --- a/submodules/BrowserUI/Sources/BrowserPdfContent.swift +++ b/submodules/BrowserUI/Sources/BrowserPdfContent.swift @@ -460,4 +460,8 @@ final class BrowserPdfContent: UIView, BrowserContent, WKNavigationDelegate, WKU func addToRecentlyVisited() { } + + func makeContentSnapshotView() -> UIView? { + return nil + } } diff --git a/submodules/BrowserUI/Sources/BrowserScreen.swift b/submodules/BrowserUI/Sources/BrowserScreen.swift index 39dff6a3f4..daaad7f05c 100644 --- a/submodules/BrowserUI/Sources/BrowserScreen.swift +++ b/submodules/BrowserUI/Sources/BrowserScreen.swift @@ -1170,16 +1170,18 @@ public class BrowserScreen: ViewController, MinimizableController { var openPreviousOnClose = false + private var validLayout: ContainerViewLayout? + public static let supportedDocumentMimeTypes: [String] = [ - "text/plain", - "text/rtf", - "application/pdf", - "application/msword", - "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "application/vnd.ms-excel", - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - "application/vnd.openxmlformats-officedocument.spreadsheetml.template", - "application/vnd.openxmlformats-officedocument.presentationml.presentation" +// "text/plain", +// "text/rtf", +// "application/pdf", +// "application/msword", +// "application/vnd.openxmlformats-officedocument.wordprocessingml.document", +// "application/vnd.ms-excel", +// "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", +// "application/vnd.openxmlformats-officedocument.spreadsheetml.template", +// "application/vnd.openxmlformats-officedocument.presentationml.presentation" ] public init(context: AccountContext, subject: Subject) { @@ -1212,6 +1214,8 @@ public class BrowserScreen: ViewController, MinimizableController { } override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + self.validLayout = layout + super.containerLayoutUpdated(layout, transition: transition) self.node.containerLayoutUpdated(layout: layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.height, transition: ComponentTransition(transition)) @@ -1221,7 +1225,15 @@ public class BrowserScreen: ViewController, MinimizableController { self.node.minimize(topEdgeOffset: topEdgeOffset, damping: 180.0, initialVelocity: initialVelocity) } - public var isMinimized = false + public var isMinimized = false { + didSet { + if let webContent = self.node.content.last as? BrowserWebContent { + if !self.isMinimized { + webContent.webView.setNeedsLayout() + } + } + } + } public var isMinimizable = true public var minimizedIcon: UIImage? { @@ -1244,6 +1256,20 @@ public class BrowserScreen: ViewController, MinimizableController { } return nil } + + public func makeContentSnapshotView() -> UIView? { + if let contentSnapshot = self.node.content.last?.makeContentSnapshotView(), let layout = self.validLayout { + if let wrapperView = self.view.snapshotView(afterScreenUpdates: false) { + contentSnapshot.frame = contentSnapshot.frame.offsetBy(dx: 0.0, dy: self.navigationLayout(layout: layout).navigationFrame.height) + wrapperView.addSubview(contentSnapshot) + return wrapperView + } else { + return contentSnapshot + } + } else { + return self.view.snapshotView(afterScreenUpdates: false) + } + } } private final class BrowserReferenceContentSource: ContextReferenceContentSource { diff --git a/submodules/BrowserUI/Sources/BrowserWebContent.swift b/submodules/BrowserUI/Sources/BrowserWebContent.swift index 82ff887339..ba252fbbf7 100644 --- a/submodules/BrowserUI/Sources/BrowserWebContent.swift +++ b/submodules/BrowserUI/Sources/BrowserWebContent.swift @@ -119,7 +119,7 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU private let context: AccountContext private var presentationData: PresentationData - private let webView: WKWebView + let webView: WKWebView private let errorView: ComponentHostView private var currentError: Error? @@ -486,7 +486,9 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU if keyPath == "title" { self.updateState { $0.withUpdatedTitle(self.webView.title ?? "") } } else if keyPath == "URL" { - self.updateState { $0.withUpdatedUrl(self.webView.url?.absoluteString ?? "") } + if let url = self.webView.url { + self.updateState { $0.withUpdatedUrl(url.absoluteString) } + } self.didSetupSearch = false } else if keyPath == "estimatedProgress" { self.updateState { $0.withUpdatedEstimatedProgress(self.webView.estimatedProgress) } @@ -571,7 +573,7 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU } func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { - if (error as NSError).code != -999 { + if [-1003, -1100].contains((error as NSError).code) { self.currentError = error } else { self.currentError = nil @@ -580,18 +582,7 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU self.updateLayout(size: size, insets: insets, fullInsets: fullInsets, transition: .immediate) } } - - func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { - if (error as NSError).code != -999 { - self.currentError = error - } else { - self.currentError = nil - } - if let (size, insets, fullInsets) = self.validLayout { - self.updateLayout(size: size, insets: insets, fullInsets: fullInsets, transition: .immediate) - } - } - + func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? { if navigationAction.targetFrame == nil { if let url = navigationAction.request.url?.absoluteString { @@ -752,7 +743,7 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU 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')) + 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({ @@ -872,6 +863,18 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU func addToRecentlyVisited() { self.addToRecentsWhenReady = true } + + func makeContentSnapshotView() -> UIView? { + let configuration = WKSnapshotConfiguration() + configuration.rect = CGRect(origin: .zero, size: self.webView.frame.size) + + let imageView = UIImageView() + imageView.frame = CGRect(origin: .zero, size: self.webView.frame.size) + self.webView.takeSnapshot(with: configuration, completionHandler: { image, _ in + imageView.image = image + }) + return imageView + } } private final class ErrorComponent: CombinedComponent { diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift index fb7682e02f..5fd4d191d2 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift @@ -16,6 +16,7 @@ public extension MediaEditorScreen { peer: EnginePeer, storyItem: EngineStoryItem, videoPlaybackPosition: Double?, + cover: Bool, repost: Bool, transitionIn: MediaEditorScreen.TransitionIn, transitionOut: MediaEditorScreen.TransitionOut?, @@ -88,6 +89,7 @@ public extension MediaEditorScreen { mode: .storyEditor, subject: subject, isEditing: !repost, + isEditingCover: cover, forwardSource: repost ? (peer, storyItem) : nil, initialCaption: initialCaption, initialPrivacy: initialPrivacy, diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCoverScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCoverScreen.swift index 70f32bbce4..db9363e5bb 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCoverScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCoverScreen.swift @@ -18,11 +18,11 @@ private final class MediaCoverScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext - let mediaEditor: MediaEditor + let mediaEditor: Signal init( context: AccountContext, - mediaEditor: MediaEditor + mediaEditor: Signal ) { self.context = context self.mediaEditor = mediaEditor @@ -57,16 +57,26 @@ private final class MediaCoverScreenComponent: Component { var playerStateDisposable: Disposable? var playerState: MediaEditorPlayerState? - init(mediaEditor: MediaEditor) { + private(set) var mediaEditor: MediaEditor? + + init(mediaEditor: Signal) { super.init() - - self.playerStateDisposable = (mediaEditor.playerState(framesCount: 16) - |> deliverOnMainQueue).start(next: { [weak self] playerState in - if let self { - if self.playerState != playerState { - self.playerState = playerState - self.updated() - } + + let _ = (mediaEditor + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] mediaEditor in + if let self, let mediaEditor { + self.mediaEditor = mediaEditor + + self.playerStateDisposable = (mediaEditor.playerState(framesCount: 16) + |> deliverOnMainQueue).start(next: { [weak self] playerState in + if let self { + if self.playerState != playerState { + self.playerState = playerState + self.updated() + } + } + }) } }) } @@ -258,7 +268,7 @@ private final class MediaCoverScreenComponent: Component { isEnabled: true, displaysProgress: false, action: { [weak controller, weak self] in - if let playerState = self?.state?.playerState, let mediaEditor = self?.component?.mediaEditor, let image = mediaEditor.resultImage { + if let playerState = self?.state?.playerState, let mediaEditor = self?.state?.mediaEditor, let image = mediaEditor.resultImage { mediaEditor.setCoverImageTimestamp(playerState.position) controller?.completed(playerState.position, image) } @@ -318,7 +328,6 @@ private final class MediaCoverScreenComponent: Component { if let playerState = state.playerState { let visibleTracks = playerState.tracks.filter { $0.id == 0 }.map { MediaScrubberComponent.Track($0) } - let mediaEditor = component.mediaEditor let scrubberInset: CGFloat = buttonSideInset let scrubberSize = self.scrubber.update( transition: transition, @@ -333,13 +342,13 @@ private final class MediaCoverScreenComponent: Component { isPlaying: playerState.isPlaying, tracks: visibleTracks, portalView: controller.portalView, - positionUpdated: { [weak mediaEditor] position, apply in - if let mediaEditor { + positionUpdated: { [weak state] position, apply in + if let mediaEditor = state?.mediaEditor { mediaEditor.seek(position, andPlay: false) } }, - coverPositionUpdated: { [weak mediaEditor] position, tap, commit in - if let mediaEditor { + coverPositionUpdated: { [weak state] position, tap, commit in + if let mediaEditor = state?.mediaEditor { if tap { mediaEditor.setOnNextDisplay { commit() @@ -436,7 +445,7 @@ final class MediaCoverScreen: ViewController { } func animateOutToEditor(completion: @escaping () -> Void) { - if let mediaEditor = self.controller?.mediaEditor { + self.controller?.withMediaEditor { mediaEditor in mediaEditor.play() } if let view = self.componentHost.view as? MediaCoverScreenComponent.View { @@ -534,18 +543,26 @@ final class MediaCoverScreen: ViewController { } fileprivate let context: AccountContext - fileprivate let mediaEditor: MediaEditor + fileprivate let mediaEditor: Signal fileprivate let previewView: MediaEditorPreviewView fileprivate let portalView: PortalView + func withMediaEditor(_ f: @escaping (MediaEditor) -> Void) { + let _ = (self.mediaEditor + |> take(1) + |> deliverOnMainQueue).start(next: { mediaEditor in + if let mediaEditor { + f(mediaEditor) + } + }) + } + var completed: (Double, UIImage) -> Void = { _, _ in } var dismissed: () -> Void = {} - private var initialValues: MediaEditorValues - init( context: AccountContext, - mediaEditor: MediaEditor, + mediaEditor: Signal, previewView: MediaEditorPreviewView, portalView: PortalView ) { @@ -553,7 +570,6 @@ final class MediaCoverScreen: ViewController { self.mediaEditor = mediaEditor self.previewView = previewView self.portalView = portalView - self.initialValues = mediaEditor.values.makeCopy() super.init(navigationBarPresentationData: nil) self.navigationPresentation = .flatModal @@ -562,10 +578,12 @@ final class MediaCoverScreen: ViewController { self.statusBar.statusBarStyle = .White - if let coverImageTimestamp = mediaEditor.values.coverImageTimestamp { - mediaEditor.seek(coverImageTimestamp, andPlay: false) - } else { - mediaEditor.seek(mediaEditor.values.videoTrimRange?.lowerBound ?? 0.0, andPlay: false) + self.withMediaEditor { mediaEditor in + if let coverImageTimestamp = mediaEditor.values.coverImageTimestamp { + mediaEditor.seek(coverImageTimestamp, andPlay: false) + } else { + mediaEditor.seek(mediaEditor.values.videoTrimRange?.lowerBound ?? 0.0, andPlay: false) + } } } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index be8a51cecc..2bae267beb 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -732,7 +732,7 @@ final class MediaEditorScreenComponent: Component { transition = transition.withUserData(nextTransitionUserData) } - let isEditingStory = controller.isEditingStory + let isEditingStory = controller.isEditingStory || controller.isEditingStoryCover if self.component == nil { if let initialCaption = controller.initialCaption { self.inputPanelExternalState.initialText = initialCaption @@ -2807,6 +2807,10 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.availableReactions = reactions } }) + + if controller.isEditingStoryCover { + self.openCoverSelection(immediate: true) + } } deinit { @@ -4563,43 +4567,30 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate controller.push(linkController) } - func addInitialLink(_ link: String) { + func addInitialLink(_ link: (url: String, name: String?)) { guard self.context.isPremium else { + Queue.mainQueue().after(0.3) { + let context = self.context + var replaceImpl: ((ViewController) -> Void)? + let demoController = context.sharedContext.makePremiumDemoController(context: context, subject: .stories, forceDark: true, action: { + let controller = context.sharedContext.makePremiumIntroController(context: context, source: .storiesLinks, forceDark: true, dismissed: {}) + replaceImpl?(controller) + }, dismissed: {}) + replaceImpl = { [weak self, weak demoController] c in + demoController?.dismiss(animated: true, completion: { + guard let self else { + return + } + self.controller?.push(c) + }) + } + self.controller?.push(demoController) + } return } - let text = link - var attributes: [MessageAttribute] = [] - attributes.append(TextEntitiesMessageAttribute(entities: [.init(range: 0 ..< (text as NSString).length, type: .Url)])) - -// attributes.append(WebpagePreviewMessageAttribute(leadingPreview: !self.positionBelowText, forceLargeMedia: self.largeMedia, isManuallyAdded: false, isSafe: true)) - - let effectiveMedia: TelegramMediaWebpage? = nil -// if let webpage = self.webpage, case .Loaded = webpage.content { -// effectiveMedia = webpage -// } - - let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(1)) - let message = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: text, attributes: attributes, media: effectiveMedia.flatMap { [$0] } ?? [], peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - - let renderer = DrawingMessageRenderer(context: self.context, messages: [message], parentView: self.view, isLink: true) - renderer.render(completion: { [weak self] renderResult in - guard let self else { - return - } - let result = CreateLinkScreen.Result( - url: link, - name: "", - webpage: effectiveMedia, - positionBelowText: false, - largeMedia: nil, - image: effectiveMedia != nil ? renderResult.dayImage : nil, - nightImage: effectiveMedia != nil ? renderResult.nightImage : nil - ) - - let entity = DrawingLinkEntity(url: result.url, name: result.name, webpage: result.webpage, positionBelowText: result.positionBelowText, largeMedia: result.largeMedia, style: .white) - self.interaction?.insertEntity(entity, position: CGPoint(x: storyDimensions.width / 2.0, y: storyDimensions.width / 3.0 * 4.0), select: false) - }) + let entity = DrawingLinkEntity(url: link.url, name: link.name ?? "", webpage: nil, positionBelowText: false, largeMedia: nil, style: .white) + self.interaction?.insertEntity(entity, position: CGPoint(x: storyDimensions.width / 2.0, y: storyDimensions.width / 3.0 * 4.0), select: false) } func addReaction() { @@ -4643,7 +4634,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate return } let weatherPromise = Promise() - weatherPromise.set(getWeather(context: self.context)) + weatherPromise.set(getWeather(context: self.context, load: true)) self.weatherPromise = weatherPromise let _ = (weatherPromise.get() @@ -4696,7 +4687,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate guard controller.checkCaptionLimit() else { return } - if controller.isEditingStory { + if controller.isEditingStory || controller.isEditingStoryCover { controller.requestStoryCompletion(animated: true) } else { if controller.checkIfCompletionIsAllowed() { @@ -4713,11 +4704,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } } - func openCoverSelection() { - guard let mediaEditor = self.mediaEditor else { - return - } - + func openCoverSelection(immediate: Bool) { guard let portalView = PortalView(matchPosition: false) else { return } @@ -4732,9 +4719,9 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } let coverController = MediaCoverScreen( context: self.context, - mediaEditor: mediaEditor, + mediaEditor: self.mediaEditorPromise.get(), previewView: self.previewView, - portalView: portalView + portalView: portalView ) coverController.dismissed = { [weak self] in if let self { @@ -4749,7 +4736,13 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } self.controller?.present(coverController, in: .current) self.coverScreen = coverController - self.animateOutToTool(tool: .cover) + + if immediate { + self.isDisplayingTool = .cover + self.requestUpdate(transition: .immediate) + } else { + self.animateOutToTool(tool: .cover) + } } func updateModalTransitionFactor(_ value: CGFloat, transition: ContainedViewLayoutTransition) { @@ -4933,6 +4926,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } } + let editorConfiguration = MediaEditorConfiguration.with(appConfiguration: self.context.currentAppConfiguration.with { $0 }) + var weatherSignal: Signal if hasInteractiveStickers { let weatherPromise: Promise @@ -4940,7 +4935,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate weatherPromise = current } else { weatherPromise = Promise() - weatherPromise.set(getWeather(context: self.context)) + weatherPromise.set(getWeather(context: self.context, load: editorConfiguration.preloadWeather)) self.weatherPromise = weatherPromise } weatherSignal = weatherPromise.get() @@ -5028,6 +5023,14 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate switch result { case let .loaded(weather): self.addWeather(weather) + case .notPreloaded: + weatherPromise.set(getWeather(context: self.context, load: true)) + let _ = (weatherPromise.get() + |> take(1)).start(next: { [weak self] result in + if let self, case let .loaded(weather) = result { + self.addWeather(weather) + } + }) case .notDetermined, .notAllowed: self.presentLocationAccessAlert() default: @@ -5223,7 +5226,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.controller?.present(controller, in: .window(.root)) self.animateOutToTool(tool: .tools) case .cover: - self.openCoverSelection() + self.openCoverSelection(immediate: false) } } }, @@ -5571,6 +5574,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate let mode: Mode let subject: Signal let isEditingStory: Bool + let isEditingStoryCover: Bool fileprivate let customTarget: EnginePeer.Id? let forwardSource: (EnginePeer, EngineStoryItem)? @@ -5578,7 +5582,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate fileprivate let initialPrivacy: EngineStoryPrivacy? fileprivate let initialMediaAreas: [MediaArea]? fileprivate let initialVideoPosition: Double? - fileprivate let initialLink: String? + fileprivate let initialLink: (url: String, name: String?)? fileprivate let transitionIn: TransitionIn? fileprivate let transitionOut: (Bool, Bool?) -> TransitionOut? @@ -5608,12 +5612,13 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate subject: Signal, customTarget: EnginePeer.Id? = nil, isEditing: Bool = false, + isEditingCover: Bool = false, forwardSource: (EnginePeer, EngineStoryItem)? = nil, initialCaption: NSAttributedString? = nil, initialPrivacy: EngineStoryPrivacy? = nil, initialMediaAreas: [MediaArea]? = nil, initialVideoPosition: Double? = nil, - initialLink: String? = nil, + initialLink: (url: String, name: String?)? = nil, transitionIn: TransitionIn?, transitionOut: @escaping (Bool, Bool?) -> TransitionOut?, completion: @escaping (MediaEditorScreen.Result, @escaping (@escaping () -> Void) -> Void) -> Void @@ -5623,6 +5628,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.subject = subject self.customTarget = customTarget self.isEditingStory = isEditing + self.isEditingStoryCover = isEditingCover self.forwardSource = forwardSource self.initialCaption = initialCaption self.initialPrivacy = initialPrivacy @@ -5799,7 +5805,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } fileprivate var isEmbeddedEditor: Bool { - return self.isEditingStory || self.forwardSource != nil + return self.isEditingStory || self.isEditingStoryCover || self.forwardSource != nil } private var currentCoverImage: UIImage? @@ -5917,7 +5923,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate editCoverImpl = { [weak self, weak controller] in if let self { - self.node.openCoverSelection() + self.node.openCoverSelection(immediate: false) } if let controller { controller.dismiss() @@ -6478,7 +6484,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate return } - if !self.isEditingStory { + if !(self.isEditingStory || self.isEditingStoryCover) { let privacy = self.state.privacy let _ = updateMediaEditorStoredStateInteractively(engine: self.context.engine, { current in if let current { @@ -8283,3 +8289,27 @@ private func stickerFile(resource: TelegramMediaResource, thumbnailResource: Tel return TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: isVideo ? "video/webm" : "image/webp", size: size, attributes: fileAttributes) } + +private struct MediaEditorConfiguration { + static var defaultValue: MediaEditorConfiguration { + return MediaEditorConfiguration(preloadWeather: true) + } + + let preloadWeather: Bool + + fileprivate init(preloadWeather: Bool) { + self.preloadWeather = preloadWeather + } + + static func with(appConfiguration: AppConfiguration) -> MediaEditorConfiguration { + if let data = appConfiguration.data { + var preloadWeather = false + if let value = data["story_weather_preload"] as? Bool { + preloadWeather = value + } + return MediaEditorConfiguration(preloadWeather: preloadWeather) + } else { + return .defaultValue + } + } +} diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/Weather.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/Weather.swift index 26b43419a5..bc9b2475cc 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/Weather.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/Weather.swift @@ -47,7 +47,7 @@ private func getWeatherData(context: AccountContext, location: CLLocationCoordin } } -func getWeather(context: AccountContext) -> Signal { +func getWeather(context: AccountContext, load: Bool) -> Signal { guard let locationManager = context.sharedContext.locationManager else { return .single(.none) } @@ -60,33 +60,37 @@ func getWeather(context: AccountContext) -> Signal then( - currentLocationManagerCoordinate(manager: locationManager, timeout: 5.0) - |> mapToSignal { location in - if let location { - return getWeatherData(context: context, location: location) - |> mapToSignal { weather in - if let weather { - let effectiveEmoji = emojiFor(for: weather.emoji.strippedEmoji, date: Date(), location: location) - if let match = context.animatedEmojiStickersValue[effectiveEmoji]?.first { - return .single(.loaded(StickerPickerScreen.Weather.LoadedWeather( - emoji: effectiveEmoji, - emojiFile: match.file, - temperature: weather.temperature - ))) + if load { + return .single(.fetching) + |> then( + currentLocationManagerCoordinate(manager: locationManager, timeout: 5.0) + |> mapToSignal { location in + if let location { + return getWeatherData(context: context, location: location) + |> mapToSignal { weather in + if let weather { + let effectiveEmoji = emojiFor(for: weather.emoji.strippedEmoji, date: Date(), location: location) + if let match = context.animatedEmojiStickersValue[effectiveEmoji]?.first { + return .single(.loaded(StickerPickerScreen.Weather.LoadedWeather( + emoji: effectiveEmoji, + emojiFile: match.file, + temperature: weather.temperature + ))) + } else { + return .single(.none) + } } else { return .single(.none) } - } else { - return .single(.none) } + } else { + return .single(.none) } - } else { - return .single(.none) } - } - ) + ) + } else { + return .single(.notPreloaded) + } } } } diff --git a/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedContainer.swift b/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedContainer.swift index 8cb8ed781a..7597459eef 100644 --- a/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedContainer.swift +++ b/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedContainer.swift @@ -616,8 +616,6 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll return } -// self.scrollView.contentOffset = CGPoint(x: 0.0, y: max(0.0, self.scrollView.contentSize.height - self.scrollView.bounds.height)) - if self.items.count == 1, let item = self.items.first { if let navigationController = self.navigationController { item.beforeMaximize(navigationController, { [weak self] in @@ -625,6 +623,12 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll }) } } else { + let contentOffset = max(0.0, self.scrollView.contentSize.height - self.scrollView.bounds.height) + self.scrollView.contentOffset = CGPoint(x: 0.0, y: contentOffset) + for itemNode in self.itemNodes.values { + itemNode.frame = itemNode.frame.offsetBy(dx: 0.0, dy: contentOffset) + } + self.isExpanded = true self.requestUpdate(transition: .animated(duration: 0.4, curve: .spring)) } @@ -646,7 +650,7 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll if scrollView.contentOffset.y < -64.0, let lastItemId = self.items.last?.id, let itemNode = self.itemNodes[lastItemId] { let velocity = scrollView.panGestureRecognizer.velocity(in: self.view).y let distance = layout.size.height - self.collapsedHeight(layout: layout) - itemNode.frame.minY - let initialVelocity = distance != 0.0 ? abs(velocity / distance) : 0.0 + let initialVelocity = min(8.0, distance != 0.0 ? abs(velocity / distance) : 0.0) self.isExpanded = false scrollView.isScrollEnabled = false @@ -747,6 +751,10 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll var index = 0 let contentHeight = frameForIndex(index: self.items.count - 1, size: layout.size, insets: itemInsets, itemCount: self.items.count, boundingSize: layout.size).midY - 70.0 + + var effectiveScrollBounds = self.scrollView.bounds + effectiveScrollBounds.origin.y = max(0.0, min(contentHeight - self.scrollView.bounds.height, effectiveScrollBounds.origin.y)) + for item in self.items { if let currentTransition = self.currentTransition { if currentTransition.matches(item: item) { @@ -859,14 +867,14 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll if self.isExpanded { let currentItemFrame = frameForIndex(index: index, size: layout.size, insets: itemInsets, itemCount: self.items.count, boundingSize: layout.size) - let currentItemTransform = final3dTransform(for: currentItemFrame.minY, size: currentItemFrame.size, contentHeight: contentHeight, itemCount: self.items.count, additionalAngle: self.highlightedItemId == item.id ? 0.04 : nil, scrollBounds: self.scrollView.bounds, insets: itemInsets) + let currentItemTransform = final3dTransform(for: currentItemFrame.minY, size: currentItemFrame.size, contentHeight: contentHeight, itemCount: self.items.count, additionalAngle: self.highlightedItemId == item.id ? 0.04 : nil, scrollBounds: effectiveScrollBounds, insets: itemInsets) var effectiveItemFrame = currentItemFrame - var effectiveItemTransform = currentItemTransform + let effectiveItemTransform = currentItemTransform if let dismissingItemId = self.dismissingItemId, let deletingIndex = self.items.firstIndex(where: { $0.id == dismissingItemId }), let offset = self.dismissingItemOffset { - var targetItemFrame: CGRect? - var targetItemTransform: CATransform3D? +// var targetItemFrame: CGRect? +// var targetItemTransform: CATransform3D? if deletingIndex == index { let effectiveOffset: CGFloat if offset <= 0.0 { @@ -875,25 +883,26 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll effectiveOffset = scrollingRubberBandingOffset(offset: offset, bandingStart: 0.0, range: 20.0) } effectiveItemFrame = effectiveItemFrame.offsetBy(dx: effectiveOffset, dy: 0.0) - } else if index < deletingIndex { - let frame = frameForIndex(index: index, size: layout.size, insets: itemInsets, itemCount: self.items.count - 1, boundingSize: layout.size) - let spacing = interitemSpacing(itemCount: self.items.count - 1, boundingSize: layout.size, insets: itemInsets) - - targetItemFrame = frame - targetItemTransform = final3dTransform(for: frame.minY, size: layout.size, contentHeight: contentHeight - layout.size.height - spacing, itemCount: self.items.count - 1, scrollBounds: self.scrollView.bounds, insets: itemInsets) - } else { - let frame = frameForIndex(index: index - 1, size: layout.size, insets: itemInsets, itemCount: self.items.count - 1, boundingSize: layout.size) - let spacing = interitemSpacing(itemCount: self.items.count - 1, boundingSize: layout.size, insets: itemInsets) - - targetItemFrame = frame - targetItemTransform = final3dTransform(for: frame.minY, size: layout.size, contentHeight: contentHeight - layout.size.height - spacing, itemCount: self.items.count - 1, scrollBounds: self.scrollView.bounds, insets: itemInsets) - } + } +// else if index < deletingIndex { +// let frame = frameForIndex(index: index, size: layout.size, insets: itemInsets, itemCount: self.items.count - 1, boundingSize: layout.size) +// let spacing = interitemSpacing(itemCount: self.items.count - 1, boundingSize: layout.size, insets: itemInsets) +// +// targetItemFrame = frame +// targetItemTransform = final3dTransform(for: frame.minY, size: layout.size, contentHeight: contentHeight - layout.size.height - spacing, itemCount: self.items.count - 1, scrollBounds: self.scrollView.bounds, insets: itemInsets) +// } else { +// let frame = frameForIndex(index: index - 1, size: layout.size, insets: itemInsets, itemCount: self.items.count - 1, boundingSize: layout.size) +// let spacing = interitemSpacing(itemCount: self.items.count - 1, boundingSize: layout.size, insets: itemInsets) +// +// targetItemFrame = frame +// targetItemTransform = final3dTransform(for: frame.minY, size: layout.size, contentHeight: contentHeight - layout.size.height - spacing, itemCount: self.items.count - 1, scrollBounds: self.scrollView.bounds, insets: itemInsets) +// } - if let targetItemFrame, let targetItemTransform { - let fraction = max(0.0, min(1.0, -1.0 * offset / (layout.size.width * 1.5))) - effectiveItemFrame = effectiveItemFrame.interpolate(with: targetItemFrame, fraction: fraction) - effectiveItemTransform = effectiveItemTransform.interpolate(with: targetItemTransform, fraction: fraction) - } +// if let targetItemFrame, let targetItemTransform { +// let fraction = max(0.0, min(1.0, -1.0 * offset / (layout.size.width * 1.5))) +// effectiveItemFrame = effectiveItemFrame.interpolate(with: targetItemFrame, fraction: fraction) +// effectiveItemTransform = effectiveItemTransform.interpolate(with: targetItemTransform, fraction: fraction) +// } } itemFrame = effectiveItemFrame itemTransform = effectiveItemTransform @@ -904,6 +913,8 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll var hideTransform = false if let currentTransition = self.currentTransition { if case let .maximize(itemId) = currentTransition { + itemOffset += self.scrollView.bounds.origin.y + itemOffset += layout.size.height * 0.25 if let lastItemNode = self.scrollView.subviews.last?.asyncdisplaykit_node as? ItemNode, lastItemNode.item.id == itemId { hideTransform = true @@ -954,7 +965,16 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll let contentSize = CGSize(width: layout.size.width, height: contentHeight) if self.scrollView.contentSize != contentSize { + var contentSizeDelta: CGFloat? + if contentSize.height < self.scrollView.contentSize.height, transition.isAnimated { + let currentContentOffset = self.scrollView.contentOffset.y + let updatedContentOffset = max(0.0, contentSize.height - self.scrollView.bounds.height) + contentSizeDelta = currentContentOffset - updatedContentOffset + } self.scrollView.contentSize = contentSize + if let contentSizeDelta { + transition.animateBounds(layer: self.scrollView.layer, from: CGRect(origin: CGPoint(x: 0.0, y: self.scrollView.contentOffset.y + contentSizeDelta), size: self.scrollView.bounds.size)) + } } if self.scrollView.frame != bounds { self.scrollView.frame = bounds diff --git a/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedHeaderNode.swift b/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedHeaderNode.swift index dfbb7ab256..0944758d99 100644 --- a/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedHeaderNode.swift +++ b/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedHeaderNode.swift @@ -77,8 +77,12 @@ final class MinimizedHeaderNode: ASDisplayNode { if titles.count == 1, let title = titles.first { self.title = title } else if let title = titles.last { + var trimmedTitle = title + if trimmedTitle.count > 20 { + trimmedTitle = "\(trimmedTitle.prefix(20).trimmingCharacters(in: .whitespacesAndNewlines))\u{2026}" + } let othersString = self.strings.WebApp_MinimizedTitle_Others(Int32(titles.count - 1)) - self.title = self.strings.WebApp_MinimizedTitleFormat(title, othersString).string + self.title = self.strings.WebApp_MinimizedTitleFormat(trimmedTitle, othersString).string } else { self.title = nil } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift index 4ab6017f5b..2c0cbe7c40 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift @@ -2541,6 +2541,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr peer: peer, storyItem: item, videoPlaybackPosition: nil, + cover: false, repost: false, transitionIn: .gallery(MediaEditorScreen.TransitionIn.GalleryTransitionIn(sourceView: self.itemGrid.view, sourceRect: foundItemLayer?.frame ?? .zero, sourceImage: sourceImage)), transitionOut: MediaEditorScreen.TransitionOut(destinationView: self.itemGrid.view, destinationRect: foundItemLayer?.frame ?? .zero, destinationCornerRadius: 0.0), diff --git a/submodules/TelegramUI/Components/StickerPickerScreen/Sources/StickerPickerScreen.swift b/submodules/TelegramUI/Components/StickerPickerScreen/Sources/StickerPickerScreen.swift index e9a4cdb727..99046442ec 100644 --- a/submodules/TelegramUI/Components/StickerPickerScreen/Sources/StickerPickerScreen.swift +++ b/submodules/TelegramUI/Components/StickerPickerScreen/Sources/StickerPickerScreen.swift @@ -2067,6 +2067,7 @@ public class StickerPickerScreen: ViewController { case none case notDetermined case notAllowed + case notPreloaded case fetching case loaded(StickerPickerScreen.Weather.LoadedWeather) } @@ -2727,7 +2728,7 @@ final class StoryStickersContentView: UIView, EmojiCustomContentView { let weatherButtonContent: AnyComponent switch self.weather { - case .notAllowed, .notDetermined: + case .notAllowed, .notDetermined, .notPreloaded: weatherButtonContent = AnyComponent( InteractiveStickerButtonContent( context: self.context, diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index f04aca6cca..e027cd2c7f 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -5340,7 +5340,7 @@ public final class StoryItemSetContainerComponent: Component { } private let updateDisposable = MetaDisposable() - func openStoryEditing(repost: Bool = false) { + func openStoryEditing(repost: Bool = false, cover: Bool = false) { guard let component = self.component else { return } @@ -5351,7 +5351,17 @@ public final class StoryItemSetContainerComponent: Component { var videoPlaybackPosition: Double? if let visibleItem = self.visibleItems[component.slice.item.id], let view = visibleItem.view.view as? StoryItemContentComponent.View { - videoPlaybackPosition = view.videoPlaybackPosition + if cover { + if case let .file(file) = component.slice.item.storyItem.media { + for attribute in file.attributes { + if case let .Video(_, _, _, _, coverTime) = attribute { + videoPlaybackPosition = coverTime + } + } + } + } else { + videoPlaybackPosition = view.videoPlaybackPosition + } } guard let controller = MediaEditorScreen.makeEditStoryController( @@ -5359,6 +5369,7 @@ public final class StoryItemSetContainerComponent: Component { peer: component.slice.effectivePeer, storyItem: component.slice.item.storyItem, videoPlaybackPosition: videoPlaybackPosition, + cover: cover, repost: repost, transitionIn: .noAnimation, transitionOut: nil, @@ -6107,6 +6118,19 @@ public final class StoryItemSetContainerComponent: Component { self.openStoryEditing() }))) + if case .file = component.slice.item.storyItem.media { + items.append(.action(ContextMenuActionItem(text: component.strings.Story_Context_EditCover, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + a(.default) + + guard let self else { + return + } + self.openStoryEditing(cover: true) + }))) + } + items.append(.separator) items.append(.action(ContextMenuActionItem(text: component.slice.item.storyItem.isPinned ? component.strings.Story_Context_RemoveFromProfile : component.strings.Story_Context_SaveToProfile, icon: { theme in @@ -6291,6 +6315,19 @@ public final class StoryItemSetContainerComponent: Component { } self.openStoryEditing() }))) + + if case .file = component.slice.item.storyItem.media { + items.append(.action(ContextMenuActionItem(text: component.strings.Story_Context_EditCover, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + a(.default) + + guard let self else { + return + } + self.openStoryEditing(cover: true) + }))) + } } if !items.isEmpty { diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 60350b684e..08bd4688f1 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -128,6 +128,7 @@ import MessageUI import PhoneNumberFormat import OwnershipTransferController import OldChannelsController +import BrowserUI public enum ChatControllerPeekActions { case standard @@ -398,11 +399,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var historyNavigationStack = ChatHistoryNavigationStack() public let canReadHistory = ValuePromise(true, ignoreRepeated: true) + public let hasBrowserOrAppInFront = Promise(false) var reminderActivity: NSUserActivity? var isReminderActivityEnabled: Bool = false - var canReadHistoryValue = false + var canReadHistoryValue = false { + didSet { + self.computedCanReadHistoryPromise.set(self.canReadHistoryValue) + } + } var canReadHistoryDisposable: Disposable? + var computedCanReadHistoryPromise = ValuePromise(false, ignoreRepeated: true) var themeEmoticonAndDarkAppearancePreviewPromise = Promise<(String?, Bool?)>((nil, nil)) var didSetPresentationData = false @@ -6502,8 +6509,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } - self.canReadHistoryDisposable = (combineLatest(context.sharedContext.applicationBindings.applicationInForeground, self.canReadHistory.get()) |> map { a, b in - return a && b + + + self.canReadHistoryDisposable = (combineLatest( + context.sharedContext.applicationBindings.applicationInForeground, + self.canReadHistory.get(), + self.hasBrowserOrAppInFront.get() + ) |> map { inForeground, globallyEnabled, hasBrowserOrWebAppInFront in + return inForeground && globallyEnabled && !hasBrowserOrWebAppInFront } |> deliverOnMainQueue).startStrict(next: { [weak self] value in if let strongSelf = self, strongSelf.canReadHistoryValue != value { strongSelf.canReadHistoryValue = value @@ -7119,6 +7132,19 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G ) } } + + let hasBrowserOrWebAppInFront: Signal = .single([]) + |> then( + self.effectiveNavigationController?.viewControllersSignal ?? .single([]) + ) + |> map { controllers in + if controllers.last is BrowserScreen || controllers.last is AttachmentController { + return true + } else { + return false + } + } + self.hasBrowserOrAppInFront.set(hasBrowserOrWebAppInFront) } var returnInputViewFocus = false @@ -7129,9 +7155,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.didAppear = true self.chatDisplayNode.historyNode.experimentalSnapScrollToItem = false - self.chatDisplayNode.historyNode.canReadHistory.set(combineLatest(self.context.sharedContext.applicationBindings.applicationInForeground, self.canReadHistory.get()) |> map { a, b in - return a && b - }) + self.chatDisplayNode.historyNode.canReadHistory.set(self.computedCanReadHistoryPromise.get()) self.chatDisplayNode.loadInputPanels(theme: self.presentationInterfaceState.theme, strings: self.presentationInterfaceState.strings, fontSize: self.presentationInterfaceState.fontSize) diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 25a88f76b4..0c4101bd01 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -2654,7 +2654,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { return editorController } - public func makeStoryMediaEditorScreen(context: AccountContext, source: Any?, text: String?, link: String?, completion: @escaping (MediaEditorScreenResult, @escaping (@escaping () -> Void) -> Void) -> Void) -> ViewController { + public func makeStoryMediaEditorScreen(context: AccountContext, source: Any?, text: String?, link: (url: String, name: String?)?, completion: @escaping (MediaEditorScreenResult, @escaping (@escaping () -> Void) -> Void) -> Void) -> ViewController { let subject: Signal if let image = source as? UIImage { subject = .single(.image(image, PixelDimensions(image.size), nil, .bottomRight)) diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index e0ab310335..4a1ad72965 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -674,7 +674,17 @@ public final class TelegramRootController: NavigationController, TelegramRootCon return nil } } - media = .video(dimensions: dimensions, duration: duration, resource: resource, firstFrameFile: firstFrameFile, stickers: result.stickers, coverTime: values.coverImageTimestamp) + + var coverTime: Double? + if let coverImageTimestamp = values.coverImageTimestamp { + if let trimRange = values.videoTrimRange { + coverTime = coverImageTimestamp - trimRange.lowerBound + } else { + coverTime = coverImageTimestamp + } + } + + media = .video(dimensions: dimensions, duration: duration, resource: resource, firstFrameFile: firstFrameFile, stickers: result.stickers, coverTime: coverTime) } default: break diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index 1191fa54c3..b4bba91d9d 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -1104,7 +1104,18 @@ public final class WebAppController: ViewController, AttachmentContainable { case "web_app_share_to_story": if let json = json, let mediaUrl = json["media_url"] as? String { let text = json["text"] as? String - let link = json["widget_link"] as? String + let link = json["widget_link"] as? [String: Any] + + var linkUrl: String? + var linkName: String? + if let link { + if let url = link["url"] as? String { + linkUrl = url + if let name = link["name"] as? String { + linkName = name + } + } + } enum FetchResult { case result(Data) @@ -1112,7 +1123,6 @@ public final class WebAppController: ViewController, AttachmentContainable { } let controller = OverlayStatusController(theme: self.presentationData.theme, type: .loading(cancelled: { - })) self.controller?.present(controller, in: .window(.root)) @@ -1154,7 +1164,7 @@ public final class WebAppController: ViewController, AttachmentContainable { isPeerArchived: false, transitionOut: nil ) - let controller = self.context.sharedContext.makeStoryMediaEditorScreen(context: self.context, source: source, text: text, link: link, completion: { result, commit in + let controller = self.context.sharedContext.makeStoryMediaEditorScreen(context: self.context, source: source, text: text, link: linkUrl.flatMap { ($0, linkName) }, completion: { result, commit in // let targetPeerId: EnginePeer.Id let target: Stories.PendingTarget // if let sendAsPeerId = result.options.sendAsPeerId { @@ -2114,8 +2124,10 @@ public final class WebAppController: ViewController, AttachmentContainable { public func isContainerPanningUpdated(_ isPanning: Bool) { self.controllerNode.isContainerPanningUpdated(isPanning) } - + + private var validLayout: ContainerViewLayout? override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + self.validLayout = layout super.containerLayoutUpdated(layout, transition: transition) self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) @@ -2171,6 +2183,7 @@ public final class WebAppController: ViewController, AttachmentContainable { self.controllerNode.webView?.hideScrollIndicators() } else { self.requestLayout(transition: .immediate) + self.controllerNode.webView?.setNeedsLayout() } } } @@ -2205,6 +2218,22 @@ public final class WebAppController: ViewController, AttachmentContainable { public var minimizedIcon: UIImage? { return self.controllerNode.icon } + + public func makeContentSnapshotView() -> UIView? { + guard let webView = self.controllerNode.webView, let _ = self.validLayout else { + return nil + } + + let configuration = WKSnapshotConfiguration() + configuration.rect = CGRect(origin: .zero, size: webView.frame.size) + + let imageView = UIImageView() + imageView.frame = CGRect(origin: .zero, size: webView.frame.size) + webView.takeSnapshot(with: configuration, completionHandler: { image, _ in + imageView.image = image + }) + return imageView + } } final class WebAppPickerContext: AttachmentMediaPickerContext { From 29917a97acc5f80095f18a9d472c9181d2b411a8 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Thu, 25 Jul 2024 13:59:21 +0800 Subject: [PATCH 06/41] Warp experiment --- .../Sources/DebugController.swift | 14 +- .../ChatMessageAnimatedStickerItemNode.swift | 2 +- .../TelegramUI/Components/SpaceWarpView/BUILD | 1 + .../SpaceWarpView/STCMeshView/BUILD | 23 + .../PublicHeaders/STCMeshView/STCMeshLayer.h | 58 ++ .../PublicHeaders/STCMeshView/STCMeshView.h | 26 + .../STCMeshView/Sources/STCMeshLayer.m | 337 ++++++++ .../STCMeshView/Sources/STCMeshView.m | 126 +++ .../SpaceWarpView/Sources/SpaceWarpView.swift | 816 ++++++++++++++++-- .../Sources/ChatControllerNode.swift | 17 +- .../Sources/ExperimentalUISettings.swift | 12 +- 11 files changed, 1320 insertions(+), 112 deletions(-) create mode 100644 submodules/TelegramUI/Components/SpaceWarpView/STCMeshView/BUILD create mode 100644 submodules/TelegramUI/Components/SpaceWarpView/STCMeshView/PublicHeaders/STCMeshView/STCMeshLayer.h create mode 100644 submodules/TelegramUI/Components/SpaceWarpView/STCMeshView/PublicHeaders/STCMeshView/STCMeshView.h create mode 100644 submodules/TelegramUI/Components/SpaceWarpView/STCMeshView/Sources/STCMeshLayer.m create mode 100644 submodules/TelegramUI/Components/SpaceWarpView/STCMeshView/Sources/STCMeshView.m diff --git a/submodules/DebugSettingsUI/Sources/DebugController.swift b/submodules/DebugSettingsUI/Sources/DebugController.swift index 4caad38b0e..98d024ee51 100644 --- a/submodules/DebugSettingsUI/Sources/DebugController.swift +++ b/submodules/DebugSettingsUI/Sources/DebugController.swift @@ -93,7 +93,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { case knockoutWallpaper(PresentationTheme, Bool) case experimentalCompatibility(Bool) case enableDebugDataDisplay(Bool) - case acceleratedStickers(Bool) + case rippleEffect(Bool) case browserExperiment(Bool) case localTranscription(Bool) case enableReactionOverrides(Bool) @@ -127,7 +127,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { return DebugControllerSection.web.rawValue case .keepChatNavigationStack, .skipReadHistory, .dustEffect, .crashOnSlowQueries, .crashOnMemoryPressure: return DebugControllerSection.experiments.rawValue - case .clearTips, .resetNotifications, .crash, .fillLocalSavedMessageCache, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .resetTagHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .storiesExperiment, .storiesJpegExperiment, .playlistPlayback, .enableQuickReactionSwitch, .experimentalCompatibility, .enableDebugDataDisplay, .acceleratedStickers, .browserExperiment, .localTranscription, .enableReactionOverrides, .restorePurchases, .disableReloginTokens, .liveStreamV2: + case .clearTips, .resetNotifications, .crash, .fillLocalSavedMessageCache, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .resetTagHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .storiesExperiment, .storiesJpegExperiment, .playlistPlayback, .enableQuickReactionSwitch, .experimentalCompatibility, .enableDebugDataDisplay, .rippleEffect, .browserExperiment, .localTranscription, .enableReactionOverrides, .restorePurchases, .disableReloginTokens, .liveStreamV2: return DebugControllerSection.experiments.rawValue case .logTranslationRecognition, .resetTranslationStates: return DebugControllerSection.translation.rawValue @@ -216,7 +216,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { return 37 case .enableDebugDataDisplay: return 38 - case .acceleratedStickers: + case .rippleEffect: return 39 case .browserExperiment: return 40 @@ -1228,12 +1228,12 @@ private enum DebugControllerEntry: ItemListNodeEntry { }) }).start() }) - case let .acceleratedStickers(value): - return ItemListSwitchItem(presentationData: presentationData, title: "Accelerated Stickers", value: value, sectionId: self.section, style: .blocks, updated: { value in + case let .rippleEffect(value): + return ItemListSwitchItem(presentationData: presentationData, title: "Ripple", value: value, sectionId: self.section, style: .blocks, updated: { value in let _ = arguments.sharedContext.accountManager.transaction ({ transaction in transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in var settings = settings?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings - settings.acceleratedStickers = value + settings.rippleEffect = value return PreferencesEntry(settings) }) }).start() @@ -1452,7 +1452,7 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present entries.append(.knockoutWallpaper(presentationData.theme, experimentalSettings.knockoutWallpaper)) entries.append(.experimentalCompatibility(experimentalSettings.experimentalCompatibility)) entries.append(.enableDebugDataDisplay(experimentalSettings.enableDebugDataDisplay)) - entries.append(.acceleratedStickers(experimentalSettings.acceleratedStickers)) + entries.append(.rippleEffect(experimentalSettings.rippleEffect)) #if DEBUG entries.append(.browserExperiment(experimentalSettings.browserExperiment)) #else diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift index 30c06596e2..00028d4736 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -402,7 +402,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { self.animationNode = animationNode } } else { - let animationNode = DefaultAnimatedStickerNodeImpl(useMetalCache: item.context.sharedContext.immediateExperimentalUISettings.acceleratedStickers) + let animationNode = DefaultAnimatedStickerNodeImpl(useMetalCache: false) animationNode.started = { [weak self] in if let strongSelf = self { strongSelf.imageNode.alpha = 0.0 diff --git a/submodules/TelegramUI/Components/SpaceWarpView/BUILD b/submodules/TelegramUI/Components/SpaceWarpView/BUILD index a3c6d68639..bfa7fe65eb 100644 --- a/submodules/TelegramUI/Components/SpaceWarpView/BUILD +++ b/submodules/TelegramUI/Components/SpaceWarpView/BUILD @@ -13,6 +13,7 @@ swift_library( "//submodules/Display", "//submodules/AsyncDisplayKit", "//submodules/ComponentFlow", + "//submodules/TelegramUI/Components/SpaceWarpView/STCMeshView", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/SpaceWarpView/STCMeshView/BUILD b/submodules/TelegramUI/Components/SpaceWarpView/STCMeshView/BUILD new file mode 100644 index 0000000000..0c1146bbcc --- /dev/null +++ b/submodules/TelegramUI/Components/SpaceWarpView/STCMeshView/BUILD @@ -0,0 +1,23 @@ + +objc_library( + name = "STCMeshView", + enable_modules = True, + module_name = "STCMeshView", + srcs = glob([ + "Sources/**/*.m", + "Sources/**/*.h", + ]), + hdrs = glob([ + "PublicHeaders/**/*.h", + ]), + includes = [ + "PublicHeaders", + ], + sdk_frameworks = [ + "Foundation", + "UIKit", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/SpaceWarpView/STCMeshView/PublicHeaders/STCMeshView/STCMeshLayer.h b/submodules/TelegramUI/Components/SpaceWarpView/STCMeshView/PublicHeaders/STCMeshView/STCMeshLayer.h new file mode 100644 index 0000000000..37f984a33a --- /dev/null +++ b/submodules/TelegramUI/Components/SpaceWarpView/STCMeshView/PublicHeaders/STCMeshView/STCMeshLayer.h @@ -0,0 +1,58 @@ +/** + Copyright (c) 2014-present, Facebook, Inc. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +/* A mesh layer allows individually transforming areas inside its subtree. */ + +@interface STCMeshLayer : CAReplicatorLayer + +/* An array of bounds regions to use for each instance. The length + * of this array is assumed to match `instanceCount'. Required. */ + +@property (atomic, assign) CGRect *instanceBounds; + +/* An array of positions to use for each instance. The length + * of this array is assumed to match `instanceCount'. Required. */ + +@property (atomic, assign) CGPoint *instancePositions; + +/* An array of anchor points to use for each instance. The length + * of this array is assumed to match `instanceCount'. Required. */ + +@property (atomic, assign) CGPoint *instanceAnchorPoints; + +/* An array of transforms to apply to each instance. The length + * of this array is assumed to match `instanceCount'. Required. */ + +@property (atomic, assign) CATransform3D *instanceTransforms; + +/* Add content to this layer to transform it in the mesh. */ + +@property (atomic, strong) CALayer *contentLayer; + +/* This CAReplicatorLayer property is used internally and is not + * available for use by clients. Do not set it. */ + +@property (atomic, assign) CFTimeInterval instanceDelay NS_UNAVAILABLE; + +/* This CAReplicatorLayer property is used internally and is not + * available for use by clients. Do not set it. */ + +@property (atomic, assign) CATransform3D instanceTransform NS_UNAVAILABLE; + +@end + +@interface STCMeshLayer (UIViewSupport) + +/* The wrapper replicator layer used to preserve a linear timespace. */ + +@property (atomic, strong) CAReplicatorLayer *wrapperLayer; + +@end diff --git a/submodules/TelegramUI/Components/SpaceWarpView/STCMeshView/PublicHeaders/STCMeshView/STCMeshView.h b/submodules/TelegramUI/Components/SpaceWarpView/STCMeshView/PublicHeaders/STCMeshView/STCMeshView.h new file mode 100644 index 0000000000..7ca8c51bbc --- /dev/null +++ b/submodules/TelegramUI/Components/SpaceWarpView/STCMeshView/PublicHeaders/STCMeshView/STCMeshView.h @@ -0,0 +1,26 @@ +/** + Copyright (c) 2014-present, Facebook, Inc. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import + +// shows different part of its subviews with different transforms +@interface STCMeshView : UIView + +@property (nonatomic, retain, readonly) STCMeshLayer *layer; +@property (nonatomic, retain, readwrite) UIView *contentView; // only subviews added to this are transformed + +@property (nonatomic, assign, readwrite) NSInteger instanceCount; // defaults to 1 +@property (nonatomic, assign, readwrite) CATransform3D *instanceTransforms; // optional +@property (nonatomic, assign, readwrite) CGRect *instanceBounds; // optional +@property (nonatomic, assign, readwrite) CGPoint *instancePositions; // optional +@property (nonatomic, assign, readwrite) CGPoint *instanceAnchorPoints; // optional + +@end diff --git a/submodules/TelegramUI/Components/SpaceWarpView/STCMeshView/Sources/STCMeshLayer.m b/submodules/TelegramUI/Components/SpaceWarpView/STCMeshView/Sources/STCMeshLayer.m new file mode 100644 index 0000000000..b520dae06b --- /dev/null +++ b/submodules/TelegramUI/Components/SpaceWarpView/STCMeshView/Sources/STCMeshLayer.m @@ -0,0 +1,337 @@ +/** + Copyright (c) 2014-present, Facebook, Inc. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +static const CFTimeInterval STCMeshLayerTotalInstanceDelay = 10000000.0; +static NSString *const STCMeshLayerBoundsAnimationKey = @"STCMeshLayerBoundsAnimation"; +static NSString *const STCMeshLayerTransformAnimationKey = @"STCMeshLayerTransformAnimation"; +static NSString *const STCMeshLayerPositionAnimationKey = @"STCMeshLayerPositionAnimation"; +static NSString *const STCMeshLayerAnchorPointAnimationKey = @"STCMeshLayerAnchorPointAnimation"; +static NSString *const STCMeshLayerInstanceDelayAnimationKey = @"STCMeshLayerInstanceDelayAnimation"; + +@implementation STCMeshLayer { + CAReplicatorLayer *_wrapperLayer; + CALayer *_contentLayer; + + CGRect *_instanceBounds; + CATransform3D *_instanceTransforms; + CGPoint *_instancePositions; + CGPoint *_instanceAnchorPoints; +} + +#pragma mark - Lifecycle + +- (instancetype)init +{ + if ((self = [super init])) { + self.wrapperLayer = [[CAReplicatorLayer alloc] init]; + self.contentLayer = [[CALayer alloc] init]; + } + + return self; +} + +- (void)dealloc +{ + free(_instanceTransforms); + _instanceTransforms = NULL; + free(_instanceBounds); + _instanceBounds = NULL; +} + +- (void)layoutSublayers +{ + [super layoutSublayers]; + + _wrapperLayer.frame = self.bounds; + _contentLayer.frame = _wrapperLayer.bounds; + [self _updateMeshAnimations]; +} + +#pragma mark - Properties + +@dynamic instanceDelay; + +@dynamic instanceTransform; + +- (void)setInstanceCount:(NSInteger)instanceCount +{ + if (instanceCount != self.instanceCount) { + [super setInstanceCount:instanceCount]; + + free(_instanceTransforms); + _instanceTransforms = NULL; + free(_instanceBounds); + _instanceBounds = NULL; + + [self setNeedsLayout]; + } +} + +- (CATransform3D *)instanceTransforms +{ + CATransform3D *instanceTransforms = _instanceTransforms; + + return instanceTransforms; +} + +- (void)setInstanceTransforms:(CATransform3D *)instanceTransforms +{ + free(_instanceTransforms); + _instanceTransforms = NULL; + + if (instanceTransforms != NULL) { + _instanceTransforms = calloc(sizeof(CATransform3D), self.instanceCount); + memcpy(_instanceTransforms, instanceTransforms, self.instanceCount * sizeof(CATransform3D)); + } + + [self setNeedsLayout]; +} + +- (CGPoint *)instancePositions +{ + CGPoint *instancePositions = _instancePositions; + + return instancePositions; +} + +- (void)setInstancePositions:(CGPoint *)instancePositions +{ + free(_instancePositions); + _instancePositions = NULL; + + if (instancePositions != NULL) { + _instancePositions = calloc(sizeof(CGPoint), self.instanceCount); + memcpy(_instancePositions, instancePositions, self.instanceCount * sizeof(CGPoint)); + } + + [self setNeedsLayout]; +} + +- (CGPoint *)instanceAnchorPoints +{ + CGPoint *instanceAnchorPoints = _instanceAnchorPoints; + + return instanceAnchorPoints; +} + +- (void)setInstanceAnchorPoints:(CGPoint *)instanceAnchorPoints +{ + free(_instanceAnchorPoints); + _instanceAnchorPoints = NULL; + + if (instanceAnchorPoints != NULL) { + _instanceAnchorPoints = calloc(sizeof(CGPoint), self.instanceCount); + memcpy(_instanceAnchorPoints, instanceAnchorPoints, self.instanceCount * sizeof(CGPoint)); + } + + [self setNeedsLayout]; +} + +- (CGRect *)instanceBounds +{ + CGRect *instanceBounds = _instanceBounds; + + return instanceBounds; +} + +- (void)setInstanceBounds:(CGRect *)instanceBounds +{ + free(_instanceBounds); + _instanceBounds = NULL; + + if (instanceBounds != NULL) { + _instanceBounds = calloc(sizeof(CGRect), self.instanceCount); + memcpy(_instanceBounds, instanceBounds, self.instanceCount * sizeof(CGRect)); + } + + [self setNeedsLayout]; +} + +- (CALayer *)contentLayer +{ + CALayer *contentLayer = _contentLayer; + + return contentLayer; +} + +- (void)setContentLayer:(CALayer *)contentLayer +{ + if (contentLayer != _contentLayer) { + if (_contentLayer != nil) { + [_contentLayer removeFromSuperlayer]; + } + + _contentLayer = contentLayer; + + if (_contentLayer != nil) { + [_wrapperLayer addSublayer:_contentLayer]; + } + } +} + +- (CAReplicatorLayer *)wrapperLayer +{ + CAReplicatorLayer *wrapperLayer = _wrapperLayer; + + return wrapperLayer; +} + +- (void)setWrapperLayer:(CAReplicatorLayer *)wrapperLayer +{ + if (wrapperLayer != _wrapperLayer) { + if (_contentLayer != nil) { + [_contentLayer removeFromSuperlayer]; + } + + if (_wrapperLayer != nil) { + [_wrapperLayer removeFromSuperlayer]; + } + + _wrapperLayer = wrapperLayer; + + if (_wrapperLayer != nil) { + _wrapperLayer.masksToBounds = YES; + _wrapperLayer.instanceCount = 2; + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + CGColorRef hiddenColor = CGColorCreate(colorSpace, (CGFloat []){ 1.0, 1.0, 1.0, 0.0 }); + _wrapperLayer.instanceColor = hiddenColor; + CGColorRelease(hiddenColor); + CGColorSpaceRelease(colorSpace); + _wrapperLayer.instanceAlphaOffset = 1.0; + [self addSublayer:_wrapperLayer]; + } + + if (_contentLayer != nil) { + [_wrapperLayer addSublayer:_contentLayer]; + } + + [self setNeedsLayout]; + } +} + +#pragma mark - Internal Methods + +- (CGRect)_boundsAtIndex:(NSUInteger)index +{ + CGRect bounds = CGRectZero; + + if (_instanceBounds != NULL) { + bounds = _instanceBounds[index]; + } + + return bounds; +} + +- (CATransform3D)_transformAtIndex:(NSUInteger)index +{ + CATransform3D transform = CATransform3DIdentity; + + if (_instanceTransforms != NULL) { + transform = _instanceTransforms[index]; + } + + return transform; +} + +- (CGPoint)_positionAtIndex:(NSUInteger)index +{ + CGPoint position = CGPointZero; + + if (_instancePositions != NULL) { + position = _instancePositions[index]; + } + + return position; +} + +- (CGPoint)_anchorPointAtIndex:(NSUInteger)index +{ + CGPoint anchorPoint = CGPointMake(0.5, 0.5); + + if (_instanceAnchorPoints != NULL) { + anchorPoint = _instanceAnchorPoints[index]; + } + + return anchorPoint; +} + +- (void)_updateMeshAnimations +{ + [_wrapperLayer removeAllAnimations]; + + super.instanceDelay = -STCMeshLayerTotalInstanceDelay / self.instanceCount; + + CAKeyframeAnimation *boundsAnimation = [CAKeyframeAnimation animationWithKeyPath:@"bounds"]; + boundsAnimation.calculationMode = kCAAnimationDiscrete; + boundsAnimation.duration = STCMeshLayerTotalInstanceDelay; + boundsAnimation.removedOnCompletion = NO; + NSMutableArray *boundsValues = [NSMutableArray array]; + for (NSUInteger i = 0; i < self.instanceCount; i++) { + CGRect bounds = [self _boundsAtIndex:i]; + NSValue *boundsValue = [NSValue valueWithBytes:&bounds objCType:@encode(CGRect)]; + [boundsValues addObject:boundsValue]; + } + boundsAnimation.values = boundsValues; + [_wrapperLayer addAnimation:boundsAnimation forKey:STCMeshLayerBoundsAnimationKey]; + + CAKeyframeAnimation *transformAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform"]; + transformAnimation.calculationMode = kCAAnimationDiscrete; + transformAnimation.duration = STCMeshLayerTotalInstanceDelay; + transformAnimation.removedOnCompletion = NO; + NSMutableArray *transformValues = [NSMutableArray array]; + for (NSUInteger i = 0; i < self.instanceCount; i++) { + CATransform3D transform = [self _transformAtIndex:i]; + NSValue *transformValue = [NSValue valueWithCATransform3D:transform]; + [transformValues addObject:transformValue]; + } + transformAnimation.values = transformValues; + [_wrapperLayer addAnimation:transformAnimation forKey:STCMeshLayerTransformAnimationKey]; + + CAKeyframeAnimation *positionAnimation = [CAKeyframeAnimation animationWithKeyPath:@"position"]; + positionAnimation.calculationMode = kCAAnimationDiscrete; + positionAnimation.duration = STCMeshLayerTotalInstanceDelay; + positionAnimation.removedOnCompletion = NO; + NSMutableArray *positionValues = [NSMutableArray array]; + for (NSUInteger i = 0; i < self.instanceCount; i++) { + CGPoint position = [self _positionAtIndex:i]; + NSValue *positionValue = [NSValue valueWithBytes:&position objCType:@encode(CGPoint)]; + [positionValues addObject:positionValue]; + } + positionAnimation.values = positionValues; + [_wrapperLayer addAnimation:positionAnimation forKey:STCMeshLayerPositionAnimationKey]; + + CAKeyframeAnimation *anchorPointAnimation = [CAKeyframeAnimation animationWithKeyPath:@"anchorPoint"]; + anchorPointAnimation.calculationMode = kCAAnimationDiscrete; + anchorPointAnimation.duration = STCMeshLayerTotalInstanceDelay; + anchorPointAnimation.removedOnCompletion = NO; + NSMutableArray *anchorPointValues = [NSMutableArray array]; + for (NSUInteger i = 0; i < self.instanceCount; i++) { + CGPoint anchorPoint = [self _anchorPointAtIndex:i]; + NSValue *anchorPointValue = [NSValue valueWithBytes:&anchorPoint objCType:@encode(CGPoint)]; + [anchorPointValues addObject:anchorPointValue]; + } + anchorPointAnimation.values = anchorPointValues; + [_wrapperLayer addAnimation:anchorPointAnimation forKey:STCMeshLayerAnchorPointAnimationKey]; + + CAKeyframeAnimation *timeAnimation = [CAKeyframeAnimation animationWithKeyPath:@"instanceDelay"]; + timeAnimation.calculationMode = kCAAnimationDiscrete; + timeAnimation.duration = STCMeshLayerTotalInstanceDelay; + timeAnimation.removedOnCompletion = NO; + NSMutableArray *timeValues = [NSMutableArray array]; + for (NSUInteger i = 0; i < self.instanceCount; i++) { + CFTimeInterval delay = -super.instanceDelay * i; + [timeValues addObject:@(delay)]; + } + timeAnimation.values = timeValues; + [_wrapperLayer addAnimation:timeAnimation forKey:STCMeshLayerInstanceDelayAnimationKey]; +} + +@end diff --git a/submodules/TelegramUI/Components/SpaceWarpView/STCMeshView/Sources/STCMeshView.m b/submodules/TelegramUI/Components/SpaceWarpView/STCMeshView/Sources/STCMeshView.m new file mode 100644 index 0000000000..b48bffcc1f --- /dev/null +++ b/submodules/TelegramUI/Components/SpaceWarpView/STCMeshView/Sources/STCMeshView.m @@ -0,0 +1,126 @@ +/** + Copyright (c) 2014-present, Facebook, Inc. + All rights reserved. + + This source code is licensed under the BSD-style license found in the + LICENSE file in the root directory of this source tree. An additional grant + of patent rights can be found in the PATENTS file in the same directory. + */ + +#import +#import + +@interface _STCMeshViewReplicatorView : UIView + +@property (nonatomic, readonly, retain) CAReplicatorLayer *layer; + +@end + +@implementation _STCMeshViewReplicatorView + +- (CAReplicatorLayer *)layer +{ + return (CAReplicatorLayer *)[super layer]; +} + ++ (Class)layerClass +{ + return [CAReplicatorLayer class]; +} + +@end + +@implementation STCMeshView { + _STCMeshViewReplicatorView *_wrapperView; +} + +- (STCMeshLayer *)layer +{ + return (STCMeshLayer *)[super layer]; +} + ++ (Class)layerClass +{ + return [STCMeshLayer class]; +} + +- (NSInteger)instanceCount +{ + return self.layer.instanceCount; +} + +- (void)setInstanceCount:(NSInteger)instanceCount +{ + self.layer.instanceCount = instanceCount; +} + +- (CATransform3D *)instanceTransforms +{ + return self.layer.instanceTransforms; +} + +- (void)setInstanceTransforms:(CATransform3D *)instanceTransforms +{ + self.layer.instanceTransforms = instanceTransforms; +} + +- (CGRect *)instanceBounds +{ + return self.layer.instanceBounds; +} + +- (void)setInstanceBounds:(CGRect *)instanceBounds +{ + self.layer.instanceBounds = instanceBounds; +} + +- (CGPoint *)instancePositions +{ + return self.layer.instancePositions; +} + +- (void)setInstancePositions:(CGPoint *)instancePositions +{ + self.layer.instancePositions = instancePositions; +} + +- (CGPoint *)instanceAnchorPoints +{ + return self.layer.instanceAnchorPoints; +} + +- (void)setInstanceAnchorPoints:(CGPoint *)instanceAnchorPoints +{ + self.layer.instanceAnchorPoints = instanceAnchorPoints; +} + +- (instancetype)initWithFrame:(CGRect)frame +{ + if ((self = [super initWithFrame:frame])) { + _wrapperView = [[_STCMeshViewReplicatorView alloc] init]; + [self addSubview:_wrapperView]; + self.layer.wrapperLayer = _wrapperView.layer; + + self.contentView = [[UIView alloc] init]; + } + + return self; +} + +- (void)setContentView:(UIView *)contentView +{ + if (contentView != _contentView) { + if (_contentView != nil) { + [_contentView removeFromSuperview]; + } + + if (contentView != nil) { + [_wrapperView addSubview:contentView]; + } + + _contentView = contentView; + self.layer.contentLayer = _contentView.layer; + } +} + +@end diff --git a/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift b/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift index fa27dca70d..48e2f5ea58 100644 --- a/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift +++ b/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift @@ -3,104 +3,57 @@ import UIKit import Display import AsyncDisplayKit import ComponentFlow +import STCMeshView -/*open class SpaceWarpView: UIView { - private final class WarpPartView: UIView { - let cloneView: PortalView - - init?(contentView: PortalSourceView) { - guard let cloneView = PortalView(matchPosition: false) else { - return nil - } - self.cloneView = cloneView - - super.init(frame: CGRect()) - - self.layer.anchorPoint = CGPoint(x: 0.5, y: 0.0) - - self.clipsToBounds = true - self.addSubview(cloneView.view) - contentView.addPortal(view: cloneView) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func update(containerSize: CGSize, rect: CGRect, transition: ComponentTransition) { - transition.setFrame(view: self.cloneView.view, frame: CGRect(origin: CGPoint(x: -rect.minX, y: -rect.minY), size: CGSize(width: containerSize.width, height: containerSize.height))) - } - } +private final class FPSView: UIView { + private var lastTimestamp: Double? + private var counter: Int = 0 + private var fpsValue: Int? + private var fpsString: NSAttributedString? - public var contentView: UIView { - return self.contentViewImpl - } - - let contentViewImpl: PortalSourceView - - private var warpViews: [WarpPartView] = [] - - override public init(frame: CGRect) { - self.contentViewImpl = PortalSourceView() - + override init(frame: CGRect) { super.init(frame: frame) - self.addSubview(self.contentView) - self.contentView.alpha = 0.1 - - for _ in 0 ..< 8 { - if let warpView = WarpPartView(contentView: self.contentViewImpl) { - self.warpViews.append(warpView) - self.addSubview(warpView) - } - } + self.layer.anchorPoint = CGPoint() + self.backgroundColor = .black } - required public init?(coder: NSCoder) { + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - public func update(size: CGSize, warpHeight: CGFloat, transition: ComponentTransition) { - transition.setFrame(view: self.contentView, frame: CGRect(origin: CGPoint(), size: size)) - - let allItemsHeight = warpHeight * 0.5 - for i in 0 ..< self.warpViews.count { - let itemHeight = warpHeight / CGFloat(self.warpViews.count) - let itemFraction = CGFloat(i + 1) / CGFloat(self.warpViews.count) - let _ = itemHeight - - let da = CGFloat.pi * 0.5 / CGFloat(self.warpViews.count) - let alpha = CGFloat.pi * 0.5 - itemFraction * CGFloat.pi * 0.5 - let endPoint = CGPoint(x: cos(alpha), y: sin(alpha)) - let prevAngle = alpha + da - let prevPt = CGPoint(x: cos(prevAngle), y: sin(prevAngle)) - var angle: CGFloat - angle = -atan2(endPoint.y - prevPt.y, endPoint.x - prevPt.x) - - let itemLengthVector = CGPoint(x: endPoint.x - prevPt.x, y: endPoint.y - prevPt.y) - let itemLength = sqrt(itemLengthVector.x * itemLengthVector.x + itemLengthVector.y * itemLengthVector.y) * warpHeight * 0.5 - let _ = itemLength - - var transform: CATransform3D - transform = CATransform3DIdentity - transform.m34 = 1.0 / 240.0 - - transform = CATransform3DTranslate(transform, 0.0, prevPt.x * allItemsHeight, (1.0 - prevPt.y) * allItemsHeight) - transform = CATransform3DRotate(transform, angle, 1.0, 0.0, 0.0) - - let positionY = size.height - allItemsHeight + 4.0 + CGFloat(i) * itemLength - let rect = CGRect(origin: CGPoint(x: 0.0, y: positionY), size: CGSize(width: size.width, height: itemLength)) - transition.setPosition(view: self.warpViews[i], position: CGPoint(x: rect.midX, y: 4.0)) - transition.setBounds(view: self.warpViews[i], bounds: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: itemLength))) - transition.setTransform(view: self.warpViews[i], transform: transform) - self.warpViews[i].update(containerSize: size, rect: rect, transition: transition) + func update() { + self.counter += 1 + let timestamp = CACurrentMediaTime() + let deltaTime: Double + if let lastTimestamp = self.lastTimestamp { + deltaTime = timestamp - lastTimestamp + } else { + deltaTime = 1.0 / 60.0 + self.lastTimestamp = timestamp + } + if deltaTime >= 1.0 { + let fpsValue = Int(Double(self.counter) / deltaTime) + if self.fpsValue != fpsValue { + self.fpsValue = fpsValue + let fpsString = NSAttributedString(string: "\(fpsValue)", attributes: [.foregroundColor: UIColor.white]) + self.bounds = fpsString.boundingRect(with: CGSize(width: 100.0, height: 100.0), context: nil).integral + self.fpsString = fpsString + self.setNeedsDisplay() + } + self.counter = 0 + self.lastTimestamp = timestamp } } - - override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - return self.contentView.hitTest(point, with: event) + + override func draw(_ rect: CGRect) { + guard let fpsString = self.fpsString else { + return + } + + fpsString.draw(at: CGPoint()) } -}*/ +} private extension CGPoint { static func -(lhs: CGPoint, rhs: CGPoint) -> CGPoint { @@ -148,7 +101,7 @@ private func transformCoordinate( // The distance of the current pixel position from `origin`. let distance = length(position - origin) - if distance < 10.0 { + if distance < 2.0 { return position } @@ -172,7 +125,7 @@ private func transformCoordinate( // // This new position moves toward or away from `origin` based on the // sign and magnitude of `rippleAmount`. - let newPosition = position + n * rippleAmount + let newPosition = position - n * rippleAmount return newPosition } @@ -222,12 +175,103 @@ private func rectToQuad( i = kEpsilon * (i > 0 ? 1.0 : -1.0) } - //CATransform3D transform = {a/i, d/i, 0, g/i, b/i, e/i, 0, h/i, 0, 0, 1, 0, c/i, f/i, 0, 1.0} let transform = CATransform3D(m11: a/i, m12: d/i, m13: 0, m14: g/i, m21: b/i, m22: e/i, m23: 0, m24: h/i, m31: 0, m32: 0, m33: 1, m34: 0, m41: c/i, m42: f/i, m43: 0, m44: 1.0) return transform } -open class SpaceWarpView: UIView { +func transformToFitQuad(frame: CGRect, topLeft tl: CGPoint, topRight tr: CGPoint, bottomLeft bl: CGPoint, bottomRight br: CGPoint) -> CATransform3D { + /*let boundingBox = UIView.boundingBox(forQuadWithTR: tr, tl: tl, bl: bl, br: br) + self.layer.transform = CATransform3DIdentity // keeps current transform from interfering + self.frame = boundingBox*/ + + let frameTopLeft = frame.origin + let transform = rectToQuad2( + rect: CGRect(origin: CGPoint(), size: frame.size), + quadTL: CGPoint(x: tl.x - frameTopLeft.x, y: tl.y - frameTopLeft.y), + quadTR: CGPoint(x: tr.x - frameTopLeft.x, y: tr.y - frameTopLeft.y), + quadBL: CGPoint(x: bl.x - frameTopLeft.x, y: bl.y - frameTopLeft.y), + quadBR: CGPoint(x: br.x - frameTopLeft.x, y: br.y - frameTopLeft.y) + ) + + // To account for anchor point, we must translate, transform, translate + let anchorPoint = frame.center + let anchorOffset = CGPoint(x: anchorPoint.x - frame.origin.x, y: anchorPoint.y - frame.origin.y) + let transPos = CATransform3DMakeTranslation(anchorOffset.x, anchorOffset.y, 0) + let transNeg = CATransform3DMakeTranslation(-anchorOffset.x, -anchorOffset.y, 0) + let fullTransform = CATransform3DConcat(CATransform3DConcat(transPos, transform), transNeg) + + return fullTransform +} + +private func boundingBox(forQuadWithTR tr: CGPoint, tl: CGPoint, bl: CGPoint, br: CGPoint) -> CGRect { + var boundingBox = CGRect.zero + + let xmin = min(min(min(tr.x, tl.x), bl.x), br.x) + let ymin = min(min(min(tr.y, tl.y), bl.y), br.y) + let xmax = max(max(max(tr.x, tl.x), bl.x), br.x) + let ymax = max(max(max(tr.y, tl.y), bl.y), br.y) + + boundingBox.origin.x = xmin + boundingBox.origin.y = ymin + boundingBox.size.width = xmax - xmin + boundingBox.size.height = ymax - ymin + + return boundingBox +} + +func rectToQuad2(rect: CGRect, quadTL topLeft: CGPoint, quadTR topRight: CGPoint, quadBL bottomLeft: CGPoint, quadBR bottomRight: CGPoint) -> CATransform3D { + return rectToQuad(rect: rect, quadTLX: topLeft.x, quadTLY: topLeft.y, quadTRX: topRight.x, quadTRY: topRight.y, quadBLX: bottomLeft.x, quadBLY: bottomLeft.y, quadBRX: bottomRight.x, quadBRY: bottomRight.y) +} + +private func rectToQuad(rect: CGRect, quadTLX x1a: CGFloat, quadTLY y1a: CGFloat, quadTRX x2a: CGFloat, quadTRY y2a: CGFloat, quadBLX x3a: CGFloat, quadBLY y3a: CGFloat, quadBRX x4a: CGFloat, quadBRY y4a: CGFloat) -> CATransform3D { + let X = rect.origin.x + let Y = rect.origin.y + let W = rect.size.width + let H = rect.size.height + + let y21 = y2a - y1a + let y32 = y3a - y2a + let y43 = y4a - y3a + let y14 = y1a - y4a + let y31 = y3a - y1a + let y42 = y4a - y2a + + let a = -H * (x2a * x3a * y14 + x2a * x4a * y31 - x1a * x4a * y32 + x1a * x3a * y42) + let b = W * (x2a * x3a * y14 + x3a * x4a * y21 + x1a * x4a * y32 + x1a * x2a * y43) + let c = H * X * (x2a * x3a * y14 + x2a * x4a * y31 - x1a * x4a * y32 + x1a * x3a * y42) - H * W * x1a * (x4a * y32 - x3a * y42 + x2a * y43) - W * Y * (x2a * x3a * y14 + x3a * x4a * y21 + x1a * x4a * y32 + x1a * x2a * y43) + + let d = H * (-x4a * y21 * y3a + x2a * y1a * y43 - x1a * y2a * y43 - x3a * y1a * y4a + x3a * y2a * y4a) + let e = W * (x4a * y2a * y31 - x3a * y1a * y42 - x2a * y31 * y4a + x1a * y3a * y42) + let f = -(W * (x4a * (Y * y2a * y31 + H * y1a * y32) - x3a * (H + Y) * y1a * y42 + H * x2a * y1a * y43 + x2a * Y * (y1a - y3a) * y4a + x1a * Y * y3a * (-y2a + y4a)) - H * X * (x4a * y21 * y3a - x2a * y1a * y43 + x3a * (y1a - y2a) * y4a + x1a * y2a * (-y3a + y4a))) + + let g = H * (x3a * y21 - x4a * y21 + (-x1a + x2a) * y43) + let h = W * (-x2a * y31 + x4a * y31 + (x1a - x3a) * y42) + var i = W * Y * (x2a * y31 - x4a * y31 - x1a * y42 + x3a * y42) + H * (X * (-(x3a * y21) + x4a * y21 + x1a * y43 - x2a * y43) + W * (-(x3a * y2a) + x4a * y2a + x2a * y3a - x4a * y3a - x2a * y4a + x3a * y4a)) + + let kEpsilon = 0.0001 + + if abs(i) < kEpsilon { + i = kEpsilon * (i > 0 ? 1.0 : -1.0) + } + + let transform = CATransform3D( + m11: a / i, m12: d / i, m13: 0, m14: g / i, + m21: b / i, m22: e / i, m23: 0, m24: h / i, + m31: 0, m32: 0, m33: 1, m34: 0, + m41: c / i, m42: f / i, m43: 0, m44: 1.0 + ) + + return transform +} + +public protocol SpaceWarpView: UIView { + var contentView: UIView { get } + + func trigger(at point: CGPoint) + func update(size: CGSize, transition: ComponentTransition) +} + +open class SpaceWarpView1: UIView, SpaceWarpView { private final class GridView: UIView { let cloneView: PortalView let gridPosition: CGPoint @@ -410,6 +454,598 @@ open class SpaceWarpView: UIView { } + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if self.alpha.isZero || self.isHidden || !self.isUserInteractionEnabled { + return nil + } + for view in self.contentView.subviews.reversed() { + if let result = view.hitTest(self.convert(point, to: view), with: event), result.isUserInteractionEnabled { + return result + } + } + + let result = super.hitTest(point, with: event) + if result != self { + return result + } else { + return nil + } + } +} + +open class SpaceWarpView2: UIView, SpaceWarpView { + public var contentView: UIView { + return self.contentViewImpl + } + + private let contentViewImpl: UIView + private var meshView: STCMeshView? + + private var link: SharedDisplayLinkDriver.Link? + private var startPoint: CGPoint? + + private var timeValue: CGFloat = 0.0 + + private var resolution: (x: Int, y: Int)? + private var size: CGSize? + + override public init(frame: CGRect) { + self.contentViewImpl = UIView() + + super.init(frame: frame) + + self.addSubview(self.contentView) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func trigger(at point: CGPoint) { + self.startPoint = point + self.timeValue = 0.0 + + if self.link == nil { + self.link = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] deltaTime in + guard let self else { + return + } + self.timeValue += deltaTime * (1.0 / CGFloat(UIView.animationDurationFactor())) + + if let size = self.size { + self.update(size: size, transition: .immediate) + } + }) + } + } + + private func updateGrid(resolutionX: Int, resolutionY: Int) { + if let resolution = self.resolution, resolution.x == resolutionX, resolution.y == resolutionY { + return + } + self.resolution = (resolutionX, resolutionY) + + if let meshView = self.meshView { + self.meshView = nil + meshView.removeFromSuperview() + self.contentViewImpl.removeFromSuperview() + } + + let meshView = STCMeshView(frame: CGRect()) + self.meshView = meshView + self.addSubview(meshView) + + meshView.instanceCount = resolutionX * resolutionY + + meshView.contentView.addSubview(self.contentViewImpl) + + /*for gridView in self.gridViews { + gridView.removeFromSuperview() + } + + var gridViews: [GridView] = [] + for y in 0 ..< resolutionY { + for x in 0 ..< resolutionX { + if let gridView = GridView(contentView: self.contentViewImpl, gridPosition: CGPoint(x: CGFloat(x) / CGFloat(resolutionX), y: CGFloat(y) / CGFloat(resolutionY))) { + gridView.isUserInteractionEnabled = false + gridViews.append(gridView) + self.addSubview(gridView) + } + } + } + self.gridViews = gridViews*/ + } + + public func update(size: CGSize, transition: ComponentTransition) { + self.size = size + if size.width <= 0.0 || size.height <= 0.0 { + return + } + + self.updateGrid(resolutionX: max(2, Int(size.width / 100.0)), resolutionY: max(2, Int(size.height / 100.0))) + guard let resolution = self.resolution, let meshView = self.meshView else { + return + } + + meshView.frame = CGRect(origin: CGPoint(), size: size) + + //let pixelStep = CGPoint(x: CGFloat(resolution.x) * 0.33, y: CGFloat(resolution.y) * 0.33) + let pixelStep = CGPoint() + let itemSize = CGSize(width: size.width / CGFloat(resolution.x), height: size.height / CGFloat(resolution.y)) + + let params = RippleParams(amplitude: 22.0, frequency: 15.0, decay: 8.0, speed: 1400.0) + + var instanceBounds: [CGRect] = [] + var instancePositions: [CGPoint] = [] + var instanceTransforms: [CATransform3D] = [] + + for y in 0 ..< resolution.y { + for x in 0 ..< resolution.x { + let gridPosition = CGPoint(x: CGFloat(x) / CGFloat(resolution.x), y: CGFloat(y) / CGFloat(resolution.y)) + + let sourceRect = CGRect(origin: CGPoint(x: gridPosition.x * (size.width + pixelStep.x), y: gridPosition.y * (size.height + pixelStep.y)), size: itemSize) + + instanceBounds.append(sourceRect) + instancePositions.append(sourceRect.center) + + //gridView.bounds = CGRect(origin: CGPoint(), size: sourceRect.size) + //gridView.update(containerSize: size, rect: sourceRect, transition: transition) + + let initialTopLeft = CGPoint(x: sourceRect.minX, y: sourceRect.minY) + let initialTopRight = CGPoint(x: sourceRect.maxX, y: sourceRect.minY) + let initialBottomLeft = CGPoint(x: sourceRect.minX, y: sourceRect.maxY) + let initialBottomRight = CGPoint(x: sourceRect.maxX, y: sourceRect.maxY) + + var topLeft = initialTopLeft + var topRight = initialTopRight + var bottomLeft = initialBottomLeft + var bottomRight = initialBottomRight + + if let startPoint = self.startPoint { + topLeft = transformCoordinate(position: topLeft, origin: startPoint, time: self.timeValue, params: params) + topRight = transformCoordinate(position: topRight, origin: startPoint, time: self.timeValue, params: params) + bottomLeft = transformCoordinate(position: bottomLeft, origin: startPoint, time: self.timeValue, params: params) + bottomRight = transformCoordinate(position: bottomRight, origin: startPoint, time: self.timeValue, params: params) + } + + let distanceTopLeft = length(topLeft - initialTopLeft) + let distanceTopRight = length(topRight - initialTopRight) + let distanceBottomLeft = length(bottomLeft - initialBottomLeft) + let distanceBottomRight = length(bottomRight - initialBottomRight) + var maxDistance = max(distanceTopLeft, distanceTopRight) + maxDistance = max(maxDistance, distanceBottomLeft) + maxDistance = max(maxDistance, distanceBottomRight) + + let transform = rectToQuad(rect: CGRect(origin: CGPoint(), size: itemSize), quadTL: topLeft - initialTopLeft, quadTR: topRight - initialTopLeft, quadBL: bottomLeft - initialTopLeft, quadBR: bottomRight - initialTopLeft) + instanceTransforms.append(transform) + + let isActive: Bool + if maxDistance <= 0.5 { + //gridView.layer.transform = CATransform3DIdentity + isActive = false + } else { + let _ = transform + //gridView.layer.transform = transform + isActive = true + } + let _ = isActive + } + } + + instanceBounds.withUnsafeMutableBufferPointer { buffer in + meshView.instanceBounds = buffer.baseAddress! + } + instancePositions.withUnsafeMutableBufferPointer { buffer in + meshView.instancePositions = buffer.baseAddress! + } + instanceTransforms.withUnsafeMutableBufferPointer { buffer in + meshView.instanceTransforms = buffer.baseAddress! + } + } + + + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if self.alpha.isZero || self.isHidden || !self.isUserInteractionEnabled { + return nil + } + for view in self.contentView.subviews.reversed() { + if let result = view.hitTest(self.convert(point, to: view), with: event), result.isUserInteractionEnabled { + return result + } + } + + let result = super.hitTest(point, with: event) + if result != self { + return result + } else { + return nil + } + } +} + +open class SpaceWarpView3: UIView, SpaceWarpView { + private final class GridView: UIView { + let cloneView: PortalView + let gridPosition: CGPoint + + init?(contentView: PortalSourceView, gridPosition: CGPoint) { + self.gridPosition = gridPosition + + guard let cloneView = PortalView(matchPosition: false) else { + return nil + } + self.cloneView = cloneView + + super.init(frame: CGRect()) + + self.layer.anchorPoint = CGPoint(x: 0.0, y: 0.0) + + self.clipsToBounds = true + self.isUserInteractionEnabled = false + self.addSubview(cloneView.view) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func updateIsActive(contentView: PortalSourceView, isActive: Bool) { + if isActive { + contentView.addPortal(view: self.cloneView) + } else { + contentView.removePortal(view: self.cloneView) + } + } + + func update(containerSize: CGSize, rect: CGRect, transition: ComponentTransition) { + transition.setFrame(view: self.cloneView.view, frame: CGRect(origin: CGPoint(x: -rect.minX - containerSize.width * 0.5, y: -rect.minY - containerSize.height * 0.5), size: CGSize(width: containerSize.width, height: containerSize.height))) + } + } + + private var gridViews: [GridView] = [] + + public var contentView: UIView { + return self.contentViewSource + } + + private let contentViewSource: UIView + private var currentCloneView: UIView? + private let contentViewImpl: PortalSourceView + + private var link: SharedDisplayLinkDriver.Link? + private var startPoint: CGPoint? + + private var timeValue: CGFloat = 0.0 + private var currentActiveViews: Int = 0 + + private var resolution: (x: Int, y: Int)? + private var size: CGSize? + + override public init(frame: CGRect) { + self.contentViewSource = UIView() + self.contentViewImpl = PortalSourceView() + + super.init(frame: frame) + + self.addSubview(self.contentViewSource) + self.addSubview(self.contentViewImpl) + + if self.link == nil { + self.link = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] deltaTime in + guard let self else { + return + } + self.timeValue += deltaTime * (1.0 / CGFloat(UIView.animationDurationFactor())) + + if let size = self.size { + self.update(size: size, transition: .immediate) + } + }) + } + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func trigger(at point: CGPoint) { + self.startPoint = point + self.timeValue = 0.0 + } + + private func updateGrid(resolutionX: Int, resolutionY: Int) { + if let resolution = self.resolution, resolution.x == resolutionX, resolution.y == resolutionY { + return + } + self.resolution = (resolutionX, resolutionY) + + for gridView in self.gridViews { + gridView.removeFromSuperview() + } + + var gridViews: [GridView] = [] + for y in 0 ..< resolutionY { + for x in 0 ..< resolutionX { + if let gridView = GridView(contentView: self.contentViewImpl, gridPosition: CGPoint(x: CGFloat(x) / CGFloat(resolutionX), y: CGFloat(y) / CGFloat(resolutionY))) { + gridView.isUserInteractionEnabled = false + gridView.isHidden = true + gridViews.append(gridView) + self.addSubview(gridView) + } + } + } + self.gridViews = gridViews + } + + public func update(size: CGSize, transition: ComponentTransition) { + if let currentCloneView = self.currentCloneView { + currentCloneView.removeFromSuperview() + self.currentCloneView = nil + } + if let cloneView = self.contentViewSource.resizableSnapshotView(from: CGRect(origin: CGPoint(), size: size), afterScreenUpdates: false, withCapInsets: UIEdgeInsets()) { + self.currentCloneView = cloneView + self.contentViewImpl.addSubview(cloneView) + } + + self.size = size + if size.width <= 0.0 || size.height <= 0.0 { + return + } + + self.updateGrid(resolutionX: max(2, Int(size.width / 50.0)), resolutionY: max(2, Int(size.height / 50.0))) + guard let resolution = self.resolution else { + return + } + + if self.timeValue >= 3.0 { + return + } + + let pixelStep = CGPoint() + let itemSize = CGSize(width: size.width / CGFloat(resolution.x), height: size.height / CGFloat(resolution.y)) + + let params = RippleParams(amplitude: 22.0, frequency: 15.0, decay: 8.0, speed: 1400.0) + + var activeViews = 0 + for gridView in self.gridViews { + let sourceRect = CGRect(origin: CGPoint(x: gridView.gridPosition.x * (size.width + pixelStep.x), y: gridView.gridPosition.y * (size.height + pixelStep.y)), size: itemSize) + + gridView.bounds = CGRect(origin: CGPoint(), size: sourceRect.size) + gridView.update(containerSize: size, rect: sourceRect, transition: transition) + + let initialTopLeft = CGPoint(x: sourceRect.minX, y: sourceRect.minY) + let initialTopRight = CGPoint(x: sourceRect.maxX, y: sourceRect.minY) + let initialBottomLeft = CGPoint(x: sourceRect.minX, y: sourceRect.maxY) + let initialBottomRight = CGPoint(x: sourceRect.maxX, y: sourceRect.maxY) + + var topLeft = initialTopLeft + var topRight = initialTopRight + var bottomLeft = initialBottomLeft + var bottomRight = initialBottomRight + + if let startPoint = self.startPoint { + topLeft = transformCoordinate(position: topLeft, origin: startPoint, time: self.timeValue, params: params) + topRight = transformCoordinate(position: topRight, origin: startPoint, time: self.timeValue, params: params) + bottomLeft = transformCoordinate(position: bottomLeft, origin: startPoint, time: self.timeValue, params: params) + bottomRight = transformCoordinate(position: bottomRight, origin: startPoint, time: self.timeValue, params: params) + } + + let distanceTopLeft = length(topLeft - initialTopLeft) + let distanceTopRight = length(topRight - initialTopRight) + let distanceBottomLeft = length(bottomLeft - initialBottomLeft) + let distanceBottomRight = length(bottomRight - initialBottomRight) + var maxDistance = max(distanceTopLeft, distanceTopRight) + maxDistance = max(maxDistance, distanceBottomLeft) + maxDistance = max(maxDistance, distanceBottomRight) + + let isActive: Bool + if maxDistance <= 0.5 { + gridView.layer.transform = CATransform3DIdentity + isActive = true + activeViews += 1 + } else { + let transform = rectToQuad(rect: CGRect(origin: CGPoint(), size: itemSize), quadTL: topLeft, quadTR: topRight, quadBL: bottomLeft, quadBR: bottomRight) + gridView.layer.transform = transform + isActive = true + activeViews += 1 + } + if gridView.isHidden != !isActive { + gridView.isHidden = !isActive + gridView.updateIsActive(contentView: self.contentViewImpl, isActive: isActive) + } + } + + if self.currentActiveViews != activeViews { + self.currentActiveViews = activeViews + #if DEBUG + print("SpaceWarpView: activeViews = \(activeViews)") + #endif + } + } + + + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if self.alpha.isZero || self.isHidden || !self.isUserInteractionEnabled { + return nil + } + for view in self.contentView.subviews.reversed() { + if let result = view.hitTest(self.convert(point, to: view), with: event), result.isUserInteractionEnabled { + return result + } + } + + let result = super.hitTest(point, with: event) + if result != self { + return result + } else { + return nil + } + } +} + +open class SpaceWarpView4: UIView, SpaceWarpView { + public var contentView: UIView { + return self.contentViewSource + } + + private let contentViewSource: UIView + private var currentCloneView: UIView? + private var meshView: STCMeshView? + private let fpsView: FPSView + + private var link: SharedDisplayLinkDriver.Link? + private var startPoint: CGPoint? + + private var timeValue: CGFloat = 0.0 + + private var resolution: (x: Int, y: Int)? + private var size: CGSize? + + override public init(frame: CGRect) { + self.contentViewSource = UIView() + self.fpsView = FPSView(frame: CGRect(origin: CGPoint(x: 4.0, y: 40.0), size: CGSize())) + + super.init(frame: frame) + + self.addSubview(self.contentViewSource) + self.addSubview(self.fpsView) + + if self.link == nil { + self.link = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] deltaTime in + guard let self else { + return + } + self.timeValue += deltaTime * (1.0 / CGFloat(UIView.animationDurationFactor())) + + if let size = self.size { + self.update(size: size, transition: .immediate) + } + }) + } + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func trigger(at point: CGPoint) { + self.startPoint = point + self.timeValue = 0.0 + } + + private func updateGrid(resolutionX: Int, resolutionY: Int) { + if let resolution = self.resolution, resolution.x == resolutionX, resolution.y == resolutionY { + return + } + self.resolution = (resolutionX, resolutionY) + + if let meshView = self.meshView { + self.meshView = nil + meshView.removeFromSuperview() + } + + let meshView = STCMeshView(frame: CGRect()) + self.meshView = meshView + self.insertSubview(meshView, aboveSubview: self.contentViewSource) + + meshView.instanceCount = resolutionX * resolutionY + } + + public func update(size: CGSize, transition: ComponentTransition) { + self.size = size + if size.width <= 0.0 || size.height <= 0.0 { + return + } + + self.fpsView.update() + + self.updateGrid(resolutionX: max(2, Int(size.width / 40.0)), resolutionY: max(2, Int(size.height / 40.0))) + guard let resolution = self.resolution, let meshView = self.meshView else { + return + } + + if let currentCloneView = self.currentCloneView { + currentCloneView.removeFromSuperview() + self.currentCloneView = nil + } + if let cloneView = self.contentViewSource.resizableSnapshotView(from: CGRect(origin: CGPoint(), size: size), afterScreenUpdates: false, withCapInsets: UIEdgeInsets()) { + self.currentCloneView = cloneView + meshView.contentView.addSubview(cloneView) + } + + meshView.frame = CGRect(origin: CGPoint(), size: size) + + let pixelStep = CGPoint() + let itemSize = CGSize(width: size.width / CGFloat(resolution.x), height: size.height / CGFloat(resolution.y)) + + let params = RippleParams(amplitude: 26.0, frequency: 15.0, decay: 8.0, speed: 1400.0) + + var instanceBounds: [CGRect] = [] + var instancePositions: [CGPoint] = [] + var instanceTransforms: [CATransform3D] = [] + + for y in 0 ..< resolution.y { + for x in 0 ..< resolution.x { + let gridPosition = CGPoint(x: CGFloat(x) / CGFloat(resolution.x), y: CGFloat(y) / CGFloat(resolution.y)) + + let sourceRect = CGRect(origin: CGPoint(x: gridPosition.x * (size.width + pixelStep.x), y: gridPosition.y * (size.height + pixelStep.y)), size: itemSize) + + instanceBounds.append(sourceRect) + instancePositions.append(sourceRect.center) + + let initialTopLeft = CGPoint(x: sourceRect.minX, y: sourceRect.minY) + let initialTopRight = CGPoint(x: sourceRect.maxX, y: sourceRect.minY) + let initialBottomLeft = CGPoint(x: sourceRect.minX, y: sourceRect.maxY) + let initialBottomRight = CGPoint(x: sourceRect.maxX, y: sourceRect.maxY) + + var topLeft = initialTopLeft + var topRight = initialTopRight + var bottomLeft = initialBottomLeft + var bottomRight = initialBottomRight + + if let startPoint = self.startPoint { + topLeft = transformCoordinate(position: topLeft, origin: startPoint, time: self.timeValue, params: params) + topRight = transformCoordinate(position: topRight, origin: startPoint, time: self.timeValue, params: params) + bottomLeft = transformCoordinate(position: bottomLeft, origin: startPoint, time: self.timeValue, params: params) + bottomRight = transformCoordinate(position: bottomRight, origin: startPoint, time: self.timeValue, params: params) + } + + let distanceTopLeft = length(topLeft - initialTopLeft) + let distanceTopRight = length(topRight - initialTopRight) + let distanceBottomLeft = length(bottomLeft - initialBottomLeft) + let distanceBottomRight = length(bottomRight - initialBottomRight) + var maxDistance = max(distanceTopLeft, distanceTopRight) + maxDistance = max(maxDistance, distanceBottomLeft) + maxDistance = max(maxDistance, distanceBottomRight) + + let transform = transformToFitQuad(frame: sourceRect, topLeft: topLeft, topRight: topRight, bottomLeft: bottomLeft, bottomRight: bottomRight) + instanceTransforms.append(transform) + + let isActive: Bool + if maxDistance <= 0.5 { + //gridView.layer.transform = CATransform3DIdentity + isActive = false + } else { + let _ = transform + //gridView.layer.transform = transform + isActive = true + } + let _ = isActive + } + } + + instanceBounds.withUnsafeMutableBufferPointer { buffer in + meshView.instanceBounds = buffer.baseAddress! + } + instancePositions.withUnsafeMutableBufferPointer { buffer in + meshView.instancePositions = buffer.baseAddress! + } + instanceTransforms.withUnsafeMutableBufferPointer { buffer in + meshView.instanceTransforms = buffer.baseAddress! + } + } + + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if self.alpha.isZero || self.isHidden || !self.isUserInteractionEnabled { return nil diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 3c84f133a8..6619f4d6cc 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -98,16 +98,17 @@ class ChatNodeContainer: ASDisplayNode { } } - override init() { + init(rippleEffect: Bool) { self.contentNodeImpl = ASDisplayNode() super.init() - #if DEBUG && false - self.setViewBlock({ - return SpaceWarpView(frame: CGRect()) - }) - #endif + if rippleEffect { + self.setViewBlock({ + return SpaceWarpView4(frame: CGRect()) + }) + self.contentNodeImpl.layer.allowsGroupOpacity = true + } (self.view as? SpaceWarpView)?.contentView.addSubnode(self.contentNodeImpl) } @@ -154,7 +155,7 @@ class HistoryNodeContainer: ASDisplayNode { #if DEBUG && false self.setViewBlock({ - return SpaceWarpView(frame: CGRect()) + return SpaceWarpView1(frame: CGRect()) }) #endif @@ -444,7 +445,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { self.backgroundNode = backgroundNode - self.contentContainerNode = ChatNodeContainer() + self.contentContainerNode = ChatNodeContainer(rippleEffect: context.sharedContext.immediateExperimentalUISettings.rippleEffect) self.contentDimNode = ASDisplayNode() self.contentDimNode.isUserInteractionEnabled = false self.contentDimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.2) diff --git a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift index c545871ff9..d5055c1b6d 100644 --- a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift +++ b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift @@ -38,7 +38,7 @@ public struct ExperimentalUISettings: Codable, Equatable { public var enableVoipTcp: Bool public var experimentalCompatibility: Bool public var enableDebugDataDisplay: Bool - public var acceleratedStickers: Bool + public var rippleEffect: Bool public var inlineStickers: Bool public var localTranscription: Bool public var enableReactionOverrides: Bool @@ -74,7 +74,7 @@ public struct ExperimentalUISettings: Codable, Equatable { enableVoipTcp: false, experimentalCompatibility: false, enableDebugDataDisplay: false, - acceleratedStickers: false, + rippleEffect: false, inlineStickers: false, localTranscription: false, enableReactionOverrides: false, @@ -111,7 +111,7 @@ public struct ExperimentalUISettings: Codable, Equatable { enableVoipTcp: Bool, experimentalCompatibility: Bool, enableDebugDataDisplay: Bool, - acceleratedStickers: Bool, + rippleEffect: Bool, inlineStickers: Bool, localTranscription: Bool, enableReactionOverrides: Bool, @@ -145,7 +145,7 @@ public struct ExperimentalUISettings: Codable, Equatable { self.enableVoipTcp = enableVoipTcp self.experimentalCompatibility = experimentalCompatibility self.enableDebugDataDisplay = enableDebugDataDisplay - self.acceleratedStickers = acceleratedStickers + self.rippleEffect = rippleEffect self.inlineStickers = inlineStickers self.localTranscription = localTranscription self.enableReactionOverrides = enableReactionOverrides @@ -183,7 +183,7 @@ public struct ExperimentalUISettings: Codable, Equatable { self.enableVoipTcp = (try container.decodeIfPresent(Int32.self, forKey: "enableVoipTcp") ?? 0) != 0 self.experimentalCompatibility = (try container.decodeIfPresent(Int32.self, forKey: "experimentalCompatibility") ?? 0) != 0 self.enableDebugDataDisplay = (try container.decodeIfPresent(Int32.self, forKey: "enableDebugDataDisplay") ?? 0) != 0 - self.acceleratedStickers = (try container.decodeIfPresent(Int32.self, forKey: "acceleratedStickers") ?? 0) != 0 + self.rippleEffect = (try container.decodeIfPresent(Int32.self, forKey: "rippleEffect") ?? 0) != 0 self.inlineStickers = (try container.decodeIfPresent(Int32.self, forKey: "inlineStickers") ?? 0) != 0 self.localTranscription = (try container.decodeIfPresent(Int32.self, forKey: "localTranscription") ?? 0) != 0 self.enableReactionOverrides = try container.decodeIfPresent(Bool.self, forKey: "enableReactionOverrides") ?? false @@ -221,7 +221,7 @@ public struct ExperimentalUISettings: Codable, Equatable { try container.encode((self.enableVoipTcp ? 1 : 0) as Int32, forKey: "enableVoipTcp") try container.encode((self.experimentalCompatibility ? 1 : 0) as Int32, forKey: "experimentalCompatibility") try container.encode((self.enableDebugDataDisplay ? 1 : 0) as Int32, forKey: "enableDebugDataDisplay") - try container.encode((self.acceleratedStickers ? 1 : 0) as Int32, forKey: "acceleratedStickers") + try container.encode((self.rippleEffect ? 1 : 0) as Int32, forKey: "rippleEffect") try container.encode((self.inlineStickers ? 1 : 0) as Int32, forKey: "inlineStickers") try container.encode((self.localTranscription ? 1 : 0) as Int32, forKey: "localTranscription") try container.encode(self.enableReactionOverrides, forKey: "enableReactionOverrides") From df31800dcd0d6492d73f32776c6f8e88b132809a Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Thu, 25 Jul 2024 14:52:20 +0800 Subject: [PATCH 07/41] Bot preview improvements --- .../ChatListUI/Sources/ChatContextMenus.swift | 3 + .../Sources/ChatListSearchContainerNode.swift | 4 ++ .../Sources/ChatListSearchListPaneNode.swift | 9 ++- .../ChatListSearchPaneContainerNode.swift | 13 ++++ .../Sources/SparseItemGrid.swift | 7 +- .../PeerInfoScreenLabeledValueItem.swift | 6 +- .../Sources/PeerInfoScreen.swift | 66 ++++++++++--------- .../Sources/PeerInfoStoryPaneNode.swift | 55 +++++++++++----- .../StoryItemSetContainerComponent.swift | 11 ---- 9 files changed, 108 insertions(+), 66 deletions(-) diff --git a/submodules/ChatListUI/Sources/ChatContextMenus.swift b/submodules/ChatListUI/Sources/ChatContextMenus.swift index b20d77d6f0..b710deca99 100644 --- a/submodules/ChatListUI/Sources/ChatContextMenus.swift +++ b/submodules/ChatListUI/Sources/ChatContextMenus.swift @@ -149,10 +149,13 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch }) }))) items.append(.separator) + case .popularApps: + break } } if case .search(.recentApps) = source { + } else if case .search(.popularApps) = source { } else { let isSavedMessages = peerId == context.account.peerId diff --git a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift index 910d42bf28..5538521545 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift @@ -330,6 +330,10 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo } } + self.paneContainerNode.requesDismissInput = { + parentController()?.view.endEditing(true) + } + self.filterContainerNode.filterPressed = { [weak self] filter in guard let strongSelf = self else { return diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 55aca75b28..57c0091630 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -298,13 +298,17 @@ private enum ChatListRecentEntry: Comparable, Identifiable { } }, deletePeer: deletePeer, - contextAction: (key == .channels) ? nil : peerContextAction.flatMap { peerContextAction in + contextAction: (key == .channels || section == .popularApps) ? nil : peerContextAction.flatMap { peerContextAction in return { node, gesture, location in if let chatPeer = peer.peer.peers[peer.peer.peerId] { let source: ChatListSearchContextActionSource if key == .apps { - source = .recentApps + if case .popularApps = section { + source = .popularApps + } else { + source = .recentApps + } } else { source = .recentSearch } @@ -1081,6 +1085,7 @@ public enum ChatListSearchContextActionSource { case recentPeers case recentSearch case recentApps + case popularApps case search(EngineMessage.Id?) } diff --git a/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift index ccf0563396..28bc9b2f8b 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift @@ -192,6 +192,7 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, ASGestureRecognizerD var currentPaneUpdated: ((ChatListSearchPaneKey?, CGFloat, ContainedViewLayoutTransition) -> Void)? var requestExpandTabs: (() -> Bool)? + var requesDismissInput: (() -> Void)? private var currentAvailablePanes: [ChatListSearchPaneKey]? @@ -227,12 +228,20 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, ASGestureRecognizerD if let (size, sideInset, bottomInset, visibleHeight, presentationData, availablePanes) = self.currentParams { self.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, presentationData: presentationData, availablePanes: availablePanes, transition: .animated(duration: 0.4, curve: .spring)) } + + if case .apps = key { + self.requesDismissInput?() + } } else if self.pendingSwitchToPaneKey != key { self.pendingSwitchToPaneKey = key if let (size, sideInset, bottomInset, visibleHeight, presentationData, availablePanes) = self.currentParams { self.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, presentationData: presentationData, availablePanes: availablePanes, transition: .animated(duration: 0.4, curve: .spring)) } + + if case .apps = key { + self.requesDismissInput?() + } } } @@ -322,6 +331,10 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, ASGestureRecognizerD let switchToKey = availablePanes[updatedIndex] if switchToKey != self.currentPaneKey && self.currentPanes[switchToKey] != nil{ self.currentPaneKey = switchToKey + + if case .apps = switchToKey { + self.requesDismissInput?() + } } } self.transitionFraction = 0.0 diff --git a/submodules/SparseItemGrid/Sources/SparseItemGrid.swift b/submodules/SparseItemGrid/Sources/SparseItemGrid.swift index de00d411bd..58dd462609 100644 --- a/submodules/SparseItemGrid/Sources/SparseItemGrid.swift +++ b/submodules/SparseItemGrid/Sources/SparseItemGrid.swift @@ -462,7 +462,7 @@ public final class SparseItemGrid: ASDisplayNode { self.itemSpacing = 1.0 let itemsPerRow: CGFloat - if containerLayout.fixedItemAspect != nil && itemCount <= 2 { + if containerLayout.fixedItemAspect != nil && itemCount <= 2 && containerLayout.adjustForSmallCount { itemsPerRow = 2.0 centerItems = itemCount == 1 } else { @@ -1607,6 +1607,7 @@ public final class SparseItemGrid: ASDisplayNode { var lockScrollingAtTop: Bool var fixedItemHeight: CGFloat? var fixedItemAspect: CGFloat? + var adjustForSmallCount: Bool } private var tapRecognizer: UITapGestureRecognizer? @@ -1932,7 +1933,7 @@ public final class SparseItemGrid: ASDisplayNode { } } - public func update(size: CGSize, insets: UIEdgeInsets, useSideInsets: Bool, scrollIndicatorInsets: UIEdgeInsets, lockScrollingAtTop: Bool, fixedItemHeight: CGFloat?, fixedItemAspect: CGFloat?, items: Items, theme: PresentationTheme, synchronous: SparseItemGrid.Synchronous, transition: ComponentTransition = .immediate) { + public func update(size: CGSize, insets: UIEdgeInsets, useSideInsets: Bool, scrollIndicatorInsets: UIEdgeInsets, lockScrollingAtTop: Bool, fixedItemHeight: CGFloat?, fixedItemAspect: CGFloat?, adjustForSmallCount: Bool = true, items: Items, theme: PresentationTheme, synchronous: SparseItemGrid.Synchronous, transition: ComponentTransition = .immediate) { self.theme = theme var headerInset: CGFloat = 0.0 @@ -1973,7 +1974,7 @@ public final class SparseItemGrid: ASDisplayNode { var insets = insets insets.top += headerInset - let containerLayout = ContainerLayout(size: size, insets: insets, useSideInsets: useSideInsets, scrollIndicatorInsets: scrollIndicatorInsets, lockScrollingAtTop: lockScrollingAtTop, fixedItemHeight: fixedItemHeight, fixedItemAspect: fixedItemAspect) + let containerLayout = ContainerLayout(size: size, insets: insets, useSideInsets: useSideInsets, scrollIndicatorInsets: scrollIndicatorInsets, lockScrollingAtTop: lockScrollingAtTop, fixedItemHeight: fixedItemHeight, fixedItemAspect: fixedItemAspect, adjustForSmallCount: adjustForSmallCount) self.containerLayout = containerLayout self.items = items self.scrollingArea.isHidden = lockScrollingAtTop diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenLabeledValueItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenLabeledValueItem.swift index 01056e97dc..344ef5873f 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenLabeledValueItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenLabeledValueItem.swift @@ -77,7 +77,7 @@ final class PeerInfoScreenLabeledValueItem: PeerInfoScreenItem { let iconAction: (() -> Void)? let button: Button? let contextAction: ((ASDisplayNode, ContextGesture?, CGPoint?) -> Void)? - let requestLayout: () -> Void + let requestLayout: (Bool) -> Void init( id: AnyHashable, @@ -95,7 +95,7 @@ final class PeerInfoScreenLabeledValueItem: PeerInfoScreenItem { iconAction: (() -> Void)? = nil, button: Button? = nil, contextAction: ((ASDisplayNode, ContextGesture?, CGPoint?) -> Void)? = nil, - requestLayout: @escaping () -> Void + requestLayout: @escaping (Bool) -> Void ) { self.id = id self.context = context @@ -336,7 +336,7 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode { @objc private func expandPressed() { self.isExpanded = true - self.item?.requestLayout() + self.item?.requestLayout(true) } @objc private func iconPressed() { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 5f768653d3..c63e6a276f 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -1273,8 +1273,8 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese interaction.openPhone(phone, node, nil, progress) }, longTapAction: nil, contextAction: { node, gesture, _ in interaction.openPhone(phone, node, gesture, nil) - }, requestLayout: { - interaction.requestLayout(false) + }, requestLayout: { animated in + interaction.requestLayout(animated) })) } if let mainUsername = user.addressName { @@ -1304,8 +1304,8 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese interaction.openQrCode() }, contextAction: { node, gesture, _ in interaction.openUsernameContextMenu(node, gesture) - }, requestLayout: { - interaction.requestLayout(false) + }, requestLayout: { animated in + interaction.requestLayout(animated) } ) ) @@ -1332,7 +1332,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese items[currentPeerInfoSection]!.append(PeerInfoScreenLabeledValueItem(id: 400, context: context, label: hasBirthdayToday ? presentationData.strings.UserInfo_BirthdayToday : presentationData.strings.UserInfo_Birthday, text: stringForCompactBirthday(birthday, strings: presentationData.strings, showAge: true), textColor: .primary, leftIcon: hasBirthdayToday ? .birthday : nil, icon: hasBirthdayToday ? .premiumGift : nil, action: birthdayAction, longTapAction: nil, iconAction: { interaction.openPremiumGift() - }, contextAction: birthdayContextAction, requestLayout: { + }, contextAction: birthdayContextAction, requestLayout: { _ in })) } @@ -1347,12 +1347,12 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese } if user.isFake { - items[currentPeerInfoSection]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: "", text: user.botInfo != nil ? presentationData.strings.UserInfo_FakeBotWarning : presentationData.strings.UserInfo_FakeUserWarning, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: user.botInfo != nil ? enabledPrivateBioEntities : []), action: nil, requestLayout: { - interaction.requestLayout(false) + items[currentPeerInfoSection]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: "", text: user.botInfo != nil ? presentationData.strings.UserInfo_FakeBotWarning : presentationData.strings.UserInfo_FakeUserWarning, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: user.botInfo != nil ? enabledPrivateBioEntities : []), action: nil, requestLayout: { animated in + interaction.requestLayout(animated) })) } else if user.isScam { - items[currentPeerInfoSection]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: user.botInfo == nil ? presentationData.strings.Profile_About : presentationData.strings.Profile_BotInfo, text: user.botInfo != nil ? presentationData.strings.UserInfo_ScamBotWarning : presentationData.strings.UserInfo_ScamUserWarning, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: user.botInfo != nil ? enabledPrivateBioEntities : []), action: nil, requestLayout: { - interaction.requestLayout(false) + items[currentPeerInfoSection]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: user.botInfo == nil ? presentationData.strings.Profile_About : presentationData.strings.Profile_BotInfo, text: user.botInfo != nil ? presentationData.strings.UserInfo_ScamBotWarning : presentationData.strings.UserInfo_ScamUserWarning, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: user.botInfo != nil ? enabledPrivateBioEntities : []), action: nil, requestLayout: { animated in + interaction.requestLayout(animated) })) } else if hasAbout || hasWebApp { var actionButton: PeerInfoScreenLabeledValueItem.Button? @@ -1385,8 +1385,8 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese items[currentPeerInfoSection]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: label, text: cachedData.about ?? "", textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: user.isPremium ? enabledPublicBioEntities : enabledPrivateBioEntities), action: isMyProfile ? { node, _ in bioContextAction(node, nil, nil) - } : nil, linkItemAction: bioLinkAction, button: actionButton, contextAction: bioContextAction, requestLayout: { - interaction.requestLayout(false) + } : nil, linkItemAction: bioLinkAction, button: actionButton, contextAction: bioContextAction, requestLayout: { animated in + interaction.requestLayout(animated) })) if let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) { @@ -1579,8 +1579,8 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese } }, iconAction: { interaction.openQrCode() - }, requestLayout: { - interaction.requestLayout(false) + }, requestLayout: { animated in + interaction.requestLayout(animated) } ) ) @@ -1636,8 +1636,8 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese } }, iconAction: { interaction.openQrCode() - }, requestLayout: { - interaction.requestLayout(false) + }, requestLayout: { animated in + interaction.requestLayout(animated) } ) ) @@ -1669,8 +1669,8 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese } items[currentPeerInfoSection]!.append(PeerInfoScreenLabeledValueItem(id: ItemAbout, label: presentationData.strings.Channel_Info_Description, text: aboutText, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledEntities), action: isMyProfile ? { node, _ in bioContextAction(node, nil, nil) - } : nil, linkItemAction: bioLinkAction, contextAction: bioContextAction, requestLayout: { - interaction.requestLayout(true) + } : nil, linkItemAction: bioLinkAction, contextAction: bioContextAction, requestLayout: { animated in + interaction.requestLayout(animated) })) } @@ -1755,8 +1755,8 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese if let aboutText = aboutText { items[currentPeerInfoSection]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: presentationData.strings.Channel_Info_Description, text: aboutText, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledPrivateBioEntities), action: isMyProfile ? { node, _ in bioContextAction(node, nil, nil) - } : nil, linkItemAction: bioLinkAction, contextAction: bioContextAction, requestLayout: { - interaction.requestLayout(true) + } : nil, linkItemAction: bioLinkAction, contextAction: bioContextAction, requestLayout: { animated in + interaction.requestLayout(animated) })) } } @@ -10916,19 +10916,21 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }))) } - items.append(.action(ContextMenuActionItem(text: "Reorder", icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor) - }, action: { [weak pane] _, a in - if ignoreNextActions { - return - } - ignoreNextActions = true - a(.default) - - if let pane { - pane.beginReordering() - } - }))) + if pane.canReorder() { + items.append(.action(ContextMenuActionItem(text: "Reorder", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor) + }, action: { [weak pane] _, a in + if ignoreNextActions { + return + } + ignoreNextActions = true + a(.default) + + if let pane { + pane.beginReordering() + } + }))) + } items.append(.action(ContextMenuActionItem(text: "Select", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift index 2c0cbe7c40..791dae59df 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift @@ -2495,7 +2495,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } self.parentController?.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: isPinned ? "anim_toastunpin" : "anim_toastpin", scale: 0.06, colors: [:], title: toastTitle, text: toastText, customUndoText: nil, timeout: 5), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) }))) - if isPinned { + if isPinned && self.canReorder() { //TODO:localize items.append(.action(ContextMenuActionItem(text: "Reorder", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in c?.dismiss(completion: { @@ -2560,7 +2560,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr }))) } - if canManage, case .botPreview = self.scope { + if canManage, case .botPreview = self.scope, self.canReorder() { //TODO:localize items.append(.action(ContextMenuActionItem(text: "Reorder", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in c?.dismiss(completion: { @@ -2571,17 +2571,6 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr self.beginReordering() }) }))) - - //TODO:localize - items.append(.action(ContextMenuActionItem(text: "Edit Preview", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in - c?.dismiss(completion: { - guard let self else { - return - } - - let _ = self - }) - }))) } if !item.isForwardingDisabled, case .everyone = item.privacy?.base { @@ -2791,8 +2780,32 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } } botPreviewLanguages.sort(by: { $0.name < $1.name }) + + var hadLocalItems = false + if let currentListState = self.currentListState { + for item in currentListState.items { + if item.storyItem.isPending { + hadLocalItems = true + } + } + } + self.currentListState = state + var hasLocalItems = false + if let currentListState = self.currentListState { + for item in currentListState.items { + if item.storyItem.isPending { + hasLocalItems = true + } + } + } + + var synchronous = synchronous + if hasLocalItems != hadLocalItems { + synchronous = true + } + self.updateItemsFromState(state: state, firstTime: firstTime, reloadAtTop: reloadAtTop, synchronous: synchronous, animated: false) if self.currentBotPreviewLanguages != botPreviewLanguages || reloadAtTop { @@ -4113,9 +4126,14 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr fixedItemHeight = nil let fixedItemAspect: CGFloat? = 0.81 + + var adjustForSmallCount = true + if case .botPreview = self.scope { + adjustForSmallCount = false + } - self.itemGrid.pinchEnabled = items.count > 2 - self.itemGrid.update(size: size, insets: UIEdgeInsets(top: gridTopInset, left: sideInset, bottom: bottomInset, right: sideInset), useSideInsets: !isList, scrollIndicatorInsets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), lockScrollingAtTop: isScrollingLockedAtTop, fixedItemHeight: fixedItemHeight, fixedItemAspect: fixedItemAspect, items: items, theme: self.itemGridBinding.chatPresentationData.theme.theme, synchronous: wasFirstTime ? .full : .none, transition: animateGridItems ? .spring(duration: 0.35) : .immediate) + self.itemGrid.pinchEnabled = items.count > 2 && !self.isReordering + self.itemGrid.update(size: size, insets: UIEdgeInsets(top: gridTopInset, left: sideInset, bottom: bottomInset, right: sideInset), useSideInsets: !isList, scrollIndicatorInsets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), lockScrollingAtTop: isScrollingLockedAtTop, fixedItemHeight: fixedItemHeight, fixedItemAspect: fixedItemAspect, adjustForSmallCount: adjustForSmallCount, items: items, theme: self.itemGridBinding.chatPresentationData.theme.theme, synchronous: wasFirstTime ? .full : .none, transition: animateGridItems ? .spring(duration: 0.35) : .immediate) } if case .botPreview = self.scope, self.canManageStories { @@ -4212,6 +4230,13 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr return items.count < self.maxBotPreviewCount } + public func canReorder() -> Bool { + guard let items = self.items else { + return false + } + return items.count > 1 + } + private func presentAddBotPreviewLanguage() { self.parentController?.push(LanguageSelectionScreen(context: self.context, selectLocalization: { [weak self] info in guard let self else { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index e027cd2c7f..a1f5b25732 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -6616,17 +6616,6 @@ public final class StoryItemSetContainerComponent: Component { component.reorder() }))) - items.append(.action(ContextMenuActionItem(text: "Edit Preview", icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, a in - a(.default) - - guard let self, let component = self.component else { - return - } - - let _ = component - }))) items.append(.action(ContextMenuActionItem(text: "Delete", textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.destructiveColor) }, action: { [weak self] _, a in From 606e33607af9c6d56745efcbc5abf90b12a70df2 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 25 Jul 2024 14:39:07 +0200 Subject: [PATCH 08/41] Browser improvements --- .../Sources/BrowserAddressBarComponent.swift | 13 +- .../Sources/BrowserAddressListComponent.swift | 2 +- .../BrowserUI/Sources/BrowserContent.swift | 4 +- .../Sources/BrowserDocumentContent.swift | 6 +- .../Sources/BrowserInstantPageContent.swift | 5 + .../BrowserNavigationBarComponent.swift | 23 +- .../BrowserUI/Sources/BrowserPdfContent.swift | 5 + .../BrowserUI/Sources/BrowserScreen.swift | 23 +- .../BrowserUI/Sources/BrowserWebContent.swift | 98 +++++- submodules/BrowserUI/Sources/Punycode.swift | 317 ++++++++++++++++++ .../Resources/WebEmbed/UIWebViewSearch.js | 9 +- submodules/TelegramUI/Sources/OpenUrl.swift | 7 +- 12 files changed, 483 insertions(+), 29 deletions(-) create mode 100644 submodules/BrowserUI/Sources/Punycode.swift diff --git a/submodules/BrowserUI/Sources/BrowserAddressBarComponent.swift b/submodules/BrowserUI/Sources/BrowserAddressBarComponent.swift index 679f3ea5c3..c326a203d5 100644 --- a/submodules/BrowserUI/Sources/BrowserAddressBarComponent.swift +++ b/submodules/BrowserUI/Sources/BrowserAddressBarComponent.swift @@ -206,9 +206,9 @@ final class AddressBarContentComponent: Component { self.activated(true) if let textField = self.textField { textField.becomeFirstResponder() - Queue.mainQueue().justDispatch { + Queue.mainQueue().after(0.3, { textField.selectAll(nil) - } + }) } } @@ -238,7 +238,9 @@ final class AddressBarContentComponent: Component { public func textFieldShouldReturn(_ textField: UITextField) -> Bool { if let component = self.component { - component.performAction.invoke(.navigateTo(explicitUrl(textField.text ?? ""))) + let finalUrl = explicitUrl(textField.text ?? "") +// finalUrl = finalUrl.addingPercentEncoding(withAllowedCharacters: .urlFragmentAllowed) ?? finalUrl + component.performAction.invoke(.navigateTo(finalUrl)) } textField.endEditing(true) return false @@ -273,6 +275,10 @@ final class AddressBarContentComponent: Component { var title: String = "" if let parsedUrl = URL(string: component.url) { title = parsedUrl.host ?? component.url + if title.hasPrefix("www.") { + title.removeSubrange(title.startIndex ..< title.index(title.startIndex, offsetBy: 4)) + } + title = title.idnaDecoded ?? title } self.update(theme: component.theme, strings: component.strings, size: availableSize, isActive: isActive, title: title.lowercased(), isSecure: component.isSecure, collapseFraction: collapseFraction, transition: transition) @@ -333,6 +339,7 @@ final class AddressBarContentComponent: Component { transition.setFrame(layer: self.backgroundLayer, frame: backgroundFrame) transition.setFrame(view: self.cancelButton, frame: CGRect(origin: CGPoint(x: backgroundFrame.maxX, y: 0.0), size: CGSize(width: cancelButtonSpacing + cancelTextSize.width, height: size.height))) + self.cancelButton.isUserInteractionEnabled = isActiveWithText let textX: CGFloat = backgroundFrame.minX + sideInset let textFrame = CGRect(origin: CGPoint(x: textX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.maxX - textX, height: backgroundFrame.height)) diff --git a/submodules/BrowserUI/Sources/BrowserAddressListComponent.swift b/submodules/BrowserUI/Sources/BrowserAddressListComponent.swift index b33621c4ee..34c91738e7 100644 --- a/submodules/BrowserUI/Sources/BrowserAddressListComponent.swift +++ b/submodules/BrowserUI/Sources/BrowserAddressListComponent.swift @@ -148,7 +148,7 @@ final class BrowserAddressListComponent: Component { } func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { - self.endEditing(true) + self.window?.endEditing(true) } private func updateScrolling(transition: ComponentTransition) { diff --git a/submodules/BrowserUI/Sources/BrowserContent.swift b/submodules/BrowserUI/Sources/BrowserContent.swift index 7096b83042..f9f1687d93 100644 --- a/submodules/BrowserUI/Sources/BrowserContent.swift +++ b/submodules/BrowserUI/Sources/BrowserContent.swift @@ -162,12 +162,14 @@ protocol BrowserContent: UIView { var present: (ViewController, Any?) -> Void { get set } var presentInGlobalOverlay: (ViewController) -> Void { get set } var getNavigationController: () -> NavigationController? { get set } + var openAppUrl: (String) -> Void { get set } var minimize: () -> Void { get set } var close: () -> Void { get set } var onScrollingUpdate: (ContentScrollingUpdate) -> Void { get set } - + func resetScrolling() + func reload() func stop() diff --git a/submodules/BrowserUI/Sources/BrowserDocumentContent.swift b/submodules/BrowserUI/Sources/BrowserDocumentContent.swift index 26b1d11e7f..1fae1b7ad3 100644 --- a/submodules/BrowserUI/Sources/BrowserDocumentContent.swift +++ b/submodules/BrowserUI/Sources/BrowserDocumentContent.swift @@ -17,7 +17,6 @@ import ShareController import UndoUI import UrlEscaping - final class BrowserDocumentContent: UIView, BrowserContent, WKNavigationDelegate, WKUIDelegate, UIScrollViewDelegate { private let context: AccountContext private var presentationData: PresentationData @@ -37,6 +36,7 @@ final class BrowserDocumentContent: UIView, BrowserContent, WKNavigationDelegate } var pushContent: (BrowserScreen.Subject) -> Void = { _ in } + var openAppUrl: (String) -> Void = { _ in } var onScrollingUpdate: (ContentScrollingUpdate) -> Void = { _ in } var minimize: () -> Void = { } var close: () -> Void = { } @@ -360,6 +360,10 @@ final class BrowserDocumentContent: UIView, BrowserContent, WKNavigationDelegate } } + func resetScrolling() { + self.updateScrollingOffset(isReset: true, transition: .spring(duration: 0.4)) + } + func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) { // self.currentError = nil self.updateFontState(self.currentFontState, force: true) diff --git a/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift b/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift index 29bed73a96..a9972dbe90 100644 --- a/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift +++ b/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift @@ -66,6 +66,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg var currentAccessibilityAreas: [AccessibilityAreaNode] = [] var pushContent: (BrowserScreen.Subject) -> Void = { _ in } + var openAppUrl: (String) -> Void = { _ in } var onScrollingUpdate: (ContentScrollingUpdate) -> Void = { _ in } var minimize: () -> Void = { } var close: () -> Void = { } @@ -766,6 +767,10 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg self.readingProgress.set(readingProgress) } + func resetScrolling() { + self.updateScrollingOffset(isReset: true, transition: .spring(duration: 0.4)) + } + private func scrollableContentOffset(item: InstantPageScrollableItem) -> CGPoint { var contentOffset = CGPoint() for (_, itemNode) in self.visibleItemsWithNodes { diff --git a/submodules/BrowserUI/Sources/BrowserNavigationBarComponent.swift b/submodules/BrowserUI/Sources/BrowserNavigationBarComponent.swift index d31d7fd6a1..e153a02294 100644 --- a/submodules/BrowserUI/Sources/BrowserNavigationBarComponent.swift +++ b/submodules/BrowserUI/Sources/BrowserNavigationBarComponent.swift @@ -35,6 +35,7 @@ final class BrowserNavigationBarComponent: CombinedComponent { let readingProgress: CGFloat let loadingProgress: Double? let collapseFraction: CGFloat + let activate: () -> Void init( backgroundColor: UIColor, @@ -50,7 +51,8 @@ final class BrowserNavigationBarComponent: CombinedComponent { centerItem: AnyComponentWithIdentity?, readingProgress: CGFloat, loadingProgress: Double?, - collapseFraction: CGFloat + collapseFraction: CGFloat, + activate: @escaping () -> Void ) { self.backgroundColor = backgroundColor self.separatorColor = separatorColor @@ -66,6 +68,7 @@ final class BrowserNavigationBarComponent: CombinedComponent { self.readingProgress = readingProgress self.loadingProgress = loadingProgress self.collapseFraction = collapseFraction + self.activate = activate } static func ==(lhs: BrowserNavigationBarComponent, rhs: BrowserNavigationBarComponent) -> Bool { @@ -122,6 +125,7 @@ final class BrowserNavigationBarComponent: CombinedComponent { let leftItems = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self) let rightItems = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self) let centerItems = ChildMap(environment: BrowserNavigationBarEnvironment.self, keyedBy: AnyHashable.self) + let activate = Child(Button.self) return { context in var availableWidth = context.availableSize.width @@ -266,6 +270,23 @@ final class BrowserNavigationBarComponent: CombinedComponent { ) } + if context.component.collapseFraction == 1.0 { + let activateAction = context.component.activate + let activate = activate.update( + component: Button( + content: AnyComponent(Rectangle(color: UIColor(rgb: 0x000000, alpha: 0.001))), + action: { + activateAction() + } + ), + availableSize: size, + transition: .immediate + ) + context.add(activate + .position(CGPoint(x: size.width / 2.0, y: size.height / 2.0)) + ) + } + return size } } diff --git a/submodules/BrowserUI/Sources/BrowserPdfContent.swift b/submodules/BrowserUI/Sources/BrowserPdfContent.swift index 3bd36c1cf6..e037f45663 100644 --- a/submodules/BrowserUI/Sources/BrowserPdfContent.swift +++ b/submodules/BrowserUI/Sources/BrowserPdfContent.swift @@ -38,6 +38,7 @@ final class BrowserPdfContent: UIView, BrowserContent, WKNavigationDelegate, WKU } var pushContent: (BrowserScreen.Subject) -> Void = { _ in } + var openAppUrl: (String) -> Void = { _ in } var onScrollingUpdate: (ContentScrollingUpdate) -> Void = { _ in } var minimize: () -> Void = { } var close: () -> Void = { } @@ -352,6 +353,10 @@ final class BrowserPdfContent: UIView, BrowserContent, WKNavigationDelegate, WKU } } + func resetScrolling() { + self.updateScrollingOffset(isReset: true, transition: .spring(duration: 0.4)) + } + func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) { // self.currentError = nil self.updateFontState(self.currentFontState, force: true) diff --git a/submodules/BrowserUI/Sources/BrowserScreen.swift b/submodules/BrowserUI/Sources/BrowserScreen.swift index daaad7f05c..b4ca2e6f30 100644 --- a/submodules/BrowserUI/Sources/BrowserScreen.swift +++ b/submodules/BrowserUI/Sources/BrowserScreen.swift @@ -188,7 +188,10 @@ private final class BrowserScreenComponent: CombinedComponent { centerItem: navigationContent, readingProgress: context.component.contentState?.readingProgress ?? 0.0, loadingProgress: context.component.contentState?.estimatedProgress, - collapseFraction: collapseFraction + collapseFraction: collapseFraction, + activate: { + performAction.invoke(.expand) + } ), availableSize: context.availableSize, transition: context.transition @@ -267,6 +270,7 @@ private final class BrowserScreenComponent: CombinedComponent { ) context.add(addressList .position(CGPoint(x: context.availableSize.width / 2.0, y: navigationBar.size.height + addressList.size.height / 2.0)) + .clipsToBounds(true) .appear(.default(alpha: true)) .disappear(.default(alpha: true)) ) @@ -314,6 +318,7 @@ public class BrowserScreen: ViewController, MinimizableController { case openAddressBar case closeAddressBar case navigateTo(String) + case expand } fileprivate final class Node: ViewControllerTracingNode { @@ -568,6 +573,10 @@ public class BrowserScreen: ViewController, MinimizableController { updatedState.addressFocused = false return updatedState }) + case .expand: + if let content = self.content.last { + content.resetScrolling() + } } } @@ -626,6 +635,14 @@ public class BrowserScreen: ViewController, MinimizableController { } self.pushContent(content, transition: .spring(duration: 0.4)) } + browserContent.openAppUrl = { [weak self] url in + guard let self else { + return + } + self.context.sharedContext.openExternalUrl(context: self.context, urlContext: .generic, url: url, forceExternal: false, presentationData: self.presentationData, navigationController: self.controller?.navigationController as? NavigationController, dismissInput: { [weak self] in + self?.view.window?.endEditing(true) + }) + } browserContent.present = { [weak self] c, a in guard let self, let controller = self.controller else { return @@ -989,6 +1006,10 @@ public class BrowserScreen: ViewController, MinimizableController { } } + if update.isReset { + scrollingPanelOffsetFraction = 0.0 + } + if scrollingPanelOffsetFraction != self.scrollingPanelOffsetFraction { self.scrollingPanelOffsetFraction = scrollingPanelOffsetFraction self.requestLayout(transition: transition) diff --git a/submodules/BrowserUI/Sources/BrowserWebContent.swift b/submodules/BrowserUI/Sources/BrowserWebContent.swift index ba252fbbf7..58e21e3b0c 100644 --- a/submodules/BrowserUI/Sources/BrowserWebContent.swift +++ b/submodules/BrowserUI/Sources/BrowserWebContent.swift @@ -18,6 +18,7 @@ import UndoUI import LottieComponent import MultilineTextComponent import UrlEscaping +import UrlHandling private final class TonSchemeHandler: NSObject, WKURLSchemeHandler { private final class PendingTask { @@ -139,6 +140,7 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU private let faviconDisposable = MetaDisposable() var pushContent: (BrowserScreen.Subject) -> Void = { _ in } + var openAppUrl: (String) -> Void = { _ in } var onScrollingUpdate: (ContentScrollingUpdate) -> Void = { _ in } var minimize: () -> Void = { } var close: () -> Void = { } @@ -201,6 +203,9 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU super.init(frame: .zero) + self.backgroundColor = presentationData.theme.list.plainBackgroundColor + self.webView.backgroundColor = presentationData.theme.list.plainBackgroundColor + self.webView.allowsBackForwardNavigationGestures = true self.webView.scrollView.delegate = self self.webView.scrollView.clipsToBounds = false @@ -214,7 +219,6 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.canGoForward), options: [], context: nil) self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.hasOnlySecureContent), options: [], context: nil) if #available(iOS 15.0, *) { - self.backgroundColor = presentationData.theme.list.plainBackgroundColor self.webView.underPageBackgroundColor = presentationData.theme.list.plainBackgroundColor } if #available(iOS 16.4, *) { @@ -460,11 +464,12 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU ErrorComponent( theme: self.presentationData.theme, title: self.presentationData.strings.Browser_ErrorTitle, - text: error.localizedDescription + text: error.localizedDescription, + insets: insets ) ), environment: {}, - containerSize: CGSize(width: size.width - insets.left - insets.right - 72.0, height: size.height) + containerSize: CGSize(width: size.width, height: size.height) ) if self.errorView.superview == nil { self.addSubview(self.errorView) @@ -521,14 +526,25 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { if !decelerate { self.snapScrollingOffsetToInsets() + + if self.ignoreUpdatesUntilScrollingStopped { + self.ignoreUpdatesUntilScrollingStopped = false + } } } public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { self.snapScrollingOffsetToInsets() + + if self.ignoreUpdatesUntilScrollingStopped { + self.ignoreUpdatesUntilScrollingStopped = false + } } private func updateScrollingOffset(isReset: Bool, transition: ComponentTransition) { + guard !self.ignoreUpdatesUntilScrollingStopped else { + return + } let scrollView = self.webView.scrollView let isInteracting = scrollView.isDragging || scrollView.isDecelerating if let previousScrollingOffsetValue = self.previousScrollingOffset { @@ -558,6 +574,30 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU } } + private var ignoreUpdatesUntilScrollingStopped = false + func resetScrolling() { + self.updateScrollingOffset(isReset: true, transition: .spring(duration: 0.4)) + self.ignoreUpdatesUntilScrollingStopped = true + } + + func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + if let url = navigationAction.request.url?.absoluteString { + if isTelegramMeLink(url) || isTelegraPhLink(url) { + decisionHandler(.cancel) + self.minimize() + self.openAppUrl(url) + } else { + if #available(iOS 14.5, *), navigationAction.shouldPerformDownload { + decisionHandler(.download) + } else { + decisionHandler(.allow) + } + } + } else { + decisionHandler(.allow) + } + } + func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) { self.currentError = nil self.updateFontState(self.currentFontState, force: true) @@ -881,15 +921,18 @@ private final class ErrorComponent: CombinedComponent { let theme: PresentationTheme let title: String let text: String + let insets: UIEdgeInsets init( theme: PresentationTheme, title: String, - text: String + text: String, + insets: UIEdgeInsets ) { self.theme = theme self.title = title self.text = text + self.insets = insets } static func ==(lhs: ErrorComponent, rhs: ErrorComponent) -> Bool { @@ -902,10 +945,14 @@ private final class ErrorComponent: CombinedComponent { if lhs.text != rhs.text { return false } + if lhs.insets != rhs.insets { + return false + } return true } static var body: Body { + let background = Child(Rectangle.self) let animation = Child(LottieComponent.self) let title = Child(MultilineTextComponent.self) let text = Child(MultilineTextComponent.self) @@ -916,6 +963,17 @@ private final class ErrorComponent: CombinedComponent { let animationSpacing: CGFloat = 8.0 let textSpacing: CGFloat = 8.0 + let constrainedWidth = context.availableSize.width - 76.0 - context.component.insets.left - context.component.insets.right + + let background = background.update( + component: Rectangle(color: context.component.theme.list.plainBackgroundColor), + availableSize: context.availableSize, + transition: .immediate + ) + context.add(background + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + ) + let animation = animation.update( component: LottieComponent( content: LottieComponent.AppBundleContent(name: "ChatListNoResults") @@ -924,9 +982,6 @@ private final class ErrorComponent: CombinedComponent { availableSize: CGSize(width: animationSize, height: animationSize), transition: .immediate ) - context.add(animation - .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + animation.size.height / 2.0)) - ) contentHeight += animation.size.height + animationSpacing let title = title.update( @@ -939,12 +994,9 @@ private final class ErrorComponent: CombinedComponent { horizontalAlignment: .center ), environment: {}, - availableSize: context.availableSize, + availableSize: CGSize(width: constrainedWidth, height: context.availableSize.height), transition: .immediate ) - context.add(title - .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + title.size.height / 2.0)) - ) contentHeight += title.size.height + textSpacing let text = text.update( @@ -958,15 +1010,27 @@ private final class ErrorComponent: CombinedComponent { maximumNumberOfLines: 0 ), environment: {}, - availableSize: context.availableSize, + availableSize: CGSize(width: constrainedWidth, height: context.availableSize.height), transition: .immediate ) - context.add(text - .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + text.size.height / 2.0)) - ) contentHeight += text.size.height - - return CGSize(width: context.availableSize.width, height: contentHeight) + + var originY = floor((context.availableSize.height - contentHeight) / 2.0) + context.add(animation + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + animation.size.height / 2.0)) + ) + originY += animation.size.height + animationSpacing + + context.add(title + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + title.size.height / 2.0)) + ) + originY += title.size.height + textSpacing + + context.add(text + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + text.size.height / 2.0)) + ) + + return context.availableSize } } } diff --git a/submodules/BrowserUI/Sources/Punycode.swift b/submodules/BrowserUI/Sources/Punycode.swift new file mode 100644 index 0000000000..2edbbb22e3 --- /dev/null +++ b/submodules/BrowserUI/Sources/Punycode.swift @@ -0,0 +1,317 @@ +// +// Created by kojirof on 2018-11-19. +// Copyright (c) 2018 Gumob. All rights reserved. +// + +//MIT License +// +//Copyright (c) 2018 Gumob +// +//Permission is hereby granted, free of charge, to any person obtaining a copy +//of this software and associated documentation files (the "Software"), to deal +//in the Software without restriction, including without limitation the rights +//to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +//copies of the Software, and to permit persons to whom the Software is +//furnished to do so, subject to the following conditions: +// +//The above copyright notice and this permission notice shall be included in all +//copies or substantial portions of the Software. +// +//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +//IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +//FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +//AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +//LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +//OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +//SOFTWARE. + +import Foundation + +public class Punycode { + + /// Punycode RFC 3492 + /// See https://www.ietf.org/rfc/rfc3492.txt for standard details + + private let base: Int = 36 + private let tMin: Int = 1 + private let tMax: Int = 26 + private let skew: Int = 38 + private let damp: Int = 700 + private let initialBias: Int = 72 + private let initialN: Int = 128 + + /// RFC 3492 specific + private let delimiter: Character = "-" + private let lowercase: ClosedRange = "a"..."z" + private let digits: ClosedRange = "0"..."9" + private let lettersBase: UInt32 = Character("a").unicodeScalars.first!.value + private let digitsBase: UInt32 = Character("0").unicodeScalars.first!.value + + /// IDNA + private let ace: String = "xn--" + + private func adaptBias(_ delta: Int, _ numberOfPoints: Int, _ firstTime: Bool) -> Int { + var delta: Int = delta + if firstTime { + delta /= damp + } else { + delta /= 2 + } + delta += delta / numberOfPoints + var k: Int = 0 + while delta > ((base - tMin) * tMax) / 2 { + delta /= base - tMin + k += base + } + return k + ((base - tMin + 1) * delta) / (delta + skew) + } + + /// Maps a punycode character to index + private func punycodeIndex(for character: Character) -> Int? { + if lowercase.contains(character) { + return Int(character.unicodeScalars.first!.value - lettersBase) + } else if digits.contains(character) { + return Int(character.unicodeScalars.first!.value - digitsBase) + 26 /// count of lowercase letters range + } else { + return nil + } + } + + /// Maps an index to corresponding punycode character + private func punycodeValue(for digit: Int) -> Character? { + guard digit < base else { return nil } + if digit < 26 { + return Character(UnicodeScalar(lettersBase.advanced(by: digit))!) + } else { + return Character(UnicodeScalar(digitsBase.advanced(by: digit - 26))!) + } + } + + /// Decodes punycode encoded string to original representation + /// + /// - Parameter punycode: Punycode encoding (RFC 3492) + /// - Returns: Decoded string or nil if the input cannot be decoded + public func decodePunycode(_ punycode: Substring) -> String? { + var n: Int = initialN + var i: Int = 0 + var bias: Int = initialBias + var output: [Character] = [] + var inputPosition = punycode.startIndex + + let delimiterPosition: Substring.Index = punycode.lastIndex(of: delimiter) ?? punycode.startIndex + if delimiterPosition > punycode.startIndex { + output.append(contentsOf: punycode[..= bias + tMax ? tMax : k - bias) + if digit < t { + break + } + w *= base - t + k += base + } while !punycodeInput.isEmpty + bias = adaptBias(i - oldI, output.count + 1, oldI == 0) + n += i / (output.count + 1) + i %= (output.count + 1) + guard n >= 0x80, let scalar = UnicodeScalar(n) else { + return nil + } + output.insert(Character(scalar), at: i) + i += 1 + } + + return String(output) + } + + /// Encodes string to punycode (RFC 3492) + /// + /// - Parameter input: Input string + /// - Returns: Punycode encoded string + public func encodePunycode(_ input: Substring) -> String? { + var n: Int = initialN + var delta: Int = 0 + var bias: Int = initialBias + var output: String = "" + for scalar in input.unicodeScalars { + if scalar.isASCII { + let char = Character(scalar) + output.append(char) + } else if !scalar.isValid { + return nil /// Encountered a scalar out of acceptable range + } + } + var handled: Int = output.count + let basic: Int = handled + if basic > 0 { + output.append(delimiter) + } + while handled < input.unicodeScalars.count { + var minimumCodepoint: Int = 0x10FFFF + for scalar: Unicode.Scalar in input.unicodeScalars { + if scalar.value < minimumCodepoint && scalar.value >= n { + minimumCodepoint = Int(scalar.value) + } + } + delta += (minimumCodepoint - n) * (handled + 1) + n = minimumCodepoint + for scalar: Unicode.Scalar in input.unicodeScalars { + if scalar.value < n { + delta += 1 + } else if scalar.value == n { + var q: Int = delta + var k: Int = base + while true { + let t = k <= bias ? tMin : (k >= bias + tMax ? tMax : k - bias) + if q < t { + break + } + guard let character: Character = punycodeValue(for: t + ((q - t) % (base - t))) else { return nil } + output.append(character) + q = (q - t) / (base - t) + k += base + } + guard let character: Character = punycodeValue(for: q) else { return nil } + output.append(character) + bias = adaptBias(delta, handled + 1, handled == basic) + delta = 0 + handled += 1 + } + } + delta += 1 + n += 1 + } + + return output + } + + /// Returns new string containing IDNA-encoded hostname + /// + /// - Returns: IDNA encoded hostname or nil if the string can't be encoded + public func encodeIDNA(_ input: Substring) -> String? { + let parts: [Substring] = input.split(separator: ".") + var output: String = "" + for part: Substring in parts { + if output.count > 0 { + output.append(".") + } + if part.rangeOfCharacter(from: CharacterSet.urlHostAllowed.inverted) != nil { + guard let encoded: String = part.lowercased().punycodeEncoded else { return nil } + output += ace + encoded + } else { + output += part + } + } + return output + } + + /// Returns new string containing hostname decoded from IDNA representation + /// + /// - Returns: Original hostname or nil if the string doesn't contain correct encoding + public func decodedIDNA(_ input: Substring) -> String? { + let parts: [Substring] = input.split(separator: ".") + var output: String = "" + for part: Substring in parts { + if output.count > 0 { + output.append(".") + } + if part.hasPrefix(ace) { + guard let decoded: String = part.dropFirst(ace.count).punycodeDecoded else { return nil } + output += decoded + } else { + output += part + } + } + return output + } +} + +private extension Substring { + func lastIndex(of element: Character) -> String.Index? { + var position: Index = endIndex + while position > startIndex { + position = self.index(before: position) + if self[position] == element { + return position + } + } + return nil + } +} + +private extension UnicodeScalar { + var isValid: Bool { + return value < 0xD880 || (value >= 0xE000 && value <= 0x1FFFFF) + } +} + +public extension Substring { + /// Returns new string in punycode encoding (RFC 3492) + /// + /// - Returns: Punycode encoded string or nil if the string can't be encoded + var punycodeEncoded: String? { + return Punycode().encodePunycode(self) + } + + /// Returns new string decoded from punycode representation (RFC 3492) + /// + /// - Returns: Original string or nil if the string doesn't contain correct encoding + var punycodeDecoded: String? { + return Punycode().decodePunycode(self) + } + + /// Returns new string containing IDNA-encoded hostname + /// + /// - Returns: IDNA encoded hostname or nil if the string can't be encoded + var idnaEncoded: String? { + return Punycode().encodeIDNA(self) + } + + /// Returns new string containing hostname decoded from IDNA representation + /// + /// - Returns: Original hostname or nil if the string doesn't contain correct encoding + var idnaDecoded: String? { + return Punycode().decodedIDNA(self) + } +} + +public extension String { + + /// Returns new string in punycode encoding (RFC 3492) + /// + /// - Returns: Punycode encoded string or nil if the string can't be encoded + var punycodeEncoded: String? { + return self[..=0; i--) { uiWebview_HighlightAllOccurencesOfStringForElement(element.childNodes[i],keyword); } diff --git a/submodules/TelegramUI/Sources/OpenUrl.swift b/submodules/TelegramUI/Sources/OpenUrl.swift index c25368c74b..4897df2846 100644 --- a/submodules/TelegramUI/Sources/OpenUrl.swift +++ b/submodules/TelegramUI/Sources/OpenUrl.swift @@ -1008,11 +1008,12 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur return } + let urlScheme = (parsedUrl.scheme ?? "").lowercased() var isInternetUrl = false - if parsedUrl.scheme == "http" || parsedUrl.scheme == "https" { + if ["http", "https"].contains(urlScheme) { isInternetUrl = true } - if parsedUrl.scheme == "tonsite" { + if urlScheme == "tonsite" { isInternetUrl = true } @@ -1032,7 +1033,7 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur settings = .defaultSettings } if accessChallengeData.data.isLockable { - if passcodeSettings.autolockTimeout != nil && settings.defaultWebBrowser == nil { + if passcodeSettings.autolockTimeout != nil && settings.defaultWebBrowser == "inApp" { settings = WebBrowserSettings(defaultWebBrowser: "safari", exceptions: []) } } From 2eeb3be2b2f9b28f22aa47a6b4f6cd193348ea78 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 25 Jul 2024 16:15:45 +0200 Subject: [PATCH 09/41] Browser improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 3 + submodules/BrowserUI/BUILD | 1 + .../Sources/BrowserAddressBarComponent.swift | 21 +++++- .../BrowserNavigationBarComponent.swift | 1 + .../BrowserUI/Sources/BrowserScreen.swift | 14 +++- .../BrowserUI/Sources/BrowserWebContent.swift | 75 ++++++++++++++++--- .../Components/MediaEditorScreen/BUILD | 1 + .../Sources/MediaEditorScreen.swift | 1 + .../PeerInfo/PeerInfoStoryGridScreen/BUILD | 1 + .../Sources/PeerInfoStoryGridScreen.swift | 2 +- .../Sources/StorySearchGridScreen.swift | 1 - .../Components/SaveProgressScreen/BUILD | 24 ++++++ .../Sources/SaveProgressScreen.swift | 0 .../Stories/StoryContainerScreen/BUILD | 1 + .../StoryItemSetContainerComponent.swift | 1 + 15 files changed, 131 insertions(+), 16 deletions(-) create mode 100644 submodules/TelegramUI/Components/SaveProgressScreen/BUILD rename submodules/TelegramUI/Components/{MediaEditorScreen => SaveProgressScreen}/Sources/SaveProgressScreen.swift (100%) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 2993d91058..5c30092651 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -12602,3 +12602,6 @@ Sorry for the inconvenience."; "Story.Privacy.ChooseCoverInfo" = "Choose a frame from the story to show in your Profile."; "Story.Privacy.ChooseCoverChannelInfo" = "Choose a frame from the story to show in channel profile."; "Story.Privacy.ChooseCoverGroupInfo" = "Choose a frame from the story to show in group profile."; + +"WebBrowser.Download.Confirmation" = "Do you want to download \"%@\"?"; +"WebBrowser.Download.Download" = "Download"; diff --git a/submodules/BrowserUI/BUILD b/submodules/BrowserUI/BUILD index d143a87202..f03e31beb4 100644 --- a/submodules/BrowserUI/BUILD +++ b/submodules/BrowserUI/BUILD @@ -46,6 +46,7 @@ swift_library( "//submodules/TelegramUI/Components/Chat/ChatHistorySearchContainerNode", "//submodules/SearchUI", "//submodules/SearchBarNode", + "//submodules/TelegramUI/Components/SaveProgressScreen", ], visibility = [ "//visibility:public", diff --git a/submodules/BrowserUI/Sources/BrowserAddressBarComponent.swift b/submodules/BrowserUI/Sources/BrowserAddressBarComponent.swift index c326a203d5..6ab928088a 100644 --- a/submodules/BrowserUI/Sources/BrowserAddressBarComponent.swift +++ b/submodules/BrowserUI/Sources/BrowserAddressBarComponent.swift @@ -280,6 +280,7 @@ final class AddressBarContentComponent: Component { } title = title.idnaDecoded ?? title } + self.update(theme: component.theme, strings: component.strings, size: availableSize, isActive: isActive, title: title.lowercased(), isSecure: component.isSecure, collapseFraction: collapseFraction, transition: transition) return availableSize @@ -438,7 +439,25 @@ final class AddressBarContentComponent: Component { textField.addTarget(self, action: #selector(self.textFieldChanged(_:)), for: .editingChanged) } - textField.text = self.component?.url ?? "" + var address = self.component?.url ?? "" + if let components = URLComponents(string: address) { + if #available(iOS 16.0, *), let encodedHost = components.encodedHost { + if let decodedHost = components.host, encodedHost != decodedHost { + address = address.replacingOccurrences(of: encodedHost, with: decodedHost) + } + } else if let encodedHost = components.host { + if let decodedHost = components.host?.idnaDecoded, encodedHost != decodedHost { + address = address.replacingOccurrences(of: encodedHost, with: decodedHost) + } + } + } + + if textField.text != address { + textField.text = address + self.clearIconView.isHidden = address.isEmpty + self.clearIconButton.isHidden = address.isEmpty + self.placeholderContent.view?.isHidden = !address.isEmpty + } textField.textColor = theme.rootController.navigationSearchBar.inputTextColor transition.setFrame(view: textField, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + sideInset, y: backgroundFrame.minY - UIScreenPixel), size: CGSize(width: backgroundFrame.width - sideInset - 32.0, height: backgroundFrame.height))) diff --git a/submodules/BrowserUI/Sources/BrowserNavigationBarComponent.swift b/submodules/BrowserUI/Sources/BrowserNavigationBarComponent.swift index e153a02294..1b0d09728c 100644 --- a/submodules/BrowserUI/Sources/BrowserNavigationBarComponent.swift +++ b/submodules/BrowserUI/Sources/BrowserNavigationBarComponent.swift @@ -245,6 +245,7 @@ final class BrowserNavigationBarComponent: CombinedComponent { if !leftItemList.isEmpty || !rightItemList.isEmpty { availableWidth -= 20.0 } + availableWidth -= context.component.sideInset * 2.0 let environment = BrowserNavigationBarEnvironment(fraction: context.component.collapseFraction) diff --git a/submodules/BrowserUI/Sources/BrowserScreen.swift b/submodules/BrowserUI/Sources/BrowserScreen.swift index b4ca2e6f30..661fbbeedb 100644 --- a/submodules/BrowserUI/Sources/BrowserScreen.swift +++ b/submodules/BrowserUI/Sources/BrowserScreen.swift @@ -1189,7 +1189,7 @@ public class BrowserScreen: ViewController, MinimizableController { private let context: AccountContext private let subject: Subject - var openPreviousOnClose = false + private var openPreviousOnClose = false private var validLayout: ContainerViewLayout? @@ -1205,9 +1205,10 @@ public class BrowserScreen: ViewController, MinimizableController { // "application/vnd.openxmlformats-officedocument.presentationml.presentation" ] - public init(context: AccountContext, subject: Subject) { + public init(context: AccountContext, subject: Subject, openPreviousOnClose: Bool = false) { self.context = context self.subject = subject + self.openPreviousOnClose = openPreviousOnClose super.init(navigationBarPresentationData: nil) @@ -1243,9 +1244,18 @@ public class BrowserScreen: ViewController, MinimizableController { } public func requestMinimize(topEdgeOffset: CGFloat?, initialVelocity: CGFloat?) { + self.openPreviousOnClose = false self.node.minimize(topEdgeOffset: topEdgeOffset, damping: 180.0, initialVelocity: initialVelocity) } + public override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + if self.openPreviousOnClose, let navigationController = self.navigationController as? NavigationController, let minimizedContainer = navigationController.minimizedContainer, let controller = minimizedContainer.controllers.last { + navigationController.maximizeViewController(controller, animated: true) + } + } + public var isMinimized = false { didSet { if let webContent = self.node.content.last as? BrowserWebContent { diff --git a/submodules/BrowserUI/Sources/BrowserWebContent.swift b/submodules/BrowserUI/Sources/BrowserWebContent.swift index 58e21e3b0c..921447f526 100644 --- a/submodules/BrowserUI/Sources/BrowserWebContent.swift +++ b/submodules/BrowserUI/Sources/BrowserWebContent.swift @@ -19,6 +19,7 @@ import LottieComponent import MultilineTextComponent import UrlEscaping import UrlHandling +import SaveProgressScreen private final class TonSchemeHandler: NSObject, WKURLSchemeHandler { private final class PendingTask { @@ -157,16 +158,12 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU let configuration = WKWebViewConfiguration() -// let bundle = Bundle.main -// let bundleVersion = bundle.infoDictionary?["CFBundleShortVersionString"] ?? "" -// var proxyServerHost = "magic.org" if let data = context.currentAppConfiguration.with({ $0 }).data, let hostValue = data["ton_proxy_address"] as? String { proxyServerHost = hostValue } configuration.setURLSchemeHandler(TonSchemeHandler(proxyServerHost: proxyServerHost), forURLScheme: "tonsite") configuration.allowsInlineMediaPlayback = true -// configuration.applicationNameForUserAgent = "Telegram-iOS/\(bundleVersion)" if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { configuration.mediaTypesRequiringUserActionForPlayback = [] } else { @@ -580,6 +577,41 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU self.ignoreUpdatesUntilScrollingStopped = true } + @available(iOS 13.0, *) + func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, preferences: WKWebpagePreferences, decisionHandler: @escaping (WKNavigationActionPolicy, WKWebpagePreferences) -> Void) { + if #available(iOS 14.5, *), navigationAction.shouldPerformDownload { + self.presentDownloadConfirmation(fileName: navigationAction.request.mainDocumentURL?.lastPathComponent ?? "file", proceed: { download in + if download { + decisionHandler(.download, preferences) + } else { + decisionHandler(.cancel, preferences) + } + }) + } else { + if let url = navigationAction.request.url?.absoluteString { + if isTelegramMeLink(url) || isTelegraPhLink(url) { + decisionHandler(.cancel, preferences) + self.minimize() + self.openAppUrl(url) + } else { + decisionHandler(.allow, preferences) + } + } else { + decisionHandler(.allow, preferences) + } + } + } + + func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) { + if navigationResponse.canShowMIMEType { + decisionHandler(.allow) + } else if #available(iOS 14.5, *) { + decisionHandler(.download) + } else { + decisionHandler(.cancel) + } + } + func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { if let url = navigationAction.request.url?.absoluteString { if isTelegramMeLink(url) || isTelegraPhLink(url) { @@ -587,11 +619,7 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU self.minimize() self.openAppUrl(url) } else { - if #available(iOS 14.5, *), navigationAction.shouldPerformDownload { - decisionHandler(.download) - } else { - decisionHandler(.allow) - } + decisionHandler(.allow) } } else { decisionHandler(.allow) @@ -636,7 +664,7 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU self.close() } - @available(iOSApplicationExtension 15.0, iOS 15.0, *) + @available(iOS 15.0, *) func webView(_ webView: WKWebView, requestMediaCapturePermissionFor origin: WKSecurityOrigin, initiatedByFrame frame: WKFrameInfo, type: WKMediaCaptureType, decisionHandler: @escaping (WKPermissionDecision) -> Void) { decisionHandler(.prompt) } @@ -739,12 +767,37 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU completionHandler(configuration) } + private func presentDownloadConfirmation(fileName: String, proceed: @escaping (Bool) -> Void) { + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + var completed = false + let alertController = textAlertController(context: self.context, updatedPresentationData: nil, title: nil, text: presentationData.strings.WebBrowser_Download_Confirmation(fileName).string, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { + if !completed { + completed = true + proceed(false) + } + }), TextAlertAction(type: .defaultAction, title: presentationData.strings.WebBrowser_Download_Download, action: { + if !completed { + completed = true + proceed(true) + } + })]) + alertController.dismissed = { byOutsideTap in + if byOutsideTap { + if !completed { + completed = true + proceed(false) + } + } + } + self.present(alertController, nil) + } + private func open(url: String, new: Bool) { let subject: BrowserScreen.Subject = .webPage(url: url) if new, let navigationController = self.getNavigationController() { navigationController._keepModalDismissProgress = true self.minimize() - let controller = BrowserScreen(context: self.context, subject: subject) + let controller = BrowserScreen(context: self.context, subject: subject, openPreviousOnClose: true) navigationController._keepModalDismissProgress = true navigationController.pushViewController(controller) } else { diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD index c5f8979db7..e74d17b390 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD +++ b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD @@ -64,6 +64,7 @@ swift_library( "//submodules/WebsiteType", "//submodules/UrlEscaping", "//submodules/DeviceLocationManager", + "//submodules/TelegramUI/Components/SaveProgressScreen", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 2bae267beb..4f2cf97358 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -48,6 +48,7 @@ import StickerPackEditTitleController import StickerPickerScreen import UIKitRuntimeUtils import ImageObjectSeparation +import SaveProgressScreen private let playbackButtonTag = GenericComponentViewTag() private let muteButtonTag = GenericComponentViewTag() diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/BUILD index 5ba2d19928..b26f1644a1 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/BUILD @@ -30,6 +30,7 @@ swift_library( "//submodules/SaveToCameraRoll", "//submodules/ShareController", "//submodules/OpenInExternalAppUI", + "//submodules/TelegramUI/Components/SaveProgressScreen", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift index e9fccb53a3..dbbf2ff7e3 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift @@ -14,7 +14,7 @@ import ChatTitleView import BottomButtonPanelComponent import UndoUI import MoreHeaderButton -import MediaEditorScreen +import SaveProgressScreen import SaveToCameraRoll final class PeerInfoStoryGridScreenComponent: Component { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/StorySearchGridScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/StorySearchGridScreen.swift index 6bdc9c2581..3cf859f663 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/StorySearchGridScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/StorySearchGridScreen.swift @@ -14,7 +14,6 @@ import ChatTitleView import BottomButtonPanelComponent import UndoUI import MoreHeaderButton -import MediaEditorScreen import SaveToCameraRoll import ShareController import OpenInExternalAppUI diff --git a/submodules/TelegramUI/Components/SaveProgressScreen/BUILD b/submodules/TelegramUI/Components/SaveProgressScreen/BUILD new file mode 100644 index 0000000000..e9de562525 --- /dev/null +++ b/submodules/TelegramUI/Components/SaveProgressScreen/BUILD @@ -0,0 +1,24 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SaveProgressScreen", + module_name = "SaveProgressScreen", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/ComponentFlow", + "//submodules/Components/ViewControllerComponent", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/BundleIconComponent", + "//submodules/Components/LottieAnimationComponent", + "//submodules/AccountContext", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/SaveProgressScreen.swift b/submodules/TelegramUI/Components/SaveProgressScreen/Sources/SaveProgressScreen.swift similarity index 100% rename from submodules/TelegramUI/Components/MediaEditorScreen/Sources/SaveProgressScreen.swift rename to submodules/TelegramUI/Components/SaveProgressScreen/Sources/SaveProgressScreen.swift diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD index 7e63c31eea..4d244dd6fc 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD @@ -98,6 +98,7 @@ swift_library( "//submodules/TelegramUI/Components/Stories/StoryQualityUpgradeSheetScreen", "//submodules/TelegramUI/Components/SliderContextItem", "//submodules/TelegramUI/Components/InteractiveTextComponent", + "//submodules/TelegramUI/Components/SaveProgressScreen", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index a1f5b25732..16fb16ac43 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -43,6 +43,7 @@ import TelegramUIPreferences import StoryFooterPanelComponent import TelegramNotices import SliderContextItem +import SaveProgressScreen public final class StoryAvailableReactions: Equatable { let reactionItems: [ReactionItem] From e0f95989d422e5318eeecc50a26ad7d5a2a43f52 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 25 Jul 2024 17:04:44 +0200 Subject: [PATCH 10/41] Browser improvements --- .../Sources/BrowserAddressListComponent.swift | 9 +++++ .../BrowserAddressListItemComponent.swift | 10 ++++-- .../BrowserUI/Sources/BrowserContent.swift | 2 +- .../Sources/BrowserDocumentContent.swift | 4 +-- .../Sources/BrowserInstantPageContent.swift | 8 ++--- .../BrowserUI/Sources/BrowserPdfContent.swift | 4 +-- .../BrowserUI/Sources/BrowserScreen.swift | 5 +-- .../BrowserUI/Sources/BrowserWebContent.swift | 35 +++++++++++++------ .../Sources/SectionHeaderComponent.swift | 8 ++++- 9 files changed, 61 insertions(+), 24 deletions(-) diff --git a/submodules/BrowserUI/Sources/BrowserAddressListComponent.swift b/submodules/BrowserUI/Sources/BrowserAddressListComponent.swift index 34c91738e7..2070587445 100644 --- a/submodules/BrowserUI/Sources/BrowserAddressListComponent.swift +++ b/submodules/BrowserUI/Sources/BrowserAddressListComponent.swift @@ -13,17 +13,20 @@ final class BrowserAddressListComponent: Component { let context: AccountContext let theme: PresentationTheme let strings: PresentationStrings + let insets: UIEdgeInsets let navigateTo: (String) -> Void init( context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, + insets: UIEdgeInsets, navigateTo: @escaping (String) -> Void ) { self.context = context self.theme = theme self.strings = strings + self.insets = insets self.navigateTo = navigateTo } @@ -37,6 +40,9 @@ final class BrowserAddressListComponent: Component { if lhs.strings !== rhs.strings { return false } + if lhs.insets != rhs.insets { + return false + } return true } @@ -211,6 +217,7 @@ final class BrowserAddressListComponent: Component { theme: component.theme, style: .plain, title: sectionTitle, + insets: component.insets, actionTitle: section.id == 0 ? "Clear" : nil, action: { [weak self] in if let self, let component = self.component { @@ -292,6 +299,7 @@ final class BrowserAddressListComponent: Component { webPage: webPage!, message: itemMessage, hasNext: true, + insets: component.insets, action: { if let url = webPage?.content.url { navigateTo(url) @@ -393,6 +401,7 @@ final class BrowserAddressListComponent: Component { webPage: TelegramMediaWebpage(webpageId: EngineMedia.Id(namespace: 0, id: 0), content: .Loaded(TelegramMediaWebpageLoadedContent(url: "https://telegram.org", displayUrl: "https://telegram.org", hash: 0, type: nil, websiteName: "Telegram", title: "Telegram Telegram", text: "Telegram", embedUrl: nil, embedType: nil, embedSize: nil, duration: nil, author: nil, isMediaLargeByDefault: nil, image: nil, file: nil, story: nil, attributes: [], instantPage: nil))), message: nil, hasNext: true, + insets: .zero, action: {} )), environment: {}, diff --git a/submodules/BrowserUI/Sources/BrowserAddressListItemComponent.swift b/submodules/BrowserUI/Sources/BrowserAddressListItemComponent.swift index 49ebcb2c20..30ad83d58b 100644 --- a/submodules/BrowserUI/Sources/BrowserAddressListItemComponent.swift +++ b/submodules/BrowserUI/Sources/BrowserAddressListItemComponent.swift @@ -16,6 +16,7 @@ final class BrowserAddressListItemComponent: Component { let webPage: TelegramMediaWebpage var message: Message? let hasNext: Bool + let insets: UIEdgeInsets let action: () -> Void init( @@ -24,6 +25,7 @@ final class BrowserAddressListItemComponent: Component { webPage: TelegramMediaWebpage, message: Message?, hasNext: Bool, + insets: UIEdgeInsets, action: @escaping () -> Void ) { self.context = context @@ -31,6 +33,7 @@ final class BrowserAddressListItemComponent: Component { self.webPage = webPage self.message = message self.hasNext = hasNext + self.insets = insets self.action = action } @@ -44,6 +47,9 @@ final class BrowserAddressListItemComponent: Component { if lhs.hasNext != rhs.hasNext { return false } + if lhs.insets != rhs.insets { + return false + } return true } @@ -92,7 +98,7 @@ final class BrowserAddressListItemComponent: Component { let iconSize = CGSize(width: 40.0, height: 40.0) let height: CGFloat = 60.0 - let leftInset: CGFloat = 11.0 + iconSize.width + 11.0 + let leftInset: CGFloat = component.insets.left + 11.0 + iconSize.width + 11.0 let rightInset: CGFloat = 16.0 let titleSpacing: CGFloat = 2.0 @@ -181,7 +187,7 @@ final class BrowserAddressListItemComponent: Component { } - let iconFrame = CGRect(origin: CGPoint(x: 11.0, y: floorToScreenPixels((height - iconSize.height) / 2.0)), size: iconSize) + let iconFrame = CGRect(origin: CGPoint(x: 11.0 + component.insets.left, y: floorToScreenPixels((height - iconSize.height) / 2.0)), size: iconSize) let iconImageLayout = self.icon.asyncLayout() var iconImageApply: (() -> Void)? diff --git a/submodules/BrowserUI/Sources/BrowserContent.swift b/submodules/BrowserUI/Sources/BrowserContent.swift index f9f1687d93..7422bed87e 100644 --- a/submodules/BrowserUI/Sources/BrowserContent.swift +++ b/submodules/BrowserUI/Sources/BrowserContent.swift @@ -188,7 +188,7 @@ protocol BrowserContent: UIView { func addToRecentlyVisited() - func updateLayout(size: CGSize, insets: UIEdgeInsets, fullInsets: UIEdgeInsets, transition: ComponentTransition) + func updateLayout(size: CGSize, insets: UIEdgeInsets, fullInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, transition: ComponentTransition) func makeContentSnapshotView() -> UIView? } diff --git a/submodules/BrowserUI/Sources/BrowserDocumentContent.swift b/submodules/BrowserUI/Sources/BrowserDocumentContent.swift index 1fae1b7ad3..316ab28538 100644 --- a/submodules/BrowserUI/Sources/BrowserDocumentContent.swift +++ b/submodules/BrowserUI/Sources/BrowserDocumentContent.swift @@ -101,7 +101,7 @@ final class BrowserDocumentContent: UIView, BrowserContent, WKNavigationDelegate self.webView.underPageBackgroundColor = presentationData.theme.list.plainBackgroundColor } if let (size, insets, fullInsets) = self.validLayout { - self.updateLayout(size: size, insets: insets, fullInsets: fullInsets, transition: .immediate) + self.updateLayout(size: size, insets: insets, fullInsets: fullInsets, safeInsets: .zero, transition: .immediate) } } @@ -240,7 +240,7 @@ final class BrowserDocumentContent: UIView, BrowserContent, WKNavigationDelegate } private var validLayout: (CGSize, UIEdgeInsets, UIEdgeInsets)? - func updateLayout(size: CGSize, insets: UIEdgeInsets, fullInsets: UIEdgeInsets, transition: ComponentTransition) { + func updateLayout(size: CGSize, insets: UIEdgeInsets, fullInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, transition: ComponentTransition) { self.validLayout = (size, insets, fullInsets) self.previousScrollingOffset = ScrollingOffsetState(value: self.webView.scrollView.contentOffset.y, isDraggingOrDecelerating: self.webView.scrollView.isDragging || self.webView.scrollView.isDecelerating) diff --git a/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift b/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift index a9972dbe90..2ceff5ec34 100644 --- a/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift +++ b/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift @@ -301,7 +301,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg guard let (size, insets, fullInsets) = self.containerLayout else { return } - self.updateLayout(size: size, insets: insets, fullInsets: fullInsets, transition: transition) + self.updateLayout(size: size, insets: insets, fullInsets: fullInsets, safeInsets: .zero, transition: transition) } func reload() { @@ -375,11 +375,11 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: true) } - func updateLayout(size: CGSize, insets: UIEdgeInsets, fullInsets: UIEdgeInsets, transition: ComponentTransition) { - self.updateLayout(size: size, insets: insets, fullInsets: fullInsets, transition: transition.containedViewLayoutTransition) + func updateLayout(size: CGSize, insets: UIEdgeInsets, fullInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, transition: ComponentTransition) { + self.updateLayout(size: size, insets: insets, fullInsets: fullInsets, safeInsets: safeInsets, transition: transition.containedViewLayoutTransition) } - func updateLayout(size: CGSize, insets: UIEdgeInsets, fullInsets: UIEdgeInsets, transition: ContainedViewLayoutTransition) { + func updateLayout(size: CGSize, insets: UIEdgeInsets, fullInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, transition: ContainedViewLayoutTransition) { self.containerLayout = (size, insets, fullInsets) var updateVisibleItems = false diff --git a/submodules/BrowserUI/Sources/BrowserPdfContent.swift b/submodules/BrowserUI/Sources/BrowserPdfContent.swift index e037f45663..cb61825774 100644 --- a/submodules/BrowserUI/Sources/BrowserPdfContent.swift +++ b/submodules/BrowserUI/Sources/BrowserPdfContent.swift @@ -111,7 +111,7 @@ final class BrowserPdfContent: UIView, BrowserContent, WKNavigationDelegate, WKU self.backgroundColor = presentationData.theme.list.plainBackgroundColor } if let (size, insets, fullInsets) = self.validLayout { - self.updateLayout(size: size, insets: insets, fullInsets: fullInsets, transition: .immediate) + self.updateLayout(size: size, insets: insets, fullInsets: fullInsets, safeInsets: .zero, transition: .immediate) } } @@ -250,7 +250,7 @@ final class BrowserPdfContent: UIView, BrowserContent, WKNavigationDelegate, WKU } private var validLayout: (CGSize, UIEdgeInsets, UIEdgeInsets)? - func updateLayout(size: CGSize, insets: UIEdgeInsets, fullInsets: UIEdgeInsets, transition: ComponentTransition) { + func updateLayout(size: CGSize, insets: UIEdgeInsets, fullInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, transition: ComponentTransition) { self.validLayout = (size, insets, fullInsets) self.previousScrollingOffset = ScrollingOffsetState(value: self.scrollView.contentOffset.y, isDraggingOrDecelerating: self.scrollView.isDragging || self.scrollView.isDecelerating) diff --git a/submodules/BrowserUI/Sources/BrowserScreen.swift b/submodules/BrowserUI/Sources/BrowserScreen.swift index 661fbbeedb..e6f681aa45 100644 --- a/submodules/BrowserUI/Sources/BrowserScreen.swift +++ b/submodules/BrowserUI/Sources/BrowserScreen.swift @@ -261,6 +261,7 @@ private final class BrowserScreenComponent: CombinedComponent { context: context.component.context, theme: environment.theme, strings: environment.strings, + insets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: 0.0, right: environment.safeInsets.right), navigateTo: { url in performAction.invoke(.navigateTo(url)) } @@ -1370,8 +1371,8 @@ private final class BrowserContentComponent: Component { let bottomInset = (49.0 + component.insets.bottom) * (1.0 - component.scrollingPanelOffsetFraction) let insets = UIEdgeInsets(top: topInset, left: component.insets.left, bottom: bottomInset, right: component.insets.right) let fullInsets = UIEdgeInsets(top: component.insets.top + component.navigationBarHeight, left: component.insets.left, bottom: 49.0 + component.insets.bottom, right: component.insets.right) - - component.content.updateLayout(size: availableSize, insets: insets, fullInsets: fullInsets, transition: transition) + + component.content.updateLayout(size: availableSize, insets: insets, fullInsets: fullInsets, safeInsets: component.insets, transition: transition) transition.setFrame(view: component.content, frame: CGRect(origin: .zero, size: availableSize)) return availableSize diff --git a/submodules/BrowserUI/Sources/BrowserWebContent.swift b/submodules/BrowserUI/Sources/BrowserWebContent.swift index 921447f526..6844c5929c 100644 --- a/submodules/BrowserUI/Sources/BrowserWebContent.swift +++ b/submodules/BrowserUI/Sources/BrowserWebContent.swift @@ -117,11 +117,23 @@ private final class TonSchemeHandler: NSObject, WKURLSchemeHandler { } } +final class WebView: WKWebView { + var customBottomInset: CGFloat = 0.0 { + didSet { + self.setNeedsLayout() + } + } + + override var safeAreaInsets: UIEdgeInsets { + return UIEdgeInsets(top: 0.0, left: 0.0, bottom: self.customBottomInset, right: 0.0) + } +} + final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKUIDelegate, UIScrollViewDelegate { private let context: AccountContext private var presentationData: PresentationData - let webView: WKWebView + let webView: WebView private let errorView: ComponentHostView private var currentError: Error? @@ -170,7 +182,7 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU configuration.mediaPlaybackRequiresUserAction = false } - self.webView = WKWebView(frame: CGRect(), configuration: configuration) + self.webView = WebView(frame: CGRect(), configuration: configuration) self.webView.allowsLinkPreview = true if #available(iOS 11.0, *) { @@ -245,8 +257,8 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU self.backgroundColor = presentationData.theme.list.plainBackgroundColor self.webView.underPageBackgroundColor = presentationData.theme.list.plainBackgroundColor } - if let (size, insets, fullInsets) = self.validLayout { - self.updateLayout(size: size, insets: insets, fullInsets: fullInsets, transition: .immediate) + if let (size, insets, fullInsets, safeInsets) = self.validLayout { + self.updateLayout(size: size, insets: insets, fullInsets: fullInsets, safeInsets: safeInsets, transition: .immediate) } } @@ -434,13 +446,13 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU self.webView.scrollView.setContentOffset(CGPoint(x: 0.0, y: -self.webView.scrollView.contentInset.top), animated: true) } - private var validLayout: (CGSize, UIEdgeInsets, UIEdgeInsets)? - func updateLayout(size: CGSize, insets: UIEdgeInsets, fullInsets: UIEdgeInsets, transition: ComponentTransition) { - self.validLayout = (size, insets, fullInsets) + private var validLayout: (CGSize, UIEdgeInsets, UIEdgeInsets, UIEdgeInsets)? + func updateLayout(size: CGSize, insets: UIEdgeInsets, fullInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, transition: ComponentTransition) { + self.validLayout = (size, insets, fullInsets, safeInsets) self.previousScrollingOffset = ScrollingOffsetState(value: self.webView.scrollView.contentOffset.y, isDraggingOrDecelerating: self.webView.scrollView.isDragging || self.webView.scrollView.isDecelerating) - let webViewFrame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: CGSize(width: size.width - insets.left - insets.right, height: size.height - insets.top - fullInsets.bottom)) + let webViewFrame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: CGSize(width: size.width - insets.left - insets.right, height: size.height - insets.top)) var refresh = false if self.webView.frame.width > 0 && webViewFrame.width != self.webView.frame.width { refresh = true @@ -451,6 +463,9 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU self.webView.reloadInputViews() } + self.webView.scrollView.contentInset = UIEdgeInsets(top: 0.0, left: 0.0, bottom: fullInsets.bottom, right: 0.0) + self.webView.customBottomInset = max(insets.bottom, safeInsets.bottom) +// self.webView.scrollView.contentInset = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 34.0, right: 0.0) self.webView.scrollView.scrollIndicatorInsets = UIEdgeInsets(top: 0.0, left: -insets.left, bottom: 0.0, right: -insets.right) self.webView.scrollView.horizontalScrollIndicatorInsets = UIEdgeInsets(top: 0.0, left: -insets.left, bottom: 0.0, right: -insets.right) @@ -646,8 +661,8 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU } else { self.currentError = nil } - if let (size, insets, fullInsets) = self.validLayout { - self.updateLayout(size: size, insets: insets, fullInsets: fullInsets, transition: .immediate) + if let (size, insets, fullInsets, safeInsets) = self.validLayout { + self.updateLayout(size: size, insets: insets, fullInsets: fullInsets, safeInsets: safeInsets, transition: .immediate) } } diff --git a/submodules/BrowserUI/Sources/SectionHeaderComponent.swift b/submodules/BrowserUI/Sources/SectionHeaderComponent.swift index 6ebe3e80c9..294d5c1a34 100644 --- a/submodules/BrowserUI/Sources/SectionHeaderComponent.swift +++ b/submodules/BrowserUI/Sources/SectionHeaderComponent.swift @@ -13,6 +13,7 @@ final class SectionHeaderComponent: Component { let theme: PresentationTheme let style: Style let title: String + let insets: UIEdgeInsets let actionTitle: String? let action: (() -> Void)? @@ -20,12 +21,14 @@ final class SectionHeaderComponent: Component { theme: PresentationTheme, style: Style, title: String, + insets: UIEdgeInsets, actionTitle: String?, action: (() -> Void)? ) { self.theme = theme self.style = style self.title = title + self.insets = insets self.actionTitle = actionTitle self.action = action } @@ -40,6 +43,9 @@ final class SectionHeaderComponent: Component { if lhs.title != rhs.title { return false } + if lhs.insets != rhs.insets { + return false + } if lhs.actionTitle != rhs.actionTitle { return false } @@ -73,7 +79,7 @@ final class SectionHeaderComponent: Component { self.state = state let height: CGFloat = 28.0 - let leftInset: CGFloat = 16.0 + let leftInset: CGFloat = 16.0 + component.insets.left let rightInset: CGFloat = 0.0 let previousTitleFrame = self.title.view?.frame From b67485c260627c808b9505abfd1379ae51a4b5d3 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Fri, 26 Jul 2024 00:03:54 +0800 Subject: [PATCH 11/41] Bot preview improvements --- .../Sources/SparseItemGrid.swift | 3 +- .../Sources/PeerInfoScreen.swift | 9 +++ .../Sources/PeerInfoStoryPaneNode.swift | 56 ++++++++++++++----- .../Sources/LanguageSelectionScreen.swift | 6 +- .../Sources/LanguageSelectionScreenNode.swift | 10 +++- .../SpaceWarpView/Sources/SpaceWarpView.swift | 1 + .../StoryItemSetContainerComponent.swift | 2 +- 7 files changed, 68 insertions(+), 19 deletions(-) diff --git a/submodules/SparseItemGrid/Sources/SparseItemGrid.swift b/submodules/SparseItemGrid/Sources/SparseItemGrid.swift index 58dd462609..9b8015eaf4 100644 --- a/submodules/SparseItemGrid/Sources/SparseItemGrid.swift +++ b/submodules/SparseItemGrid/Sources/SparseItemGrid.swift @@ -552,7 +552,8 @@ public final class SparseItemGrid: ASDisplayNode { } var contentBottomOffset: CGFloat { - return -self.scrollView.contentOffset.y + self.scrollView.contentSize.height + let bottomInset = self.layout?.containerLayout.insets.bottom ?? 0.0 + return -self.scrollView.contentOffset.y + self.scrollView.contentSize.height - bottomInset } let coveringOffsetUpdated: (Viewport, ContainedViewLayoutTransition) -> Void diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index c63e6a276f..e0669283b2 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -1363,6 +1363,15 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese return } + if let navigationController = parentController.navigationController as? NavigationController, let minimizedContainer = navigationController.minimizedContainer { + for controller in minimizedContainer.controllers { + if let controller = controller as? AttachmentController, let mainController = controller.mainController as? WebAppController, mainController.botId == user.id && mainController.source == .generic { + navigationController.maximizeViewController(controller, animated: true) + return + } + } + } + context.sharedContext.openWebApp( context: context, parentController: parentController, diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift index 791dae59df..6a13db508e 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift @@ -1629,6 +1629,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr public private(set) var isSelectionModeActive: Bool private var currentParams: (size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, navigationHeight: CGFloat, presentationData: PresentationData)? + private var listBottomInset: CGFloat? private let ready = Promise() private var didSetReady: Bool = false @@ -2053,10 +2054,17 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr return SparseItemGrid.ShimmerColors(background: 0xffffff, foreground: 0xffffff) } - let backgroundColor = presentationData.theme.list.mediaPlaceholderColor - let foregroundColor = presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.6) - - return SparseItemGrid.ShimmerColors(background: backgroundColor.argb, foreground: foregroundColor.argb) + if case .botPreview = scope { + let backgroundColor = presentationData.theme.list.plainBackgroundColor + let foregroundColor = presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.6) + + return SparseItemGrid.ShimmerColors(background: backgroundColor.argb, foreground: foregroundColor.argb) + } else { + let backgroundColor = presentationData.theme.list.mediaPlaceholderColor + let foregroundColor = presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.6) + + return SparseItemGrid.ShimmerColors(background: backgroundColor.argb, foreground: foregroundColor.argb) + } } self.itemGridBinding.updateShimmerLayersImpl = { [weak self] layer in @@ -3396,7 +3404,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } if case .botPreview = self.scope, self.canManageStories { self.updateBotPreviewLanguageTab(size: currentParams.size, topInset: currentParams.topInset, transition: transition) - self.updateBotPreviewFooter(size: currentParams.size, bottomInset: currentParams.bottomInset, transition: transition) + self.updateBotPreviewFooter(size: currentParams.size, bottomInset: 0.0, transition: transition) } } } @@ -3664,7 +3672,11 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr guard let self else { return } - self.emptyAction?() + if self.canAddMoreBotPreviews() { + self.emptyAction?() + } else { + self.presentUnableToAddMorePreviewsAlert() + } }, additionalActionTitle: isMainLanguage ? "Create a Translation" : nil, additionalAction: { [weak self] in @@ -3680,7 +3692,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr environment: {}, containerSize: CGSize(width: size.width, height: 1000.0) ) - let botPreviewFooterFrame = CGRect(origin: CGPoint(x: floor((size.width - botPreviewFooterSize.width) * 0.5), y: self.itemGrid.contentBottomOffset - botPreviewFooterSize.height - bottomInset), size: botPreviewFooterSize) + let botPreviewFooterFrame = CGRect(origin: CGPoint(x: floor((size.width - botPreviewFooterSize.width) * 0.5), y: self.itemGrid.contentBottomOffset + 16.0), size: botPreviewFooterSize) if let botPreviewFooterView = botPreviewFooter.view { if botPreviewFooterView.superview == nil { self.view.addSubview(botPreviewFooterView) @@ -3746,16 +3758,16 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr transition.updateFrame(layer: barBackgroundLayer, frame: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: gridTopInset))) } - let defaultBottomInset = bottomInset + var listBottomInset = bottomInset var bottomInset = bottomInset if case .botPreview = self.scope, self.canManageStories { updateBotPreviewLanguageTab(size: size, topInset: topInset, transition: transition) gridTopInset += 50.0 - updateBotPreviewFooter(size: size, bottomInset: defaultBottomInset, transition: transition) + updateBotPreviewFooter(size: size, bottomInset: 0.0, transition: transition) if let botPreviewFooterView = self.botPreviewFooter?.view { - bottomInset += 18.0 + botPreviewFooterView.bounds.height + listBottomInset += 18.0 + botPreviewFooterView.bounds.height } } @@ -3886,6 +3898,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr selectionPanelTransition.setFrame(view: selectionPanelView, frame: selectionPanelFrame) } bottomInset = selectionPanelSize.height + listBottomInset += selectionPanelSize.height } else if self.isProfileEmbedded, let selectedIds = self.itemInteraction.selectedIds, self.canManageStories, case .botPreview = self.scope { let selectionPanel: ComponentView var selectionPanelTransition = ComponentTransition(transition) @@ -3932,6 +3945,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr selectionPanelTransition.setFrame(view: selectionPanelView, frame: selectionPanelFrame) } bottomInset = selectionPanelSize.height + listBottomInset += selectionPanelSize.height } else if let selectionPanel = self.selectionPanel { self.selectionPanel = nil if let selectionPanelView = selectionPanel.view { @@ -4039,7 +4053,11 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr guard let self else { return } - self.emptyAction?() + if self.canAddMoreBotPreviews() { + self.emptyAction?() + } else { + self.presentUnableToAddMorePreviewsAlert() + } }, additionalActionTitle: self.canManageStories ? (isMainLanguage ? "Create a Translation" : "Delete this Translation") : nil, additionalAction: { @@ -4133,11 +4151,12 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } self.itemGrid.pinchEnabled = items.count > 2 && !self.isReordering - self.itemGrid.update(size: size, insets: UIEdgeInsets(top: gridTopInset, left: sideInset, bottom: bottomInset, right: sideInset), useSideInsets: !isList, scrollIndicatorInsets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), lockScrollingAtTop: isScrollingLockedAtTop, fixedItemHeight: fixedItemHeight, fixedItemAspect: fixedItemAspect, adjustForSmallCount: adjustForSmallCount, items: items, theme: self.itemGridBinding.chatPresentationData.theme.theme, synchronous: wasFirstTime ? .full : .none, transition: animateGridItems ? .spring(duration: 0.35) : .immediate) + self.itemGrid.update(size: size, insets: UIEdgeInsets(top: gridTopInset, left: sideInset, bottom: listBottomInset, right: sideInset), useSideInsets: !isList, scrollIndicatorInsets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), lockScrollingAtTop: isScrollingLockedAtTop, fixedItemHeight: fixedItemHeight, fixedItemAspect: fixedItemAspect, adjustForSmallCount: adjustForSmallCount, items: items, theme: self.itemGridBinding.chatPresentationData.theme.theme, synchronous: wasFirstTime ? .full : .none, transition: animateGridItems ? .spring(duration: 0.35) : .immediate) } + self.listBottomInset = listBottomInset if case .botPreview = self.scope, self.canManageStories { - updateBotPreviewFooter(size: size, bottomInset: defaultBottomInset, transition: transition) + updateBotPreviewFooter(size: size, bottomInset: 0.0, transition: transition) } } @@ -4238,7 +4257,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } private func presentAddBotPreviewLanguage() { - self.parentController?.push(LanguageSelectionScreen(context: self.context, selectLocalization: { [weak self] info in + let excludeIds: [String] = self.currentBotPreviewLanguages.map(\.id) + self.parentController?.push(LanguageSelectionScreen(context: self.context, excludeIds: excludeIds, selectLocalization: { [weak self] info in guard let self else { return } @@ -4246,6 +4266,14 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr })) } + public func presentUnableToAddMorePreviewsAlert() { + //TODO:localize + self.parentController?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: "You can add at most \(self.maxBotPreviewCount) previews.", actions: [ + TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: { + }) + ], parseMarkdown: true), in: .window(.root)) + } + public func presentDeleteBotPreviewLanguage() { //TODO:localize self.parentController?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: "Delete Translation", text: "Are you sure you want to delete this translation?", actions: [ diff --git a/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/Sources/LanguageSelectionScreen.swift b/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/Sources/LanguageSelectionScreen.swift index 04229ce599..7b5bebee31 100644 --- a/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/Sources/LanguageSelectionScreen.swift +++ b/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/Sources/LanguageSelectionScreen.swift @@ -11,6 +11,7 @@ import SearchUI public class LanguageSelectionScreen: ViewController { private let context: AccountContext + private let excludeIds: [String] private let selectLocalization: (LocalizationInfo) -> Void private var controllerNode: LanguageSelectionScreenNode { @@ -29,8 +30,9 @@ public class LanguageSelectionScreen: ViewController { private var previousContentOffset: ListViewVisibleContentOffset? - public init(context: AccountContext, selectLocalization: @escaping (LocalizationInfo) -> Void) { + public init(context: AccountContext, excludeIds: [String] = [], selectLocalization: @escaping (LocalizationInfo) -> Void) { self.context = context + self.excludeIds = excludeIds self.selectLocalization = selectLocalization self.presentationData = context.sharedContext.currentPresentationData.with { $0 } @@ -92,7 +94,7 @@ public class LanguageSelectionScreen: ViewController { } override public func loadDisplayNode() { - self.displayNode = LanguageSelectionScreenNode(context: self.context, presentationData: self.presentationData, navigationBar: self.navigationBar!, requestActivateSearch: { [weak self] in + self.displayNode = LanguageSelectionScreenNode(context: self.context, presentationData: self.presentationData, navigationBar: self.navigationBar!, excludeIds: self.excludeIds, requestActivateSearch: { [weak self] in self?.activateSearch() }, requestDeactivateSearch: { [weak self] in self?.deactivateSearch() diff --git a/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/Sources/LanguageSelectionScreenNode.swift b/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/Sources/LanguageSelectionScreenNode.swift index ef9438ed6e..b31bb91475 100644 --- a/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/Sources/LanguageSelectionScreenNode.swift +++ b/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/Sources/LanguageSelectionScreenNode.swift @@ -294,6 +294,7 @@ final class LanguageSelectionScreenNode: ViewControllerTracingNode { private let context: AccountContext private var presentationData: PresentationData private weak var navigationBar: NavigationBar? + private let excludeIds: [String] private let requestActivateSearch: () -> Void private let requestDeactivateSearch: () -> Void private let present: (ViewController, Any?) -> Void @@ -316,11 +317,12 @@ final class LanguageSelectionScreenNode: ViewControllerTracingNode { private var currentListState: LocalizationListState? - init(context: AccountContext, presentationData: PresentationData, navigationBar: NavigationBar, requestActivateSearch: @escaping () -> Void, requestDeactivateSearch: @escaping () -> Void, present: @escaping (ViewController, Any?) -> Void, push: @escaping (ViewController) -> Void, selectLocalization: @escaping (LocalizationInfo) -> Void) { + init(context: AccountContext, presentationData: PresentationData, navigationBar: NavigationBar, excludeIds: [String], requestActivateSearch: @escaping () -> Void, requestDeactivateSearch: @escaping () -> Void, present: @escaping (ViewController, Any?) -> Void, push: @escaping (ViewController) -> Void, selectLocalization: @escaping (LocalizationInfo) -> Void) { self.context = context self.presentationData = presentationData self.presentationDataValue.set(.single(presentationData)) self.navigationBar = navigationBar + self.excludeIds = excludeIds self.requestActivateSearch = requestActivateSearch self.requestDeactivateSearch = requestDeactivateSearch self.present = present @@ -362,6 +364,12 @@ final class LanguageSelectionScreenNode: ViewControllerTracingNode { var entries: [LanguageListEntry] = [] var existingIds = Set() + var localizationListState = localizationListState + localizationListState.availableOfficialLocalizations = localizationListState.availableOfficialLocalizations.filter { + !strongSelf.excludeIds.contains($0.languageCode) + } + localizationListState.availableSavedLocalizations = [] + if !localizationListState.availableOfficialLocalizations.isEmpty { strongSelf.currentListState = localizationListState diff --git a/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift b/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift index 48e2f5ea58..7f516bdd6d 100644 --- a/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift +++ b/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift @@ -976,6 +976,7 @@ open class SpaceWarpView4: UIView, SpaceWarpView { meshView.frame = CGRect(origin: CGPoint(), size: size) let pixelStep = CGPoint() + //let pixelStep = CGPoint(x: CGFloat(resolution.x) * 0.33, y: CGFloat(resolution.y) * 0.33) let itemSize = CGSize(width: size.width / CGFloat(resolution.x), height: size.height / CGFloat(resolution.y)) let params = RippleParams(amplitude: 26.0, frequency: 15.0, decay: 8.0, speed: 1400.0) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index a1f5b25732..738ada201f 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -6617,7 +6617,7 @@ public final class StoryItemSetContainerComponent: Component { component.reorder() }))) items.append(.action(ContextMenuActionItem(text: "Delete", textColor: .destructive, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.destructiveColor) + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak self] _, a in a(.default) From 16b72bc8ed71aa6af05f15361d708982d8cf4ae6 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Fri, 26 Jul 2024 00:40:31 +0800 Subject: [PATCH 12/41] Update localization --- .../Telegram-iOS/en.lproj/Localizable.strings | 41 ++++ .../ChatListSearchFiltersContainerNode.swift | 3 +- .../Sources/ChatListSearchListPaneNode.swift | 11 +- .../Sources/VoiceChatController.swift | 3 +- .../Sources/MiniAppListScreen.swift | 6 +- .../Sources/PeerInfoPaneContainerNode.swift | 3 +- .../Sources/PeerInfoScreen.swift | 206 +----------------- .../Sources/PeerInfoStoryPaneNode.swift | 63 ++---- .../Sources/LanguageSelectionScreen.swift | 3 +- .../StoryItemSetContainerComponent.swift | 8 +- .../Sources/UndoOverlayControllerNode.swift | 1 - .../UrlHandling/Sources/UrlHandling.swift | 6 +- 12 files changed, 87 insertions(+), 267 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 2993d91058..61724087e1 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -12602,3 +12602,44 @@ Sorry for the inconvenience."; "Story.Privacy.ChooseCoverInfo" = "Choose a frame from the story to show in your Profile."; "Story.Privacy.ChooseCoverChannelInfo" = "Choose a frame from the story to show in channel profile."; "Story.Privacy.ChooseCoverGroupInfo" = "Choose a frame from the story to show in group profile."; + +"ChatList.Search.FilterApps" = "Apps"; +"ChatList.Search.SectionPopularApps" = "POPULAR APPS"; +"ChatList.Search.SectionRecentApps" = "APPS YOU USE"; +"ChatList.Search.Apps.Empty.Text" = "No Apps Found"; + +"VoiceChat.MicrophoneModes" = "Microphone Modes"; + +"MiniAppList.Title" = "Examples"; +"MiniAppList.ListSectionHeader" = "APPS THAT ACCEPT STARS"; + +"PeerInfo.PaneBotPreviews" = "Preview"; +"PeerInfo.OpenAppButton" = "Open App"; +"PeerInfo.AppFooterAdmin" = "By publishing this mini app, you agree to the [Telegram Terms of Service for Developers](https://telegram.org/privacy)."; +"PeerInfo.AppFooter" = "By launching this mini app, you agree to the [Terms of Service for Mini Apps](https://telegram.org/privacy)."; +"BotPreviews.MenuAddPreview" = "Add Preview"; +"BotPreviews.MenuReorder" = "Reorder"; +"BotPreviews.MenuDeleteLanguage" = "Delete %@"; +"BotPreviews.SubtitleLoading" = "loading"; +"BotPreviews.SubtitleEmpty" = "no preview added"; +"BotPreviews.SubtitleCount_1" = "1 preview"; +"BotPreviews.SubtitleCount_any" = "1 previews"; +"BotPreviews.SheetDeleteTitle_1" = "Delete 1 Preview?"; +"BotPreviews.SheetDeleteTitle_any" = "Delete %d Previews?"; +"BotPreviews.LanguageTab.Main" = "Main"; +"BotPreviews.LanguageTab.Add" = "+ Add Language"; +"BotPreviews.Empty.Title" = "No Preview"; +"BotPreviews.Empty.Text_1" = "Upload up to 1 screenshot and video demos for your mini app."; +"BotPreviews.Empty.Text_any" = "Upload up to %d screenshots and video demos for your mini app."; +"BotPreviews.Empty.Add" = "Add Preview"; +"BotPreviews.Empty.AddTranslation" = "Create a Translation"; +"BotPreviews.Empty.DeleteTranslation" = "Delete this Translation"; +"BotPreviews.Empty.Separator" = "or"; +"BotPreviews.AlertTooManyPreviews_1" = "You can add at most 1 preview."; +"BotPreviews.AlertTooManyPreviews_any" = "You can add at most 1 previews."; +"BotPreviews.DeleteTranslationAlert.Title" = "Delete Translation"; +"BotPreviews.DeleteTranslationAlert.Text" = "Are you sure you want to delete this translation?"; +"BotPreviews.TranslationFooter.Text" = "This preview will be displayed for all users who have %@ set as their language."; +"BotPreviews.DefaultFooter.Text" = "This preview will be shown by default. You can also add translations into specific languages."; +"BotPreviews.SelectLanguage.Title" = "Add a Translation"; +"BotPreview.ViewContextDelete" = "Delete Preview"; diff --git a/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift index 4ac67d6a96..127b853313 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift @@ -88,8 +88,7 @@ private final class ItemNode: ASDisplayNode { title = presentationData.strings.ChatList_Search_FilterChannels icon = nil case .apps: - //TODO:localize - title = "Apps" + title = presentationData.strings.ChatList_Search_FilterApps icon = nil case .media: title = presentationData.strings.ChatList_Search_FilterMedia diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 57c0091630..bcb62df9a9 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -254,16 +254,15 @@ private enum ChatListRecentEntry: Comparable, Identifiable { } } } else if case .apps = key { - //TODO:localize if case .popularApps = section { - header = ChatListSearchItemHeader(type: .text("POPULAR APPS", 1), theme: theme, strings: strings) + header = ChatListSearchItemHeader(type: .text(presentationData.strings.ChatList_Search_SectionPopularApps, 1), theme: theme, strings: strings) } else { if let isChannelsTabExpanded { - header = ChatListSearchItemHeader(type: .text("APPS YOU USE", 0), theme: theme, strings: strings, actionTitle: isChannelsTabExpanded ? presentationData.strings.ChatList_Search_SectionActionShowLess : presentationData.strings.ChatList_Search_SectionActionShowMore, action: { + header = ChatListSearchItemHeader(type: .text(presentationData.strings.ChatList_Search_SectionRecentApps, 0), theme: theme, strings: strings, actionTitle: isChannelsTabExpanded ? presentationData.strings.ChatList_Search_SectionActionShowLess : presentationData.strings.ChatList_Search_SectionActionShowMore, action: { toggleChannelsTabExpanded() }) } else { - header = ChatListSearchItemHeader(type: .text("APPS YOU USE", 0), theme: theme, strings: strings, actionTitle: nil, action: nil) + header = ChatListSearchItemHeader(type: .text(presentationData.strings.ChatList_Search_SectionRecentApps, 0), theme: theme, strings: strings, actionTitle: nil, action: nil) } } } else { @@ -1454,8 +1453,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { if key == .channels { emptyRecentTextNode.attributedText = NSAttributedString(string: presentationData.strings.ChatList_Search_RecommendedChannelsEmpty_Text, font: Font.regular(15.0), textColor: presentationData.theme.list.freeTextColor) } else if key == .apps { - //TODO:localize - emptyRecentTextNode.attributedText = NSAttributedString(string: "No Apps Found", font: Font.regular(15.0), textColor: presentationData.theme.list.freeTextColor) + emptyRecentTextNode.attributedText = NSAttributedString(string: presentationData.strings.ChatList_Search_Apps_Empty_Text, font: Font.regular(15.0), textColor: presentationData.theme.list.freeTextColor) } self.emptyRecentTextNode = emptyRecentTextNode @@ -3417,7 +3415,6 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { continue } let peerNotificationSettings = notificationSettings[id] - //TODO:localize let subpeerSummary: RecentlySearchedPeerSubpeerSummary? = nil var peerStoryStats: PeerStoryStats? if let value = storyStats[peer.id] { diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift index dc9a008717..b4a2ac7239 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift @@ -2627,8 +2627,7 @@ public final class VoiceChatControllerImpl: ViewController, VoiceChatController if !isScheduled && canSpeak { if #available(iOS 15.0, *) { - //TODO:localize - items.append(.action(ContextMenuActionItem(text: "Microphone Modes", textColor: .primary, icon: { theme in + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_MicrophoneModes, textColor: .primary, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Noise"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in f(.dismissWithoutContent) diff --git a/submodules/TelegramUI/Components/MiniAppListScreen/Sources/MiniAppListScreen.swift b/submodules/TelegramUI/Components/MiniAppListScreen/Sources/MiniAppListScreen.swift index 08ecaa53f2..579bd7b8d9 100644 --- a/submodules/TelegramUI/Components/MiniAppListScreen/Sources/MiniAppListScreen.swift +++ b/submodules/TelegramUI/Components/MiniAppListScreen/Sources/MiniAppListScreen.swift @@ -246,8 +246,7 @@ final class MiniAppListScreenComponent: Component { ) -> CGFloat { let rightButtons: [AnyComponentWithIdentity] = [] - //TODO:localize - let titleText: String = "Examples" + let titleText: String = strings.MiniAppList_Title let closeTitle: String = strings.Common_Close let headerContent: ChatListHeaderComponent.Content? = ChatListHeaderComponent.Content( @@ -316,12 +315,11 @@ final class MiniAppListScreenComponent: Component { containerSize: size ) - //TODO:localize let sectionHeaderSize = self.sectionHeader.update( transition: transition, component: AnyComponent(ListHeaderComponent( theme: theme, - title: "APPS THAT ACCEPT STARS" + title: strings.MiniAppList_ListSectionHeader )), environment: {}, containerSize: CGSize(width: size.width, height: 1000.0) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift index 91606b4938..dd48610be3 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift @@ -1126,8 +1126,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat case .storyArchive: title = presentationData.strings.PeerInfo_PaneArchivedStories case .botPreview: - //TODO:localize - title = "Preview" + title = presentationData.strings.PeerInfo_PaneBotPreviews case .media: title = presentationData.strings.PeerInfo_PaneMedia case .files: diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index e0669283b2..2ae84f8593 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -1357,8 +1357,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese } else if hasAbout || hasWebApp { var actionButton: PeerInfoScreenLabeledValueItem.Button? if hasWebApp { - //TODO:localize - actionButton = PeerInfoScreenLabeledValueItem.Button(title: "Open App", action: { + actionButton = PeerInfoScreenLabeledValueItem.Button(title: presentationData.strings.PeerInfo_OpenAppButton, action: { guard let parentController = interaction.getController() else { return } @@ -1399,8 +1398,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese })) if let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) { - //TODO:localize - items[currentPeerInfoSection]!.append(PeerInfoScreenCommentItem(id: 800, text: "By publishing this mini app, you agree to the [Telegram Terms of Service for Developers](https://telegram.org/privacy).", linkAction: { action in + items[currentPeerInfoSection]!.append(PeerInfoScreenCommentItem(id: 800, text: presentationData.strings.PeerInfo_AppFooterAdmin, linkAction: { action in if case let .tap(url) = action { context.sharedContext.applicationBindings.openUrl(url) } @@ -1408,8 +1406,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese currentPeerInfoSection = .peerInfoTrailing } else if actionButton != nil { - //TODO:localize - items[currentPeerInfoSection]!.append(PeerInfoScreenCommentItem(id: 800, text: "By launching this mini app, you agree to the [Terms of Service for Mini Apps](https://telegram.org/privacy).", linkAction: { action in + items[currentPeerInfoSection]!.append(PeerInfoScreenCommentItem(id: 800, text: presentationData.strings.PeerInfo_AppFooter, linkAction: { action in if case let .tap(url) = action { context.sharedContext.applicationBindings.openUrl(url) } @@ -10903,14 +10900,11 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro var items: [ContextMenuItem] = [] let strings = self.presentationData.strings - let _ = strings - - //TODO:localize var ignoreNextActions = false if pane.canAddMoreBotPreviews() { - items.append(.action(ContextMenuActionItem(text: "Add Preview", icon: { theme in + items.append(.action(ContextMenuActionItem(text: strings.BotPreviews_MenuAddPreview, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, a in if ignoreNextActions { @@ -10956,8 +10950,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }))) if let language = pane.currentBotPreviewLanguage { - //TODO:localize - items.append(.action(ContextMenuActionItem(text: "Delete \(language.name)", textColor: .destructive, icon: { theme in + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.BotPreviews_MenuDeleteLanguage(language.name).string, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak pane] _, a in if ignoreNextActions { @@ -14038,192 +14031,3 @@ private final class HeaderContextReferenceContentSource: ContextReferenceContent return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds) } } - -/*private func openWebApp(parentController: ViewController, context: AccountContext, peer: EnginePeer, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource) { - let presentationData = context.sharedContext.currentPresentationData.with({ $0 }) - - let botName: String - let botAddress: String - let botVerified: Bool - if case let .inline(bot) = source { - botName = bot.compactDisplayTitle - botAddress = bot.addressName ?? "" - botVerified = bot.isVerified - } else { - botName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - botAddress = peer.addressName ?? "" - botVerified = peer.isVerified - } - - let _ = botAddress - - let openWebView = { [weak parentController] in - guard let parentController else { - return - } - - if source == .menu { - if let navigationController = parentController.navigationController as? NavigationController, let minimizedContainer = navigationController.minimizedContainer { - for controller in minimizedContainer.controllers { - if let controller = controller as? AttachmentController, let mainController = controller.mainController as? WebAppController, mainController.botId == peer.id && mainController.source == .menu { - navigationController.maximizeViewController(controller, animated: true) - return - } - } - } - - var fullSize = false - if isTelegramMeLink(url), let internalUrl = parseFullInternalUrl(sharedContext: context.sharedContext, url: url), case .peer(_, .appStart) = internalUrl { - fullSize = !url.contains("?mode=compact") - } - - 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) - //TODO:localize - //updatedPresentationData - let controller = standaloneWebAppController(context: context, updatedPresentationData: nil, params: params, threadId: nil, openUrl: { [weak parentController] url, concealed, commit in - guard let parentController else { - return - } - let _ = parentController - /*ChatControllerImpl.botOpenUrl(context: context, peerId: peerId, controller: self, url: url, concealed: concealed, present: { c, a in - presentImpl?(c, a) - }, commit: commit)*/ - }, requestSwitchInline: { [weak parentController] query, chatTypes, completion in - guard let parentController else { - return - } - let _ = parentController - //ChatControllerImpl.botRequestSwitchInline(context: context, controller: self, peerId: peerId, botAddress: botAddress, query: query, chatTypes: chatTypes, completion: completion) - }, getInputContainerNode: { - return nil - }, completion: { - }, willDismiss: { - }, didDismiss: { - }, getNavigationController: { [weak parentController] () -> NavigationController? in - guard let parentController else { - return nil - } - return parentController.navigationController as? NavigationController ?? context.sharedContext.mainWindow?.viewController as? NavigationController - }) - controller.navigationPresentation = .flatModal - parentController.push(controller) - - presentImpl = { [weak controller] c, a in - controller?.present(c, in: .window(.root), with: a) - } - let _ = presentImpl - } else if simple { - var isInline = false - var botId = peer.id - var botName = botName - var botAddress = "" - var botVerified = false - if case let .inline(bot) = source { - isInline = true - botId = bot.id - botName = bot.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - botAddress = bot.addressName ?? "" - botVerified = bot.isVerified - } - - let _ = botAddress - - let _ = ((context.engine.messages.requestSimpleWebView(botId: botId, url: url, source: isInline ? .inline : .generic, themeParams: generateWebAppThemeParams(presentationData.theme))) - |> deliverOnMainQueue).startStandalone(next: { [weak parentController] result in - guard let parentController else { - return - } - var presentImpl: ((ViewController, Any?) -> Void)? - let params = WebAppParameters(source: isInline ? .inline : .simple, peerId: peer.id, botId: botId, botName: botName, botVerified: botVerified, url: result.url, queryId: nil, payload: nil, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false, fullSize: result.flags.contains(.fullSize)) - let controller = standaloneWebAppController(context: context, updatedPresentationData: nil, params: params, threadId: nil, openUrl: { [weak parentController] url, concealed, commit in - guard let parentController else { - return - } - let _ = parentController - /*ChatControllerImpl.botOpenUrl(context: context, peerId: peerId, controller: self, url: url, concealed: concealed, present: { c, a in - presentImpl?(c, a) - }, commit: commit)*/ - }, requestSwitchInline: { query, chatTypes, completion in - //ChatControllerImpl.botRequestSwitchInline(context: context, controller: self, peerId: peerId, botAddress: botAddress, query: query, chatTypes: chatTypes, completion: completion) - }, getNavigationController: { [weak parentController] in - guard let parentController else { - return nil - } - return parentController.navigationController as? NavigationController ?? context.sharedContext.mainWindow?.viewController as? NavigationController - }) - controller.navigationPresentation = .flatModal - parentController.push(controller) - - presentImpl = { [weak controller] c, a in - controller?.present(c, in: .window(.root), with: a) - } - let _ = presentImpl - }, error: { [weak parentController] error in - guard let parentController else { - return - } - parentController.present(textAlertController(context: context, updatedPresentationData: nil, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { - })]), in: .window(.root)) - }) - } else { - let _ = ((context.engine.messages.requestWebView(peerId: peer.id, botId: peer.id, url: !url.isEmpty ? url : nil, payload: nil, themeParams: generateWebAppThemeParams(presentationData.theme), fromMenu: false, replyToMessageId: nil, threadId: nil)) - |> deliverOnMainQueue).startStandalone(next: { [weak parentController] result in - guard let parentController else { - return - } - var presentImpl: ((ViewController, Any?) -> Void)? - let context = context - 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: nil, params: params, threadId: nil, openUrl: { [weak parentController] url, concealed, commit in - guard let parentController else { - return - } - let _ = parentController - /*ChatControllerImpl.botOpenUrl(context: context, peerId: peerId, controller: self, url: url, concealed: concealed, present: { c, a in - presentImpl?(c, a) - }, commit: commit)*/ - }, completion: { - }, getNavigationController: { [weak parentController] in - guard let parentController else { - return nil - } - return parentController.navigationController as? NavigationController ?? context.sharedContext.mainWindow?.viewController as? NavigationController - }) - controller.navigationPresentation = .flatModal - parentController.push(controller) - - presentImpl = { [weak controller] c, a in - controller?.present(c, in: .window(.root), with: a) - } - let _ = presentImpl - }, error: { [weak parentController] error in - guard let parentController else { - return - } - parentController.present(textAlertController(context: context, updatedPresentationData: nil, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { - })]), in: .window(.root)) - }) - } - } - - var botPeer = peer - if case let .inline(bot) = source { - botPeer = bot - } - let _ = (ApplicationSpecificNotice.getBotGameNotice(accountManager: context.sharedContext.accountManager, peerId: botPeer.id) - |> deliverOnMainQueue).startStandalone(next: { [weak parentController] value in - guard let parentController else { - return - } - if value { - openWebView() - } else { - let controller = webAppLaunchConfirmationController(context: context, updatedPresentationData: nil, peer: botPeer, completion: { _ in - let _ = ApplicationSpecificNotice.setBotGameNotice(accountManager: context.sharedContext.accountManager, peerId: botPeer.id).startStandalone() - openWebView() - }, showMore: nil) - parentController.present(controller, in: .window(.root)) - } - }) -}*/ diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift index 6a13db508e..15327d43df 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift @@ -2504,8 +2504,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr self.parentController?.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: isPinned ? "anim_toastunpin" : "anim_toastpin", scale: 0.06, colors: [:], title: toastTitle, text: toastText, customUndoText: nil, timeout: 5), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) }))) if isPinned && self.canReorder() { - //TODO:localize - items.append(.action(ContextMenuActionItem(text: "Reorder", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.BotPreviews_MenuReorder, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in c?.dismiss(completion: { guard let self else { return @@ -2569,8 +2568,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } if canManage, case .botPreview = self.scope, self.canReorder() { - //TODO:localize - items.append(.action(ContextMenuActionItem(text: "Reorder", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.BotPreviews_MenuReorder, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in c?.dismiss(completion: { guard let self else { return @@ -2738,11 +2736,10 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr let title: String if state.totalCount == 0 { if case .botPreview = self.scope { - //TODO:localize if state.isLoading { - title = "loading" + title = self.presentationData.strings.BotPreviews_SubtitleLoading } else { - title = "no preview added" + title = self.presentationData.strings.BotPreviews_SubtitleEmpty } } else { title = "" @@ -2756,12 +2753,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr title = self.presentationData.strings.StoryList_SubtitleCount(Int32(state.totalCount)) } } else if case .botPreview = self.scope { - //TODO:localize - if state.totalCount == 1 { - title = "1 preview" - } else { - title = "\(state.totalCount) previews" - } + title = self.presentationData.strings.BotPreviews_SubtitleCount(Int32(state.totalCount)) } else { title = "" } @@ -3357,18 +3349,12 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr return } - //TODO:localize - let title: String - if mappedMedia.count == 1 { - title = "Delete 1 Preview?" - } else { - title = "Delete \(mappedMedia.count) Previews?" - } + let title: String = presentationData.strings.BotPreviews_SheetDeleteTitle(Int32(mappedMedia.count)) controller.setItemGroups([ ActionSheetItemGroup(items: [ ActionSheetTextItem(title: title), - ActionSheetButtonItem(title: "Delete", color: .destructive, action: { [weak self] in + ActionSheetButtonItem(title: presentationData.strings.Common_Delete, color: .destructive, action: { [weak self] in dismissAction() guard let self else { @@ -3572,11 +3558,10 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr self.botPreviewLanguageTab = botPreviewLanguageTab } - //TODO:localize var languageItems: [TabSelectorComponent.Item] = [] languageItems.append(TabSelectorComponent.Item( id: AnyHashable("_main"), - title: "Main" + title: self.presentationData.strings.BotPreviews_LanguageTab_Main )) for language in self.currentBotPreviewLanguages { languageItems.append(TabSelectorComponent.Item( @@ -3586,7 +3571,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } languageItems.append(TabSelectorComponent.Item( id: AnyHashable("_add"), - title: "+ Add Language" + title: self.presentationData.strings.BotPreviews_LanguageTab_Add )) var selectedLanguageId = "_main" if let listSource = self.listSource as? BotPreviewStoryListContext, let language = listSource.language { @@ -3653,9 +3638,10 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr let text: String if let listSource = self.listSource as? BotPreviewStoryListContext, let id = listSource.language, let language = self.currentBotPreviewLanguages.first(where: { $0.id == id }) { isMainLanguage = false - text = "This preview will be displayed for all users who have \(language.name) set as their language." + + text = self.presentationData.strings.BotPreviews_TranslationFooter_Text(language.name).string } else { - text = "This preview will be shown by default. You can also add translations into specific languages." + text = self.presentationData.strings.BotPreviews_DefaultFooter_Text } let botPreviewFooterSize = botPreviewFooter.update( @@ -3667,7 +3653,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr animationName: nil, title: nil, text: text, - actionTitle: "Add Preview", + actionTitle: self.presentationData.strings.BotPreviews_Empty_Add, action: { [weak self] in guard let self else { return @@ -3678,7 +3664,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr self.presentUnableToAddMorePreviewsAlert() } }, - additionalActionTitle: isMainLanguage ? "Create a Translation" : nil, + additionalActionTitle: isMainLanguage ? self.presentationData.strings.BotPreviews_Empty_AddTranslation : nil, additionalAction: { [weak self] in guard let self else { return @@ -3687,7 +3673,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr self.presentAddBotPreviewLanguage() } }, - additionalActionSeparator: isMainLanguage ? "or" : nil + additionalActionSeparator: isMainLanguage ? self.presentationData.strings.BotPreviews_Empty_Separator : nil )), environment: {}, containerSize: CGSize(width: size.width, height: 1000.0) @@ -4032,7 +4018,6 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr emptyStateView = ComponentView() self.emptyStateView = emptyStateView } - //TODO:localize var isMainLanguage = true if let listSource = self.listSource as? BotPreviewStoryListContext, let _ = listSource.language { @@ -4046,9 +4031,9 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr theme: presentationData.theme, fitToHeight: self.isProfileEmbedded, animationName: nil, - title: "No Preview", - text: "Upload up to \(self.maxBotPreviewCount) screenshots and video demos for your mini app.", - actionTitle: self.canManageStories ? "Add Preview" : nil, + title: presentationData.strings.BotPreviews_Empty_Title, + text: presentationData.strings.BotPreviews_Empty_Text(Int32(self.maxBotPreviewCount)), + actionTitle: self.canManageStories ? presentationData.strings.BotPreviews_Empty_Add : nil, action: { [weak self] in guard let self else { return @@ -4059,7 +4044,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr self.presentUnableToAddMorePreviewsAlert() } }, - additionalActionTitle: self.canManageStories ? (isMainLanguage ? "Create a Translation" : "Delete this Translation") : nil, + additionalActionTitle: self.canManageStories ? (isMainLanguage ? presentationData.strings.BotPreviews_Empty_AddTranslation : presentationData.strings.BotPreviews_Empty_DeleteTranslation) : nil, additionalAction: { if isMainLanguage { self.presentAddBotPreviewLanguage() @@ -4067,7 +4052,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr self.presentDeleteBotPreviewLanguage() } }, - additionalActionSeparator: self.canManageStories ? "or" : nil + additionalActionSeparator: self.canManageStories ? presentationData.strings.BotPreviews_Empty_Separator : nil )), environment: {}, containerSize: CGSize(width: size.width, height: size.height - gridTopInset - bottomInset) @@ -4267,19 +4252,17 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr } public func presentUnableToAddMorePreviewsAlert() { - //TODO:localize - self.parentController?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: "You can add at most \(self.maxBotPreviewCount) previews.", actions: [ + self.parentController?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.BotPreviews_AlertTooManyPreviews(Int32(self.maxBotPreviewCount)), actions: [ TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: { }) ], parseMarkdown: true), in: .window(.root)) } public func presentDeleteBotPreviewLanguage() { - //TODO:localize - self.parentController?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: "Delete Translation", text: "Are you sure you want to delete this translation?", actions: [ + self.parentController?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: self.presentationData.strings.BotPreviews_DeleteTranslationAlert_Title, text: self.presentationData.strings.BotPreviews_DeleteTranslationAlert_Text, actions: [ TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_Cancel, action: { }), - TextAlertAction(type: .destructiveAction, title: "OK", action: { [weak self] in + TextAlertAction(type: .destructiveAction, title: self.presentationData.strings.Common_OK, action: { [weak self] in guard let self else { return } diff --git a/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/Sources/LanguageSelectionScreen.swift b/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/Sources/LanguageSelectionScreen.swift index 7b5bebee31..9d698608da 100644 --- a/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/Sources/LanguageSelectionScreen.swift +++ b/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/Sources/LanguageSelectionScreen.swift @@ -42,8 +42,7 @@ public class LanguageSelectionScreen: ViewController { self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style self.navigationPresentation = .modal - //TODO:localize - self.title = "Add a Translation" + self.title = self.presentationData.strings.BotPreviews_SelectLanguage_Title self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 738ada201f..2db32693a4 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -5655,8 +5655,7 @@ public final class StoryItemSetContainerComponent: Component { let deleteTitle: String if case let .user(user) = component.slice.peer, user.botInfo != nil { - //TODO:localize - deleteTitle = "Delete Preview" + deleteTitle = component.strings.BotPreview_ViewContextDelete } else { deleteTitle = component.strings.Story_ContextDeleteStory } @@ -6604,8 +6603,7 @@ public final class StoryItemSetContainerComponent: Component { if case let .user(user) = component.slice.peer, let botInfo = user.botInfo { if botInfo.flags.contains(.canEdit) { - //TODO:localize - items.append(.action(ContextMenuActionItem(text: "Reorder", icon: { theme in + items.append(.action(ContextMenuActionItem(text: presentationData.strings.BotPreviews_MenuReorder, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, a in a(.default) @@ -6616,7 +6614,7 @@ public final class StoryItemSetContainerComponent: Component { component.reorder() }))) - items.append(.action(ContextMenuActionItem(text: "Delete", textColor: .destructive, icon: { theme in + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Common_Delete, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak self] _, a in a(.default) diff --git a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift index 59b63dd909..d9398de8a4 100644 --- a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift +++ b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift @@ -437,7 +437,6 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.textNode.visibility = true } - //TODO:localize self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white) displayUndo = false diff --git a/submodules/UrlHandling/Sources/UrlHandling.swift b/submodules/UrlHandling/Sources/UrlHandling.swift index f8fb3f07c5..af85edc262 100644 --- a/submodules/UrlHandling/Sources/UrlHandling.swift +++ b/submodules/UrlHandling/Sources/UrlHandling.swift @@ -1203,7 +1203,11 @@ public func resolveUrlImpl(context: AccountContext, peerId: PeerId?, url: String var url = url if !url.contains("://") && !url.hasPrefix("tel:") && !url.hasPrefix("mailto:") && !url.hasPrefix("calshow:") { if !(url.hasPrefix("http") || url.hasPrefix("https")) { - url = "http://\(url)" + if let mappedURL = URL(string: "https://\(url)"), let host = mappedURL.host, host.lowercased().hasSuffix(".ton") { + url = "tonsite://\(url)" + } else { + url = "http://\(url)" + } } } From 14d36ceb0e7dda85fadc48669a3f3f083541d8d3 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 25 Jul 2024 19:19:31 +0200 Subject: [PATCH 13/41] Various fixes --- .../Sources/BrowserAddressBarComponent.swift | 25 ++- .../BrowserNavigationBarComponent.swift | 33 +-- .../BrowserUI/Sources/BrowserScreen.swift | 190 ++++++++++++++++-- .../BrowserUI/Sources/BrowserWebContent.swift | 6 +- .../Sources/ChannelStatsController.swift | 28 ++- .../MediaEditor/Sources/MediaEditor.swift | 14 +- .../Sources/MediaCoverScreen.swift | 11 +- .../Sources/MediaEditorScreen.swift | 25 ++- .../Resources/WebEmbed/UIWebViewSearch.js | 9 +- 9 files changed, 272 insertions(+), 69 deletions(-) diff --git a/submodules/BrowserUI/Sources/BrowserAddressBarComponent.swift b/submodules/BrowserUI/Sources/BrowserAddressBarComponent.swift index 6ab928088a..cb5f0d373c 100644 --- a/submodules/BrowserUI/Sources/BrowserAddressBarComponent.swift +++ b/submodules/BrowserUI/Sources/BrowserAddressBarComponent.swift @@ -15,6 +15,7 @@ final class AddressBarContentComponent: Component { let theme: PresentationTheme let strings: PresentationStrings + let metrics: LayoutMetrics let url: String let isSecure: Bool let isExpanded: Bool @@ -23,6 +24,7 @@ final class AddressBarContentComponent: Component { init( theme: PresentationTheme, strings: PresentationStrings, + metrics: LayoutMetrics, url: String, isSecure: Bool, isExpanded: Bool, @@ -30,6 +32,7 @@ final class AddressBarContentComponent: Component { ) { self.theme = theme self.strings = strings + self.metrics = metrics self.url = url self.isSecure = isSecure self.isExpanded = isExpanded @@ -43,6 +46,9 @@ final class AddressBarContentComponent: Component { if lhs.strings !== rhs.strings { return false } + if lhs.metrics != rhs.metrics { + return false + } if lhs.url != rhs.url { return false } @@ -78,6 +84,7 @@ final class AddressBarContentComponent: Component { var title: String var isSecure: Bool var collapseFraction: CGFloat + var isTablet: Bool static func ==(lhs: Params, rhs: Params) -> Bool { if lhs.theme !== rhs.theme { @@ -101,6 +108,9 @@ final class AddressBarContentComponent: Component { if lhs.collapseFraction != rhs.collapseFraction { return false } + if lhs.isTablet != rhs.isTablet { + return false + } return true } } @@ -254,7 +264,7 @@ final class AddressBarContentComponent: Component { self.placeholderContent.view?.isHidden = !text.isEmpty if let params = self.params { - self.update(theme: params.theme, strings: params.strings, size: params.size, isActive: params.isActive, title: params.title, isSecure: params.isSecure, collapseFraction: params.collapseFraction, transition: .immediate) + self.update(theme: params.theme, strings: params.strings, size: params.size, isActive: params.isActive, title: params.title, isSecure: params.isSecure, collapseFraction: params.collapseFraction, isTablet: params.isTablet, transition: .immediate) } } @@ -281,12 +291,12 @@ final class AddressBarContentComponent: Component { title = title.idnaDecoded ?? title } - self.update(theme: component.theme, strings: component.strings, size: availableSize, isActive: isActive, title: title.lowercased(), isSecure: component.isSecure, collapseFraction: collapseFraction, transition: transition) + self.update(theme: component.theme, strings: component.strings, size: availableSize, isActive: isActive, title: title.lowercased(), isSecure: component.isSecure, collapseFraction: collapseFraction, isTablet: component.metrics.isTablet, transition: transition) return availableSize } - public func update(theme: PresentationTheme, strings: PresentationStrings, size: CGSize, isActive: Bool, title: String, isSecure: Bool, collapseFraction: CGFloat, transition: ComponentTransition) { + public func update(theme: PresentationTheme, strings: PresentationStrings, size: CGSize, isActive: Bool, title: String, isSecure: Bool, collapseFraction: CGFloat, isTablet: Bool, transition: ComponentTransition) { let params = Params( theme: theme, strings: strings, @@ -294,7 +304,8 @@ final class AddressBarContentComponent: Component { isActive: isActive, title: title, isSecure: isSecure, - collapseFraction: collapseFraction + collapseFraction: collapseFraction, + isTablet: isTablet ) if self.params == params { @@ -334,13 +345,13 @@ final class AddressBarContentComponent: Component { let cancelButtonSpacing: CGFloat = 8.0 var backgroundFrame = CGRect(origin: CGPoint(x: sideInset, y: topInset), size: CGSize(width: size.width - sideInset * 2.0, height: inputHeight)) - if isActiveWithText { + if isActiveWithText && !isTablet { backgroundFrame.size.width -= cancelTextSize.width + cancelButtonSpacing } transition.setFrame(layer: self.backgroundLayer, frame: backgroundFrame) transition.setFrame(view: self.cancelButton, frame: CGRect(origin: CGPoint(x: backgroundFrame.maxX, y: 0.0), size: CGSize(width: cancelButtonSpacing + cancelTextSize.width, height: size.height))) - self.cancelButton.isUserInteractionEnabled = isActiveWithText + self.cancelButton.isUserInteractionEnabled = isActiveWithText && !isTablet let textX: CGFloat = backgroundFrame.minX + sideInset let textFrame = CGRect(origin: CGPoint(x: textX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.maxX - textX, height: backgroundFrame.height)) @@ -418,7 +429,7 @@ final class AddressBarContentComponent: Component { cancelButtonTitleComponentView.isUserInteractionEnabled = false } transition.setFrame(view: cancelButtonTitleComponentView, frame: CGRect(origin: CGPoint(x: backgroundFrame.maxX + cancelButtonSpacing, y: floor((size.height - cancelTextSize.height) / 2.0)), size: cancelTextSize)) - transition.setAlpha(view: cancelButtonTitleComponentView, alpha: isActiveWithText ? 1.0 : 0.0) + transition.setAlpha(view: cancelButtonTitleComponentView, alpha: isActiveWithText && !isTablet ? 1.0 : 0.0) } let textFieldFrame = CGRect(origin: CGPoint(x: textFrame.minX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.maxX - textFrame.minX, height: backgroundFrame.height)) diff --git a/submodules/BrowserUI/Sources/BrowserNavigationBarComponent.swift b/submodules/BrowserUI/Sources/BrowserNavigationBarComponent.swift index 1b0d09728c..674b5a7b9c 100644 --- a/submodules/BrowserUI/Sources/BrowserNavigationBarComponent.swift +++ b/submodules/BrowserUI/Sources/BrowserNavigationBarComponent.swift @@ -29,6 +29,7 @@ final class BrowserNavigationBarComponent: CombinedComponent { let topInset: CGFloat let height: CGFloat let sideInset: CGFloat + let metrics: LayoutMetrics let leftItems: [AnyComponentWithIdentity] let rightItems: [AnyComponentWithIdentity] let centerItem: AnyComponentWithIdentity? @@ -46,6 +47,7 @@ final class BrowserNavigationBarComponent: CombinedComponent { topInset: CGFloat, height: CGFloat, sideInset: CGFloat, + metrics: LayoutMetrics, leftItems: [AnyComponentWithIdentity], rightItems: [AnyComponentWithIdentity], centerItem: AnyComponentWithIdentity?, @@ -62,6 +64,7 @@ final class BrowserNavigationBarComponent: CombinedComponent { self.topInset = topInset self.height = height self.sideInset = sideInset + self.metrics = metrics self.leftItems = leftItems self.rightItems = rightItems self.centerItem = centerItem @@ -96,6 +99,9 @@ final class BrowserNavigationBarComponent: CombinedComponent { if lhs.sideInset != rhs.sideInset { return false } + if lhs.metrics != rhs.metrics { + return false + } if lhs.leftItems != rhs.leftItems { return false } @@ -135,6 +141,8 @@ final class BrowserNavigationBarComponent: CombinedComponent { let expandedHeight = context.component.height let contentHeight: CGFloat = expandedHeight * (1.0 - context.component.collapseFraction) + collapsedHeight * context.component.collapseFraction let size = CGSize(width: context.availableSize.width, height: context.component.topInset + contentHeight) + let verticalOffset: CGFloat = context.component.metrics.isTablet ? -3.0 : 0.0 + let itemSpacing: CGFloat = context.component.metrics.isTablet ? 24.0 : 8.0 let background = background.update( component: Rectangle(color: context.component.backgroundColor.withAlphaComponent(1.0)), @@ -164,7 +172,7 @@ final class BrowserNavigationBarComponent: CombinedComponent { availableSize: CGSize(width: size.width, height: size.height), transition: context.transition ) - + var leftItemList: [_UpdatedChildComponent] = [] for item in context.component.leftItems { let item = leftItems[item.id].update( @@ -186,11 +194,7 @@ final class BrowserNavigationBarComponent: CombinedComponent { rightItemList.append(item) availableWidth -= item.size.width } - - if !leftItemList.isEmpty || !rightItemList.isEmpty { - availableWidth -= 14.0 - } - + context.add(background .position(CGPoint(x: size.width / 2.0, y: size.height / 2.0)) ) @@ -216,36 +220,37 @@ final class BrowserNavigationBarComponent: CombinedComponent { var leftItemX = sideInset for item in leftItemList { context.add(item - .position(CGPoint(x: leftItemX + item.size.width / 2.0 - (item.size.width / 2.0 * 0.35 * context.component.collapseFraction), y: context.component.topInset + contentHeight / 2.0)) + .position(CGPoint(x: leftItemX + item.size.width / 2.0 - (item.size.width / 2.0 * 0.35 * context.component.collapseFraction), y: context.component.topInset + contentHeight / 2.0 + verticalOffset)) .scale(1.0 - 0.35 * context.component.collapseFraction) .opacity(1.0 - context.component.collapseFraction) .appear(.default(scale: true, alpha: true)) .disappear(.default(scale: true, alpha: true)) ) - leftItemX += item.size.width + 8.0 - centerLeftInset += item.size.width + 8.0 + leftItemX += item.size.width + itemSpacing + centerLeftInset += item.size.width + itemSpacing } var centerRightInset = sideInset - 5.0 var rightItemX = context.availableSize.width - (sideInset - 5.0) for item in rightItemList.reversed() { context.add(item - .position(CGPoint(x: rightItemX - item.size.width / 2.0 + (item.size.width / 2.0 * 0.35 * context.component.collapseFraction), y: context.component.topInset + contentHeight / 2.0)) + .position(CGPoint(x: rightItemX - item.size.width / 2.0 + (item.size.width / 2.0 * 0.35 * context.component.collapseFraction), y: context.component.topInset + contentHeight / 2.0 + verticalOffset)) .scale(1.0 - 0.35 * context.component.collapseFraction) .opacity(1.0 - context.component.collapseFraction) .appear(.default(scale: true, alpha: true)) .disappear(.default(scale: true, alpha: true)) ) - rightItemX -= item.size.width + 8.0 - centerRightInset += item.size.width + 8.0 + rightItemX -= item.size.width + itemSpacing + centerRightInset += item.size.width + itemSpacing } let maxCenterInset = max(centerLeftInset, centerRightInset) if !leftItemList.isEmpty || !rightItemList.isEmpty { - availableWidth -= 20.0 + availableWidth -= itemSpacing * CGFloat(max(0, leftItemList.count - 1)) + itemSpacing * CGFloat(max(0, rightItemList.count - 1)) + 30.0 } availableWidth -= context.component.sideInset * 2.0 + availableWidth = min(660.0, availableWidth) let environment = BrowserNavigationBarEnvironment(fraction: context.component.collapseFraction) @@ -264,7 +269,7 @@ final class BrowserNavigationBarComponent: CombinedComponent { } if let centerItem = centerItem { context.add(centerItem - .position(CGPoint(x: centerX, y: context.component.topInset + contentHeight / 2.0)) + .position(CGPoint(x: centerX, y: context.component.topInset + contentHeight / 2.0 + verticalOffset)) .scale(1.0 - 0.35 * context.component.collapseFraction) .appear(.default(scale: false, alpha: true)) .disappear(.default(scale: false, alpha: true)) diff --git a/submodules/BrowserUI/Sources/BrowserScreen.swift b/submodules/BrowserUI/Sources/BrowserScreen.swift index e6f681aa45..c3288ad5a1 100644 --- a/submodules/BrowserUI/Sources/BrowserScreen.swift +++ b/submodules/BrowserUI/Sources/BrowserScreen.swift @@ -80,6 +80,8 @@ private final class BrowserScreenComponent: CombinedComponent { let performAction = context.component.performAction let performHoldAction = context.component.performHoldAction + let isTablet = environment.metrics.isTablet + let navigationContent: AnyComponentWithIdentity? var navigationLeftItems: [AnyComponentWithIdentity] var navigationRightItems: [AnyComponentWithIdentity] @@ -106,6 +108,7 @@ private final class BrowserScreenComponent: CombinedComponent { AddressBarContentComponent( theme: environment.theme, strings: environment.strings, + metrics: environment.metrics, url: context.component.contentState?.url ?? "", isSecure: context.component.contentState?.isSecure ?? false, isExpanded: context.component.presentationState.addressFocused, @@ -126,7 +129,7 @@ private final class BrowserScreenComponent: CombinedComponent { ) } - if context.component.presentationState.addressFocused { + if context.component.presentationState.addressFocused && !isTablet { navigationLeftItems = [] navigationRightItems = [] } else { @@ -146,6 +149,68 @@ private final class BrowserScreenComponent: CombinedComponent { ) ] + if isTablet { + navigationLeftItems.append( + AnyComponentWithIdentity( + id: "minimize", + component: AnyComponent( + Button( + content: AnyComponent( + BundleIconComponent( + name: "Media Gallery/PictureInPictureButton", + tintColor: environment.theme.rootController.navigationBar.accentTextColor + ) + ), + action: { + performAction.invoke(.close) + } + ) + ) + ) + ) + + let canGoBack = context.component.contentState?.canGoBack ?? false + let canGoForward = context.component.contentState?.canGoForward ?? false + + navigationLeftItems.append( + AnyComponentWithIdentity( + id: "back", + component: AnyComponent( + Button( + content: AnyComponent( + BundleIconComponent( + name: "Instant View/Back", + tintColor: environment.theme.rootController.navigationBar.accentTextColor.withAlphaComponent(canGoBack ? 1.0 : 0.4) + ) + ), + action: { + performAction.invoke(.navigateBack) + } + ) + ) + ) + ) + + navigationLeftItems.append( + AnyComponentWithIdentity( + id: "forward", + component: AnyComponent( + Button( + content: AnyComponent( + BundleIconComponent( + name: "Instant View/Forward", + tintColor: environment.theme.rootController.navigationBar.accentTextColor.withAlphaComponent(canGoForward ? 1.0 : 0.4) + ) + ), + action: { + performAction.invoke(.navigateForward) + } + ) + ) + ) + ) + } + navigationRightItems = [ AnyComponentWithIdentity( id: "settings", @@ -168,6 +233,65 @@ private final class BrowserScreenComponent: CombinedComponent { ) ) ] + + if isTablet { + navigationRightItems.insert( + AnyComponentWithIdentity( + id: "bookmarks", + component: AnyComponent( + Button( + content: AnyComponent( + BundleIconComponent( + name: "Instant View/Bookmark", + tintColor: environment.theme.rootController.navigationBar.accentTextColor + ) + ), + action: { + performAction.invoke(.openBookmarks) + } + ) + ) + ), + at: 0 + ) + navigationRightItems.insert( + AnyComponentWithIdentity( + id: "share", + component: AnyComponent( + Button( + content: AnyComponent( + BundleIconComponent( + name: "Chat List/NavigationShare", + tintColor: environment.theme.rootController.navigationBar.accentTextColor + ) + ), + action: { + performAction.invoke(.share) + } + ) + ) + ), + at: 0 + ) + navigationRightItems.append( + AnyComponentWithIdentity( + id: "openIn", + component: AnyComponent( + Button( + content: AnyComponent( + BundleIconComponent( + name: "Instant View/Browser", + tintColor: environment.theme.rootController.navigationBar.accentTextColor + ) + ), + action: { + performAction.invoke(.openIn) + } + ) + ) + ) + ) + } } } @@ -183,6 +307,7 @@ private final class BrowserScreenComponent: CombinedComponent { topInset: environment.statusBarHeight, height: environment.navigationHeight - environment.statusBarHeight, sideInset: environment.safeInsets.left, + metrics: environment.metrics, leftItems: navigationLeftItems, rightItems: navigationRightItems, centerItem: navigationContent, @@ -238,22 +363,36 @@ private final class BrowserScreenComponent: CombinedComponent { toolbarBottomInset = environment.safeInsets.bottom } - let toolbar = toolbar.update( - component: BrowserToolbarComponent( - backgroundColor: environment.theme.rootController.navigationBar.blurredBackgroundColor, - separatorColor: environment.theme.rootController.navigationBar.separatorColor, - textColor: environment.theme.rootController.navigationBar.primaryTextColor, - bottomInset: toolbarBottomInset, - sideInset: environment.safeInsets.left, - item: toolbarContent, - collapseFraction: collapseFraction - ), - availableSize: context.availableSize, - transition: context.transition - ) - context.add(toolbar - .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - toolbar.size.height / 2.0)) - ) + var toolbarSize: CGFloat = 0.0 + if isTablet && !context.component.presentationState.isSearching { + + } else { + let toolbar = toolbar.update( + component: BrowserToolbarComponent( + backgroundColor: environment.theme.rootController.navigationBar.blurredBackgroundColor, + separatorColor: environment.theme.rootController.navigationBar.separatorColor, + textColor: environment.theme.rootController.navigationBar.primaryTextColor, + bottomInset: toolbarBottomInset, + sideInset: environment.safeInsets.left, + item: toolbarContent, + collapseFraction: collapseFraction + ), + availableSize: context.availableSize, + transition: context.transition + ) + context.add(toolbar + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - toolbar.size.height / 2.0)) + .appear(ComponentTransition.Appear { _, view, transition in + transition.animatePosition(view: view, from: CGPoint(x: 0.0, y: view.frame.height), to: CGPoint(), additive: true) + }) + .disappear(ComponentTransition.Disappear { view, transition, completion in + transition.animatePosition(view: view, from: CGPoint(), to: CGPoint(x: 0.0, y: view.frame.height), additive: true, completion: { _ in + completion() + }) + }) + ) + toolbarSize = toolbar.size.height + } if context.component.presentationState.addressFocused { let addressList = addressList.update( @@ -266,7 +405,7 @@ private final class BrowserScreenComponent: CombinedComponent { performAction.invoke(.navigateTo(url)) } ), - availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height - navigationBar.size.height - toolbar.size.height), + availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height - navigationBar.size.height - toolbarSize), transition: context.transition ) context.add(addressList @@ -1241,7 +1380,11 @@ public class BrowserScreen: ViewController, MinimizableController { super.containerLayoutUpdated(layout, transition: transition) - self.node.containerLayoutUpdated(layout: layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.height, transition: ComponentTransition(transition)) + var navigationHeight = self.navigationLayout(layout: layout).navigationFrame.height + if layout.metrics.isTablet, layout.size.width > layout.size.height { + navigationHeight += 6.0 + } + self.node.containerLayoutUpdated(layout: layout, navigationBarHeight: navigationHeight, transition: ComponentTransition(transition)) } public func requestMinimize(topEdgeOffset: CGFloat?, initialVelocity: CGFloat?) { @@ -1249,6 +1392,15 @@ public class BrowserScreen: ViewController, MinimizableController { self.node.minimize(topEdgeOffset: topEdgeOffset, damping: 180.0, initialVelocity: initialVelocity) } + private var didPlayAppearanceAnimation = false + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + if !self.didPlayAppearanceAnimation, let layout = self.validLayout, layout.metrics.isTablet { + self.node.layer.animatePosition(from: CGPoint(x: 0.0, y: layout.size.height), to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + } + } + public override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) diff --git a/submodules/BrowserUI/Sources/BrowserWebContent.swift b/submodules/BrowserUI/Sources/BrowserWebContent.swift index 6844c5929c..67b9bfa514 100644 --- a/submodules/BrowserUI/Sources/BrowserWebContent.swift +++ b/submodules/BrowserUI/Sources/BrowserWebContent.swift @@ -465,7 +465,7 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU self.webView.scrollView.contentInset = UIEdgeInsets(top: 0.0, left: 0.0, bottom: fullInsets.bottom, right: 0.0) self.webView.customBottomInset = max(insets.bottom, safeInsets.bottom) -// self.webView.scrollView.contentInset = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 34.0, right: 0.0) + self.webView.scrollView.scrollIndicatorInsets = UIEdgeInsets(top: 0.0, left: -insets.left, bottom: 0.0, right: -insets.right) self.webView.scrollView.horizontalScrollIndicatorInsets = UIEdgeInsets(top: 0.0, left: -insets.left, bottom: 0.0, right: -insets.right) @@ -589,7 +589,9 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU private var ignoreUpdatesUntilScrollingStopped = false func resetScrolling() { self.updateScrollingOffset(isReset: true, transition: .spring(duration: 0.4)) - self.ignoreUpdatesUntilScrollingStopped = true + if self.webView.scrollView.isDecelerating { + self.ignoreUpdatesUntilScrollingStopped = true + } } @available(iOS 13.0, *) diff --git a/submodules/StatisticsUI/Sources/ChannelStatsController.swift b/submodules/StatisticsUI/Sources/ChannelStatsController.swift index a294b33a05..13650ccf00 100644 --- a/submodules/StatisticsUI/Sources/ChannelStatsController.swift +++ b/submodules/StatisticsUI/Sources/ChannelStatsController.swift @@ -2049,6 +2049,7 @@ public func channelStatsController(context: AccountContext, updatedPresentationD let peer = Promise() peer.set(context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))) + let canViewStatsValue = Atomic(value: true) let peerData = context.engine.data.get( TelegramEngine.EngineData.Item.Peer.CanViewStats(id: peerId), TelegramEngine.EngineData.Item.Peer.AdsRestricted(id: peerId), @@ -2081,6 +2082,8 @@ public func channelStatsController(context: AccountContext, updatedPresentationD |> 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 _ = canViewStatsValue.swap(canViewStats) + var isGroup = false if let peer, case let .channel(channel) = peer, case .group = channel.info { isGroup = true @@ -2157,9 +2160,17 @@ public func channelStatsController(context: AccountContext, updatedPresentationD case .stats: index = 0 case .boosts: - index = 1 + if canViewStats { + index = 1 + } else { + index = 0 + } case .monetization: - index = 2 + if canViewStats { + index = 2 + } else { + index = 1 + } } var tabs: [String] = [] if canViewStats { @@ -2195,12 +2206,21 @@ public func channelStatsController(context: AccountContext, updatedPresentationD } controller.titleControlValueChanged = { value in updateState { state in + let canViewStats = canViewStatsValue.with { $0 } let section: ChannelStatsSection switch value { case 0: - section = .stats + if canViewStats { + section = .stats + } else { + section = .boosts + } case 1: - section = .boosts + if canViewStats { + section = .boosts + } else { + section = .monetization + } case 2: section = .monetization let _ = (ApplicationSpecificNotice.monetizationIntroDismissed(accountManager: context.sharedContext.accountManager) diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift index 7480b59332..7fe98db357 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift @@ -520,7 +520,7 @@ public final class MediaEditor { self.renderer.consume(main: .texture(texture, time, hasTransparency), additional: additionalTexture.flatMap { .texture($0, time, false) }, render: true, displayEnabled: false) } - private func setupSource() { + private func setupSource(andPlay: Bool) { guard let renderTarget = self.previewView else { return } @@ -830,6 +830,9 @@ public final class MediaEditor { self.setupTimeObservers() Queue.mainQueue().justDispatch { let startPlayback = { + guard andPlay else { + return + } player.playImmediately(atRate: 1.0) // additionalPlayer?.playImmediately(atRate: 1.0) self.audioPlayer?.playImmediately(atRate: 1.0) @@ -941,13 +944,13 @@ public final class MediaEditor { self.audioDelayTimer = nil } - public func attachPreviewView(_ previewView: MediaEditorPreviewView) { + public func attachPreviewView(_ previewView: MediaEditorPreviewView, andPlay: Bool) { self.previewView?.renderer = nil self.previewView = previewView previewView.renderer = self.renderer - self.setupSource() + self.setupSource(andPlay: andPlay) } private var skipRendering = false @@ -1118,8 +1121,9 @@ public final class MediaEditor { self.initialSeekPosition = position return } - self.renderer.setRate(1.0) - if !play { + if play { + self.renderer.setRate(1.0) + } else { self.player?.pause() self.additionalPlayer?.pause() self.audioPlayer?.pause() diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCoverScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCoverScreen.swift index db9363e5bb..db1dba00b3 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCoverScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCoverScreen.swift @@ -225,7 +225,7 @@ private final class MediaCoverScreenComponent: Component { transition: transition, component: AnyComponent(Button( content: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: "Cancel", font: Font.regular(17.0), textColor: .white))) + MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Common_Cancel, font: Font.regular(17.0), textColor: .white))) ), action: { [weak controller] in controller?.requestDismiss(animated: true) @@ -546,6 +546,7 @@ final class MediaCoverScreen: ViewController { fileprivate let mediaEditor: Signal fileprivate let previewView: MediaEditorPreviewView fileprivate let portalView: PortalView + fileprivate let exclusive: Bool func withMediaEditor(_ f: @escaping (MediaEditor) -> Void) { let _ = (self.mediaEditor @@ -564,12 +565,14 @@ final class MediaCoverScreen: ViewController { context: AccountContext, mediaEditor: Signal, previewView: MediaEditorPreviewView, - portalView: PortalView + portalView: PortalView, + exclusive: Bool ) { self.context = context self.mediaEditor = mediaEditor self.previewView = previewView self.portalView = portalView + self.exclusive = exclusive super.init(navigationBarPresentationData: nil) self.navigationPresentation = .flatModal @@ -601,7 +604,9 @@ final class MediaCoverScreen: ViewController { self.dismissed() self.node.animateOutToEditor(completion: { - self.dismiss() + if !self.exclusive { + self.dismiss() + } }) } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 4f2cf97358..41c0d95ca2 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -2810,7 +2810,9 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate }) if controller.isEditingStoryCover { - self.openCoverSelection(immediate: true) + Queue.mainQueue().justDispatch { + self.openCoverSelection(exclusive: true) + } } } @@ -2985,7 +2987,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } self.controller?.stickerRecommendedEmoji = emojiForClasses(classes.map { $0.0 }) } - mediaEditor.attachPreviewView(self.previewView) + mediaEditor.attachPreviewView(self.previewView, andPlay: !(self.controller?.isEditingStoryCover ?? false)) if case .empty = effectiveSubject { self.stickerMaskDrawingView?.emptyColor = .black @@ -4705,7 +4707,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } } - func openCoverSelection(immediate: Bool) { + func openCoverSelection(exclusive: Bool) { guard let portalView = PortalView(matchPosition: false) else { return } @@ -4722,12 +4724,17 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate context: self.context, mediaEditor: self.mediaEditorPromise.get(), previewView: self.previewView, - portalView: portalView + portalView: portalView, + exclusive: exclusive ) coverController.dismissed = { [weak self] in if let self { - self.animateInFromTool() - self.requestCompletion(playHaptic: false) + if exclusive { + self.controller?.requestDismiss(saveDraft: false, animated: true) + } else { + self.animateInFromTool() + self.requestCompletion(playHaptic: false) + } } } coverController.completed = { [weak self] position, image in @@ -4738,7 +4745,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.controller?.present(coverController, in: .current) self.coverScreen = coverController - if immediate { + if exclusive { self.isDisplayingTool = .cover self.requestUpdate(transition: .immediate) } else { @@ -5227,7 +5234,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.controller?.present(controller, in: .window(.root)) self.animateOutToTool(tool: .tools) case .cover: - self.openCoverSelection(immediate: false) + self.openCoverSelection(exclusive: false) } } }, @@ -5924,7 +5931,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate editCoverImpl = { [weak self, weak controller] in if let self { - self.node.openCoverSelection(immediate: false) + self.node.openCoverSelection(exclusive: false) } if let controller { controller.dismiss() diff --git a/submodules/TelegramUI/Resources/WebEmbed/UIWebViewSearch.js b/submodules/TelegramUI/Resources/WebEmbed/UIWebViewSearch.js index abefd029fa..34e5ddd288 100644 --- a/submodules/TelegramUI/Resources/WebEmbed/UIWebViewSearch.js +++ b/submodules/TelegramUI/Resources/WebEmbed/UIWebViewSearch.js @@ -9,9 +9,8 @@ var uiWebview_SearchResultCount = 0; keyword - string to search */ -function isElementVisible(element) { - var style = window.getComputedStyle(element); - return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0'; +function isElementVisible(e) { + return true } function uiWebview_HighlightAllOccurencesOfStringForElement(element,keyword) { @@ -24,8 +23,6 @@ function uiWebview_HighlightAllOccurencesOfStringForElement(element,keyword) { var idx = value.toLowerCase().indexOf(keyword); if (idx < 0) break; - -// if (!isElementVisible(element)) break; count++; elementTmp = document.createTextNode(value.substr(idx+keyword.length)); @@ -93,7 +90,7 @@ function uiWebview_HighlightAllOccurencesOfStringForElement(element,keyword) { } else if (element.nodeType == 1) { // Element node - if (element.nodeName.toLowerCase() != 'select') { + if (element.nodeName.toLowerCase() != 'select' && isElementVisible(element)) { for (var i=element.childNodes.length-1; i>=0; i--) { uiWebview_HighlightAllOccurencesOfStringForElement(element.childNodes[i],keyword); } From 5061fa9f2b0cf9829c2ddd31e118a8a761d2fa63 Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Thu, 25 Jul 2024 14:43:23 -0300 Subject: [PATCH 14/41] BotAppReference : Equatable --- .../Sources/TelegramEngine/Messages/AttachMenuBots.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AttachMenuBots.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AttachMenuBots.swift index 2649a4b3aa..0123ff375c 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AttachMenuBots.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AttachMenuBots.swift @@ -592,7 +592,7 @@ func _internal_getAttachMenuBot(accountPeerId: PeerId, postbox: Postbox, network |> switchToLatest } -public enum BotAppReference { +public enum BotAppReference : Equatable { case id(id: Int64, accessHash: Int64) case shortName(peerId: PeerId, shortName: String) } From 1fde514783f981f258ebb209a1a8351e310b8028 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 25 Jul 2024 19:47:12 +0200 Subject: [PATCH 15/41] Various fixes --- .../Components/CameraScreen/Sources/CameraScreen.swift | 3 +++ .../MediaEditorScreen/Sources/MediaEditorScreen.swift | 8 ++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift index 5d42d5677e..8c63e8750b 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift @@ -1543,6 +1543,9 @@ public class CameraScreen: ViewController { isDualCameraEnabled = isDualCameraEnabledValue.boolValue } } + if case .sticker = controller.mode { + isDualCameraEnabled = false + } var dualCameraPosition: PIPPosition = .topRight if let dualCameraPositionValue = UserDefaults.standard.object(forKey: "TelegramStoryCameraDualPosition") as? NSNumber { diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 41c0d95ca2..f101c7174b 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -6507,8 +6507,12 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.saveDraft(id: randomId) var firstFrame: Signal<(UIImage?, UIImage?), NoError> - let firstFrameTime = CMTime(seconds: mediaEditor.values.videoTrimRange?.lowerBound ?? 0.0, preferredTimescale: CMTimeScale(60)) - + let firstFrameTime: CMTime + if let coverImageTimestamp = mediaEditor.values.coverImageTimestamp { + firstFrameTime = CMTime(seconds: coverImageTimestamp, preferredTimescale: CMTimeScale(60)) + } else { + firstFrameTime = CMTime(seconds: mediaEditor.values.videoTrimRange?.lowerBound ?? 0.0, preferredTimescale: CMTimeScale(60)) + } let videoResult: Signal var videoIsMirrored = false let duration: Double From 4414753d7f15b872ee88adbc8b21722481e89b78 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 25 Jul 2024 21:44:05 +0200 Subject: [PATCH 16/41] Various fixes --- .../Telegram-iOS/en.lproj/Localizable.strings | 3 + .../BrowserUI/Sources/BrowserWebContent.swift | 8 ++- .../MediaEditor/Sources/MediaEditor.swift | 3 + .../Sources/EditStories.swift | 22 +++++- .../Sources/MediaCoverScreen.swift | 71 ++++++++----------- .../Sources/MediaEditorScreen.swift | 22 ++++-- 6 files changed, 79 insertions(+), 50 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 6e466ac255..9adce68c74 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -12646,3 +12646,6 @@ Sorry for the inconvenience."; "WebBrowser.Download.Confirmation" = "Do you want to download \"%@\"?"; "WebBrowser.Download.Download" = "Download"; + +"Story.Cover" = "Story Cover"; +"Story.SaveCover" = "Save Cover"; diff --git a/submodules/BrowserUI/Sources/BrowserWebContent.swift b/submodules/BrowserUI/Sources/BrowserWebContent.swift index 67b9bfa514..632ebe486a 100644 --- a/submodules/BrowserUI/Sources/BrowserWebContent.swift +++ b/submodules/BrowserUI/Sources/BrowserWebContent.swift @@ -623,7 +623,13 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU if navigationResponse.canShowMIMEType { decisionHandler(.allow) } else if #available(iOS 14.5, *) { - decisionHandler(.download) + self.presentDownloadConfirmation(fileName: navigationResponse.response.suggestedFilename ?? "file", proceed: { download in + if download { + decisionHandler(.download) + } else { + decisionHandler(.cancel) + } + }) } else { decisionHandler(.cancel) } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift index 7fe98db357..e696d413e3 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift @@ -499,6 +499,9 @@ public final class MediaEditor { } else if case let .video(_, _, _, _, _, duration) = subject { self.playerPlaybackState = PlaybackState(duration: duration, position: 0.0, isPlaying: false, hasAudio: true) self.playerPlaybackStatePromise.set(.single(self.playerPlaybackState)) + } else if case let .draft(mediaEditorDraft) = subject, mediaEditorDraft.isVideo { + self.playerPlaybackState = PlaybackState(duration: mediaEditorDraft.duration ?? 0.0, position: 0.0, isPlaying: false, hasAudio: true) + self.playerPlaybackStatePromise.set(.single(self.playerPlaybackState)) } } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift index 5fd4d191d2..bd7d76ca41 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift @@ -154,11 +154,15 @@ public extension MediaEditorScreen { }) } else { var updatedText: String? + var updatedCoverTimestamp: Double? var updatedEntities: [MessageTextEntity]? if result.caption.string != storyItem.text || entities != storyItem.entities { updatedText = result.caption.string updatedEntities = entities } + if let coverTimestamp = result.coverTimestamp { + updatedCoverTimestamp = coverTimestamp + } if let mediaResult = result.media { switch mediaResult { @@ -237,8 +241,22 @@ public extension MediaEditorScreen { default: break } - } else if updatedText != nil { - let _ = (context.engine.messages.editStory(peerId: peer.id, id: storyItem.id, media: nil, mediaAreas: nil, text: updatedText, entities: updatedEntities, privacy: nil) + } else if updatedText != nil || updatedCoverTimestamp != nil { + var media: EngineStoryInputMedia? + if let updatedCoverTimestamp { + if case let .file(file) = storyItem.media { + var updatedAttributes: [TelegramMediaFileAttribute] = [] + for attribute in file.attributes { + if case let .Video(duration, size, flags, preloadSize, _) = attribute { + updatedAttributes.append(.Video(duration: duration, size: size, flags: flags, preloadSize: preloadSize, coverTime: updatedCoverTimestamp)) + } else { + updatedAttributes.append(attribute) + } + } + media = .existing(media: file.withUpdatedAttributes(updatedAttributes)) + } + } + let _ = (context.engine.messages.editStory(peerId: peer.id, id: storyItem.id, media: media, mediaAreas: nil, text: updatedText, entities: updatedEntities, privacy: nil) |> deliverOnMainQueue).startStandalone(next: { result in switch result { case .completed: diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCoverScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCoverScreen.swift index db1dba00b3..4a3c7407a8 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCoverScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCoverScreen.swift @@ -19,41 +19,29 @@ private final class MediaCoverScreenComponent: Component { let context: AccountContext let mediaEditor: Signal + let exclusive: Bool init( context: AccountContext, - mediaEditor: Signal + mediaEditor: Signal, + exclusive: Bool ) { self.context = context self.mediaEditor = mediaEditor + self.exclusive = exclusive } static func ==(lhs: MediaCoverScreenComponent, rhs: MediaCoverScreenComponent) -> Bool { if lhs.context !== rhs.context { return false } + if lhs.exclusive != rhs.exclusive { + return false + } return true } final class State: ComponentState { - enum ImageKey: Hashable { - case done - } - private var cachedImages: [ImageKey: UIImage] = [:] - func image(_ key: ImageKey) -> UIImage { - if let image = self.cachedImages[key] { - return image - } else { - var image: UIImage - switch key { - case .done: - image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Done"), color: .white)! - } - cachedImages[key] = image - return image - } - } - var playerStateDisposable: Disposable? var playerState: MediaEditorPlayerState? @@ -176,14 +164,6 @@ private final class MediaCoverScreenComponent: Component { self.state?.updated() } -// override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { -// let result = super.hitTest(point, with: event) -// if let controller = self.environment?.controller() as? MediaCoverScreen, [.erase, .restore].contains(controller.mode), result == self.previewContainerView { -// return nil -// } -// return result -// } - func update(component: MediaCoverScreenComponent, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { let environment = environment[ViewControllerComponentContainer.Environment.self].value self.environment = environment @@ -192,7 +172,6 @@ private final class MediaCoverScreenComponent: Component { return .zero } -// let isFirstTime = self.component == nil self.component = component self.state = state @@ -217,9 +196,9 @@ private final class MediaCoverScreenComponent: Component { controlsBottomInset = -75.0 } } - - let previewContainerFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - previewSize.width) / 2.0), y: environment.safeInsets.top), size: CGSize(width: previewSize.width, height: availableSize.height - environment.safeInsets.top - environment.safeInsets.bottom + controlsBottomInset)) - let buttonsContainerFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - environment.safeInsets.bottom + controlsBottomInset - 31.0), size: CGSize(width: availableSize.width, height: environment.safeInsets.bottom - controlsBottomInset)) + + let previewContainerFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - previewSize.width) / 2.0), y: topInset), size: CGSize(width: previewSize.width, height: availableSize.height - environment.safeInsets.top - environment.safeInsets.bottom + controlsBottomInset)) + let buttonsContainerFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - environment.safeInsets.bottom + controlsBottomInset), size: CGSize(width: availableSize.width, height: environment.safeInsets.bottom - controlsBottomInset)) let cancelButtonSize = self.cancelButton.update( transition: transition, @@ -235,7 +214,7 @@ private final class MediaCoverScreenComponent: Component { containerSize: CGSize(width: 120.0, height: 44.0) ) let cancelButtonFrame = CGRect( - origin: CGPoint(x: 16.0, y: 80.0), + origin: CGPoint(x: 16.0, y: previewContainerFrame.minY + 28.0), size: cancelButtonSize ) if let cancelButtonView = self.cancelButton.view { @@ -258,7 +237,7 @@ private final class MediaCoverScreenComponent: Component { content: AnyComponentWithIdentity( id: AnyHashable(0), component: AnyComponent(ButtonTextContentComponent( - text: "Save Cover", + text: environment.strings.Story_SaveCover, badge: 0, textColor: environment.theme.list.itemCheckColors.foregroundColor, badgeBackground: .clear, @@ -268,11 +247,16 @@ private final class MediaCoverScreenComponent: Component { isEnabled: true, displaysProgress: false, action: { [weak controller, weak self] in + guard let controller else { + return + } if let playerState = self?.state?.playerState, let mediaEditor = self?.state?.mediaEditor, let image = mediaEditor.resultImage { mediaEditor.setCoverImageTimestamp(playerState.position) - controller?.completed(playerState.position, image) + controller.completed(playerState.position, image) + } + if !controller.exclusive { + controller.requestDismiss(animated: true) } - controller?.requestDismiss(animated: true) } ) ), @@ -280,7 +264,7 @@ private final class MediaCoverScreenComponent: Component { containerSize: CGSize(width: availableSize.width - buttonSideInset * 2.0, height: 50.0) ) let doneButtonFrame = CGRect( - origin: CGPoint(x: floor((availableSize.width - doneButtonSize.width) / 2.0), y: availableSize.height - 99.0), + origin: CGPoint(x: floor((availableSize.width - doneButtonSize.width) / 2.0), y: min(buttonsContainerFrame.minY, availableSize.height - doneButtonSize.height - buttonSideInset)), size: doneButtonSize ) if let doneButtonView = self.doneButton.view { @@ -292,12 +276,12 @@ private final class MediaCoverScreenComponent: Component { let labelSize = self.label.update( transition: transition, - component: AnyComponent(Text(text: "Story Cover", font: Font.semibold(17.0), color: UIColor(rgb: 0xffffff))), + component: AnyComponent(Text(text: environment.strings.Story_Cover, font: Font.semibold(17.0), color: UIColor(rgb: 0xffffff))), environment: {}, containerSize: CGSize(width: availableSize.width - 88.0, height: 44.0) ) let labelFrame = CGRect( - origin: CGPoint(x: floorToScreenPixels((availableSize.width - labelSize.width) / 2.0), y: 80.0), + origin: CGPoint(x: floorToScreenPixels((availableSize.width - labelSize.width) / 2.0), y: previewContainerFrame.minY + 28.0), size: labelSize ) if let labelView = self.label.view { @@ -319,9 +303,11 @@ private final class MediaCoverScreenComponent: Component { labelView.bounds = CGRect(origin: .zero, size: labelFrame.size) transition.setPosition(view: labelView, position: labelFrame.center) } + + let buttonCoverFrame = CGRect(origin: CGPoint(x: 0.0, y: doneButtonFrame.minY - buttonSideInset - 11.0), size: CGSize(width: previewContainerFrame.width, height: 100.0)) - transition.setFrame(view: self.buttonsContainerView, frame: buttonsContainerFrame) - transition.setFrame(view: self.buttonsBackgroundView, frame: CGRect(origin: .zero, size: buttonsContainerFrame.size)) + transition.setFrame(view: self.buttonsContainerView, frame: buttonCoverFrame) + transition.setFrame(view: self.buttonsBackgroundView, frame: CGRect(origin: .zero, size: buttonCoverFrame.size)) transition.setFrame(view: self.previewContainerView, frame: previewContainerFrame) @@ -371,7 +357,7 @@ private final class MediaCoverScreenComponent: Component { containerSize: CGSize(width: previewSize.width - scrubberInset * 2.0, height: availableSize.height) ) - let scrubberFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - scrubberSize.width) / 2.0), y: availableSize.height - environment.safeInsets.bottom - scrubberSize.height + controlsBottomInset + 3.0 - 40.0), size: scrubberSize) + let scrubberFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - scrubberSize.width) / 2.0), y: min(previewContainerFrame.maxY, buttonCoverFrame.minY) - scrubberSize.height - 4.0), size: scrubberSize) if let scrubberView = self.scrubber.view { var animateIn = false if scrubberView.superview == nil { @@ -514,7 +500,8 @@ final class MediaCoverScreen: ViewController { component: AnyComponent( MediaCoverScreenComponent( context: self.context, - mediaEditor: controller.mediaEditor + mediaEditor: controller.mediaEditor, + exclusive: controller.exclusive ) ), environment: { diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index f101c7174b..e75f6b39d4 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -4740,6 +4740,9 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate coverController.completed = { [weak self] position, image in if let self { self.controller?.currentCoverImage = image + if exclusive { + self.requestCompletion() + } } } self.controller?.present(coverController, in: .current) @@ -5548,6 +5551,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate public let media: MediaResult? public let mediaAreas: [MediaArea] public let caption: NSAttributedString + public let coverTimestamp: Double? public let options: MediaEditorResultPrivacy public let stickers: [TelegramMediaFile] public let randomId: Int64 @@ -5556,6 +5560,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.media = nil self.mediaAreas = [] self.caption = NSAttributedString() + self.coverTimestamp = nil self.options = MediaEditorResultPrivacy(sendAsPeerId: nil, privacy: EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []), timeout: 0, isForwardingDisabled: false, pin: false) self.stickers = [] self.randomId = 0 @@ -5565,6 +5570,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate media: MediaResult?, mediaAreas: [MediaArea] = [], caption: NSAttributedString = NSAttributedString(), + coverTimestamp: Double? = nil, options: MediaEditorResultPrivacy = MediaEditorResultPrivacy(sendAsPeerId: nil, privacy: EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []), timeout: 0, isForwardingDisabled: false, pin: false), stickers: [TelegramMediaFile] = [], randomId: Int64 = 0 @@ -5572,6 +5578,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.media = media self.mediaAreas = mediaAreas self.caption = caption + self.coverTimestamp = coverTimestamp self.options = options self.stickers = stickers self.randomId = randomId @@ -6479,8 +6486,13 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } } - if self.isEmbeddedEditor && !(self.node.hasAnyChanges || hasEntityChanges) { - self.completion(MediaEditorScreen.Result(media: nil, mediaAreas: [], caption: caption, options: self.state.privacy, stickers: stickers, randomId: randomId), { [weak self] finished in + var hasAnyChanges = self.node.hasAnyChanges + if self.isEditingStoryCover { + hasAnyChanges = false + } + + if self.isEmbeddedEditor && !(hasAnyChanges || hasEntityChanges) { + self.completion(MediaEditorScreen.Result(media: nil, mediaAreas: [], caption: caption, coverTimestamp: mediaEditor.values.coverImageTimestamp, options: self.state.privacy, stickers: stickers, randomId: randomId), { [weak self] finished in self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in self?.dismiss() Queue.mainQueue().justDispatch { @@ -6488,7 +6500,6 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } }) }) - return } @@ -6754,7 +6765,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate makeEditorImageComposition(context: self.node.ciContext, postbox: self.context.account.postbox, inputImage: inputImage, dimensions: storyDimensions, values: mediaEditor.values, time: firstFrameTime, textScale: 2.0, completion: { [weak self] coverImage in if let self { Logger.shared.log("MediaEditor", "Completed with video \(videoResult)") - self.completion(MediaEditorScreen.Result(media: .video(video: videoResult, coverImage: coverImage, values: mediaEditor.values, duration: duration, dimensions: mediaEditor.values.resultDimensions), mediaAreas: mediaAreas, caption: caption, options: self.state.privacy, stickers: stickers, randomId: randomId), { [weak self] finished in + self.completion(MediaEditorScreen.Result(media: .video(video: videoResult, coverImage: coverImage, values: mediaEditor.values, duration: duration, dimensions: mediaEditor.values.resultDimensions), mediaAreas: mediaAreas, caption: caption, coverTimestamp: mediaEditor.values.coverImageTimestamp, options: self.state.privacy, stickers: stickers, randomId: randomId), { [weak self] finished in self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in self?.dismiss() Queue.mainQueue().justDispatch { @@ -6777,7 +6788,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate makeEditorImageComposition(context: self.node.ciContext, postbox: self.context.account.postbox, inputImage: image, dimensions: storyDimensions, values: mediaEditor.values, time: .zero, textScale: 2.0, completion: { [weak self] resultImage in if let self, let resultImage { Logger.shared.log("MediaEditor", "Completed with image \(resultImage)") - self.completion(MediaEditorScreen.Result(media: .image(image: resultImage, dimensions: PixelDimensions(resultImage.size)), mediaAreas: mediaAreas, caption: caption, options: self.state.privacy, stickers: stickers, randomId: randomId), { [weak self] finished in + self.completion(MediaEditorScreen.Result(media: .image(image: resultImage, dimensions: PixelDimensions(resultImage.size)), mediaAreas: mediaAreas, caption: caption, coverTimestamp: nil, options: self.state.privacy, stickers: stickers, randomId: randomId), { [weak self] finished in self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in self?.dismiss() Queue.mainQueue().justDispatch { @@ -6925,6 +6936,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate media: .sticker(file: file, emoji: self.effectiveStickerEmoji()), mediaAreas: [], caption: NSAttributedString(), + coverTimestamp: nil, options: MediaEditorResultPrivacy(sendAsPeerId: nil, privacy: EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []), timeout: 0, isForwardingDisabled: false, pin: false), stickers: [], randomId: 0 From aa5ad8e1944995e1a3fab6269abdff6809f012a2 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 25 Jul 2024 21:48:06 +0200 Subject: [PATCH 17/41] Various fixes --- .../Sources/StoryItemSetContainerComponent.swift | 4 ++-- .../EditCover.imageset/Contents.json | 12 ++++++++++++ .../EditCover.imageset/storycover_24.pdf | Bin 0 -> 2441 bytes 3 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 submodules/TelegramUI/Images.xcassets/Stories/Context Menu/EditCover.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Stories/Context Menu/EditCover.imageset/storycover_24.pdf diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 4c70ed44a4..1da76de05d 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -6120,7 +6120,7 @@ public final class StoryItemSetContainerComponent: Component { if case .file = component.slice.item.storyItem.media { items.append(.action(ContextMenuActionItem(text: component.strings.Story_Context_EditCover, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) + return generateTintedImage(image: UIImage(bundleImageName: "Stories/Context Menu/EditCover"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, a in a(.default) @@ -6318,7 +6318,7 @@ public final class StoryItemSetContainerComponent: Component { if case .file = component.slice.item.storyItem.media { items.append(.action(ContextMenuActionItem(text: component.strings.Story_Context_EditCover, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) + return generateTintedImage(image: UIImage(bundleImageName: "Stories/Context Menu/EditCover"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, a in a(.default) diff --git a/submodules/TelegramUI/Images.xcassets/Stories/Context Menu/EditCover.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Stories/Context Menu/EditCover.imageset/Contents.json new file mode 100644 index 0000000000..b6215a7a9d --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Stories/Context Menu/EditCover.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "storycover_24.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Stories/Context Menu/EditCover.imageset/storycover_24.pdf b/submodules/TelegramUI/Images.xcassets/Stories/Context Menu/EditCover.imageset/storycover_24.pdf new file mode 100644 index 0000000000000000000000000000000000000000..cef5ed510f48da9496feae5a6ff82a121de451a9 GIT binary patch literal 2441 zcmZXWdpy(oAII&K+m00Fc5?X=GPl_g9#)&{}&AZuMV{i2%-@v0Mt4NMGr8f8{r(YJP zwb8CC4tm8_)m@*DgA*D4-NYV`k3v< z-wcxd9ouIk)7-lJivDazTaAvLQs`m9kAUV1NXNs%w5i(t_{O>mwk&n?Wo~`Z-KP;H zQOHK72HRchO+n$s4?5~L$uEg&%JXpc1eKWj;vIc9?9Il@W5)NH?SnfxEbU})ge`Js zx|dj6SEZNrxxHE>E^S)F;)*n4u&N9#qIpbi^S+E-9I~B&x!aK|LtIgB%hq#EI+-tn z@o9$wYe^>48pD4 z2eB+IEr?^6l^sTVSNzqR4WdF(4|Eg(_lpBtz>||8Q%j5@S5v(G(xtxOZW#A~rc2Ou z6Bz?p35KlO{hKt#HZ{uK;zFMkc#LgQ5>{kyuZQbNOpA3A+R*r2OhReR^yyF5$`;v- zlKaS(PvW(IRJUC2?BR}$_Z)=SLv>o(=7jDAyw5lhld;io8ix3*+BmZao@w;-0G^(J z6hj)M`uE87S{#-x(?yHt`6JlH6nTlnxJOs7qEyD}yd|?!FK}v4KzbXQN;2}HlB%0$ z92OIU-?ooS`wH#%#zxwj)u%P&!>0p7ij|Yj%z)z=zGqVlvsBa&uLmkRRz#U&J~v`) zDs-|1o$uSSbB`Cgj7DmQU^T=hG?!OO5K1o;N7T31ANf36lx#73pgw^j^7l|KidB)S zrxIUo(^BcBVB(~GEz8|?M|VDP!bMg}qypNcfwY2$m#FN#Gbt$PxXgc| zC8-hpjnl0SOSSCVGM!lDm^fCMh5M)& zCvL;ph6-KoWe=FCxSDQCOpL$E6fqRW74op)N{w21HHm}>zg=uq=RQbdo>^TI*v+1j zdfl5A5FKGv21&w8yf!w(skYTRfYM&qCV#LEmfuKu4j=^%j z9Wo?Ea7%oLhW{)uM%-UDAoc2m%4&F}Pfzw1F5O<{qewel$Mu>NRzATtD9Wd<Ftk}Ft^T%(?EM(g)h>84LSUmd9iOJpWZlu|3 zGii|)!gs&6n&fm~HaZADf61Dxnoa*YX539{5EV9KJS|Es(t1b#Op*gR&{c*!tJ1Bb z87jwUYHM7Bp-Ox|_S?E;|&uI}pH z_hDOK{DX6G@eHDVZV)KHZdg@3fr0lZPk9wQ43omJ&yS^z*J9i%L1bb=q<>htz?Nr> z7=uA|6tZfbD%OpqEu?Nu=q9Sj74N?Ch~aYqv5@*g*o~3?oWA-sUu)Bg$Dy1eFT!^eZ%im zCxpDg4Nh|Miz$l201F(Y=x$LKDZcN?IHIrMK%%kdmy!g{*|!d+T?MZ*-{9@zRh!fb zz@@}xqUYHK;Qby?w6-drnm@3&#!o&FzjGC4WCUNws=>OIr@fg7wzKfSe+&;%+ul;TUE0(`*p$;^kf6`4dM zka#!p>Aog9;V3)}F#I9?X9wU*>$?HeE&$If72tP(2G;JeB8T#FVgFY@_4_WcHVwot zC-EDku3yM6?0e%@I2ta9?DuPG1ch)EtOpo@zqil-0tiDxeM5jJ|1g*y@XZ5A>ykdN zPQI;6Fk_Sdje?oLc+d1p6kN}kSL?454DmCOM!^LH5h!5(>)E1s Date: Fri, 26 Jul 2024 00:06:19 +0200 Subject: [PATCH 18/41] Various fixes --- .../BrowserAddressListItemComponent.swift | 59 +++++++++- .../BrowserUI/Sources/BrowserScreen.swift | 107 ++++++++++++------ .../Sources/BrowserToolbarComponent.swift | 54 +++++---- .../BrowserUI/Sources/BrowserWebContent.swift | 49 +++++++- .../TelegramUI/Sources/ChatController.swift | 30 +++-- 5 files changed, 226 insertions(+), 73 deletions(-) diff --git a/submodules/BrowserUI/Sources/BrowserAddressListItemComponent.swift b/submodules/BrowserUI/Sources/BrowserAddressListItemComponent.swift index 30ad83d58b..23a3604a0e 100644 --- a/submodules/BrowserUI/Sources/BrowserAddressListItemComponent.swift +++ b/submodules/BrowserUI/Sources/BrowserAddressListItemComponent.swift @@ -10,6 +10,9 @@ import TelegramPresentationData import PhotoResources import AccountContext +private let iconFont = Font.with(size: 30.0, design: .round, weight: .bold) +private let iconTextBackgroundImage = generateStretchableFilledCircleImage(radius: 6.0, color: UIColor(rgb: 0xFF9500)) + final class BrowserAddressListItemComponent: Component { let context: AccountContext let theme: PresentationTheme @@ -57,6 +60,7 @@ final class BrowserAddressListItemComponent: Component { private let containerButton: HighlightTrackingButton private var emptyIcon: UIImageView? + private var emptyLabel: ComponentView? private var icon = TransformImageNode() private let title = ComponentView() private let subtitle = ComponentView() @@ -104,12 +108,14 @@ final class BrowserAddressListItemComponent: Component { let title: String let subtitle: String + var parsedUrl: URL? var iconImageReferenceAndRepresentation: (AnyMediaReference, TelegramMediaImageRepresentation)? var updateIconImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? if case let .Loaded(content) = component.webPage.content { title = content.title ?? content.url subtitle = content.url + parsedUrl = URL(string: content.url) if let image = content.image { if let representation = imageRepresentationLargerThan(image.representations, size: PixelDimensions(width: 80, height: 80)) { @@ -211,17 +217,58 @@ final class BrowserAddressListItemComponent: Component { iconImageApply() -// if strongSelf.iconTextBackgroundNode.supernode != nil { -// strongSelf.iconTextBackgroundNode.removeFromSupernode() -// } -// if strongSelf.iconTextNode.supernode != nil { -// strongSelf.iconTextNode.removeFromSupernode() -// } + if let emptyIcon = self.emptyIcon { + self.emptyIcon = nil + emptyIcon.removeFromSuperview() + } + if let emptyLabel = self.emptyLabel { + self.emptyLabel = nil + emptyLabel.view?.removeFromSuperview() + } } else { if self.icon.supernode != nil { self.icon.view.removeFromSuperview() } + let icon: UIImageView + let label: ComponentView + if let currentEmptyIcon = self.emptyIcon, let currentEmptyLabel = self.emptyLabel { + icon = currentEmptyIcon + label = currentEmptyLabel + } else { + icon = UIImageView() + icon.image = iconTextBackgroundImage + self.addSubview(icon) + + label = ComponentView() + } + icon.frame = iconFrame + + var iconText = "" + if let parsedUrl, let host = parsedUrl.host { + if parsedUrl.path.hasPrefix("/addstickers/") { + iconText = "S" + } else if parsedUrl.path.hasPrefix("/addemoji/") { + iconText = "E" + } else { + iconText = host[..? var navigationLeftItems: [AnyComponentWithIdentity] @@ -148,7 +149,7 @@ private final class BrowserScreenComponent: CombinedComponent { ) ) ] - + if isTablet { navigationLeftItems.append( AnyComponentWithIdentity( @@ -273,24 +274,26 @@ private final class BrowserScreenComponent: CombinedComponent { ), at: 0 ) - navigationRightItems.append( - AnyComponentWithIdentity( - id: "openIn", - component: AnyComponent( - Button( - content: AnyComponent( - BundleIconComponent( - name: "Instant View/Browser", - tintColor: environment.theme.rootController.navigationBar.accentTextColor - ) - ), - action: { - performAction.invoke(.openIn) - } + if canOpenIn { + navigationRightItems.append( + AnyComponentWithIdentity( + id: "openIn", + component: AnyComponent( + Button( + content: AnyComponent( + BundleIconComponent( + name: "Instant View/Browser", + tintColor: environment.theme.rootController.navigationBar.accentTextColor + ) + ), + action: { + performAction.invoke(.openIn) + } + ) ) ) ) - ) + } } } } @@ -349,6 +352,7 @@ private final class BrowserScreenComponent: CombinedComponent { textColor: environment.theme.rootController.navigationBar.primaryTextColor, canGoBack: context.component.contentState?.canGoBack ?? false, canGoForward: context.component.contentState?.canGoForward ?? false, + canOpenIn: canOpenIn, performAction: performAction, performHoldAction: performHoldAction ) @@ -395,6 +399,12 @@ private final class BrowserScreenComponent: CombinedComponent { } if context.component.presentationState.addressFocused { + let addressListSize: CGSize + if isTablet { + addressListSize = CGSize(width: 660.0, height: 420.0) + } else { + addressListSize = CGSize(width: context.availableSize.width, height: context.availableSize.height - navigationBar.size.height - toolbarSize) + } let addressList = addressList.update( component: BrowserAddressListComponent( context: context.component.context, @@ -405,15 +415,26 @@ private final class BrowserScreenComponent: CombinedComponent { performAction.invoke(.navigateTo(url)) } ), - availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height - navigationBar.size.height - toolbarSize), + availableSize: addressListSize, transition: context.transition ) - context.add(addressList - .position(CGPoint(x: context.availableSize.width / 2.0, y: navigationBar.size.height + addressList.size.height / 2.0)) - .clipsToBounds(true) - .appear(.default(alpha: true)) - .disappear(.default(alpha: true)) - ) + + if isTablet { + context.add(addressList + .position(CGPoint(x: context.availableSize.width / 2.0, y: navigationBar.size.height + addressList.size.height / 2.0 - 3.0)) + .cornerRadius(10.0) + .clipsToBounds(true) + .appear(.default(alpha: true)) + .disappear(.default(alpha: true)) + ) + } else { + context.add(addressList + .position(CGPoint(x: context.availableSize.width / 2.0, y: navigationBar.size.height + addressList.size.height / 2.0)) + .clipsToBounds(true) + .appear(.default(alpha: true)) + .disappear(.default(alpha: true)) + ) + } } return context.availableSize @@ -992,7 +1013,7 @@ public class BrowserScreen: ViewController, MinimizableController { let _ = (settings |> deliverOnMainQueue).start(next: { [weak self] settings in - guard let self, let controller = self.controller, let contentState = self.contentState else { + guard let self, let controller = self.controller, let contentState = self.contentState, let layout = self.validLayout?.0 else { return } @@ -1072,22 +1093,26 @@ public class BrowserScreen: ViewController, MinimizableController { }))) } - items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.WebBrowser_Share, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Share"), color: theme.contextMenu.primaryColor) }, action: { (controller, action) in - performAction.invoke(.share) - action(.default) - }))) + if !layout.metrics.isTablet { + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.WebBrowser_Share, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Share"), color: theme.contextMenu.primaryColor) }, action: { (controller, action) in + performAction.invoke(.share) + action(.default) + }))) + } if [.webPage, .instantPage].contains(contentState.contentType) { items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.WebBrowser_AddBookmark, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Fave"), color: theme.contextMenu.primaryColor) }, action: { (controller, action) in performAction.invoke(.addBookmark) action(.default) }))) - items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.InstantPage_OpenInBrowser(openInTitle).string, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Browser"), color: theme.contextMenu.primaryColor) }, action: { [weak self] (controller, action) in - if let self { - self.context.sharedContext.applicationBindings.openUrl(openInUrl) - } - action(.default) - }))) + if !layout.metrics.isTablet { + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.InstantPage_OpenInBrowser(openInTitle).string, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Browser"), color: theme.contextMenu.primaryColor) }, action: { [weak self] (controller, action) in + if let self { + self.context.sharedContext.applicationBindings.openUrl(openInUrl) + } + action(.default) + }))) + } } let contextController = ContextController(presentationData: self.presentationData, source: source, items: .single(ContextController.Items(content: .list(items)))) @@ -1278,7 +1303,7 @@ public class BrowserScreen: ViewController, MinimizableController { BrowserContentComponent( content: content, insets: UIEdgeInsets( - top: environment.statusBarHeight, + top: layout.statusBarHeight ?? 0.0, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom, right: layout.safeInsets.right @@ -1401,6 +1426,16 @@ public class BrowserScreen: ViewController, MinimizableController { } } + public override func dismiss(completion: (() -> Void)? = nil) { + if let layout = self.validLayout, layout.metrics.isTablet { + self.node.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: layout.size.height), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true, completion: { _ in + super.dismiss(completion: completion) + }) + } else { + super.dismiss(completion: completion) + } + } + public override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) @@ -1519,7 +1554,7 @@ private final class BrowserContentComponent: Component { } let collapsedHeight: CGFloat = 24.0 - let topInset: CGFloat = component.insets.top + component.navigationBarHeight * (1.0 - component.scrollingPanelOffsetFraction) + collapsedHeight * component.scrollingPanelOffsetFraction + let topInset: CGFloat = component.navigationBarHeight * (1.0 - component.scrollingPanelOffsetFraction) + (component.insets.top + collapsedHeight) * component.scrollingPanelOffsetFraction let bottomInset = (49.0 + component.insets.bottom) * (1.0 - component.scrollingPanelOffsetFraction) let insets = UIEdgeInsets(top: topInset, left: component.insets.left, bottom: bottomInset, right: component.insets.right) let fullInsets = UIEdgeInsets(top: component.insets.top + component.navigationBarHeight, left: component.insets.left, bottom: 49.0 + component.insets.bottom, right: component.insets.right) diff --git a/submodules/BrowserUI/Sources/BrowserToolbarComponent.swift b/submodules/BrowserUI/Sources/BrowserToolbarComponent.swift index 2c35387e73..4c1e2c5a32 100644 --- a/submodules/BrowserUI/Sources/BrowserToolbarComponent.swift +++ b/submodules/BrowserUI/Sources/BrowserToolbarComponent.swift @@ -124,6 +124,7 @@ final class NavigationToolbarContentComponent: CombinedComponent { let textColor: UIColor let canGoBack: Bool let canGoForward: Bool + let canOpenIn: Bool let performAction: ActionSlot let performHoldAction: (UIView, ContextGesture?, BrowserScreen.Action) -> Void @@ -132,6 +133,7 @@ final class NavigationToolbarContentComponent: CombinedComponent { textColor: UIColor, canGoBack: Bool, canGoForward: Bool, + canOpenIn: Bool, performAction: ActionSlot, performHoldAction: @escaping (UIView, ContextGesture?, BrowserScreen.Action) -> Void ) { @@ -139,6 +141,7 @@ final class NavigationToolbarContentComponent: CombinedComponent { self.textColor = textColor self.canGoBack = canGoBack self.canGoForward = canGoForward + self.canOpenIn = canOpenIn self.performAction = performAction self.performHoldAction = performHoldAction } @@ -156,6 +159,9 @@ final class NavigationToolbarContentComponent: CombinedComponent { if lhs.canGoForward != rhs.canGoForward { return false } + if lhs.canOpenIn != rhs.canOpenIn { + return false + } return true } @@ -170,10 +176,16 @@ final class NavigationToolbarContentComponent: CombinedComponent { let availableSize = context.availableSize let performAction = context.component.performAction let performHoldAction = context.component.performHoldAction - + let sideInset: CGFloat = 5.0 let buttonSize = CGSize(width: 50.0, height: availableSize.height) - let spacing = (availableSize.width - buttonSize.width * 5.0 - sideInset * 2.0) / 4.0 + + var buttonCount = 4 + if context.component.canOpenIn { + buttonCount += 1 + } + + let spacing = (availableSize.width - buttonSize.width * CGFloat(buttonCount) - sideInset * 2.0) / CGFloat(buttonCount - 1) let canGoBack = context.component.canGoBack let back = back.update( @@ -269,24 +281,26 @@ final class NavigationToolbarContentComponent: CombinedComponent { .position(CGPoint(x: sideInset + back.size.width + spacing + forward.size.width + spacing + share.size.width + spacing + bookmark.size.width / 2.0, y: availableSize.height / 2.0)) ) - let openIn = openIn.update( - component: Button( - content: AnyComponent( - BundleIconComponent( - name: "Instant View/Browser", - tintColor: context.component.accentColor - ) - ), - action: { - performAction.invoke(.openIn) - } - ).minSize(buttonSize), - availableSize: buttonSize, - transition: .easeInOut(duration: 0.2) - ) - context.add(openIn - .position(CGPoint(x: sideInset + back.size.width + spacing + forward.size.width + spacing + share.size.width + spacing + bookmark.size.width + spacing + openIn.size.width / 2.0, y: availableSize.height / 2.0)) - ) + if context.component.canOpenIn { + let openIn = openIn.update( + component: Button( + content: AnyComponent( + BundleIconComponent( + name: "Instant View/Browser", + tintColor: context.component.accentColor + ) + ), + action: { + performAction.invoke(.openIn) + } + ).minSize(buttonSize), + availableSize: buttonSize, + transition: .easeInOut(duration: 0.2) + ) + context.add(openIn + .position(CGPoint(x: sideInset + back.size.width + spacing + forward.size.width + spacing + share.size.width + spacing + bookmark.size.width + spacing + openIn.size.width / 2.0, y: availableSize.height / 2.0)) + ) + } return availableSize } diff --git a/submodules/BrowserUI/Sources/BrowserWebContent.swift b/submodules/BrowserUI/Sources/BrowserWebContent.swift index 632ebe486a..5a3d848674 100644 --- a/submodules/BrowserUI/Sources/BrowserWebContent.swift +++ b/submodules/BrowserUI/Sources/BrowserWebContent.swift @@ -182,6 +182,11 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU configuration.mediaPlaybackRequiresUserAction = false } + let contentController = WKUserContentController() + let videoScript = WKUserScript(source: videoSource, injectionTime: .atDocumentStart, forMainFrameOnly: false) + contentController.addUserScript(videoScript) + configuration.userContentController = contentController + self.webView = WebView(frame: CGRect(), configuration: configuration) self.webView.allowsLinkPreview = true @@ -601,7 +606,7 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU if download { decisionHandler(.download, preferences) } else { - decisionHandler(.cancel, preferences) +// decisionHandler(.cancel, preferences) } }) } else { @@ -1147,6 +1152,48 @@ let setupFontFunctions = """ })(); """ +private let videoSource = """ +function disableWebkitEnterFullscreen(videoElement) { + if (videoElement && videoElement.webkitEnterFullscreen) { + Object.defineProperty(videoElement, 'webkitEnterFullscreen', { + value: undefined + }); + } +} + +function disableFullscreenOnExistingVideos() { + document.querySelectorAll('video').forEach(disableWebkitEnterFullscreen); +} + +function handleMutations(mutations) { + mutations.forEach((mutation) => { + if (mutation.addedNodes && mutation.addedNodes.length > 0) { + mutation.addedNodes.forEach((newNode) => { + if (newNode.tagName === 'VIDEO') { + disableWebkitEnterFullscreen(newNode); + } + if (newNode.querySelectorAll) { + newNode.querySelectorAll('video').forEach(disableWebkitEnterFullscreen); + } + }); + } + }); +} + +disableFullscreenOnExistingVideos(); + +const observer = new MutationObserver(handleMutations); + +observer.observe(document.body, { + childList: true, + subtree: true +}); + +function disconnectObserver() { + observer.disconnect(); +} +""" + @available(iOS 16.0, *) final class BrowserSearchOptions: UITextSearchOptions { override var wordMatchMethod: UITextSearchOptions.WordMatchMethod { diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 08bd4688f1..b397f693cd 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -245,6 +245,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var botStart: ChatControllerInitialBotStart? var attachBotStart: ChatControllerInitialAttachBotStart? var botAppStart: ChatControllerInitialBotAppStart? + let mode: ChatControllerPresentationMode let peerDisposable = MetaDisposable() let titleDisposable = MetaDisposable() @@ -658,6 +659,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.botStart = botStart self.attachBotStart = attachBotStart self.botAppStart = botAppStart + self.mode = mode self.peekData = peekData self.currentChatListFilter = chatListFilter self.chatNavigationStack = chatNavigationStack @@ -6548,6 +6550,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return value - 1 } + self.hasBrowserOrAppInFront.set(.single(false)) + let deallocate: () -> Void = { self.historyStateDisposable?.dispose() self.messageIndexDisposable.dispose() @@ -7133,18 +7137,20 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - let hasBrowserOrWebAppInFront: Signal = .single([]) - |> then( - self.effectiveNavigationController?.viewControllersSignal ?? .single([]) - ) - |> map { controllers in - if controllers.last is BrowserScreen || controllers.last is AttachmentController { - return true - } else { - return false + if case .standard(.default) = self.mode { + let hasBrowserOrWebAppInFront: Signal = .single([]) + |> then( + self.effectiveNavigationController?.viewControllersSignal ?? .single([]) + ) + |> map { controllers in + if controllers.last is BrowserScreen || controllers.last is AttachmentController { + return true + } else { + return false + } } + self.hasBrowserOrAppInFront.set(hasBrowserOrWebAppInFront) } - self.hasBrowserOrAppInFront.set(hasBrowserOrWebAppInFront) } var returnInputViewFocus = false @@ -7634,6 +7640,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let _ = self.peekData { self.peekTimerDisposable.set(nil) } + + if case .standard(.default) = self.mode { + self.hasBrowserOrAppInFront.set(.single(false)) + } } func saveInterfaceState(includeScrollState: Bool = true) { From c5179bc4a62d8bb90cfa6730804ec2888d6937f1 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 26 Jul 2024 00:11:24 +0200 Subject: [PATCH 19/41] Various fixes --- .../BrowserUI/Sources/BrowserScreen.swift | 5 +- .../BrowserUI/Sources/BrowserWebContent.swift | 50 +++++++++---------- 2 files changed, 29 insertions(+), 26 deletions(-) diff --git a/submodules/BrowserUI/Sources/BrowserScreen.swift b/submodules/BrowserUI/Sources/BrowserScreen.swift index bfd14aca62..c63dd013d7 100644 --- a/submodules/BrowserUI/Sources/BrowserScreen.swift +++ b/submodules/BrowserUI/Sources/BrowserScreen.swift @@ -1064,6 +1064,9 @@ public class BrowserScreen: ViewController, MinimizableController { openInUrl = url } + + let canOpenIn = !(self.contentState?.url.hasPrefix("tonsite") ?? false) + var items: [ContextMenuItem] = [] items.append(.custom(fontItem, false)) @@ -1105,7 +1108,7 @@ public class BrowserScreen: ViewController, MinimizableController { performAction.invoke(.addBookmark) action(.default) }))) - if !layout.metrics.isTablet { + if !layout.metrics.isTablet && canOpenIn { items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.InstantPage_OpenInBrowser(openInTitle).string, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Browser"), color: theme.contextMenu.primaryColor) }, action: { [weak self] (controller, action) in if let self { self.context.sharedContext.applicationBindings.openUrl(openInUrl) diff --git a/submodules/BrowserUI/Sources/BrowserWebContent.swift b/submodules/BrowserUI/Sources/BrowserWebContent.swift index 5a3d848674..acf2633086 100644 --- a/submodules/BrowserUI/Sources/BrowserWebContent.swift +++ b/submodules/BrowserUI/Sources/BrowserWebContent.swift @@ -601,15 +601,15 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU @available(iOS 13.0, *) func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, preferences: WKWebpagePreferences, decisionHandler: @escaping (WKNavigationActionPolicy, WKWebpagePreferences) -> Void) { - if #available(iOS 14.5, *), navigationAction.shouldPerformDownload { - self.presentDownloadConfirmation(fileName: navigationAction.request.mainDocumentURL?.lastPathComponent ?? "file", proceed: { download in - if download { - decisionHandler(.download, preferences) - } else { -// decisionHandler(.cancel, preferences) - } - }) - } else { +// if #available(iOS 14.5, *), navigationAction.shouldPerformDownload { +// self.presentDownloadConfirmation(fileName: navigationAction.request.mainDocumentURL?.lastPathComponent ?? "file", proceed: { download in +// if download { +// decisionHandler(.download, preferences) +// } else { +//// decisionHandler(.cancel, preferences) +// } +// }) +// } else { if let url = navigationAction.request.url?.absoluteString { if isTelegramMeLink(url) || isTelegraPhLink(url) { decisionHandler(.cancel, preferences) @@ -621,24 +621,24 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU } else { decisionHandler(.allow, preferences) } - } +// } } - func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) { - if navigationResponse.canShowMIMEType { - decisionHandler(.allow) - } else if #available(iOS 14.5, *) { - self.presentDownloadConfirmation(fileName: navigationResponse.response.suggestedFilename ?? "file", proceed: { download in - if download { - decisionHandler(.download) - } else { - decisionHandler(.cancel) - } - }) - } else { - decisionHandler(.cancel) - } - } +// func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) { +// if navigationResponse.canShowMIMEType { +// decisionHandler(.allow) +// } else if #available(iOS 14.5, *) { +// self.presentDownloadConfirmation(fileName: navigationResponse.response.suggestedFilename ?? "file", proceed: { download in +// if download { +// decisionHandler(.download) +// } else { +// decisionHandler(.cancel) +// } +// }) +// } else { +// decisionHandler(.cancel) +// } +// } func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { if let url = navigationAction.request.url?.absoluteString { From cd810b940ab7d789a4fa4699503dc768bd2ee570 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 26 Jul 2024 12:26:37 +0200 Subject: [PATCH 20/41] Various fixes --- .../Telegram-iOS/en.lproj/Localizable.strings | 6 + .../Sources/BrowserAddressListComponent.swift | 202 +++++++++++++++--- .../BrowserAddressListItemComponent.swift | 81 ++++++- .../BrowserNavigationBarComponent.swift | 22 +- .../BrowserUI/Sources/BrowserScreen.swift | 17 +- .../DeviceAccess/Sources/DeviceAccess.swift | 36 ++-- .../WebBrowserSettingsController.swift | 40 ++-- .../MediaEditor/Sources/MediaEditor.swift | 5 +- .../Sources/MediaCoverScreen.swift | 3 - .../Sources/MediaEditorDrafts.swift | 6 +- .../Sources/MediaEditorScreen.swift | 2 + .../Sources/ShareWithPeersScreen.swift | 2 +- 12 files changed, 337 insertions(+), 85 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 9adce68c74..2dccba4ca3 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -12585,6 +12585,9 @@ Sorry for the inconvenience."; "WebBrowser.Exceptions.ClearConfirmation.Text" = "Are you sure you want to clear this list?"; "WebBrowser.Exceptions.ClearConfirmation.Clear" = "Clear"; +"WebBrowser.ClearCookies.ClearConfirmation.Text" = "Are you sure you want to clear cookies?"; +"WebBrowser.ClearCookies.ClearConfirmation.Clear" = "Clear"; + "WebBrowser.Done" = "Done"; "AccessDenied.LocationWeather" = "Telegram needs access to your location so that you can add the weather widget to your stories.\n\nPlease go to Settings > Privacy > Location Services and set Telegram to ON."; @@ -12649,3 +12652,6 @@ Sorry for the inconvenience."; "Story.Cover" = "Story Cover"; "Story.SaveCover" = "Save Cover"; + +"WebBrowser.DeleteBookmark" = "Delete Bookmark"; +"WebBrowser.RemoveRecent" = "Remove from Recent"; diff --git a/submodules/BrowserUI/Sources/BrowserAddressListComponent.swift b/submodules/BrowserUI/Sources/BrowserAddressListComponent.swift index 2070587445..b22bb333e9 100644 --- a/submodules/BrowserUI/Sources/BrowserAddressListComponent.swift +++ b/submodules/BrowserUI/Sources/BrowserAddressListComponent.swift @@ -8,26 +8,36 @@ import Postbox import TelegramCore import AccountContext import TelegramPresentationData +import ContextUI final class BrowserAddressListComponent: Component { let context: AccountContext let theme: PresentationTheme let strings: PresentationStrings let insets: UIEdgeInsets - let navigateTo: (String) -> Void + let metrics: LayoutMetrics + let addressBarFrame: CGRect + let performAction: ActionSlot + let presentInGlobalOverlay: (ViewController) -> Void init( context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, insets: UIEdgeInsets, - navigateTo: @escaping (String) -> Void + metrics: LayoutMetrics, + addressBarFrame: CGRect, + performAction: ActionSlot, + presentInGlobalOverlay: @escaping (ViewController) -> Void ) { self.context = context self.theme = theme self.strings = strings self.insets = insets - self.navigateTo = navigateTo + self.metrics = metrics + self.addressBarFrame = addressBarFrame + self.performAction = performAction + self.presentInGlobalOverlay = presentInGlobalOverlay } static func ==(lhs: BrowserAddressListComponent, rhs: BrowserAddressListComponent) -> Bool { @@ -43,6 +53,12 @@ final class BrowserAddressListComponent: Component { if lhs.insets != rhs.insets { return false } + if lhs.metrics != rhs.metrics { + return false + } + if lhs.addressBarFrame != rhs.addressBarFrame { + return false + } return true } @@ -109,6 +125,8 @@ final class BrowserAddressListComponent: Component { let bookmarks: [Message] } + private let outerView = UIButton() + private let shadowView = UIImageView() private let backgroundView = UIView() private let scrollView = ScrollView() private let itemContainerView = UIView() @@ -130,13 +148,19 @@ final class BrowserAddressListComponent: Component { override init(frame: CGRect) { super.init(frame: frame) + self.backgroundView.clipsToBounds = true + self.scrollView.alwaysBounceVertical = true self.scrollView.delegate = self self.scrollView.showsVerticalScrollIndicator = false + self.addSubview(self.outerView) + self.addSubview(self.shadowView) self.addSubview(self.backgroundView) - self.addSubview(self.scrollView) + self.backgroundView.addSubview(self.scrollView) self.scrollView.addSubview(self.itemContainerView) + + self.outerView.addTarget(self, action: #selector(self.outerPressed), for: .touchUpInside) } required init?(coder: NSCoder) { @@ -147,6 +171,10 @@ final class BrowserAddressListComponent: Component { self.stateDisposable?.dispose() } + @objc private func outerPressed() { + self.component?.performAction.invoke(.closeAddressBar) + } + func scrollViewDidScroll(_ scrollView: UIScrollView) { if !self.ignoreScrolling { self.updateScrolling(transition: .immediate) @@ -155,6 +183,8 @@ final class BrowserAddressListComponent: Component { func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { self.window?.endEditing(true) + + cancelContextGestures(view: scrollView) } private func updateScrolling(transition: ComponentTransition) { @@ -230,7 +260,7 @@ final class BrowserAddressListComponent: Component { ) if let sectionHeaderView = sectionHeader.view { if sectionHeaderView.superview == nil { - self.addSubview(sectionHeaderView) + self.backgroundView.addSubview(sectionHeaderView) if !transition.animation.isImmediate { sectionHeaderView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) @@ -289,7 +319,7 @@ final class BrowserAddressListComponent: Component { } } - let navigateTo = component.navigateTo + let performAction = component.performAction let _ = visibleItem.update( transition: itemTransition, component: AnyComponent( @@ -302,8 +332,49 @@ final class BrowserAddressListComponent: Component { insets: component.insets, action: { if let url = webPage?.content.url { - navigateTo(url) + performAction.invoke(.navigateTo(url)) } + }, + contextAction: { [weak self] webPage, message, sourceView, gesture in + guard let self, let component = self.component else { + return + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + + var itemList: [ContextMenuItem] = [] + + if let message { + itemList.append(.action(ContextMenuActionItem(text: presentationData.strings.WebBrowser_DeleteBookmark, textColor: .destructive, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) + }, action: { [weak self] _, f in + f(.dismissWithoutContent) + + if let self, let component = self.component { + let _ = component.context.engine.messages.deleteMessagesInteractively(messageIds: [message.id], type: .forEveryone).startStandalone() + } + }))) + } else { + itemList.append(.action(ContextMenuActionItem(text: presentationData.strings.WebBrowser_RemoveRecent, textColor: .destructive, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) + }, action: { [weak self] _, f in + f(.dismissWithoutContent) + + if let self, let component = self.component, let url = webPage.content.url { + let _ = removeRecentlyVisitedLink(engine: component.context.engine, url: url).startStandalone() + } + }))) + } + + let items = ContextController.Items(content: .list(itemList)) + let controller = ContextController( + presentationData: presentationData, + source: .extracted(BrowserAddressListContextExtractedContentSource(contentView: sourceView)), + items: .single(items), + recognizer: nil, + gesture: gesture + ) + component.presentInGlobalOverlay(controller) }) ), environment: {}, @@ -387,7 +458,22 @@ final class BrowserAddressListComponent: Component { self.component = component self.state = state - let resetScrolling = self.scrollView.bounds.width != availableSize.width + self.outerView.isHidden = !component.metrics.isTablet + self.outerView.frame = CGRect(origin: .zero, size: availableSize) + + let containerFrame: CGRect + if component.metrics.isTablet { + let containerSize = CGSize(width: component.addressBarFrame.width + 32.0, height: 540.0) + containerFrame = CGRect(origin: CGPoint(x: floor(component.addressBarFrame.center.x - containerSize.width / 2.0), y: 72.0), size: containerSize) + + self.backgroundView.layer.cornerRadius = 10.0 + } else { + containerFrame = CGRect(origin: .zero, size: availableSize) + + self.backgroundView.layer.cornerRadius = 0.0 + } + + let resetScrolling = self.scrollView.bounds.width != containerFrame.width if themeUpdated { self.backgroundView.backgroundColor = component.theme.list.plainBackgroundColor } @@ -402,16 +488,13 @@ final class BrowserAddressListComponent: Component { message: nil, hasNext: true, insets: .zero, - action: {} + action: {}, + contextAction: nil )), environment: {}, containerSize: CGSize(width: itemsContainerWidth, height: 1000.0) ) - let _ = resetScrolling - let _ = addressItemSize - - var sections: [ItemLayout.Section] = [] if let state = self.stateValue { if !state.recent.isEmpty { @@ -432,37 +515,50 @@ final class BrowserAddressListComponent: Component { } } - let itemLayout = ItemLayout(containerSize: availableSize, insets: .zero, sections: sections) + let itemLayout = ItemLayout(containerSize: containerFrame.size, insets: .zero, sections: sections) self.itemLayout = itemLayout - let containerWidth = availableSize.width - let scrollContentHeight = max(itemLayout.contentHeight, availableSize.height) + let containerWidth = containerFrame.size.width + let scrollContentHeight = max(itemLayout.contentHeight, containerFrame.size.height) self.ignoreScrolling = true - transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: containerWidth, height: availableSize.height))) + transition.setFrame(view: self.scrollView, frame: CGRect(origin: .zero, size: containerFrame.size)) let contentSize = CGSize(width: containerWidth, height: scrollContentHeight) if contentSize != self.scrollView.contentSize { self.scrollView.contentSize = contentSize } -// let contentInset: UIEdgeInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: bottomPanelHeight + bottomPanelInset, right: 0.0) -// let indicatorInset = UIEdgeInsets(top: max(itemLayout.containerInset, environment.safeInsets.top + navigationHeight), left: 0.0, bottom: contentInset.bottom, right: 0.0) -// if indicatorInset != self.scrollView.scrollIndicatorInsets { -// self.scrollView.scrollIndicatorInsets = indicatorInset -// } -// if contentInset != self.scrollView.contentInset { -// self.scrollView.contentInset = contentInset -// } if resetScrolling { - self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: containerWidth, height: availableSize.height)) + self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: containerWidth, height: containerFrame.size.height)) } self.ignoreScrolling = false self.updateScrolling(transition: transition) - transition.setFrame(view: self.backgroundView, frame: CGRect(origin: .zero, size: availableSize)) + transition.setFrame(view: self.backgroundView, frame: containerFrame) transition.setFrame(view: self.itemContainerView, frame: CGRect(origin: .zero, size: CGSize(width: containerWidth, height: scrollContentHeight))) + if component.metrics.isTablet { + transition.setFrame(view: self.shadowView, frame: containerFrame.insetBy(dx: -60.0, dy: -60.0)) + self.shadowView.isHidden = false + if self.shadowView.image == nil { + self.shadowView.image = generateShadowImage() + } + } else { + self.shadowView.isHidden = true + } + return availableSize } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let result = super.hitTest(point, with: event) + if let component = self.component, component.metrics.isTablet { + let addressFrame = CGRect(origin: CGPoint(x: self.backgroundView.frame.minX, y: self.backgroundView.frame.minY - 48.0), size: CGSize(width: self.backgroundView.frame.width, height: 48.0)) + if addressFrame.contains(point) { + return nil + } + } + return result + } } func makeView() -> View { @@ -473,3 +569,55 @@ final class BrowserAddressListComponent: Component { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } + +private func generateShadowImage() -> UIImage? { + return generateImage(CGSize(width: 140.0, height: 140.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.saveGState() + context.setShadow(offset: CGSize(), blur: 60.0, color: UIColor(white: 0.0, alpha: 0.4).cgColor) + + let path = UIBezierPath(roundedRect: CGRect(x: 60.0, y: 60.0, width: 20.0, height: 20.0), cornerRadius: 10.0).cgPath + context.addPath(path) + context.fillPath() + + context.restoreGState() + + context.setBlendMode(.clear) + context.addPath(path) + context.fillPath() + })?.stretchableImage(withLeftCapWidth: 70, topCapHeight: 70) +} + +private final class BrowserAddressListContextExtractedContentSource: ContextExtractedContentSource { + let keepInPlace: Bool = false + let ignoreContentTouches: Bool = false + let blurBackground: Bool = true + + private let contentView: ContextExtractedContentContainingView + + init(contentView: ContextExtractedContentContainingView) { + self.contentView = contentView + } + + func takeView() -> ContextControllerTakeViewInfo? { + return ContextControllerTakeViewInfo(containingItem: .view(self.contentView), contentAreaInScreenSpace: UIScreen.main.bounds) + } + + func putBack() -> ContextControllerPutBackViewInfo? { + return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds) + } +} + +private func cancelContextGestures(view: UIView) { + if let gestureRecognizers = view.gestureRecognizers { + for gesture in gestureRecognizers { + if let gesture = gesture as? ContextGesture { + gesture.cancel() + } + } + } + for subview in view.subviews { + cancelContextGestures(view: subview) + } +} diff --git a/submodules/BrowserUI/Sources/BrowserAddressListItemComponent.swift b/submodules/BrowserUI/Sources/BrowserAddressListItemComponent.swift index 23a3604a0e..2d01b5de92 100644 --- a/submodules/BrowserUI/Sources/BrowserAddressListItemComponent.swift +++ b/submodules/BrowserUI/Sources/BrowserAddressListItemComponent.swift @@ -9,6 +9,7 @@ import MultilineTextComponent import TelegramPresentationData import PhotoResources import AccountContext +import ContextUI private let iconFont = Font.with(size: 30.0, design: .round, weight: .bold) private let iconTextBackgroundImage = generateStretchableFilledCircleImage(radius: 6.0, color: UIColor(rgb: 0xFF9500)) @@ -21,6 +22,7 @@ final class BrowserAddressListItemComponent: Component { let hasNext: Bool let insets: UIEdgeInsets let action: () -> Void + let contextAction: ((TelegramMediaWebpage, Message?, ContextExtractedContentContainingView, ContextGesture) -> Void)? init( context: AccountContext, @@ -29,7 +31,8 @@ final class BrowserAddressListItemComponent: Component { message: Message?, hasNext: Bool, insets: UIEdgeInsets, - action: @escaping () -> Void + action: @escaping () -> Void, + contextAction: ((TelegramMediaWebpage, Message?, ContextExtractedContentContainingView, ContextGesture) -> Void)? ) { self.context = context self.theme = theme @@ -38,6 +41,7 @@ final class BrowserAddressListItemComponent: Component { self.hasNext = hasNext self.insets = insets self.action = action + self.contextAction = contextAction } static func ==(lhs: BrowserAddressListItemComponent, rhs: BrowserAddressListItemComponent) -> Bool { @@ -56,16 +60,19 @@ final class BrowserAddressListItemComponent: Component { return true } - final class View: UIView { - private let containerButton: HighlightTrackingButton + final class View: ContextControllerSourceView { + private let extractedContainerView = ContextExtractedContentContainingView() + private let containerButton = HighlightTrackingButton() + private let separatorLayer = SimpleLayer() + private var highlightedBackgroundLayer = SimpleLayer() private var emptyIcon: UIImageView? private var emptyLabel: ComponentView? private var icon = TransformImageNode() private let title = ComponentView() private let subtitle = ComponentView() - private let separatorLayer: SimpleLayer + private var isExtractedToContextMenu: Bool = false private var component: BrowserAddressListItemComponent? private weak var state: EmptyComponentState? @@ -73,16 +80,64 @@ final class BrowserAddressListItemComponent: Component { private var currentIconImageRepresentation: TelegramMediaImageRepresentation? override init(frame: CGRect) { - self.separatorLayer = SimpleLayer() - - self.containerButton = HighlightTrackingButton() - super.init(frame: frame) + self.addSubview(self.extractedContainerView) + self.targetViewForActivationProgress = self.extractedContainerView.contentView + + self.highlightedBackgroundLayer.opacity = 0.0 + self.layer.addSublayer(self.separatorLayer) - self.addSubview(self.containerButton) + self.layer.addSublayer(self.highlightedBackgroundLayer) + + self.extractedContainerView.contentView.addSubview(self.containerButton) self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + + self.containerButton.highligthedChanged = { [weak self] highlighted in + guard let self else { + return + } + if highlighted { + self.superview?.bringSubviewToFront(self) + self.highlightedBackgroundLayer.removeAnimation(forKey: "opacity") + self.highlightedBackgroundLayer.opacity = 1.0 + } else { + self.highlightedBackgroundLayer.opacity = 0.0 + self.highlightedBackgroundLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + } + } + + self.extractedContainerView.isExtractedToContextPreviewUpdated = { [weak self] value in + guard let self, let component = self.component else { + return + } + self.containerButton.clipsToBounds = value + self.containerButton.backgroundColor = value ? component.theme.list.plainBackgroundColor : 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.webPage, component.message, self.extractedContainerView, gesture) + } } required init?(coder: NSCoder) { @@ -282,14 +337,22 @@ final class BrowserAddressListItemComponent: Component { } if themeUpdated { + self.highlightedBackgroundLayer.backgroundColor = component.theme.list.itemHighlightedBackgroundColor.cgColor self.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor } + transition.setFrame(layer: self.highlightedBackgroundLayer, frame: CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: height + UIScreenPixel))) 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 containerFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: height)) transition.setFrame(view: self.containerButton, frame: containerFrame) + transition.setFrame(view: self.extractedContainerView, frame: containerFrame) + transition.setFrame(view: self.extractedContainerView.contentView, frame: containerFrame) + self.extractedContainerView.contentRect = containerFrame + + self.isGestureEnabled = component.contextAction != nil + return CGSize(width: availableSize.width, height: height) } } diff --git a/submodules/BrowserUI/Sources/BrowserNavigationBarComponent.swift b/submodules/BrowserUI/Sources/BrowserNavigationBarComponent.swift index 674b5a7b9c..ab0d7066ce 100644 --- a/submodules/BrowserUI/Sources/BrowserNavigationBarComponent.swift +++ b/submodules/BrowserUI/Sources/BrowserNavigationBarComponent.swift @@ -21,6 +21,14 @@ final class BrowserNavigationBarEnvironment: Equatable { } final class BrowserNavigationBarComponent: CombinedComponent { + public class ExternalState { + public fileprivate(set) var centerItemFrame: CGRect + + public init() { + self.centerItemFrame = .zero + } + } + let backgroundColor: UIColor let separatorColor: UIColor let textColor: UIColor @@ -30,6 +38,7 @@ final class BrowserNavigationBarComponent: CombinedComponent { let height: CGFloat let sideInset: CGFloat let metrics: LayoutMetrics + let externalState: ExternalState? let leftItems: [AnyComponentWithIdentity] let rightItems: [AnyComponentWithIdentity] let centerItem: AnyComponentWithIdentity? @@ -48,6 +57,7 @@ final class BrowserNavigationBarComponent: CombinedComponent { height: CGFloat, sideInset: CGFloat, metrics: LayoutMetrics, + externalState: ExternalState?, leftItems: [AnyComponentWithIdentity], rightItems: [AnyComponentWithIdentity], centerItem: AnyComponentWithIdentity?, @@ -65,6 +75,7 @@ final class BrowserNavigationBarComponent: CombinedComponent { self.height = height self.sideInset = sideInset self.metrics = metrics + self.externalState = externalState self.leftItems = leftItems self.rightItems = rightItems self.centerItem = centerItem @@ -135,14 +146,14 @@ final class BrowserNavigationBarComponent: CombinedComponent { return { context in var availableWidth = context.availableSize.width - let sideInset: CGFloat = 16.0 + context.component.sideInset + let sideInset: CGFloat = (context.component.metrics.isTablet ? 20.0 : 16.0) + context.component.sideInset let collapsedHeight: CGFloat = 24.0 let expandedHeight = context.component.height let contentHeight: CGFloat = expandedHeight * (1.0 - context.component.collapseFraction) + collapsedHeight * context.component.collapseFraction let size = CGSize(width: context.availableSize.width, height: context.component.topInset + contentHeight) - let verticalOffset: CGFloat = context.component.metrics.isTablet ? -3.0 : 0.0 - let itemSpacing: CGFloat = context.component.metrics.isTablet ? 24.0 : 8.0 + let verticalOffset: CGFloat = context.component.metrics.isTablet ? -2.0 : 0.0 + let itemSpacing: CGFloat = context.component.metrics.isTablet ? 26.0 : 8.0 let background = background.update( component: Rectangle(color: context.component.backgroundColor.withAlphaComponent(1.0)), @@ -268,12 +279,15 @@ final class BrowserNavigationBarComponent: CombinedComponent { centerX = centerLeftInset + (context.availableSize.width - centerLeftInset - centerRightInset) / 2.0 } if let centerItem = centerItem { + let centerItemPosition = CGPoint(x: centerX, y: context.component.topInset + contentHeight / 2.0 + verticalOffset) context.add(centerItem - .position(CGPoint(x: centerX, y: context.component.topInset + contentHeight / 2.0 + verticalOffset)) + .position(centerItemPosition) .scale(1.0 - 0.35 * context.component.collapseFraction) .appear(.default(scale: false, alpha: true)) .disappear(.default(scale: false, alpha: true)) ) + + context.component.externalState?.centerItemFrame = centerItem.size.centered(around: centerItemPosition) } if context.component.collapseFraction == 1.0 { diff --git a/submodules/BrowserUI/Sources/BrowserScreen.swift b/submodules/BrowserUI/Sources/BrowserScreen.swift index c63dd013d7..ebd10a37e0 100644 --- a/submodules/BrowserUI/Sources/BrowserScreen.swift +++ b/submodules/BrowserUI/Sources/BrowserScreen.swift @@ -75,6 +75,8 @@ private final class BrowserScreenComponent: CombinedComponent { let toolbar = Child(BrowserToolbarComponent.self) let addressList = Child(BrowserAddressListComponent.self) + let navigationBarExternalState = BrowserNavigationBarComponent.ExternalState() + return { context in let environment = context.environment[ViewControllerComponentContainer.Environment.self].value let performAction = context.component.performAction @@ -311,6 +313,7 @@ private final class BrowserScreenComponent: CombinedComponent { height: environment.navigationHeight - environment.statusBarHeight, sideInset: environment.safeInsets.left, metrics: environment.metrics, + externalState: navigationBarExternalState, leftItems: navigationLeftItems, rightItems: navigationRightItems, centerItem: navigationContent, @@ -401,18 +404,22 @@ private final class BrowserScreenComponent: CombinedComponent { if context.component.presentationState.addressFocused { let addressListSize: CGSize if isTablet { - addressListSize = CGSize(width: 660.0, height: 420.0) + addressListSize = context.availableSize } else { addressListSize = CGSize(width: context.availableSize.width, height: context.availableSize.height - navigationBar.size.height - toolbarSize) } + let controller = environment.controller let addressList = addressList.update( component: BrowserAddressListComponent( context: context.component.context, theme: environment.theme, strings: environment.strings, insets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: 0.0, right: environment.safeInsets.right), - navigateTo: { url in - performAction.invoke(.navigateTo(url)) + metrics: environment.metrics, + addressBarFrame: navigationBarExternalState.centerItemFrame, + performAction: performAction, + presentInGlobalOverlay: { c in + controller()?.presentInGlobalOverlay(c) } ), availableSize: addressListSize, @@ -421,9 +428,7 @@ private final class BrowserScreenComponent: CombinedComponent { if isTablet { context.add(addressList - .position(CGPoint(x: context.availableSize.width / 2.0, y: navigationBar.size.height + addressList.size.height / 2.0 - 3.0)) - .cornerRadius(10.0) - .clipsToBounds(true) + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) .appear(.default(alpha: true)) .disappear(.default(alpha: true)) ) diff --git a/submodules/DeviceAccess/Sources/DeviceAccess.swift b/submodules/DeviceAccess/Sources/DeviceAccess.swift index 9228902cc0..cfa335f52d 100644 --- a/submodules/DeviceAccess/Sources/DeviceAccess.swift +++ b/submodules/DeviceAccess/Sources/DeviceAccess.swift @@ -353,24 +353,26 @@ public final class DeviceAccess { } else { completion(true) } - } else if [.restricted, .denied].contains(status), let presentationData = presentationData { - let text: String - if case .restricted = status { - text = presentationData.strings.AccessDenied_CameraRestricted - } else { - switch cameraSubject { - case .video: - text = presentationData.strings.AccessDenied_Camera - case .videoCall: - text = presentationData.strings.AccessDenied_VideoCallCamera - case .qrCode: - text = presentationData.strings.AccessDenied_QrCamera - } - } + } else if [.restricted, .denied].contains(status) { completion(false) - present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: { - openSettings() - })]), nil) + if let presentationData = presentationData { + let text: String + if case .restricted = status { + text = presentationData.strings.AccessDenied_CameraRestricted + } else { + switch cameraSubject { + case .video: + text = presentationData.strings.AccessDenied_Camera + case .videoCall: + text = presentationData.strings.AccessDenied_VideoCallCamera + case .qrCode: + text = presentationData.strings.AccessDenied_QrCamera + } + } + present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: { + openSettings() + })]), nil) + } } else if case .authorized = status { completion(true) } else { diff --git a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift index 29a4b1b14e..8a303cdf76 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift @@ -310,21 +310,35 @@ public func webBrowserSettingsController(context: AccountContext) -> ViewControl let controller = ItemListController(context: context, state: signal) clearCookiesImpl = { [weak controller] in - WKWebsiteDataStore.default().removeData(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(), modifiedSince: Date(timeIntervalSince1970: 0), completionHandler:{}) - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - controller?.present(UndoOverlayController( - presentationData: presentationData, - content: .info( - title: nil, - text: presentationData.strings.WebBrowser_ClearCookies_Succeed, - timeout: nil, - customUndoText: nil - ), - elevatedLayout: false, - position: .bottom, - action: { _ in return false }), in: .current + + let alertController = textAlertController( + context: context, + updatedPresentationData: nil, + title: nil, + text: presentationData.strings.WebBrowser_ClearCookies_ClearConfirmation_Text, + actions: [ + TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), + TextAlertAction(type: .defaultAction, title: presentationData.strings.WebBrowser_ClearCookies_ClearConfirmation_Clear, action: { + WKWebsiteDataStore.default().removeData(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(), modifiedSince: Date(timeIntervalSince1970: 0), completionHandler:{}) + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + controller?.present(UndoOverlayController( + presentationData: presentationData, + content: .info( + title: nil, + text: presentationData.strings.WebBrowser_ClearCookies_Succeed, + timeout: nil, + customUndoText: nil + ), + elevatedLayout: false, + position: .bottom, + action: { _ in return false }), in: .current + ) + }) + ] ) + controller?.present(alertController, in: .window(.root)) } addExceptionImpl = { [weak controller] in diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift index e696d413e3..09b076bbd2 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift @@ -1124,9 +1124,8 @@ public final class MediaEditor { self.initialSeekPosition = position return } - if play { - self.renderer.setRate(1.0) - } else { + self.renderer.setRate(1.0) + if !play { self.player?.pause() self.additionalPlayer?.pause() self.audioPlayer?.pause() diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCoverScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCoverScreen.swift index 4a3c7407a8..db9dc21545 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCoverScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCoverScreen.swift @@ -431,9 +431,6 @@ final class MediaCoverScreen: ViewController { } func animateOutToEditor(completion: @escaping () -> Void) { - self.controller?.withMediaEditor { mediaEditor in - mediaEditor.play() - } if let view = self.componentHost.view as? MediaCoverScreenComponent.View { view.animateOutToEditor(completion: completion) } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorDrafts.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorDrafts.swift index b5c74027ad..d29a7083b9 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorDrafts.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorDrafts.swift @@ -46,7 +46,7 @@ extension MediaEditorScreen { return true } - func saveDraft(id: Int64?) { + func saveDraft(id: Int64?, edit: Bool = false) { guard let subject = self.node.subject, let actualSubject = self.node.actualSubject, let mediaEditor = self.node.mediaEditor else { return } @@ -83,7 +83,9 @@ extension MediaEditorScreen { } if let resultImage = mediaEditor.resultImage { - mediaEditor.seek(0.0, andPlay: false) + if !edit { + mediaEditor.seek(0.0, andPlay: false) + } makeEditorImageComposition(context: self.node.ciContext, postbox: self.context.account.postbox, inputImage: resultImage, dimensions: storyDimensions, values: values, time: .zero, textScale: 2.0, completion: { resultImage in guard let resultImage else { return diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index e75f6b39d4..d4f897c57e 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -6492,6 +6492,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } if self.isEmbeddedEditor && !(hasAnyChanges || hasEntityChanges) { + self.saveDraft(id: randomId, edit: true) + self.completion(MediaEditorScreen.Result(media: nil, mediaAreas: [], caption: caption, coverTimestamp: mediaEditor.values.coverImageTimestamp, options: self.state.privacy, stickers: stickers, randomId: randomId), { [weak self] finished in self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in self?.dismiss() diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift index 1fe793e04b..42f157d5d9 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift @@ -2919,7 +2919,7 @@ final class ShareWithPeersScreenComponent: Component { contentTransition.setFrame(view: self.itemContainerView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: containerWidth, height: itemLayout.contentHeight + footersTotalHeight))) - let scrollContentHeight = max(topInset + itemLayout.contentHeight + containerInset, availableSize.height - containerInset) + let scrollContentHeight = max(topInset + itemLayout.contentHeight + containerInset + bottomPanelHeight, availableSize.height - containerInset) transition.setFrame(view: self.scrollContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset + containerInset), size: CGSize(width: containerWidth, height: itemLayout.contentHeight))) From 7acb39e657c1757117fe1b0ca285847ec8869b28 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 26 Jul 2024 12:31:40 +0200 Subject: [PATCH 21/41] Various fixes --- .../BrowserAddressListItemComponent.swift | 26 ++++++------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/submodules/BrowserUI/Sources/BrowserAddressListItemComponent.swift b/submodules/BrowserUI/Sources/BrowserAddressListItemComponent.swift index 2d01b5de92..5ffe9b5e58 100644 --- a/submodules/BrowserUI/Sources/BrowserAddressListItemComponent.swift +++ b/submodules/BrowserUI/Sources/BrowserAddressListItemComponent.swift @@ -82,14 +82,14 @@ final class BrowserAddressListItemComponent: Component { override init(frame: CGRect) { super.init(frame: frame) + self.layer.addSublayer(self.separatorLayer) + self.layer.addSublayer(self.highlightedBackgroundLayer) + self.addSubview(self.extractedContainerView) self.targetViewForActivationProgress = self.extractedContainerView.contentView self.highlightedBackgroundLayer.opacity = 0.0 - self.layer.addSublayer(self.separatorLayer) - self.layer.addSublayer(self.highlightedBackgroundLayer) - self.extractedContainerView.contentView.addSubview(self.containerButton) self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) @@ -160,6 +160,7 @@ final class BrowserAddressListItemComponent: Component { let leftInset: CGFloat = component.insets.left + 11.0 + iconSize.width + 11.0 let rightInset: CGFloat = 16.0 let titleSpacing: CGFloat = 2.0 + let contextInset: CGFloat = self.isExtractedToContextMenu ? 12.0 : 0.0 let title: String let subtitle: String @@ -264,7 +265,7 @@ final class BrowserAddressListItemComponent: Component { } if self.icon.supernode == nil { - self.addSubview(self.icon.view) + self.containerButton.addSubview(self.icon.view) self.icon.frame = iconFrame } else { transition.setFrame(view: self.icon.view, frame: iconFrame) @@ -293,7 +294,7 @@ final class BrowserAddressListItemComponent: Component { } else { icon = UIImageView() icon.image = iconTextBackgroundImage - self.addSubview(icon) + self.containerButton.addSubview(icon) label = ComponentView() } @@ -319,21 +320,10 @@ final class BrowserAddressListItemComponent: Component { let labelFrame = CGRect(origin: CGPoint(x: iconFrame.minX + floorToScreenPixels((iconFrame.width - labelSize.width) / 2.0), y: iconFrame.minY + floorToScreenPixels((iconFrame.height - labelSize.height) / 2.0)), size: labelSize) if let labelView = label.view { if labelView.superview == nil { - self.addSubview(labelView) + self.containerButton.addSubview(labelView) } labelView.frame = labelFrame } - -// if strongSelf.iconTextBackgroundNode.supernode == nil { -// strongSelf.iconTextBackgroundNode.image = applyIconTextBackgroundImage -// strongSelf.offsetContainerNode.addSubnode(strongSelf.iconTextBackgroundNode) -// strongSelf.iconTextBackgroundNode.frame = iconFrame -// } else { -// transition.updateFrame(node: strongSelf.iconTextBackgroundNode, frame: iconFrame) -// } -// if strongSelf.iconTextNode.supernode == nil { -// strongSelf.offsetContainerNode.addSubnode(strongSelf.iconTextNode) -// } } if themeUpdated { @@ -345,7 +335,7 @@ final class BrowserAddressListItemComponent: Component { self.separatorLayer.isHidden = !component.hasNext let containerFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: height)) - transition.setFrame(view: self.containerButton, frame: containerFrame) + transition.setFrame(view: self.containerButton, frame: containerFrame.insetBy(dx: contextInset, dy: 0.0)) transition.setFrame(view: self.extractedContainerView, frame: containerFrame) transition.setFrame(view: self.extractedContainerView.contentView, frame: containerFrame) From 07c774eea7226db3a755e413e863b355a2b960ef Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 26 Jul 2024 12:55:47 +0200 Subject: [PATCH 22/41] Various fixes --- .../BrowserAddressListItemComponent.swift | 20 +++++++- .../Sources/ListMessageSnippetItemNode.swift | 47 ++++++++++++++++++- .../Sources/Punycode.swift | 0 3 files changed, 65 insertions(+), 2 deletions(-) rename submodules/{BrowserUI => UrlEscaping}/Sources/Punycode.swift (100%) diff --git a/submodules/BrowserUI/Sources/BrowserAddressListItemComponent.swift b/submodules/BrowserUI/Sources/BrowserAddressListItemComponent.swift index 5ffe9b5e58..97079f1686 100644 --- a/submodules/BrowserUI/Sources/BrowserAddressListItemComponent.swift +++ b/submodules/BrowserUI/Sources/BrowserAddressListItemComponent.swift @@ -10,6 +10,7 @@ import TelegramPresentationData import PhotoResources import AccountContext import ContextUI +import UrlEscaping private let iconFont = Font.with(size: 30.0, design: .round, weight: .bold) private let iconTextBackgroundImage = generateStretchableFilledCircleImage(radius: 6.0, color: UIColor(rgb: 0xFF9500)) @@ -170,7 +171,24 @@ final class BrowserAddressListItemComponent: Component { if case let .Loaded(content) = component.webPage.content { title = content.title ?? content.url - subtitle = content.url + + var address = content.url + if let components = URLComponents(string: address) { + if #available(iOS 16.0, *), let encodedHost = components.encodedHost { + if let decodedHost = components.host, encodedHost != decodedHost { + address = address.replacingOccurrences(of: encodedHost, with: decodedHost) + } + } else if let encodedHost = components.host { + if let decodedHost = components.host?.idnaDecoded, encodedHost != decodedHost { + address = address.replacingOccurrences(of: encodedHost, with: decodedHost) + } + } + } + address = address.replacingOccurrences(of: "https://www.", with: "") + address = address.replacingOccurrences(of: "https://", with: "") + address = address.trimmingCharacters(in: CharacterSet(charactersIn: "/")) + subtitle = address + parsedUrl = URL(string: content.url) if let image = content.image { diff --git a/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift b/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift index 4bbb6d07fa..d0308f9d5d 100644 --- a/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift +++ b/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift @@ -16,6 +16,7 @@ import UrlWhitelist import AccountContext import TelegramStringFormatting import WallpaperResources +import UrlEscaping private let iconFont = Font.with(size: 30.0, design: .round, weight: .bold) @@ -333,7 +334,21 @@ public final class ListMessageSnippetItemNode: ListMessageNode { mutableDescriptionText.append(NSAttributedString(string: text + "\n", font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor)) } - let plainUrlString = NSAttributedString(string: content.url.replacingOccurrences(of: "https://", with: ""), font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemAccentColor) + var address = content.url + if let components = URLComponents(string: address) { + if #available(iOS 16.0, *), let encodedHost = components.encodedHost { + if let decodedHost = components.host, encodedHost != decodedHost { + address = address.replacingOccurrences(of: encodedHost, with: decodedHost) + } + } else if let encodedHost = components.host { + if let decodedHost = components.host?.idnaDecoded, encodedHost != decodedHost { + address = address.replacingOccurrences(of: encodedHost, with: decodedHost) + } + } + } + address = address.trimmingCharacters(in: CharacterSet(charactersIn: "/")) + + let plainUrlString = NSAttributedString(string: address.replacingOccurrences(of: "https://", with: ""), font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemAccentColor) let urlString = NSMutableAttributedString() urlString.append(plainUrlString) urlString.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.URL), value: content.url, range: NSMakeRange(0, urlString.length)) @@ -432,6 +447,21 @@ public final class ListMessageSnippetItemNode: ListMessageNode { mutableDescriptionText.append(NSAttributedString(string: messageText + "\n", font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor)) } + var address = urlString + if let components = URLComponents(string: address) { + if #available(iOS 16.0, *), let encodedHost = components.encodedHost { + if let decodedHost = components.host, encodedHost != decodedHost { + address = address.replacingOccurrences(of: encodedHost, with: decodedHost) + } + } else if let encodedHost = components.host { + if let decodedHost = components.host?.idnaDecoded, encodedHost != decodedHost { + address = address.replacingOccurrences(of: encodedHost, with: decodedHost) + } + } + } + address = address.trimmingCharacters(in: CharacterSet(charactersIn: "/")) + urlString = address + let urlAttributedString = NSMutableAttributedString() urlAttributedString.append(NSAttributedString(string: urlString.replacingOccurrences(of: "https://", with: ""), font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemAccentColor)) if item.presentationData.theme.theme.list.itemAccentColor.isEqual(item.presentationData.theme.theme.list.itemPrimaryTextColor) { @@ -487,6 +517,21 @@ public final class ListMessageSnippetItemNode: ListMessageNode { mutableDescriptionText.append(NSAttributedString(string: messageText + "\n", font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor)) } + var address = urlString + if let components = URLComponents(string: address) { + if #available(iOS 16.0, *), let encodedHost = components.encodedHost { + if let decodedHost = components.host, encodedHost != decodedHost { + address = address.replacingOccurrences(of: encodedHost, with: decodedHost) + } + } else if let encodedHost = components.host { + if let decodedHost = components.host?.idnaDecoded, encodedHost != decodedHost { + address = address.replacingOccurrences(of: encodedHost, with: decodedHost) + } + } + } + address = address.trimmingCharacters(in: CharacterSet(charactersIn: "/")) + urlString = address + let urlAttributedString = NSMutableAttributedString() urlAttributedString.append(NSAttributedString(string: urlString.replacingOccurrences(of: "https://", with: ""), font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemAccentColor)) if item.presentationData.theme.theme.list.itemAccentColor.isEqual(item.presentationData.theme.theme.list.itemPrimaryTextColor) { diff --git a/submodules/BrowserUI/Sources/Punycode.swift b/submodules/UrlEscaping/Sources/Punycode.swift similarity index 100% rename from submodules/BrowserUI/Sources/Punycode.swift rename to submodules/UrlEscaping/Sources/Punycode.swift From e1359c56aef056ce720c99fb3c7d1d72c138c1bd Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Fri, 26 Jul 2024 19:37:32 +0800 Subject: [PATCH 23/41] Various improvements --- ...CheckoutWebInteractionControllerNode.swift | 8 + .../Sources/ChatListSearchListPaneNode.swift | 6 +- .../SpaceWarpView/Sources/SpaceWarpView.swift | 895 ++++-------------- .../Sources/TabSelectorComponent.swift | 1 + .../Sources/ChatControllerNode.swift | 92 +- 5 files changed, 217 insertions(+), 785 deletions(-) diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutWebInteractionControllerNode.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutWebInteractionControllerNode.swift index e6d52efedd..d24ef2092d 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutWebInteractionControllerNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutWebInteractionControllerNode.swift @@ -146,6 +146,14 @@ final class BotCheckoutWebInteractionControllerNode: ViewControllerTracingNode, decisionHandler(.cancel) completion(true) } else { + if let url = navigationAction.request.url, let scheme = url.scheme { + let defaultSchemes: [String] = ["http", "https"] + if !defaultSchemes.contains(scheme) { + decisionHandler(.cancel) + self.context.sharedContext.applicationBindings.openUrl(url.absoluteString) + return + } + } decisionHandler(.allow) } } else { diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index bcb62df9a9..f86f73e0a5 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -34,7 +34,7 @@ import AvatarNode private enum ChatListRecentEntryStableId: Hashable { case topPeers - case peerId(EnginePeer.Id) + case peerId(EnginePeer.Id, ChatListRecentEntry.Section) } private enum ChatListRecentEntry: Comparable, Identifiable { @@ -51,8 +51,8 @@ private enum ChatListRecentEntry: Comparable, Identifiable { switch self { case .topPeers: return .topPeers - case let .peer(_, peer, _, _, _, _, _, _, _, _, _): - return .peerId(peer.peer.peerId) + case let .peer(_, peer, section, _, _, _, _, _, _, _, _): + return .peerId(peer.peer.peerId, section) } } diff --git a/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift b/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift index 7f516bdd6d..380a56f2c6 100644 --- a/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift +++ b/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift @@ -99,9 +99,9 @@ private func transformCoordinate( params: RippleParams ) -> CGPoint { // The distance of the current pixel position from `origin`. - let distance = length(position - origin) + let distance: CGFloat = length(position - origin) - if distance < 2.0 { + if distance < 1.0 { return position } @@ -115,10 +115,17 @@ private func transformCoordinate( // The ripple is a sine wave that Metal scales by an exponential decay // function. - let rippleAmount = params.amplitude * sin(params.frequency * time) * exp(-params.decay * time) + var rippleAmount = params.amplitude * sin(params.frequency * time) * exp(-params.decay * time) + let absRippleAmount = abs(rippleAmount) + if rippleAmount < 0.0 { + rippleAmount = -absRippleAmount + } else { + rippleAmount = absRippleAmount + } // A vector of length `amplitude` that points away from position. - let n = normalize(position - origin) + let n: CGPoint + n = normalize(position - origin) // Scale `n` by the ripple amount at the current pixel position and add it // to the current pixel position. @@ -129,63 +136,31 @@ private func transformCoordinate( return newPosition } -private func rectToQuad( - rect: CGRect, - quadTL: CGPoint, - quadTR: CGPoint, - quadBL: CGPoint, - quadBR: CGPoint -) -> CATransform3D { - let x1a = quadTL.x - let y1a = quadTL.y - let x2a = quadTR.x - let y2a = quadTR.y - let x3a = quadBL.x - let y3a = quadBL.y - let x4a = quadBR.x - let y4a = quadBR.y +func transformToFitQuad2(frame: CGRect, topLeft tl: CGPoint, topRight tr: CGPoint, bottomLeft bl: CGPoint, bottomRight br: CGPoint) -> (frame: CGRect, transform: CATransform3D) { + let frameTopLeft = frame.origin - let X = rect.origin.x - let Y = rect.origin.y - let W = rect.size.width - let H = rect.size.height + let transform = rectToQuad( + rect: CGRect(origin: CGPoint(), size: frame.size), + quadTL: CGPoint(x: tl.x - frameTopLeft.x, y: tl.y - frameTopLeft.y), + quadTR: CGPoint(x: tr.x - frameTopLeft.x, y: tr.y - frameTopLeft.y), + quadBL: CGPoint(x: bl.x - frameTopLeft.x, y: bl.y - frameTopLeft.y), + quadBR: CGPoint(x: br.x - frameTopLeft.x, y: br.y - frameTopLeft.y) + ) - let y21 = y2a - y1a - let y32 = y3a - y2a - let y43 = y4a - y3a - let y14 = y1a - y4a - let y31 = y3a - y1a - let y42 = y4a - y2a + let anchorPoint = frame.center + let anchorOffset = CGPoint(x: anchorPoint.x - frame.origin.x, y: anchorPoint.y - frame.origin.y) + let transPos = CATransform3DMakeTranslation(anchorOffset.x, anchorOffset.y, 0) + let transNeg = CATransform3DMakeTranslation(-anchorOffset.x, -anchorOffset.y, 0) + let fullTransform = CATransform3DConcat(CATransform3DConcat(transPos, transform), transNeg) - let a = -H*(x2a*x3a*y14 + x2a*x4a*y31 - x1a*x4a*y32 + x1a*x3a*y42) - let b = W*(x2a*x3a*y14 + x3a*x4a*y21 + x1a*x4a*y32 + x1a*x2a*y43) - let c = H*X*(x2a*x3a*y14 + x2a*x4a*y31 - x1a*x4a*y32 + x1a*x3a*y42) - H*W*x1a*(x4a*y32 - x3a*y42 + x2a*y43) - W*Y*(x2a*x3a*y14 + x3a*x4a*y21 + x1a*x4a*y32 + x1a*x2a*y43) - - let d = H*(-x4a*y21*y3a + x2a*y1a*y43 - x1a*y2a*y43 - x3a*y1a*y4a + x3a*y2a*y4a) - let e = W*(x4a*y2a*y31 - x3a*y1a*y42 - x2a*y31*y4a + x1a*y3a*y42) - let f = -(W*(x4a*(Y*y2a*y31 + H*y1a*y32) - x3a*(H + Y)*y1a*y42 + H*x2a*y1a*y43 + x2a*Y*(y1a - y3a)*y4a + x1a*Y*y3a*(-y2a + y4a)) - H*X*(x4a*y21*y3a - x2a*y1a*y43 + x3a*(y1a - y2a)*y4a + x1a*y2a*(-y3a + y4a))) - - let g = H*(x3a*y21 - x4a*y21 + (-x1a + x2a)*y43) - let h = W*(-x2a*y31 + x4a*y31 + (x1a - x3a)*y42) - var i = W*Y*(x2a*y31 - x4a*y31 - x1a*y42 + x3a*y42) + H*(X*(-(x3a*y21) + x4a*y21 + x1a*y43 - x2a*y43) + W*(-(x3a*y2a) + x4a*y2a + x2a*y3a - x4a*y3a - x2a*y4a + x3a*y4a)) - - let kEpsilon = 0.0001 - - if fabs(i) < kEpsilon { - i = kEpsilon * (i > 0 ? 1.0 : -1.0) - } - - let transform = CATransform3D(m11: a/i, m12: d/i, m13: 0, m14: g/i, m21: b/i, m22: e/i, m23: 0, m24: h/i, m31: 0, m32: 0, m33: 1, m34: 0, m41: c/i, m42: f/i, m43: 0, m44: 1.0) - return transform + return (frame, fullTransform) } -func transformToFitQuad(frame: CGRect, topLeft tl: CGPoint, topRight tr: CGPoint, bottomLeft bl: CGPoint, bottomRight br: CGPoint) -> CATransform3D { - /*let boundingBox = UIView.boundingBox(forQuadWithTR: tr, tl: tl, bl: bl, br: br) - self.layer.transform = CATransform3DIdentity // keeps current transform from interfering - self.frame = boundingBox*/ +func transformToFitQuad(frame: CGRect, topLeft tl: CGPoint, topRight tr: CGPoint, bottomLeft bl: CGPoint, bottomRight br: CGPoint) -> (frame: CGRect, transform: CATransform3D) { + let boundingBox = boundingBox(forQuadWithTR: tr, tl: tl, bl: bl, br: br) - let frameTopLeft = frame.origin - let transform = rectToQuad2( + let frameTopLeft = boundingBox.origin + let transform = rectToQuad( rect: CGRect(origin: CGPoint(), size: frame.size), quadTL: CGPoint(x: tl.x - frameTopLeft.x, y: tl.y - frameTopLeft.y), quadTR: CGPoint(x: tr.x - frameTopLeft.x, y: tr.y - frameTopLeft.y), @@ -195,12 +170,13 @@ func transformToFitQuad(frame: CGRect, topLeft tl: CGPoint, topRight tr: CGPoint // To account for anchor point, we must translate, transform, translate let anchorPoint = frame.center - let anchorOffset = CGPoint(x: anchorPoint.x - frame.origin.x, y: anchorPoint.y - frame.origin.y) + let anchorOffset = CGPoint(x: anchorPoint.x - boundingBox.origin.x, y: anchorPoint.y - boundingBox.origin.y) let transPos = CATransform3DMakeTranslation(anchorOffset.x, anchorOffset.y, 0) let transNeg = CATransform3DMakeTranslation(-anchorOffset.x, -anchorOffset.y, 0) let fullTransform = CATransform3DConcat(CATransform3DConcat(transPos, transform), transNeg) - return fullTransform + // Now we set our transform + return (boundingBox, fullTransform) } private func boundingBox(forQuadWithTR tr: CGPoint, tl: CGPoint, bl: CGPoint, br: CGPoint) -> CGRect { @@ -219,7 +195,22 @@ private func boundingBox(forQuadWithTR tr: CGPoint, tl: CGPoint, bl: CGPoint, br return boundingBox } -func rectToQuad2(rect: CGRect, quadTL topLeft: CGPoint, quadTR topRight: CGPoint, quadBL bottomLeft: CGPoint, quadBR bottomRight: CGPoint) -> CATransform3D { +func rectToQuad(rect: CGRect, quadTL topLeft: CGPoint, quadTR topRight: CGPoint, quadBL bottomLeft: CGPoint, quadBR bottomRight: CGPoint) -> CATransform3D { + /*if "".isEmpty { + let destination = Perspective(Quadrilateral( + topLeft, + topRight, + bottomLeft, + bottomRight + )) + + // Starting perspective is the current overlay frame or could be another 4 points. + let start = Perspective(Quadrilateral(rect.origin, rect.size)) + + // Caclulate CATransform3D from start to destination + return start.projectiveTransform(destination: destination) + }*/ + return rectToQuad(rect: rect, quadTLX: topLeft.x, quadTLY: topLeft.y, quadTRX: topRight.x, quadTRY: topRight.y, quadBLX: bottomLeft.x, quadBLY: bottomLeft.y, quadBRX: bottomRight.x, quadBRY: bottomRight.y) } @@ -264,244 +255,62 @@ private func rectToQuad(rect: CGRect, quadTLX x1a: CGFloat, quadTLY y1a: CGFloat return transform } -public protocol SpaceWarpView: UIView { - var contentView: UIView { get } +public protocol SpaceWarpNode: ASDisplayNode { + var contentNode: ASDisplayNode { get } - func trigger(at point: CGPoint) - func update(size: CGSize, transition: ComponentTransition) + func triggerRipple(at point: CGPoint) + func update(size: CGSize, cornerRadius: CGFloat, transition: ComponentTransition) } -open class SpaceWarpView1: UIView, SpaceWarpView { - private final class GridView: UIView { - let cloneView: PortalView - let gridPosition: CGPoint - - init?(contentView: PortalSourceView, gridPosition: CGPoint) { - self.gridPosition = gridPosition - - guard let cloneView = PortalView(matchPosition: false) else { - return nil - } - self.cloneView = cloneView - - super.init(frame: CGRect()) - - self.layer.anchorPoint = CGPoint(x: 0.0, y: 0.0) - - self.clipsToBounds = true - self.isUserInteractionEnabled = false - self.addSubview(cloneView.view) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func updateIsActive(contentView: PortalSourceView, isActive: Bool) { - if isActive { - contentView.addPortal(view: self.cloneView) - } else { - contentView.removePortal(view: self.cloneView) - } - } - - func update(containerSize: CGSize, rect: CGRect, transition: ComponentTransition) { - transition.setFrame(view: self.cloneView.view, frame: CGRect(origin: CGPoint(x: -rect.minX - containerSize.width * 0.5, y: -rect.minY - containerSize.height * 0.5), size: CGSize(width: containerSize.width, height: containerSize.height))) - } +open class SpaceWarpNodeImpl: ASDisplayNode, SpaceWarpNode { + public var contentNode: ASDisplayNode { + return self.contentNodeSource } - private var gridViews: [GridView] = [] - - public var contentView: UIView { - return self.contentViewImpl - } - - let contentViewImpl: PortalSourceView - - private var link: SharedDisplayLinkDriver.Link? - private var startPoint: CGPoint? - - private var timeValue: CGFloat = 0.0 - private var currentActiveViews: Int = 0 - - private var resolution: (x: Int, y: Int)? - private var size: CGSize? - - override public init(frame: CGRect) { - self.contentViewImpl = PortalSourceView() - - super.init(frame: frame) - - self.addSubview(self.contentView) - } - - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - public func trigger(at point: CGPoint) { - self.startPoint = point - self.timeValue = 0.0 - - if self.link == nil { - self.link = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] deltaTime in - guard let self else { - return - } - self.timeValue += deltaTime * (1.0 / CGFloat(UIView.animationDurationFactor())) - - if let size = self.size { - self.update(size: size, transition: .immediate) - } - }) - } - } - - private func updateGrid(resolutionX: Int, resolutionY: Int) { - if let resolution = self.resolution, resolution.x == resolutionX, resolution.y == resolutionY { - return - } - self.resolution = (resolutionX, resolutionY) - - for gridView in self.gridViews { - gridView.removeFromSuperview() - } - - var gridViews: [GridView] = [] - for y in 0 ..< resolutionY { - for x in 0 ..< resolutionX { - if let gridView = GridView(contentView: self.contentViewImpl, gridPosition: CGPoint(x: CGFloat(x) / CGFloat(resolutionX), y: CGFloat(y) / CGFloat(resolutionY))) { - gridView.isUserInteractionEnabled = false - gridViews.append(gridView) - self.addSubview(gridView) - } - } - } - self.gridViews = gridViews - } - - public func update(size: CGSize, transition: ComponentTransition) { - self.size = size - if size.width <= 0.0 || size.height <= 0.0 { - return - } - - self.updateGrid(resolutionX: max(2, Int(size.width / 100.0)), resolutionY: max(2, Int(size.height / 100.0))) - guard let resolution = self.resolution else { - return - } - - //let pixelStep = CGPoint(x: CGFloat(resolution.x) * 0.33, y: CGFloat(resolution.y) * 0.33) - let pixelStep = CGPoint() - let itemSize = CGSize(width: size.width / CGFloat(resolution.x), height: size.height / CGFloat(resolution.y)) - - let params = RippleParams(amplitude: 22.0, frequency: 15.0, decay: 8.0, speed: 1400.0) - - var activeViews = 0 - for gridView in self.gridViews { - let sourceRect = CGRect(origin: CGPoint(x: gridView.gridPosition.x * (size.width + pixelStep.x), y: gridView.gridPosition.y * (size.height + pixelStep.y)), size: itemSize) - - gridView.bounds = CGRect(origin: CGPoint(), size: sourceRect.size) - gridView.update(containerSize: size, rect: sourceRect, transition: transition) - - let initialTopLeft = CGPoint(x: sourceRect.minX, y: sourceRect.minY) - let initialTopRight = CGPoint(x: sourceRect.maxX, y: sourceRect.minY) - let initialBottomLeft = CGPoint(x: sourceRect.minX, y: sourceRect.maxY) - let initialBottomRight = CGPoint(x: sourceRect.maxX, y: sourceRect.maxY) - - var topLeft = initialTopLeft - var topRight = initialTopRight - var bottomLeft = initialBottomLeft - var bottomRight = initialBottomRight - - if let startPoint = self.startPoint { - topLeft = transformCoordinate(position: topLeft, origin: startPoint, time: self.timeValue, params: params) - topRight = transformCoordinate(position: topRight, origin: startPoint, time: self.timeValue, params: params) - bottomLeft = transformCoordinate(position: bottomLeft, origin: startPoint, time: self.timeValue, params: params) - bottomRight = transformCoordinate(position: bottomRight, origin: startPoint, time: self.timeValue, params: params) - } - - let distanceTopLeft = length(topLeft - initialTopLeft) - let distanceTopRight = length(topRight - initialTopRight) - let distanceBottomLeft = length(bottomLeft - initialBottomLeft) - let distanceBottomRight = length(bottomRight - initialBottomRight) - var maxDistance = max(distanceTopLeft, distanceTopRight) - maxDistance = max(maxDistance, distanceBottomLeft) - maxDistance = max(maxDistance, distanceBottomRight) - - let isActive: Bool - if maxDistance <= 0.5 { - gridView.layer.transform = CATransform3DIdentity - isActive = false - } else { - let transform = rectToQuad(rect: CGRect(origin: CGPoint(), size: itemSize), quadTL: topLeft, quadTR: topRight, quadBL: bottomLeft, quadBR: bottomRight) - gridView.layer.transform = transform - isActive = true - activeViews += 1 - } - if gridView.isHidden != !isActive { - gridView.isHidden = !isActive - gridView.updateIsActive(contentView: self.contentViewImpl, isActive: isActive) - } - } - - if self.currentActiveViews != activeViews { - self.currentActiveViews = activeViews - #if DEBUG - print("SpaceWarpView: activeViews = \(activeViews)") - #endif - } - } - - - override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if self.alpha.isZero || self.isHidden || !self.isUserInteractionEnabled { - return nil - } - for view in self.contentView.subviews.reversed() { - if let result = view.hitTest(self.convert(point, to: view), with: event), result.isUserInteractionEnabled { - return result - } - } - - let result = super.hitTest(point, with: event) - if result != self { - return result - } else { - return nil - } - } -} - -open class SpaceWarpView2: UIView, SpaceWarpView { - public var contentView: UIView { - return self.contentViewImpl - } - - private let contentViewImpl: UIView + private let contentNodeSource: ASDisplayNode + private let backgroundView: UIView + private var currentCloneView: UIView? private var meshView: STCMeshView? + private var gradientLayer: SimpleGradientLayer? + + private var debugLayers: [SimpleLayer] = [] + + #if DEBUG + private var fpsView: FPSView? + #endif + private var link: SharedDisplayLinkDriver.Link? private var startPoint: CGPoint? private var timeValue: CGFloat = 0.0 private var resolution: (x: Int, y: Int)? - private var size: CGSize? + private var layoutParams: (size: CGSize, cornerRadius: CGFloat)? - override public init(frame: CGRect) { - self.contentViewImpl = UIView() + override public init() { + self.contentNodeSource = ASDisplayNode() - super.init(frame: frame) + self.backgroundView = UIView() + self.backgroundView.backgroundColor = .black - self.addSubview(self.contentView) + #if DEBUG && false + self.fpsView = FPSView(frame: CGRect(origin: CGPoint(x: 4.0, y: 40.0), size: CGSize())) + #endif + + super.init() + + self.addSubnode(self.contentNodeSource) + self.view.addSubview(self.backgroundView) + + #if DEBUG + if let fpsView = self.fpsView { + self.view.addSubview(fpsView) + } + #endif } - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - public func trigger(at point: CGPoint) { + public func triggerRipple(at point: CGPoint) { self.startPoint = point self.timeValue = 0.0 @@ -512,8 +321,8 @@ open class SpaceWarpView2: UIView, SpaceWarpView { } self.timeValue += deltaTime * (1.0 / CGFloat(UIView.animationDurationFactor())) - if let size = self.size { - self.update(size: size, transition: .immediate) + if let (size, cornerRadius) = self.layoutParams { + self.update(size: size, cornerRadius: cornerRadius, transition: .immediate) } }) } @@ -528,459 +337,122 @@ open class SpaceWarpView2: UIView, SpaceWarpView { if let meshView = self.meshView { self.meshView = nil meshView.removeFromSuperview() - self.contentViewImpl.removeFromSuperview() } + for debugLayer in self.debugLayers { + debugLayer.removeFromSuperlayer() + } + self.debugLayers.removeAll() let meshView = STCMeshView(frame: CGRect()) self.meshView = meshView - self.addSubview(meshView) + self.view.insertSubview(meshView, aboveSubview: self.backgroundView) meshView.instanceCount = resolutionX * resolutionY - meshView.contentView.addSubview(self.contentViewImpl) - - /*for gridView in self.gridViews { - gridView.removeFromSuperview() - } - - var gridViews: [GridView] = [] - for y in 0 ..< resolutionY { - for x in 0 ..< resolutionX { - if let gridView = GridView(contentView: self.contentViewImpl, gridPosition: CGPoint(x: CGFloat(x) / CGFloat(resolutionX), y: CGFloat(y) / CGFloat(resolutionY))) { - gridView.isUserInteractionEnabled = false - gridViews.append(gridView) - self.addSubview(gridView) - } - } - } - self.gridViews = gridViews*/ + /*for _ in 0 ..< resolutionX * resolutionY { + let debugLayer = SimpleLayer() + debugLayer.backgroundColor = UIColor.red.cgColor + debugLayer.opacity = 1.0 + self.layer.addSublayer(debugLayer) + self.debugLayers.append(debugLayer) + }*/ } - public func update(size: CGSize, transition: ComponentTransition) { - self.size = size + public func update(size: CGSize, cornerRadius: CGFloat, transition: ComponentTransition) { + self.layoutParams = (size, cornerRadius) if size.width <= 0.0 || size.height <= 0.0 { return } - self.updateGrid(resolutionX: max(2, Int(size.width / 100.0)), resolutionY: max(2, Int(size.height / 100.0))) - guard let resolution = self.resolution, let meshView = self.meshView else { - return - } + self.contentNodeSource.frame = CGRect(origin: CGPoint(), size: size) - meshView.frame = CGRect(origin: CGPoint(), size: size) + transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size)) - //let pixelStep = CGPoint(x: CGFloat(resolution.x) * 0.33, y: CGFloat(resolution.y) * 0.33) - let pixelStep = CGPoint() - let itemSize = CGSize(width: size.width / CGFloat(resolution.x), height: size.height / CGFloat(resolution.y)) + let params = RippleParams(amplitude: 26.0, frequency: 15.0, decay: 8.0, speed: 1400.0) - let params = RippleParams(amplitude: 22.0, frequency: 15.0, decay: 8.0, speed: 1400.0) - - var instanceBounds: [CGRect] = [] - var instancePositions: [CGPoint] = [] - var instanceTransforms: [CATransform3D] = [] - - for y in 0 ..< resolution.y { - for x in 0 ..< resolution.x { - let gridPosition = CGPoint(x: CGFloat(x) / CGFloat(resolution.x), y: CGFloat(y) / CGFloat(resolution.y)) - - let sourceRect = CGRect(origin: CGPoint(x: gridPosition.x * (size.width + pixelStep.x), y: gridPosition.y * (size.height + pixelStep.y)), size: itemSize) - - instanceBounds.append(sourceRect) - instancePositions.append(sourceRect.center) - - //gridView.bounds = CGRect(origin: CGPoint(), size: sourceRect.size) - //gridView.update(containerSize: size, rect: sourceRect, transition: transition) - - let initialTopLeft = CGPoint(x: sourceRect.minX, y: sourceRect.minY) - let initialTopRight = CGPoint(x: sourceRect.maxX, y: sourceRect.minY) - let initialBottomLeft = CGPoint(x: sourceRect.minX, y: sourceRect.maxY) - let initialBottomRight = CGPoint(x: sourceRect.maxX, y: sourceRect.maxY) - - var topLeft = initialTopLeft - var topRight = initialTopRight - var bottomLeft = initialBottomLeft - var bottomRight = initialBottomRight - - if let startPoint = self.startPoint { - topLeft = transformCoordinate(position: topLeft, origin: startPoint, time: self.timeValue, params: params) - topRight = transformCoordinate(position: topRight, origin: startPoint, time: self.timeValue, params: params) - bottomLeft = transformCoordinate(position: bottomLeft, origin: startPoint, time: self.timeValue, params: params) - bottomRight = transformCoordinate(position: bottomRight, origin: startPoint, time: self.timeValue, params: params) - } - - let distanceTopLeft = length(topLeft - initialTopLeft) - let distanceTopRight = length(topRight - initialTopRight) - let distanceBottomLeft = length(bottomLeft - initialBottomLeft) - let distanceBottomRight = length(bottomRight - initialBottomRight) - var maxDistance = max(distanceTopLeft, distanceTopRight) - maxDistance = max(maxDistance, distanceBottomLeft) - maxDistance = max(maxDistance, distanceBottomRight) - - let transform = rectToQuad(rect: CGRect(origin: CGPoint(), size: itemSize), quadTL: topLeft - initialTopLeft, quadTR: topRight - initialTopLeft, quadBL: bottomLeft - initialTopLeft, quadBR: bottomRight - initialTopLeft) - instanceTransforms.append(transform) - - let isActive: Bool - if maxDistance <= 0.5 { - //gridView.layer.transform = CATransform3DIdentity - isActive = false - } else { - let _ = transform - //gridView.layer.transform = transform - isActive = true - } - let _ = isActive - } - } - - instanceBounds.withUnsafeMutableBufferPointer { buffer in - meshView.instanceBounds = buffer.baseAddress! - } - instancePositions.withUnsafeMutableBufferPointer { buffer in - meshView.instancePositions = buffer.baseAddress! - } - instanceTransforms.withUnsafeMutableBufferPointer { buffer in - meshView.instanceTransforms = buffer.baseAddress! - } - } - - - override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if self.alpha.isZero || self.isHidden || !self.isUserInteractionEnabled { - return nil - } - for view in self.contentView.subviews.reversed() { - if let result = view.hitTest(self.convert(point, to: view), with: event), result.isUserInteractionEnabled { - return result - } - } - - let result = super.hitTest(point, with: event) - if result != self { - return result - } else { - return nil - } - } -} - -open class SpaceWarpView3: UIView, SpaceWarpView { - private final class GridView: UIView { - let cloneView: PortalView - let gridPosition: CGPoint - - init?(contentView: PortalSourceView, gridPosition: CGPoint) { - self.gridPosition = gridPosition - - guard let cloneView = PortalView(matchPosition: false) else { - return nil - } - self.cloneView = cloneView - - super.init(frame: CGRect()) - - self.layer.anchorPoint = CGPoint(x: 0.0, y: 0.0) - - self.clipsToBounds = true - self.isUserInteractionEnabled = false - self.addSubview(cloneView.view) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func updateIsActive(contentView: PortalSourceView, isActive: Bool) { - if isActive { - contentView.addPortal(view: self.cloneView) - } else { - contentView.removePortal(view: self.cloneView) - } - } - - func update(containerSize: CGSize, rect: CGRect, transition: ComponentTransition) { - transition.setFrame(view: self.cloneView.view, frame: CGRect(origin: CGPoint(x: -rect.minX - containerSize.width * 0.5, y: -rect.minY - containerSize.height * 0.5), size: CGSize(width: containerSize.width, height: containerSize.height))) - } - } - - private var gridViews: [GridView] = [] - - public var contentView: UIView { - return self.contentViewSource - } - - private let contentViewSource: UIView - private var currentCloneView: UIView? - private let contentViewImpl: PortalSourceView - - private var link: SharedDisplayLinkDriver.Link? - private var startPoint: CGPoint? - - private var timeValue: CGFloat = 0.0 - private var currentActiveViews: Int = 0 - - private var resolution: (x: Int, y: Int)? - private var size: CGSize? - - override public init(frame: CGRect) { - self.contentViewSource = UIView() - self.contentViewImpl = PortalSourceView() - - super.init(frame: frame) - - self.addSubview(self.contentViewSource) - self.addSubview(self.contentViewImpl) - - if self.link == nil { - self.link = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] deltaTime in - guard let self else { - return - } - self.timeValue += deltaTime * (1.0 / CGFloat(UIView.animationDurationFactor())) - - if let size = self.size { - self.update(size: size, transition: .immediate) - } - }) - } - } - - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - public func trigger(at point: CGPoint) { - self.startPoint = point - self.timeValue = 0.0 - } - - private func updateGrid(resolutionX: Int, resolutionY: Int) { - if let resolution = self.resolution, resolution.x == resolutionX, resolution.y == resolutionY { - return - } - self.resolution = (resolutionX, resolutionY) - - for gridView in self.gridViews { - gridView.removeFromSuperview() - } - - var gridViews: [GridView] = [] - for y in 0 ..< resolutionY { - for x in 0 ..< resolutionX { - if let gridView = GridView(contentView: self.contentViewImpl, gridPosition: CGPoint(x: CGFloat(x) / CGFloat(resolutionX), y: CGFloat(y) / CGFloat(resolutionY))) { - gridView.isUserInteractionEnabled = false - gridView.isHidden = true - gridViews.append(gridView) - self.addSubview(gridView) - } - } - } - self.gridViews = gridViews - } - - public func update(size: CGSize, transition: ComponentTransition) { if let currentCloneView = self.currentCloneView { currentCloneView.removeFromSuperview() self.currentCloneView = nil } - if let cloneView = self.contentViewSource.resizableSnapshotView(from: CGRect(origin: CGPoint(), size: size), afterScreenUpdates: false, withCapInsets: UIEdgeInsets()) { - self.currentCloneView = cloneView - self.contentViewImpl.addSubview(cloneView) - } - self.size = size - if size.width <= 0.0 || size.height <= 0.0 { + let maxEdge = (max(size.width, size.height) * 0.5) * 2.0 + let maxDistance = sqrt(maxEdge * maxEdge + maxEdge * maxEdge) + + let maxDelay = maxDistance / params.speed + guard let startPoint = self.startPoint, self.timeValue < maxDelay else { + if let link = self.link { + self.link = nil + link.invalidate() + } + + if let meshView = self.meshView { + self.meshView = nil + meshView.removeFromSuperview() + } + + for debugLayer in self.debugLayers { + debugLayer.removeFromSuperlayer() + } + self.debugLayers.removeAll() + + self.resolution = nil + self.startPoint = nil + self.backgroundView.isHidden = true + self.contentNodeSource.clipsToBounds = false + self.contentNodeSource.layer.cornerRadius = 0.0 + + if let gradientLayer = self.gradientLayer { + self.gradientLayer = nil + gradientLayer.removeFromSuperlayer() + } + return } - self.updateGrid(resolutionX: max(2, Int(size.width / 50.0)), resolutionY: max(2, Int(size.height / 50.0))) - guard let resolution = self.resolution else { - return - } + self.backgroundView.isHidden = false + self.contentNodeSource.clipsToBounds = true + self.contentNodeSource.layer.cornerRadius = cornerRadius - if self.timeValue >= 3.0 { - return - } - - let pixelStep = CGPoint() - let itemSize = CGSize(width: size.width / CGFloat(resolution.x), height: size.height / CGFloat(resolution.y)) - - let params = RippleParams(amplitude: 22.0, frequency: 15.0, decay: 8.0, speed: 1400.0) - - var activeViews = 0 - for gridView in self.gridViews { - let sourceRect = CGRect(origin: CGPoint(x: gridView.gridPosition.x * (size.width + pixelStep.x), y: gridView.gridPosition.y * (size.height + pixelStep.y)), size: itemSize) - - gridView.bounds = CGRect(origin: CGPoint(), size: sourceRect.size) - gridView.update(containerSize: size, rect: sourceRect, transition: transition) - - let initialTopLeft = CGPoint(x: sourceRect.minX, y: sourceRect.minY) - let initialTopRight = CGPoint(x: sourceRect.maxX, y: sourceRect.minY) - let initialBottomLeft = CGPoint(x: sourceRect.minX, y: sourceRect.maxY) - let initialBottomRight = CGPoint(x: sourceRect.maxX, y: sourceRect.maxY) - - var topLeft = initialTopLeft - var topRight = initialTopRight - var bottomLeft = initialBottomLeft - var bottomRight = initialBottomRight - - if let startPoint = self.startPoint { - topLeft = transformCoordinate(position: topLeft, origin: startPoint, time: self.timeValue, params: params) - topRight = transformCoordinate(position: topRight, origin: startPoint, time: self.timeValue, params: params) - bottomLeft = transformCoordinate(position: bottomLeft, origin: startPoint, time: self.timeValue, params: params) - bottomRight = transformCoordinate(position: bottomRight, origin: startPoint, time: self.timeValue, params: params) - } - - let distanceTopLeft = length(topLeft - initialTopLeft) - let distanceTopRight = length(topRight - initialTopRight) - let distanceBottomLeft = length(bottomLeft - initialBottomLeft) - let distanceBottomRight = length(bottomRight - initialBottomRight) - var maxDistance = max(distanceTopLeft, distanceTopRight) - maxDistance = max(maxDistance, distanceBottomLeft) - maxDistance = max(maxDistance, distanceBottomRight) - - let isActive: Bool - if maxDistance <= 0.5 { - gridView.layer.transform = CATransform3DIdentity - isActive = true - activeViews += 1 - } else { - let transform = rectToQuad(rect: CGRect(origin: CGPoint(), size: itemSize), quadTL: topLeft, quadTR: topRight, quadBL: bottomLeft, quadBR: bottomRight) - gridView.layer.transform = transform - isActive = true - activeViews += 1 - } - if gridView.isHidden != !isActive { - gridView.isHidden = !isActive - gridView.updateIsActive(contentView: self.contentViewImpl, isActive: isActive) - } - } - - if self.currentActiveViews != activeViews { - self.currentActiveViews = activeViews - #if DEBUG - print("SpaceWarpView: activeViews = \(activeViews)") - #endif - } - } - - - override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if self.alpha.isZero || self.isHidden || !self.isUserInteractionEnabled { - return nil - } - for view in self.contentView.subviews.reversed() { - if let result = view.hitTest(self.convert(point, to: view), with: event), result.isUserInteractionEnabled { - return result - } - } - - let result = super.hitTest(point, with: event) - if result != self { - return result + /*let gradientLayer: SimpleGradientLayer + if let current = self.gradientLayer { + gradientLayer = current } else { - return nil + gradientLayer = SimpleGradientLayer() + self.gradientLayer = gradientLayer + self.layer.addSublayer(gradientLayer) + + gradientLayer.type = .radial + gradientLayer.colors = [UIColor.clear.cgColor, UIColor.clear.cgColor, UIColor.white.cgColor, UIColor.clear.cgColor, UIColor.clear.cgColor] } - } -} - -open class SpaceWarpView4: UIView, SpaceWarpView { - public var contentView: UIView { - return self.contentViewSource - } - - private let contentViewSource: UIView - private var currentCloneView: UIView? - private var meshView: STCMeshView? - private let fpsView: FPSView - - private var link: SharedDisplayLinkDriver.Link? - private var startPoint: CGPoint? - - private var timeValue: CGFloat = 0.0 - - private var resolution: (x: Int, y: Int)? - private var size: CGSize? - - override public init(frame: CGRect) { - self.contentViewSource = UIView() - self.fpsView = FPSView(frame: CGRect(origin: CGPoint(x: 4.0, y: 40.0), size: CGSize())) + gradientLayer.frame = CGRect(origin: CGPoint(), size: size) - super.init(frame: frame) + gradientLayer.startPoint = CGPoint(x: startPoint.x / size.width, y: startPoint.x / size.height) + let radius = CGSize(width: maxEdge, height: maxEdge) + let endEndPoint = CGPoint(x: (gradientLayer.startPoint.x + radius.width) * 1.0, y: (gradientLayer.startPoint.y + radius.height) * 1.0) + gradientLayer.endPoint = endEndPoint - self.addSubview(self.contentViewSource) - self.addSubview(self.fpsView) + let progress = max(0.0, min(1.0, self.timeValue / maxDelay))*/ - if self.link == nil { - self.link = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] deltaTime in - guard let self else { - return - } - self.timeValue += deltaTime * (1.0 / CGFloat(UIView.animationDurationFactor())) - - if let size = self.size { - self.update(size: size, transition: .immediate) - } - }) + #if DEBUG + if let fpsView = self.fpsView { + fpsView.update() } - } - - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - public func trigger(at point: CGPoint) { - self.startPoint = point - self.timeValue = 0.0 - } - - private func updateGrid(resolutionX: Int, resolutionY: Int) { - if let resolution = self.resolution, resolution.x == resolutionX, resolution.y == resolutionY { - return - } - self.resolution = (resolutionX, resolutionY) - - if let meshView = self.meshView { - self.meshView = nil - meshView.removeFromSuperview() - } - - let meshView = STCMeshView(frame: CGRect()) - self.meshView = meshView - self.insertSubview(meshView, aboveSubview: self.contentViewSource) - - meshView.instanceCount = resolutionX * resolutionY - } - - public func update(size: CGSize, transition: ComponentTransition) { - self.size = size - if size.width <= 0.0 || size.height <= 0.0 { - return - } - - self.fpsView.update() + #endif self.updateGrid(resolutionX: max(2, Int(size.width / 40.0)), resolutionY: max(2, Int(size.height / 40.0))) guard let resolution = self.resolution, let meshView = self.meshView else { return } - if let currentCloneView = self.currentCloneView { - currentCloneView.removeFromSuperview() - self.currentCloneView = nil - } - if let cloneView = self.contentViewSource.resizableSnapshotView(from: CGRect(origin: CGPoint(), size: size), afterScreenUpdates: false, withCapInsets: UIEdgeInsets()) { + if let cloneView = self.contentNodeSource.view.resizableSnapshotView(from: CGRect(origin: CGPoint(), size: size), afterScreenUpdates: false, withCapInsets: UIEdgeInsets()) { self.currentCloneView = cloneView meshView.contentView.addSubview(cloneView) } meshView.frame = CGRect(origin: CGPoint(), size: size) - let pixelStep = CGPoint() - //let pixelStep = CGPoint(x: CGFloat(resolution.x) * 0.33, y: CGFloat(resolution.y) * 0.33) let itemSize = CGSize(width: size.width / CGFloat(resolution.x), height: size.height / CGFloat(resolution.y)) - let params = RippleParams(amplitude: 26.0, frequency: 15.0, decay: 8.0, speed: 1400.0) - var instanceBounds: [CGRect] = [] var instancePositions: [CGPoint] = [] var instanceTransforms: [CATransform3D] = [] @@ -989,10 +461,7 @@ open class SpaceWarpView4: UIView, SpaceWarpView { for x in 0 ..< resolution.x { let gridPosition = CGPoint(x: CGFloat(x) / CGFloat(resolution.x), y: CGFloat(y) / CGFloat(resolution.y)) - let sourceRect = CGRect(origin: CGPoint(x: gridPosition.x * (size.width + pixelStep.x), y: gridPosition.y * (size.height + pixelStep.y)), size: itemSize) - - instanceBounds.append(sourceRect) - instancePositions.append(sourceRect.center) + let sourceRect = CGRect(origin: CGPoint(x: gridPosition.x * (size.width), y: gridPosition.y * (size.height)), size: itemSize) let initialTopLeft = CGPoint(x: sourceRect.minX, y: sourceRect.minY) let initialTopRight = CGPoint(x: sourceRect.maxX, y: sourceRect.minY) @@ -1004,12 +473,10 @@ open class SpaceWarpView4: UIView, SpaceWarpView { var bottomLeft = initialBottomLeft var bottomRight = initialBottomRight - if let startPoint = self.startPoint { - topLeft = transformCoordinate(position: topLeft, origin: startPoint, time: self.timeValue, params: params) - topRight = transformCoordinate(position: topRight, origin: startPoint, time: self.timeValue, params: params) - bottomLeft = transformCoordinate(position: bottomLeft, origin: startPoint, time: self.timeValue, params: params) - bottomRight = transformCoordinate(position: bottomRight, origin: startPoint, time: self.timeValue, params: params) - } + topLeft = transformCoordinate(position: topLeft, origin: startPoint, time: self.timeValue, params: params) + topRight = transformCoordinate(position: topRight, origin: startPoint, time: self.timeValue, params: params) + bottomLeft = transformCoordinate(position: bottomLeft, origin: startPoint, time: self.timeValue, params: params) + bottomRight = transformCoordinate(position: bottomRight, origin: startPoint, time: self.timeValue, params: params) let distanceTopLeft = length(topLeft - initialTopLeft) let distanceTopRight = length(topRight - initialTopRight) @@ -1019,19 +486,16 @@ open class SpaceWarpView4: UIView, SpaceWarpView { maxDistance = max(maxDistance, distanceBottomLeft) maxDistance = max(maxDistance, distanceBottomRight) - let transform = transformToFitQuad(frame: sourceRect, topLeft: topLeft, topRight: topRight, bottomLeft: bottomLeft, bottomRight: bottomRight) - instanceTransforms.append(transform) + var (frame, transform) = transformToFitQuad2(frame: sourceRect, topLeft: topLeft, topRight: topRight, bottomLeft: bottomLeft, bottomRight: bottomRight) - let isActive: Bool - if maxDistance <= 0.5 { - //gridView.layer.transform = CATransform3DIdentity - isActive = false - } else { - let _ = transform - //gridView.layer.transform = transform - isActive = true + if maxDistance <= 0.005 { + transform = CATransform3DIdentity } - let _ = isActive + + instanceBounds.append(frame) + instancePositions.append(frame.center) + + instanceTransforms.append(transform) } } @@ -1044,15 +508,20 @@ open class SpaceWarpView4: UIView, SpaceWarpView { instanceTransforms.withUnsafeMutableBufferPointer { buffer in meshView.instanceTransforms = buffer.baseAddress! } + + for i in 0 ..< self.debugLayers.count { + self.debugLayers[i].bounds = instanceBounds[i] + self.debugLayers[i].position = instancePositions[i] + self.debugLayers[i].transform = instanceTransforms[i] + } } - - override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + override open func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if self.alpha.isZero || self.isHidden || !self.isUserInteractionEnabled { return nil } - for view in self.contentView.subviews.reversed() { - if let result = view.hitTest(self.convert(point, to: view), with: event), result.isUserInteractionEnabled { + for view in self.contentNode.view.subviews.reversed() { + if let result = view.hitTest(self.view.convert(point, to: view), with: event), result.isUserInteractionEnabled { return result } } diff --git a/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift b/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift index 4f47e53ca3..dc2815c8c0 100644 --- a/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift +++ b/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift @@ -113,6 +113,7 @@ public final class TabSelectorComponent: Component { self.canCancelContentTouches = true self.contentInsetAdjustmentBehavior = .never self.alwaysBounceVertical = false + self.clipsToBounds = false self.addSubview(self.selectionView) } diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 6619f4d6cc..079ddffb4c 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -88,38 +88,12 @@ private struct ChatControllerNodeDerivedLayoutState { } class ChatNodeContainer: ASDisplayNode { - private let contentNodeImpl: ASDisplayNode - var contentNode: ASDisplayNode { - if self.view is SpaceWarpView { - return self.contentNodeImpl - } else { - return self - } + return self } - init(rippleEffect: Bool) { - self.contentNodeImpl = ASDisplayNode() - + override init() { super.init() - - if rippleEffect { - self.setViewBlock({ - return SpaceWarpView4(frame: CGRect()) - }) - self.contentNodeImpl.layer.allowsGroupOpacity = true - } - - (self.view as? SpaceWarpView)?.contentView.addSubnode(self.contentNodeImpl) - } - - func triggerRipple(at point: CGPoint) { - (self.view as? SpaceWarpView)?.trigger(at: point) - } - - func update(size: CGSize, transition: ContainedViewLayoutTransition) { - transition.updateFrame(node: self.contentNodeImpl, frame: CGRect(origin: CGPoint(), size: size)) - (self.view as? SpaceWarpView)?.update(size: size, transition: ComponentTransition(transition)) } } @@ -132,19 +106,11 @@ class HistoryNodeContainer: ASDisplayNode { } } - private let contentNodeImpl: ASDisplayNode - var contentNode: ASDisplayNode { - if self.view is SpaceWarpView { - return self.contentNodeImpl - } else { - return self - } + return self } init(isSecret: Bool) { - self.contentNodeImpl = ASDisplayNode() - self.isSecret = isSecret super.init() @@ -152,23 +118,6 @@ class HistoryNodeContainer: ASDisplayNode { if self.isSecret { setLayerDisableScreenshots(self.layer, self.isSecret) } - - #if DEBUG && false - self.setViewBlock({ - return SpaceWarpView1(frame: CGRect()) - }) - #endif - - (self.view as? SpaceWarpView)?.contentView.addSubnode(self.contentNodeImpl) - } - - func triggerRipple(at point: CGPoint) { - (self.view as? SpaceWarpView)?.trigger(at: point) - } - - func update(size: CGSize, transition: ContainedViewLayoutTransition) { - transition.updateFrame(node: self.contentNodeImpl, frame: CGRect(origin: CGPoint(), size: size)) - (self.view as? SpaceWarpView)?.update(size: size, transition: ComponentTransition(transition)) } } @@ -193,6 +142,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } } + let wrappingNode: SpaceWarpNode let contentContainerNode: ChatNodeContainer let contentDimNode: ASDisplayNode let backgroundNode: WallpaperBackgroundNode @@ -445,7 +395,9 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { self.backgroundNode = backgroundNode - self.contentContainerNode = ChatNodeContainer(rippleEffect: context.sharedContext.immediateExperimentalUISettings.rippleEffect) + self.wrappingNode = SpaceWarpNodeImpl() + + self.contentContainerNode = ChatNodeContainer() self.contentDimNode = ASDisplayNode() self.contentDimNode.isUserInteractionEnabled = false self.contentDimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.2) @@ -852,7 +804,8 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } self.historyNode.enableExtractedBackgrounds = true - self.addSubnode(self.contentContainerNode) + self.addSubnode(self.wrappingNode) + self.wrappingNode.contentNode.addSubnode(self.contentContainerNode) self.contentContainerNode.contentNode.addSubnode(self.backgroundNode) self.contentContainerNode.contentNode.addSubnode(self.historyNodeContainer) @@ -872,9 +825,9 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } } - self.addSubnode(self.inputContextPanelContainer) - self.addSubnode(self.inputPanelContainerNode) - self.addSubnode(self.inputContextOverTextPanelContainer) + self.wrappingNode.contentNode.addSubnode(self.inputContextPanelContainer) + self.wrappingNode.contentNode.addSubnode(self.inputPanelContainerNode) + self.wrappingNode.contentNode.addSubnode(self.inputContextOverTextPanelContainer) self.inputPanelContainerNode.addSubnode(self.inputPanelClippingNode) self.inputPanelContainerNode.addSubnode(self.inputPanelOverlayNode) @@ -882,9 +835,9 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { self.inputPanelClippingNode.addSubnode(self.inputPanelBackgroundSeparatorNode) self.inputPanelBackgroundNode.addSubnode(self.inputPanelBottomBackgroundSeparatorNode) - self.addSubnode(self.messageTransitionNode) + self.wrappingNode.contentNode.addSubnode(self.messageTransitionNode) self.contentContainerNode.contentNode.addSubnode(self.navigateButtons) - self.addSubnode(self.presentationContextMarker) + self.wrappingNode.contentNode.addSubnode(self.presentationContextMarker) self.contentContainerNode.contentNode.addSubnode(self.contentDimNode) self.navigationBar?.additionalContentNode.addSubnode(self.titleAccessoryPanelContainer) @@ -1131,6 +1084,9 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { transition = protoTransition } + transition.updateFrame(node: self.wrappingNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + self.wrappingNode.update(size: layout.size, cornerRadius: layout.deviceMetrics.screenCornerRadius, transition: ComponentTransition(transition)) + if let statusBar = self.statusBar { switch self.chatPresentationInterfaceState.mode { case .standard: @@ -1159,9 +1115,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } self.messageTransitionNode.frame = CGRect(origin: CGPoint(), size: layout.size) - self.contentContainerNode.frame = CGRect(origin: CGPoint(), size: layout.size) - self.contentContainerNode.update(size: layout.size, transition: transition) let isOverlay: Bool switch self.chatPresentationInterfaceState.mode { @@ -1186,7 +1140,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { animateFromFraction = 1.0 navigationModalFrame = NavigationModalFrame() self.navigationModalFrame = navigationModalFrame - self.insertSubnode(navigationModalFrame, aboveSubnode: self.contentContainerNode) + self.wrappingNode.contentNode.insertSubnode(navigationModalFrame, aboveSubnode: self.contentContainerNode) } if transition.isAnimated, let animateFromFraction = animateFromFraction, animateFromFraction != 1.0 - self.inputPanelContainerNode.expansionFraction { navigationModalFrame.update(layout: layout, transition: .immediate) @@ -1240,7 +1194,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { if self.backgroundEffectNode == nil { let backgroundEffectNode = ASDisplayNode() backgroundEffectNode.backgroundColor = self.chatPresentationInterfaceState.theme.chatList.backgroundColor.withAlphaComponent(0.8) - self.insertSubnode(backgroundEffectNode, at: 0) + self.wrappingNode.contentNode.insertSubnode(backgroundEffectNode, at: 0) self.backgroundEffectNode = backgroundEffectNode backgroundEffectNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.backgroundEffectTap(_:)))) } @@ -1252,7 +1206,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { scrollContainerNode.view.contentInsetAdjustmentBehavior = .never } - self.insertSubnode(scrollContainerNode, aboveSubnode: self.backgroundEffectNode!) + self.wrappingNode.contentNode.insertSubnode(scrollContainerNode, aboveSubnode: self.backgroundEffectNode!) self.scrollContainerNode = scrollContainerNode } if self.containerBackgroundNode == nil { @@ -1797,7 +1751,6 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { transition.updateBounds(node: self.historyNodeContainer, bounds: contentBounds) transition.updatePosition(node: self.historyNodeContainer, position: contentBounds.center) - self.historyNodeContainer.update(size: contentBounds.size, transition: transition) transition.updateBounds(node: self.historyNode, bounds: CGRect(origin: CGPoint(), size: contentBounds.size)) transition.updatePosition(node: self.historyNode, position: CGPoint(x: contentBounds.size.width / 2.0, y: contentBounds.size.height / 2.0)) @@ -3345,8 +3298,9 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } if let view, let location { - self.contentContainerNode.triggerRipple(at: self.contentContainerNode.view.convert(location, from: view)) - self.historyNodeContainer.triggerRipple(at: self.historyNodeContainer.view.convert(location, from: view)) + if context.sharedContext.immediateExperimentalUISettings.rippleEffect { + self.wrappingNode.triggerRipple(at: self.contentContainerNode.view.convert(location, from: view)) + } } switch self.chatPresentationInterfaceState.inputMode { From b7d572e0352d8440c5c07c069b00eaf82c696f80 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 26 Jul 2024 13:43:42 +0200 Subject: [PATCH 24/41] Various fixes --- .../Telegram-iOS/en.lproj/Localizable.strings | 1 + .../Sources/BrowserAddressListComponent.swift | 13 +++- .../BrowserAddressListItemComponent.swift | 1 + .../Sources/BrowserBookmarksScreen.swift | 67 ++++++++++++++++++- .../BrowserNavigationBarComponent.swift | 8 ++- .../BrowserUI/Sources/BrowserScreen.swift | 36 +++++----- .../Sources/ListMessageSnippetItemNode.swift | 24 +++++-- 7 files changed, 122 insertions(+), 28 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 2dccba4ca3..aa70fb4365 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -12653,5 +12653,6 @@ Sorry for the inconvenience."; "Story.Cover" = "Story Cover"; "Story.SaveCover" = "Save Cover"; +"WebBrowser.CopyLink" = "Copy Link"; "WebBrowser.DeleteBookmark" = "Delete Bookmark"; "WebBrowser.RemoveRecent" = "Remove from Recent"; diff --git a/submodules/BrowserUI/Sources/BrowserAddressListComponent.swift b/submodules/BrowserUI/Sources/BrowserAddressListComponent.swift index b22bb333e9..e17589cd96 100644 --- a/submodules/BrowserUI/Sources/BrowserAddressListComponent.swift +++ b/submodules/BrowserUI/Sources/BrowserAddressListComponent.swift @@ -9,6 +9,7 @@ import TelegramCore import AccountContext import TelegramPresentationData import ContextUI +import UndoUI final class BrowserAddressListComponent: Component { let context: AccountContext @@ -336,13 +337,23 @@ final class BrowserAddressListComponent: Component { } }, contextAction: { [weak self] webPage, message, sourceView, gesture in - guard let self, let component = self.component else { + guard let self, let component = self.component, let url = webPage.content.url else { return } let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } var itemList: [ContextMenuItem] = [] + itemList.append(.action(ContextMenuActionItem(text: presentationData.strings.WebBrowser_CopyLink, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, f in + f(.default) + + UIPasteboard.general.string = url + if let self, let component = self.component { + component.presentInGlobalOverlay(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false })) + } + }))) if let message { itemList.append(.action(ContextMenuActionItem(text: presentationData.strings.WebBrowser_DeleteBookmark, textColor: .destructive, icon: { theme in diff --git a/submodules/BrowserUI/Sources/BrowserAddressListItemComponent.swift b/submodules/BrowserUI/Sources/BrowserAddressListItemComponent.swift index 97079f1686..05ff000d7d 100644 --- a/submodules/BrowserUI/Sources/BrowserAddressListItemComponent.swift +++ b/submodules/BrowserUI/Sources/BrowserAddressListItemComponent.swift @@ -186,6 +186,7 @@ final class BrowserAddressListItemComponent: Component { } address = address.replacingOccurrences(of: "https://www.", with: "") address = address.replacingOccurrences(of: "https://", with: "") + address = address.replacingOccurrences(of: "tonsite://", with: "") address = address.trimmingCharacters(in: CharacterSet(charactersIn: "/")) subtitle = address diff --git a/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift b/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift index a3bcafe42c..64ec20912b 100644 --- a/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift +++ b/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift @@ -16,6 +16,8 @@ import UrlWhitelist import SearchUI import SearchBarNode import ChatHistorySearchContainerNode +import ContextUI +import UndoUI public final class BrowserBookmarksScreen: ViewController { final class Node: ViewControllerTracingNode, ASScrollViewDelegate { @@ -39,6 +41,7 @@ public final class BrowserBookmarksScreen: ViewController { self.presentationData = presentationData var openMessageImpl: ((Message) -> Bool)? + var openContextMenuImpl: ((Message, ASDisplayNode, CGRect, UIGestureRecognizer?) -> Void)? self.controllerInteraction = ChatControllerInteraction(openMessage: { message, _ in if let openMessageImpl = openMessageImpl { return openMessageImpl(message) @@ -47,7 +50,8 @@ public final class BrowserBookmarksScreen: ViewController { } }, openPeer: { _, _, _, _ in }, openPeerMention: { _, _ in - }, openMessageContextMenu: { _, _, _, _, _, _ in + }, openMessageContextMenu: { message, _, sourceView, rect, gesture, _ in + openContextMenuImpl?(message, sourceView, rect, gesture) }, openMessageReactionContextMenu: { _, _, _, _ in }, updateMessageReaction: { _, _, _, _ in }, activateMessagePinch: { _ in @@ -220,6 +224,47 @@ public final class BrowserBookmarksScreen: ViewController { self.containerLayoutUpdated(layout: layout, navigationBarHeight: navigationBarHeight, actualNavigationBarHeight: actualNavigationBarHeight, transition: .animated(duration: 0.4, curve: .spring)) } } + + openContextMenuImpl = { [weak self] message, sourceNode, rect, gesture in + guard let self, let sourceNode = sourceNode as? ContextExtractedContentContainingNode else { + return + } + + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + + var itemList: [ContextMenuItem] = [] + if let webPage = message.media.first(where: { $0 is TelegramMediaWebpage }) as? TelegramMediaWebpage, let url = webPage.content.url { + itemList.append(.action(ContextMenuActionItem(text: presentationData.strings.WebBrowser_CopyLink, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, f in + f(.default) + + UIPasteboard.general.string = url + if let self { + self.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) + } + }))) + } + itemList.append(.action(ContextMenuActionItem(text: presentationData.strings.WebBrowser_DeleteBookmark, textColor: .destructive, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) + }, action: { [weak self] _, f in + f(.dismissWithoutContent) + + if let self { + let _ = self.context.engine.messages.deleteMessagesInteractively(messageIds: [message.id], type: .forEveryone).startStandalone() + } + }))) + + let items = ContextController.Items(content: .list(itemList)) + let controller = ContextController( + presentationData: presentationData, + source: .extracted(BrowserBookmarksContextExtractedContentSource(contentNode: sourceNode)), + items: .single(items), + recognizer: nil, + gesture: gesture as? ContextGesture + ) + self.controller?.presentInGlobalOverlay(controller) + } } func activateSearch(placeholderNode: SearchBarPlaceholderNode) { @@ -515,3 +560,23 @@ private class BottomPanelNode: ASDisplayNode { } } + +final class BrowserBookmarksContextExtractedContentSource: ContextExtractedContentSource { + let keepInPlace: Bool = false + let ignoreContentTouches: Bool = false + let blurBackground: Bool = true + + private let contentNode: ContextExtractedContentContainingNode + + init(contentNode: ContextExtractedContentContainingNode) { + self.contentNode = contentNode + } + + func takeView() -> ContextControllerTakeViewInfo? { + return ContextControllerTakeViewInfo(containingItem: .node(self.contentNode), contentAreaInScreenSpace: UIScreen.main.bounds) + } + + func putBack() -> ContextControllerPutBackViewInfo? { + return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds) + } +} diff --git a/submodules/BrowserUI/Sources/BrowserNavigationBarComponent.swift b/submodules/BrowserUI/Sources/BrowserNavigationBarComponent.swift index ab0d7066ce..89e0387e3d 100644 --- a/submodules/BrowserUI/Sources/BrowserNavigationBarComponent.swift +++ b/submodules/BrowserUI/Sources/BrowserNavigationBarComponent.swift @@ -261,6 +261,8 @@ final class BrowserNavigationBarComponent: CombinedComponent { availableWidth -= itemSpacing * CGFloat(max(0, leftItemList.count - 1)) + itemSpacing * CGFloat(max(0, rightItemList.count - 1)) + 30.0 } availableWidth -= context.component.sideInset * 2.0 + + let canCenter = availableWidth > 660.0 availableWidth = min(660.0, availableWidth) let environment = BrowserNavigationBarEnvironment(fraction: context.component.collapseFraction) @@ -276,7 +278,11 @@ final class BrowserNavigationBarComponent: CombinedComponent { var centerX = maxCenterInset + (context.availableSize.width - maxCenterInset * 2.0) / 2.0 if "".isEmpty { - centerX = centerLeftInset + (context.availableSize.width - centerLeftInset - centerRightInset) / 2.0 + if canCenter { + centerX = context.availableSize.width / 2.0 + } else { + centerX = centerLeftInset + (context.availableSize.width - centerLeftInset - centerRightInset) / 2.0 + } } if let centerItem = centerItem { let centerItemPosition = CGPoint(x: centerX, y: context.component.topInset + contentHeight / 2.0 + verticalOffset) diff --git a/submodules/BrowserUI/Sources/BrowserScreen.swift b/submodules/BrowserUI/Sources/BrowserScreen.swift index ebd10a37e0..c1d0d9ff8a 100644 --- a/submodules/BrowserUI/Sources/BrowserScreen.swift +++ b/submodules/BrowserUI/Sources/BrowserScreen.swift @@ -153,24 +153,24 @@ private final class BrowserScreenComponent: CombinedComponent { ] if isTablet { - navigationLeftItems.append( - AnyComponentWithIdentity( - id: "minimize", - component: AnyComponent( - Button( - content: AnyComponent( - BundleIconComponent( - name: "Media Gallery/PictureInPictureButton", - tintColor: environment.theme.rootController.navigationBar.accentTextColor - ) - ), - action: { - performAction.invoke(.close) - } - ) - ) - ) - ) +// navigationLeftItems.append( +// AnyComponentWithIdentity( +// id: "minimize", +// component: AnyComponent( +// Button( +// content: AnyComponent( +// BundleIconComponent( +// name: "Media Gallery/PictureInPictureButton", +// tintColor: environment.theme.rootController.navigationBar.accentTextColor +// ) +// ), +// action: { +// performAction.invoke(.close) +// } +// ) +// ) +// ) +// ) let canGoBack = context.component.contentState?.canGoBack ?? false let canGoForward = context.component.contentState?.canGoForward ?? false diff --git a/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift b/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift index d0308f9d5d..de53829137 100644 --- a/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift +++ b/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift @@ -348,7 +348,7 @@ public final class ListMessageSnippetItemNode: ListMessageNode { } address = address.trimmingCharacters(in: CharacterSet(charactersIn: "/")) - let plainUrlString = NSAttributedString(string: address.replacingOccurrences(of: "https://", with: ""), font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemAccentColor) + let plainUrlString = NSAttributedString(string: address.replacingOccurrences(of: "https://", with: "").replacingOccurrences(of: "tonsite://", with: ""), font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemAccentColor) let urlString = NSMutableAttributedString() urlString.append(plainUrlString) urlString.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.URL), value: content.url, range: NSMakeRange(0, urlString.length)) @@ -412,8 +412,13 @@ public final class ListMessageSnippetItemNode: ListMessageNode { let rawUrlString = urlString var parsedUrl = URL(string: urlString) if (parsedUrl == nil || parsedUrl!.host == nil || parsedUrl!.host!.isEmpty) && !urlString.contains("@") { - urlString = "http://" + urlString - parsedUrl = URL(string: urlString) + if let mappedURL = URL(string: "https://\(urlString)"), let host = mappedURL.host, host.lowercased().hasSuffix(".ton") { + urlString = "tonsite://" + urlString + parsedUrl = URL(string: urlString) + } else { + urlString = "http://" + urlString + parsedUrl = URL(string: urlString) + } } var host: String? = concealed ? urlString : parsedUrl?.host if host == nil { @@ -463,7 +468,7 @@ public final class ListMessageSnippetItemNode: ListMessageNode { urlString = address let urlAttributedString = NSMutableAttributedString() - urlAttributedString.append(NSAttributedString(string: urlString.replacingOccurrences(of: "https://", with: ""), font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemAccentColor)) + urlAttributedString.append(NSAttributedString(string: urlString.replacingOccurrences(of: "https://", with: "").replacingOccurrences(of: "tonsite://", with: ""), font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemAccentColor)) if item.presentationData.theme.theme.list.itemAccentColor.isEqual(item.presentationData.theme.theme.list.itemPrimaryTextColor) { urlAttributedString.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: NSMakeRange(0, urlAttributedString.length)) } @@ -495,8 +500,13 @@ public final class ListMessageSnippetItemNode: ListMessageNode { let rawUrlString = urlString var parsedUrl = URL(string: urlString) if (parsedUrl == nil || parsedUrl!.host == nil || parsedUrl!.host!.isEmpty) && !urlString.contains("@") { - urlString = "http://" + urlString - parsedUrl = URL(string: urlString) + if let mappedURL = URL(string: "https://\(urlString)"), let host = mappedURL.host, host.lowercased().hasSuffix(".ton") { + urlString = "tonsite://" + urlString + parsedUrl = URL(string: urlString) + } else { + urlString = "http://" + urlString + parsedUrl = URL(string: urlString) + } } let host: String? = concealed ? urlString : parsedUrl?.host if let url = parsedUrl, let host = host { @@ -533,7 +543,7 @@ public final class ListMessageSnippetItemNode: ListMessageNode { urlString = address let urlAttributedString = NSMutableAttributedString() - urlAttributedString.append(NSAttributedString(string: urlString.replacingOccurrences(of: "https://", with: ""), font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemAccentColor)) + urlAttributedString.append(NSAttributedString(string: urlString.replacingOccurrences(of: "https://", with: "").replacingOccurrences(of: "tonsite://", with: ""), font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemAccentColor)) if item.presentationData.theme.theme.list.itemAccentColor.isEqual(item.presentationData.theme.theme.list.itemPrimaryTextColor) { urlAttributedString.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: NSMakeRange(0, urlAttributedString.length)) } From 65d4417101b4a3c695fec963b2e66676717b2ef6 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Fri, 26 Jul 2024 20:08:08 +0800 Subject: [PATCH 25/41] Adjust ripple --- .../STCMeshView/Sources/STCMeshLayer.m | 2 +- .../SpaceWarpView/Sources/SpaceWarpView.swift | 23 +++++-------------- 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/submodules/TelegramUI/Components/SpaceWarpView/STCMeshView/Sources/STCMeshLayer.m b/submodules/TelegramUI/Components/SpaceWarpView/STCMeshView/Sources/STCMeshLayer.m index b520dae06b..5d27431f1f 100644 --- a/submodules/TelegramUI/Components/SpaceWarpView/STCMeshView/Sources/STCMeshLayer.m +++ b/submodules/TelegramUI/Components/SpaceWarpView/STCMeshView/Sources/STCMeshLayer.m @@ -254,7 +254,7 @@ static NSString *const STCMeshLayerInstanceDelayAnimationKey = @"STCMeshLayerIns - (CGPoint)_anchorPointAtIndex:(NSUInteger)index { - CGPoint anchorPoint = CGPointMake(0.5, 0.5); + CGPoint anchorPoint = CGPointMake(0.0, 0.0); if (_instanceAnchorPoints != NULL) { anchorPoint = _instanceAnchorPoints[index]; diff --git a/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift b/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift index 380a56f2c6..b697665450 100644 --- a/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift +++ b/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift @@ -122,6 +122,10 @@ private func transformCoordinate( } else { rippleAmount = absRippleAmount } + + if distance <= 40.0 { + rippleAmount *= 0.5 + } // A vector of length `amplitude` that points away from position. let n: CGPoint @@ -147,7 +151,7 @@ func transformToFitQuad2(frame: CGRect, topLeft tl: CGPoint, topRight tr: CGPoin quadBR: CGPoint(x: br.x - frameTopLeft.x, y: br.y - frameTopLeft.y) ) - let anchorPoint = frame.center + let anchorPoint = frame.origin let anchorOffset = CGPoint(x: anchorPoint.x - frame.origin.x, y: anchorPoint.y - frame.origin.y) let transPos = CATransform3DMakeTranslation(anchorOffset.x, anchorOffset.y, 0) let transNeg = CATransform3DMakeTranslation(-anchorOffset.x, -anchorOffset.y, 0) @@ -196,21 +200,6 @@ private func boundingBox(forQuadWithTR tr: CGPoint, tl: CGPoint, bl: CGPoint, br } func rectToQuad(rect: CGRect, quadTL topLeft: CGPoint, quadTR topRight: CGPoint, quadBL bottomLeft: CGPoint, quadBR bottomRight: CGPoint) -> CATransform3D { - /*if "".isEmpty { - let destination = Perspective(Quadrilateral( - topLeft, - topRight, - bottomLeft, - bottomRight - )) - - // Starting perspective is the current overlay frame or could be another 4 points. - let start = Perspective(Quadrilateral(rect.origin, rect.size)) - - // Caclulate CATransform3D from start to destination - return start.projectiveTransform(destination: destination) - }*/ - return rectToQuad(rect: rect, quadTLX: topLeft.x, quadTLY: topLeft.y, quadTRX: topRight.x, quadTRY: topRight.y, quadBLX: bottomLeft.x, quadBLY: bottomLeft.y, quadBRX: bottomRight.x, quadBRY: bottomRight.y) } @@ -493,7 +482,7 @@ open class SpaceWarpNodeImpl: ASDisplayNode, SpaceWarpNode { } instanceBounds.append(frame) - instancePositions.append(frame.center) + instancePositions.append(frame.origin) instanceTransforms.append(transform) } From 9975f939ce3848beba1817584ce2c5306cdbfa22 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 26 Jul 2024 14:11:25 +0200 Subject: [PATCH 26/41] Various fixes --- .../Telegram-iOS/en.lproj/Localizable.strings | 4 ++++ .../Sources/BrowserAddressListComponent.swift | 15 ++++++++------- .../Sources/BrowserAddressListItemComponent.swift | 4 ++++ .../StarsTransactionsListPanelComponent.swift | 7 +++---- 4 files changed, 19 insertions(+), 11 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index aa70fb4365..f85d7b974b 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -12564,6 +12564,7 @@ Sorry for the inconvenience."; "WebBrowser.AddressBar.RecentlyVisited.Clear" = "Clear"; "WebBrowser.AddressBar.Bookmarks" = "BOOKMARKS"; +"WebBrowser.AddressBar.ShowMore" = "Show More"; "WebBrowser.OpenLinksIn.Title" = "OPEN LINKS IN"; "WebBrowser.AutoLogin" = "Auto-Login via Telegram"; @@ -12656,3 +12657,6 @@ Sorry for the inconvenience."; "WebBrowser.CopyLink" = "Copy Link"; "WebBrowser.DeleteBookmark" = "Delete Bookmark"; "WebBrowser.RemoveRecent" = "Remove from Recent"; + +"Stars.Intro.Transaction.Gift.Title" = "Received Gift"; +"Stars.Intro.Transaction.Gift.UnknownUser" = "Unknown User"; diff --git a/submodules/BrowserUI/Sources/BrowserAddressListComponent.swift b/submodules/BrowserUI/Sources/BrowserAddressListComponent.swift index e17589cd96..fadb799c85 100644 --- a/submodules/BrowserUI/Sources/BrowserAddressListComponent.swift +++ b/submodules/BrowserUI/Sources/BrowserAddressListComponent.swift @@ -235,9 +235,9 @@ final class BrowserAddressListComponent: Component { let sectionTitle: String if section.id == 0 { - sectionTitle = "RECENTLY VISITED" + sectionTitle = component.strings.WebBrowser_AddressBar_RecentlyVisited } else if section.id == 1 { - sectionTitle = "BOOKMARKS" + sectionTitle = component.strings.WebBrowser_AddressBar_Bookmarks } else { sectionTitle = "" } @@ -249,7 +249,7 @@ final class BrowserAddressListComponent: Component { style: .plain, title: sectionTitle, insets: component.insets, - actionTitle: section.id == 0 ? "Clear" : nil, + actionTitle: section.id == 0 ? component.strings.WebBrowser_AddressBar_RecentlyVisited_Clear : nil, action: { [weak self] in if let self, let component = self.component { let _ = clearRecentlyVisitedLinks(engine: component.context.engine).start() @@ -279,11 +279,11 @@ final class BrowserAddressListComponent: Component { continue } - var id = 0 + var id: String = "" if section.id == 0 { - id += i + id = "recent_\(state.recent[i].content.url ?? "")" } else if section.id == 1 { - id += 1000 + i + id = "bookmark_\(state.bookmarks[i].id.id)" } let itemId = AnyHashable(id) @@ -458,11 +458,12 @@ final class BrowserAddressListComponent: Component { bookmarks.append(entry.message) } + let isFirstTime = self.stateValue == nil self.stateValue = State( recent: recent, bookmarks: bookmarks ) - self.state?.updated(transition: .immediate) + self.state?.updated(transition: isFirstTime ? .immediate : .easeInOut(duration: 0.25)) }) } diff --git a/submodules/BrowserUI/Sources/BrowserAddressListItemComponent.swift b/submodules/BrowserUI/Sources/BrowserAddressListItemComponent.swift index 05ff000d7d..aca503c26d 100644 --- a/submodules/BrowserUI/Sources/BrowserAddressListItemComponent.swift +++ b/submodules/BrowserUI/Sources/BrowserAddressListItemComponent.swift @@ -116,6 +116,10 @@ final class BrowserAddressListItemComponent: Component { self.containerButton.clipsToBounds = value self.containerButton.backgroundColor = value ? component.theme.list.plainBackgroundColor : nil self.containerButton.layer.cornerRadius = value ? 10.0 : 0.0 + + if value { + self.highlightedBackgroundLayer.opacity = 0.0 + } } self.extractedContainerView.willUpdateIsExtractedToContextPreview = { [weak self] value, transition in guard let self else { diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift index 7fb087911e..61a909dff3 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift @@ -218,8 +218,7 @@ final class StarsTransactionsListPanelComponent: Component { } else { itemTitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast) if item.flags.contains(.isGift) { - //TODO:localize - itemSubtitle = "Received Gift" + itemSubtitle = environment.strings.Stars_Intro_Transaction_Gift_Title } else { itemSubtitle = nil } @@ -233,8 +232,8 @@ final class StarsTransactionsListPanelComponent: Component { case .fragment: if component.isAccount { if item.flags.contains(.isGift) { - itemTitle = "Unknown User" - itemSubtitle = "Received Gift" + itemTitle = environment.strings.Stars_Intro_Transaction_Gift_UnknownUser + itemSubtitle = environment.strings.Stars_Intro_Transaction_Gift_Title itemPeer = .fragment } else { itemTitle = environment.strings.Stars_Intro_Transaction_FragmentTopUp_Title From f6b2464ace606dc9b0a867e9bc6c8f0f2251104b Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 26 Jul 2024 14:31:17 +0200 Subject: [PATCH 27/41] Various fixes --- .../Sources/BrowserInstantPageContent.swift | 175 +++++++++--------- .../TelegramUI/Sources/ChatController.swift | 6 +- 2 files changed, 92 insertions(+), 89 deletions(-) diff --git a/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift b/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift index 2ceff5ec34..8661daac51 100644 --- a/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift +++ b/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift @@ -46,6 +46,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg private var pendingAnchor: String? private var initialState: InstantPageStoredState? + private let wrapperNode: ASDisplayNode fileprivate let scrollNode: ASScrollNode private let scrollNodeFooter: ASDisplayNode private var linkHighlightingNode: LinkHighlightingNode? @@ -109,6 +110,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg self._state = BrowserContentState(title: title, url: url, estimatedProgress: 0.0, readingProgress: 0.0, contentType: .instantPage) self.statePromise = Promise(self._state) + self.wrapperNode = ASDisplayNode() self.scrollNode = ASScrollNode() self.scrollNode.backgroundColor = self.theme.pageBackgroundColor @@ -128,7 +130,8 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg } )) - self.addSubnode(self.scrollNode) + self.addSubnode(self.wrapperNode) + self.wrapperNode.addSubnode(self.scrollNode) self.scrollNode.addSubnode(self.scrollNodeFooter) self.scrollNode.view.delaysContentTouches = false @@ -392,6 +395,8 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg self.scrollNode.view.scrollIndicatorInsets = scrollInsets } + self.wrapperNode.frame = CGRect(origin: .zero, size: size) + let scrollFrame = CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: size.width, height: size.height - insets.top)) let scrollFrameUpdated = self.scrollNode.bounds.size != scrollFrame.size if scrollFrameUpdated { @@ -1108,12 +1113,12 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg } })], catchTapsOutside: true) self.present(controller, ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in - if let _ = self { -// for (_, itemNode) in self.visibleItemsWithNodes { -// if let (node, _, _) = itemNode.transitionNode(media: media) { -// return (self.scrollNode, node.convert(node.bounds, to: self.scrollNode), self, self.bounds) -// } -// } + if let self { + for (_, itemNode) in self.visibleItemsWithNodes { + if let (node, _, _) = itemNode.transitionNode(media: media) { + return (self.scrollNode, node.convert(node.bounds, to: self.scrollNode), self.wrapperNode, self.wrapperNode.bounds) + } + } } return nil })) @@ -1201,84 +1206,84 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg } private func updateTextSelectionRects(_ rects: [CGRect], text: String?) { -// if let text = text, !rects.isEmpty { -// let textSelectionNode: LinkHighlightingNode -// if let current = self.textSelectionNode { -// textSelectionNode = current -// } else { -// textSelectionNode = LinkHighlightingNode(color: UIColor.lightGray.withAlphaComponent(0.4)) -// textSelectionNode.isUserInteractionEnabled = false -// self.textSelectionNode = textSelectionNode -// self.scrollNode.addSubnode(textSelectionNode) -// } -// textSelectionNode.frame = CGRect(origin: CGPoint(), size: self.scrollNode.bounds.size) -// textSelectionNode.updateRects(rects) -// -//// var coveringRect = rects[0] -//// for i in 1 ..< rects.count { -//// coveringRect = coveringRect.union(rects[i]) -//// } -// -//// let context = self.context -//// let strings = self.presentationData.strings -//// let _ = (context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.translationSettings]) -//// |> take(1) -//// |> deliverOnMainQueue).start(next: { [weak self] sharedData in -//// let translationSettings: TranslationSettings -//// if let current = sharedData.entries[ApplicationSpecificSharedDataKeys.translationSettings]?.get(TranslationSettings.self) { -//// translationSettings = current -//// } else { -//// translationSettings = TranslationSettings.defaultSettings -//// } -//// -//// var actions: [ContextMenuAction] = [ContextMenuAction(content: .text(title: strings.Conversation_ContextMenuCopy, accessibilityLabel: strings.Conversation_ContextMenuCopy), action: { [weak self] in -//// UIPasteboard.general.string = text -//// -//// if let strongSelf = self { -//// let presentationData = context.sharedContext.currentPresentationData.with { $0 } -//// strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .copy(text: strings.Conversation_TextCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) -//// } -//// }), ContextMenuAction(content: .text(title: strings.Conversation_ContextMenuShare, accessibilityLabel: strings.Conversation_ContextMenuShare), action: { [weak self] in -//// if let strongSelf = self, let webPage = strongSelf.webPage, case let .Loaded(content) = webPage.content { -//// strongSelf.present(ShareController(context: strongSelf.context, subject: .quote(text: text, url: content.url)), nil) -//// } -//// })] -//// -//// let (canTranslate, language) = canTranslateText(context: context, text: text, showTranslate: translationSettings.showTranslate, showTranslateIfTopical: false, ignoredLanguages: translationSettings.ignoredLanguages) -//// if canTranslate { -//// actions.append(ContextMenuAction(content: .text(title: strings.Conversation_ContextMenuTranslate, accessibilityLabel: strings.Conversation_ContextMenuTranslate), action: { [weak self] in -//// let controller = TranslateScreen(context: context, text: text, canCopy: true, fromLanguage: language) -//// controller.pushController = { [weak self] c in -//// (self?.controller?.navigationController as? NavigationController)?._keepModalDismissProgress = true -//// self?.controller?.push(c) -//// } -//// controller.presentController = { [weak self] c in -//// self?.controller?.present(c, in: .window(.root)) -//// } -//// self?.present(controller, nil) -//// })) -//// } -//// -//// let controller = makeContextMenuController(actions: actions) -//// controller.dismissed = { [weak self] in -//// self?.updateTextSelectionRects([], text: nil) -//// } -//// self?.present(controller, ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in -//// if let strongSelf = self { -//// return (strongSelf.scrollNode, coveringRect.insetBy(dx: -3.0, dy: -3.0), strongSelf, strongSelf.bounds) -//// } else { -//// return nil -//// } -//// })) -//// }) -// -// textSelectionNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18) -// } else if let textSelectionNode = self.textSelectionNode { -// self.textSelectionNode = nil -// textSelectionNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak textSelectionNode] _ in -// textSelectionNode?.removeFromSupernode() -// }) -// } + if let text = text, !rects.isEmpty { + let textSelectionNode: LinkHighlightingNode + if let current = self.textSelectionNode { + textSelectionNode = current + } else { + textSelectionNode = LinkHighlightingNode(color: UIColor.lightGray.withAlphaComponent(0.4)) + textSelectionNode.isUserInteractionEnabled = false + self.textSelectionNode = textSelectionNode + self.scrollNode.addSubnode(textSelectionNode) + } + textSelectionNode.frame = CGRect(origin: CGPoint(), size: self.scrollNode.bounds.size) + textSelectionNode.updateRects(rects) + + var coveringRect = rects[0] + for i in 1 ..< rects.count { + coveringRect = coveringRect.union(rects[i]) + } + + let context = self.context + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let strings = self.presentationData.strings + let _ = (context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.translationSettings]) + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] sharedData in + let translationSettings: TranslationSettings + if let current = sharedData.entries[ApplicationSpecificSharedDataKeys.translationSettings]?.get(TranslationSettings.self) { + translationSettings = current + } else { + translationSettings = TranslationSettings.defaultSettings + } + + var actions: [ContextMenuAction] = [ContextMenuAction(content: .text(title: strings.Conversation_ContextMenuCopy, accessibilityLabel: strings.Conversation_ContextMenuCopy), action: { [weak self] in + UIPasteboard.general.string = text + + if let strongSelf = self { + strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .copy(text: strings.Conversation_TextCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) + } + }), ContextMenuAction(content: .text(title: strings.Conversation_ContextMenuShare, accessibilityLabel: strings.Conversation_ContextMenuShare), action: { [weak self] in + if let strongSelf = self, let webPage = strongSelf.webPage, case let .Loaded(content) = webPage.content { + strongSelf.present(ShareController(context: strongSelf.context, subject: .quote(text: text, url: content.url)), nil) + } + })] + + let (canTranslate, language) = canTranslateText(context: context, text: text, showTranslate: translationSettings.showTranslate, showTranslateIfTopical: false, ignoredLanguages: translationSettings.ignoredLanguages) + if canTranslate { + actions.append(ContextMenuAction(content: .text(title: strings.Conversation_ContextMenuTranslate, accessibilityLabel: strings.Conversation_ContextMenuTranslate), action: { [weak self] in + let controller = TranslateScreen(context: context, text: text, canCopy: true, fromLanguage: language) + controller.pushController = { [weak self] c in + self?.getNavigationController()?._keepModalDismissProgress = true + self?.push(c) + } + controller.presentController = { [weak self] c in + self?.present(c, nil) + } + self?.present(controller, nil) + })) + } + + let controller = makeContextMenuController(actions: actions) + controller.dismissed = { [weak self] in + self?.updateTextSelectionRects([], text: nil) + } + self?.present(controller, ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in + if let strongSelf = self { + return (strongSelf.scrollNode, coveringRect.insetBy(dx: -3.0, dy: -3.0), strongSelf.wrapperNode, strongSelf.wrapperNode.bounds) + } else { + return nil + } + })) + }) + + textSelectionNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18) + } else if let textSelectionNode = self.textSelectionNode { + self.textSelectionNode = nil + textSelectionNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak textSelectionNode] _ in + textSelectionNode?.removeFromSupernode() + }) + } } private func findAnchorItem(_ anchor: String, items: [InstantPageItem]) -> (InstantPageItem, CGFloat, Bool, [InstantPageDetailsItem])? { diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index b397f693cd..14b97b553c 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -6549,9 +6549,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let _ = ChatControllerCount.modify { value in return value - 1 } - - self.hasBrowserOrAppInFront.set(.single(false)) - + let deallocate: () -> Void = { self.historyStateDisposable?.dispose() self.messageIndexDisposable.dispose() @@ -7137,7 +7135,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - if case .standard(.default) = self.mode { + if case .standard(.default) = self.mode, !"".isEmpty { let hasBrowserOrWebAppInFront: Signal = .single([]) |> then( self.effectiveNavigationController?.viewControllersSignal ?? .single([]) From 0d10cd695737c6c45199197f604bfa2cdabab5a6 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Fri, 26 Jul 2024 20:36:34 +0800 Subject: [PATCH 28/41] Adjust ripple --- .../SpaceWarpView/Sources/SpaceWarpView.swift | 52 +++++++++++++------ .../TelegramUI/Sources/ChatController.swift | 6 ++- 2 files changed, 42 insertions(+), 16 deletions(-) diff --git a/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift b/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift index b697665450..7c3bc20527 100644 --- a/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift +++ b/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift @@ -92,7 +92,7 @@ private struct RippleParams { } } -private func transformCoordinate( +private func rippleOffset( position: CGPoint, origin: CGPoint, time: CGFloat, @@ -123,8 +123,8 @@ private func transformCoordinate( rippleAmount = absRippleAmount } - if distance <= 40.0 { - rippleAmount *= 0.5 + if distance <= 60.0 { + rippleAmount *= 0.4 } // A vector of length `amplitude` that points away from position. @@ -136,8 +136,7 @@ private func transformCoordinate( // // This new position moves toward or away from `origin` based on the // sign and magnitude of `rippleAmount`. - let newPosition = position - n * rippleAmount - return newPosition + return n * (-rippleAmount) } func transformToFitQuad2(frame: CGRect, topLeft tl: CGPoint, topRight tr: CGPoint, bottomLeft bl: CGPoint, bottomRight br: CGPoint) -> (frame: CGRect, transform: CATransform3D) { @@ -252,6 +251,15 @@ public protocol SpaceWarpNode: ASDisplayNode { } open class SpaceWarpNodeImpl: ASDisplayNode, SpaceWarpNode { + private final class Shockwave { + let startPoint: CGPoint + var timeValue: CGFloat = 0.0 + + init(startPoint: CGPoint) { + self.startPoint = startPoint + } + } + public var contentNode: ASDisplayNode { return self.contentNodeSource } @@ -270,9 +278,8 @@ open class SpaceWarpNodeImpl: ASDisplayNode, SpaceWarpNode { #endif private var link: SharedDisplayLinkDriver.Link? - private var startPoint: CGPoint? - private var timeValue: CGFloat = 0.0 + private var shockwaves: [Shockwave] = [] private var resolution: (x: Int, y: Int)? private var layoutParams: (size: CGSize, cornerRadius: CGFloat)? @@ -300,15 +307,19 @@ open class SpaceWarpNodeImpl: ASDisplayNode, SpaceWarpNode { } public func triggerRipple(at point: CGPoint) { - self.startPoint = point - self.timeValue = 0.0 + self.shockwaves.append(Shockwave(startPoint: point)) + if self.shockwaves.count > 8 { + self.shockwaves.removeFirst() + } if self.link == nil { self.link = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] deltaTime in guard let self else { return } - self.timeValue += deltaTime * (1.0 / CGFloat(UIView.animationDurationFactor())) + for shockwave in self.shockwaves { + shockwave.timeValue += deltaTime * (1.0 / CGFloat(UIView.animationDurationFactor())) + } if let (size, cornerRadius) = self.layoutParams { self.update(size: size, cornerRadius: cornerRadius, transition: .immediate) @@ -366,9 +377,15 @@ open class SpaceWarpNodeImpl: ASDisplayNode, SpaceWarpNode { let maxEdge = (max(size.width, size.height) * 0.5) * 2.0 let maxDistance = sqrt(maxEdge * maxEdge + maxEdge * maxEdge) - let maxDelay = maxDistance / params.speed - guard let startPoint = self.startPoint, self.timeValue < maxDelay else { + + for i in (0 ..< self.shockwaves.count).reversed() { + if self.shockwaves[i].timeValue >= maxDelay { + self.shockwaves.remove(at: i) + } + } + + guard !self.shockwaves.isEmpty else { if let link = self.link { self.link = nil link.invalidate() @@ -385,7 +402,6 @@ open class SpaceWarpNodeImpl: ASDisplayNode, SpaceWarpNode { self.debugLayers.removeAll() self.resolution = nil - self.startPoint = nil self.backgroundView.isHidden = true self.contentNodeSource.clipsToBounds = false self.contentNodeSource.layer.cornerRadius = 0.0 @@ -462,10 +478,16 @@ open class SpaceWarpNodeImpl: ASDisplayNode, SpaceWarpNode { var bottomLeft = initialBottomLeft var bottomRight = initialBottomRight - topLeft = transformCoordinate(position: topLeft, origin: startPoint, time: self.timeValue, params: params) + for shockwave in self.shockwaves { + topLeft = topLeft + rippleOffset(position: initialTopLeft, origin: shockwave.startPoint, time: shockwave.timeValue, params: params) + topRight = topRight + rippleOffset(position: initialTopRight, origin: shockwave.startPoint, time: shockwave.timeValue, params: params) + bottomLeft = bottomLeft + rippleOffset(position: initialBottomLeft, origin: shockwave.startPoint, time: shockwave.timeValue, params: params) + bottomRight = bottomRight + rippleOffset(position: initialBottomRight, origin: shockwave.startPoint, time: shockwave.timeValue, params: params) + } + /*topLeft = transformCoordinate(position: topLeft, origin: startPoint, time: self.timeValue, params: params) topRight = transformCoordinate(position: topRight, origin: startPoint, time: self.timeValue, params: params) bottomLeft = transformCoordinate(position: bottomLeft, origin: startPoint, time: self.timeValue, params: params) - bottomRight = transformCoordinate(position: bottomRight, origin: startPoint, time: self.timeValue, params: params) + bottomRight = transformCoordinate(position: bottomRight, origin: startPoint, time: self.timeValue, params: params)*/ let distanceTopLeft = length(topLeft - initialTopLeft) let distanceTopRight = length(topRight - initialTopRight) diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index b397f693cd..e59260916b 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -1772,7 +1772,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }) { let _ = (strongSelf.context.engine.stickers.resolveInlineStickers(fileIds: [MessageReaction.starsReactionId]) - |> deliverOnMainQueue).start(next: { [weak strongSelf] files in + |> deliverOnMainQueue).start(next: { [weak strongSelf, weak itemNode] files in guard let strongSelf, let file = files[MessageReaction.starsReactionId] else { return } @@ -1780,6 +1780,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .starsSent(context: strongSelf.context, file: file, amount: 1, title: "Star Sent", text: "Long tap on {star} to select custom quantity of stars."), elevatedLayout: false, action: { _ in return false }), in: .current) + + if let itemNode = itemNode, let targetView = itemNode.targetReactionView(value: chosenReaction) { + strongSelf.chatDisplayNode.wrappingNode.triggerRipple(at: targetView.convert(targetView.bounds.center, to: strongSelf.chatDisplayNode.view)) + } }) } } From 50b48b78234b218f401cd75ea68d140f0b6fbbe8 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 26 Jul 2024 15:05:20 +0200 Subject: [PATCH 29/41] Various fixes --- submodules/BrowserUI/BUILD | 1 + .../Sources/BrowserAddressListComponent.swift | 247 +++++++++++------- 2 files changed, 160 insertions(+), 88 deletions(-) diff --git a/submodules/BrowserUI/BUILD b/submodules/BrowserUI/BUILD index f03e31beb4..b49526f0c2 100644 --- a/submodules/BrowserUI/BUILD +++ b/submodules/BrowserUI/BUILD @@ -47,6 +47,7 @@ swift_library( "//submodules/SearchUI", "//submodules/SearchBarNode", "//submodules/TelegramUI/Components/SaveProgressScreen", + "//submodules/TelegramUI/Components/ListActionItemComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/BrowserUI/Sources/BrowserAddressListComponent.swift b/submodules/BrowserUI/Sources/BrowserAddressListComponent.swift index fadb799c85..b07f068e49 100644 --- a/submodules/BrowserUI/Sources/BrowserAddressListComponent.swift +++ b/submodules/BrowserUI/Sources/BrowserAddressListComponent.swift @@ -10,6 +10,7 @@ import AccountContext import TelegramPresentationData import ContextUI import UndoUI +import ListActionItemComponent final class BrowserAddressListComponent: Component { let context: AccountContext @@ -69,6 +70,7 @@ final class BrowserAddressListComponent: Component { var insets: UIEdgeInsets var itemHeight: CGFloat var itemCount: Int + var hasMore: Bool var totalHeight: CGFloat @@ -76,14 +78,21 @@ final class BrowserAddressListComponent: Component { id: Int, insets: UIEdgeInsets, itemHeight: CGFloat, - itemCount: Int + itemCount: Int, + hasMore: Bool ) { self.id = id self.insets = insets self.itemHeight = itemHeight self.itemCount = itemCount + self.hasMore = hasMore - self.totalHeight = insets.top + itemHeight * CGFloat(itemCount) + insets.bottom + var totalHeight = insets.top + itemHeight * CGFloat(itemCount) + insets.bottom + if hasMore { + totalHeight -= itemHeight + totalHeight += 44.0 + } + self.totalHeight = totalHeight } } @@ -123,6 +132,7 @@ final class BrowserAddressListComponent: Component { final class View: UIView, UIScrollViewDelegate { struct State { let recent: [TelegramMediaWebpage] + let isRecentExpanded: Bool let bookmarks: [Message] } @@ -145,6 +155,7 @@ final class BrowserAddressListComponent: Component { private var stateDisposable: Disposable? private var stateValue: State? + private let isRecentExpanded = ValuePromise(false) override init(frame: CGRect) { super.init(frame: frame) @@ -274,16 +285,28 @@ final class BrowserAddressListComponent: Component { } for i in 0 ..< section.itemCount { - let itemFrame = CGRect(origin: CGPoint(x: sideInset, y: sectionOffset + section.insets.top + CGFloat(i) * section.itemHeight), size: CGSize(width: itemLayout.containerSize.width, height: section.itemHeight)) + var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: sectionOffset + section.insets.top + CGFloat(i) * section.itemHeight), size: CGSize(width: itemLayout.containerSize.width, height: section.itemHeight)) if !visibleBounds.intersects(itemFrame) { continue } - + + var isMore = false + if section.hasMore && i == 3 { + isMore = true + itemFrame.size.height = 44.0 + } + var id: String = "" if section.id == 0 { id = "recent_\(state.recent[i].content.url ?? "")" + if isMore { + id = "recent_more" + } } else if section.id == 1 { id = "bookmark_\(state.bookmarks[i].id.id)" + if isMore { + id = "bookmark_more" + } } let itemId = AnyHashable(id) @@ -301,99 +324,137 @@ final class BrowserAddressListComponent: Component { self.visibleItems[itemId] = visibleItem } - var webPage: TelegramMediaWebpage? - var itemMessage: Message? - - if section.id == 0 { - webPage = state.recent[i] - } else if section.id == 1 { - let message = state.bookmarks[i] - if let primaryUrl = getPrimaryUrl(message: message) { - if let media = message.media.first(where: { $0 is TelegramMediaWebpage }) as? TelegramMediaWebpage { - webPage = media + if isMore { + let _ = visibleItem.update( + transition: itemTransition, + component: AnyComponent( + ListActionItemComponent( + theme: component.theme, + title: AnyComponent(Text( + text: component.strings.WebBrowser_AddressBar_ShowMore, + font: Font.regular(17.0), + color: component.theme.list.itemAccentColor + )), + leftIcon: .custom( + AnyComponentWithIdentity( + id: "icon", + component: AnyComponent(Image( + image: PresentationResourcesItemList.downArrowImage(component.theme), + size: CGSize(width: 30.0, height: 30.0) + )) + ), + false + ), + accessory: nil, + action: { [weak self] _ in + self?.isRecentExpanded.set(true) + }, + highlighting: .default, + updateIsHighlighted: { view, _ in + + }) + ), + environment: {}, + containerSize: itemFrame.size + ) + } else { + var webPage: TelegramMediaWebpage? + var itemMessage: Message? + + if section.id == 0 { + webPage = state.recent[i] + } else if section.id == 1 { + let message = state.bookmarks[i] + if let primaryUrl = getPrimaryUrl(message: message) { + if let media = message.media.first(where: { $0 is TelegramMediaWebpage }) as? TelegramMediaWebpage { + webPage = media + } else { + webPage = TelegramMediaWebpage(webpageId: MediaId(namespace: 0, id: 0), content: .Loaded(TelegramMediaWebpageLoadedContent(url: primaryUrl, displayUrl: "", hash: 0, type: nil, websiteName: "", title: message.text, text: "", embedUrl: nil, embedType: nil, embedSize: nil, duration: nil, author: nil, isMediaLargeByDefault: nil, image: nil, file: nil, story: nil, attributes: [], instantPage: nil))) + } + itemMessage = message } else { - webPage = TelegramMediaWebpage(webpageId: MediaId(namespace: 0, id: 0), content: .Loaded(TelegramMediaWebpageLoadedContent(url: primaryUrl, displayUrl: "", hash: 0, type: nil, websiteName: "", title: message.text, text: "", embedUrl: nil, embedType: nil, embedSize: nil, duration: nil, author: nil, isMediaLargeByDefault: nil, image: nil, file: nil, story: nil, attributes: [], instantPage: nil))) + continue } - itemMessage = message - } else { - continue } - } - let performAction = component.performAction - let _ = visibleItem.update( - transition: itemTransition, - component: AnyComponent( - BrowserAddressListItemComponent( - context: component.context, - theme: component.theme, - webPage: webPage!, - message: itemMessage, - hasNext: true, - insets: component.insets, - action: { - if let url = webPage?.content.url { - performAction.invoke(.navigateTo(url)) - } - }, - contextAction: { [weak self] webPage, message, sourceView, gesture in - guard let self, let component = self.component, let url = webPage.content.url else { - return - } - - let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - - var itemList: [ContextMenuItem] = [] - itemList.append(.action(ContextMenuActionItem(text: presentationData.strings.WebBrowser_CopyLink, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, f in - f(.default) - - UIPasteboard.general.string = url - if let self, let component = self.component { - component.presentInGlobalOverlay(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false })) + let performAction = component.performAction + let _ = visibleItem.update( + transition: itemTransition, + component: AnyComponent( + BrowserAddressListItemComponent( + context: component.context, + theme: component.theme, + webPage: webPage!, + message: itemMessage, + hasNext: true, + insets: component.insets, + action: { + if let url = webPage?.content.url { + performAction.invoke(.navigateTo(url)) } - }))) - - if let message { - itemList.append(.action(ContextMenuActionItem(text: presentationData.strings.WebBrowser_DeleteBookmark, textColor: .destructive, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) + }, + contextAction: { [weak self] webPage, message, sourceView, gesture in + guard let self, let component = self.component, let url = webPage.content.url else { + return + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + + var itemList: [ContextMenuItem] = [] + itemList.append(.action(ContextMenuActionItem(text: presentationData.strings.WebBrowser_CopyLink, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in - f(.dismissWithoutContent) - + f(.default) + + UIPasteboard.general.string = url if let self, let component = self.component { - let _ = component.context.engine.messages.deleteMessagesInteractively(messageIds: [message.id], type: .forEveryone).startStandalone() + component.presentInGlobalOverlay(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false })) } }))) - } else { - itemList.append(.action(ContextMenuActionItem(text: presentationData.strings.WebBrowser_RemoveRecent, textColor: .destructive, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) - }, action: { [weak self] _, f in - f(.dismissWithoutContent) - - if let self, let component = self.component, let url = webPage.content.url { - let _ = removeRecentlyVisitedLink(engine: component.context.engine, url: url).startStandalone() - } - }))) - } - - let items = ContextController.Items(content: .list(itemList)) - let controller = ContextController( - presentationData: presentationData, - source: .extracted(BrowserAddressListContextExtractedContentSource(contentView: sourceView)), - items: .single(items), - recognizer: nil, - gesture: gesture - ) - component.presentInGlobalOverlay(controller) - }) - ), - environment: {}, - containerSize: itemFrame.size - ) + + if let message { + itemList.append(.action(ContextMenuActionItem(text: presentationData.strings.WebBrowser_DeleteBookmark, textColor: .destructive, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) + }, action: { [weak self] _, f in + f(.dismissWithoutContent) + + if let self, let component = self.component { + let _ = component.context.engine.messages.deleteMessagesInteractively(messageIds: [message.id], type: .forEveryone).startStandalone() + } + }))) + } else { + itemList.append(.action(ContextMenuActionItem(text: presentationData.strings.WebBrowser_RemoveRecent, textColor: .destructive, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) + }, action: { [weak self] _, f in + f(.dismissWithoutContent) + + if let self, let component = self.component, let url = webPage.content.url { + let _ = removeRecentlyVisitedLink(engine: component.context.engine, url: url).startStandalone() + } + }))) + } + + let items = ContextController.Items(content: .list(itemList)) + let controller = ContextController( + presentationData: presentationData, + source: .extracted(BrowserAddressListContextExtractedContentSource(contentView: sourceView)), + items: .single(items), + recognizer: nil, + gesture: gesture + ) + component.presentInGlobalOverlay(controller) + }) + ), + environment: {}, + containerSize: itemFrame.size + ) + } if let itemView = visibleItem.view { if itemView.superview == nil { self.itemContainerView.addSubview(itemView) + if !transition.animation.isImmediate { + transition.animateAlpha(view: itemView, from: 0.0, to: 1.0) + } } itemTransition.setFrame(view: itemView, frame: itemFrame) } @@ -447,8 +508,9 @@ final class BrowserAddressListComponent: Component { if self.component == nil { self.stateDisposable = combineLatest(queue: Queue.mainQueue(), recentlyVisitedLinks(engine: component.context.engine), + self.isRecentExpanded.get(), component.context.account.viewTracker.aroundMessageHistoryViewForLocation(.peer(peerId: component.context.account.peerId, threadId: nil), index: .upperBound, anchorIndex: .upperBound, count: 100, fixedCombinedReadStates: nil, tag: .tag(.webPage)) - ).start(next: { [weak self] recent, view in + ).start(next: { [weak self] recent, isRecentExpanded, view in guard let self else { return } @@ -461,6 +523,7 @@ final class BrowserAddressListComponent: Component { let isFirstTime = self.stateValue == nil self.stateValue = State( recent: recent, + isRecentExpanded: isRecentExpanded, bookmarks: bookmarks ) self.state?.updated(transition: isFirstTime ? .immediate : .easeInOut(duration: 0.25)) @@ -510,11 +573,18 @@ final class BrowserAddressListComponent: Component { var sections: [ItemLayout.Section] = [] if let state = self.stateValue { if !state.recent.isEmpty { + var recentCount = state.recent.count + var hasMore = false + if recentCount > 4 && !state.isRecentExpanded { + recentCount = 4 + hasMore = true + } sections.append(ItemLayout.Section( id: 0, insets: UIEdgeInsets(top: 28.0, left: 0.0, bottom: 0.0, right: 0.0), itemHeight: addressItemSize.height, - itemCount: state.recent.count + itemCount: recentCount, + hasMore: hasMore )) } if !state.bookmarks.isEmpty { @@ -522,7 +592,8 @@ final class BrowserAddressListComponent: Component { id: 1, insets: UIEdgeInsets(top: 28.0, left: 0.0, bottom: 0.0, right: 0.0), itemHeight: addressItemSize.height, - itemCount: state.bookmarks.count + itemCount: state.bookmarks.count, + hasMore: false )) } } From 7f29c3e95c385d9e7ebea067f6f0b5ef67d5f8c3 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 26 Jul 2024 16:10:03 +0200 Subject: [PATCH 30/41] Various fixes --- .../BrowserUI/Sources/BrowserWebContent.swift | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/submodules/BrowserUI/Sources/BrowserWebContent.swift b/submodules/BrowserUI/Sources/BrowserWebContent.swift index acf2633086..ac1e939f04 100644 --- a/submodules/BrowserUI/Sources/BrowserWebContent.swift +++ b/submodules/BrowserUI/Sources/BrowserWebContent.swift @@ -239,6 +239,13 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU self.webView.isInspectable = true } self.addSubview(self.webView) + + self.webView.interactiveTransitionGestureRecognizerTest = { [weak self] point -> Bool in + if let self, self.webView.canGoBack { + return true + } + return point.x > 44.0 + } } required init?(coder: NSCoder) { @@ -1204,3 +1211,14 @@ final class BrowserSearchOptions: UITextSearchOptions { return .caseInsensitive } } + +private func findScrollView(view: UIView?) -> UIScrollView? { + if let view = view { + if let view = view as? UIScrollView { + return view + } + return findScrollView(view: view.superview) + } else { + return nil + } +} From 8e219193d0cd6b681433e153503b6249b09fd35d Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 26 Jul 2024 17:29:12 +0200 Subject: [PATCH 31/41] Various fixes --- .../Telegram-iOS/en.lproj/Localizable.strings | 4 +- .../BrowserUI/Sources/BrowserScreen.swift | 26 +++- .../BrowserUI/Sources/BrowserWebContent.swift | 139 +++++++++++++++++- .../Navigation/NavigationModalContainer.swift | 1 + .../Sources/MediaEditorScreen.swift | 6 +- .../Sources/VideoMessageCameraScreen.swift | 42 +++--- .../WebUI/Sources/WebAppController.swift | 11 +- 7 files changed, 191 insertions(+), 38 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index f85d7b974b..da0a67d6af 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -12593,9 +12593,7 @@ Sorry for the inconvenience."; "AccessDenied.LocationWeather" = "Telegram needs access to your location so that you can add the weather widget to your stories.\n\nPlease go to Settings > Privacy > Location Services and set Telegram to ON."; -"Story.Editor.TooltipWeatherLimitValue_1" = "**%@** weather stickers"; -"Story.Editor.TooltipWeatherLimitValue_any" = "**%@** weather stickers"; -"Story.Editor.TooltipWeatherLimitText" = "You can't add more than %@ to a story."; +"Story.Editor.TooltipWeatherLimitText" = "You can't add more than one weather sticker to a story."; "WebBrowser.AddressPlaceholder" = "Enter URL"; diff --git a/submodules/BrowserUI/Sources/BrowserScreen.swift b/submodules/BrowserUI/Sources/BrowserScreen.swift index c1d0d9ff8a..2b0df2a489 100644 --- a/submodules/BrowserUI/Sources/BrowserScreen.swift +++ b/submodules/BrowserUI/Sources/BrowserScreen.swift @@ -775,12 +775,18 @@ public class BrowserScreen: ViewController, MinimizableController { self.presentationState = f(self.presentationState) self.requestLayout(transition: transition) } - + func pushContent(_ content: BrowserScreen.Subject, transition: ComponentTransition) { let browserContent: BrowserContent switch content { case let .webPage(url): - browserContent = BrowserWebContent(context: self.context, presentationData: self.presentationData, url: url) + let webContent = BrowserWebContent(context: self.context, presentationData: self.presentationData, url: url) + webContent.cancelInteractiveTransitionGestures = { [weak self] in + if let self, let view = self.controller?.view { + cancelInteractiveTransitionGestures(view: view) + } + } + browserContent = webContent case let .instantPage(webPage, anchor, sourceLocation): let instantPageContent = BrowserInstantPageContent(context: self.context, presentationData: self.presentationData, webPage: webPage, anchor: anchor, url: webPage.content.url ?? "", sourceLocation: sourceLocation) instantPageContent.openPeer = { [weak self] peer in @@ -1582,3 +1588,19 @@ private final class BrowserContentComponent: Component { return view.update(component: self, availableSize: availableSize, transition: transition) } } + +private func cancelInteractiveTransitionGestures(view: UIView) { + if let gestureRecognizers = view.gestureRecognizers { + for gesture in gestureRecognizers { + if let gesture = gesture as? InteractiveTransitionGestureRecognizer { + gesture.cancel() + } else if let scrollView = gesture.view as? UIScrollView, gesture.isEnabled, scrollView.tag == 0x5C4011 { + gesture.isEnabled = false + gesture.isEnabled = true + } + } + } + if let superview = view.superview { + cancelInteractiveTransitionGestures(view: superview) + } +} diff --git a/submodules/BrowserUI/Sources/BrowserWebContent.swift b/submodules/BrowserUI/Sources/BrowserWebContent.swift index ac1e939f04..438236dd96 100644 --- a/submodules/BrowserUI/Sources/BrowserWebContent.swift +++ b/submodules/BrowserUI/Sources/BrowserWebContent.swift @@ -127,6 +127,28 @@ final class WebView: WKWebView { override var safeAreaInsets: UIEdgeInsets { return UIEdgeInsets(top: 0.0, left: 0.0, bottom: self.customBottomInset, right: 0.0) } + + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + var result = super.point(inside: point, with: event) + if !result && point.x > 0.0 && point.y < self.frame.width && point.y > 0.0 && point.y < self.frame.height + 83.0 { + result = true + } + return result + } +} + +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 BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKUIDelegate, UIScrollViewDelegate { @@ -160,6 +182,7 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU var present: (ViewController, Any?) -> Void = { _, _ in } var presentInGlobalOverlay: (ViewController) -> Void = { _ in } var getNavigationController: () -> NavigationController? = { return nil } + var cancelInteractiveTransitionGestures: () -> Void = {} private var tempFile: TempBoxFile? @@ -185,8 +208,17 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU let contentController = WKUserContentController() let videoScript = WKUserScript(source: videoSource, injectionTime: .atDocumentStart, forMainFrameOnly: false) contentController.addUserScript(videoScript) + let touchScript = WKUserScript(source: setupTouchObservers, injectionTime: .atDocumentStart, forMainFrameOnly: false) + contentController.addUserScript(touchScript) configuration.userContentController = contentController + var handleScriptMessageImpl: ((WKScriptMessage) -> Void)? + let eventProxyScript = WKUserScript(source: eventProxySource, injectionTime: .atDocumentStart, forMainFrameOnly: false) + contentController.addUserScript(eventProxyScript) + contentController.add(WeakScriptMessageHandler { message in + handleScriptMessageImpl?(message) + }, name: "performAction") + self.webView = WebView(frame: CGRect(), configuration: configuration) self.webView.allowsLinkPreview = true @@ -240,11 +272,27 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU } self.addSubview(self.webView) - self.webView.interactiveTransitionGestureRecognizerTest = { [weak self] point -> Bool in + self.webView.disablesInteractiveTransitionGestureRecognizerNow = { [weak self] in if let self, self.webView.canGoBack { return true + } else { + return false } - return point.x > 44.0 + } + + self.webView.interactiveTransitionGestureRecognizerTest = { [weak self] point in + if let self { + if let result = self.webView.hitTest(point, with: nil), let scrollView = findScrollView(view: result), scrollView.isDescendant(of: self.webView) { + if scrollView.contentSize.width > scrollView.frame.width, scrollView.contentOffset.x > -scrollView.contentInset.left { + return true + } + } + } + return false + } + + handleScriptMessageImpl = { [weak self] message in + self?.handleScriptMessage(message) } } @@ -263,6 +311,22 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU self.faviconDisposable.dispose() } + private func handleScriptMessage(_ message: WKScriptMessage) { + guard let body = message.body as? [String: Any] else { + return + } + guard let eventName = body["eventName"] as? String else { + return + } + + switch eventName { + case "cancellingTouch": + self.cancelInteractiveTransitionGestures() + default: + break + } + } + func updatePresentationData(_ presentationData: PresentationData) { self.presentationData = presentationData if #available(iOS 15.0, *) { @@ -523,7 +587,6 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU self.updateState { $0.withUpdatedEstimatedProgress(self.webView.estimatedProgress) } } else if keyPath == "canGoBack" { self.updateState { $0.withUpdatedCanGoBack(self.webView.canGoBack) } - self.webView.disablesInteractiveTransitionGestureRecognizer = self.webView.canGoBack } else if keyPath == "canGoForward" { self.updateState { $0.withUpdatedCanGoForward(self.webView.canGoForward) } } else if keyPath == "hasOnlySecureContent" { @@ -1201,6 +1264,76 @@ function disconnectObserver() { } """ +let setupTouchObservers = +""" +(function() { + function saveOriginalCssProperties(element) { + while (element) { + const computedStyle = window.getComputedStyle(element); + const propertiesToSave = ['transform', 'top', 'left']; + + element._originalProperties = {}; + + for (const property of propertiesToSave) { + element._originalProperties[property] = computedStyle.getPropertyValue(property); + } + + element = element.parentElement; + } + } + + function checkForCssChanges(element) { + while (element) { + if (!element._originalProperties) return false; + const computedStyle = window.getComputedStyle(element); + const modifiedProperties = ['transform', 'top', 'left']; + + for (const property of modifiedProperties) { + if (computedStyle.getPropertyValue(property) !== element._originalProperties[property]) { + return true; + } + } + + element = element.parentElement; + } + + return false; + } + + function clearOriginalCssProperties(element) { + while (element) { + delete element._originalProperties; + element = element.parentElement; + } + } + + let touchedElement = null; + + document.addEventListener('touchstart', function(event) { + touchedElement = event.target; + saveOriginalCssProperties(touchedElement); + }, { passive: true }); + + document.addEventListener('touchmove', function(event) { + if (checkForCssChanges(touchedElement)) { + TelegramWebviewProxy.postEvent("cancellingTouch", {}) + console.log('CSS properties changed during touchmove'); + } + }, { passive: true }); + + document.addEventListener('touchend', function() { + clearOriginalCssProperties(touchedElement); + touchedElement = null; + }, { passive: true }); +})(); +""" + +private let eventProxySource = "var TelegramWebviewProxyProto = function() {}; " + + "TelegramWebviewProxyProto.prototype.postEvent = function(eventName, eventData) { " + + "window.webkit.messageHandlers.performAction.postMessage({'eventName': eventName, 'eventData': eventData}); " + + "}; " + +"var TelegramWebviewProxy = new TelegramWebviewProxyProto();" + @available(iOS 16.0, *) final class BrowserSearchOptions: UITextSearchOptions { override var wordMatchMethod: UITextSearchOptions.WordMatchMethod { diff --git a/submodules/Display/Source/Navigation/NavigationModalContainer.swift b/submodules/Display/Source/Navigation/NavigationModalContainer.swift index 91dc19258f..185ac4432b 100644 --- a/submodules/Display/Source/Navigation/NavigationModalContainer.swift +++ b/submodules/Display/Source/Navigation/NavigationModalContainer.swift @@ -90,6 +90,7 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes self.scrollNode.view.delaysContentTouches = false self.scrollNode.view.clipsToBounds = false self.scrollNode.view.delegate = self.wrappedScrollViewDelegate + self.scrollNode.view.tag = 0x5C4011 let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] _ in guard let strongSelf = self, !strongSelf.isDismissed else { diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index d4f897c57e..49f044c0b2 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -4654,7 +4654,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate return } - let maxWeatherCount = 3 + let maxWeatherCount = 1 var currentWeatherCount = 0 self.entitiesView.eachView { entityView in if entityView.entity is DrawingWeatherEntity { @@ -6290,12 +6290,10 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate let context = self.context let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let limit: Int32 = 3 - let value = presentationData.strings.Story_Editor_TooltipWeatherLimitValue(limit) let content: UndoOverlayContent = .info( title: nil, - text: presentationData.strings.Story_Editor_TooltipWeatherLimitText(value).string, + text: presentationData.strings.Story_Editor_TooltipWeatherLimitText.string, timeout: nil, customUndoText: nil ) diff --git a/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift b/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift index 02de03e48c..2676399bd6 100644 --- a/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift +++ b/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift @@ -645,28 +645,30 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent { ) } - let flashButton = flashButton.update( - component: CameraButton( - content: flashContentComponent, - minSize: CGSize(width: 44.0, height: 44.0), - isExclusive: false, - action: { [weak state] in - if let state { - state.toggleFlashMode() - Queue.mainQueue().justDispatch { - flashAction.invoke(Void()) + if !environment.metrics.isTablet { + let flashButton = flashButton.update( + component: CameraButton( + content: flashContentComponent, + minSize: CGSize(width: 44.0, height: 44.0), + isExclusive: false, + action: { [weak state] in + if let state { + state.toggleFlashMode() + Queue.mainQueue().justDispatch { + flashAction.invoke(Void()) + } } } - } - ), - availableSize: availableSize, - transition: context.transition - ) - context.add(flashButton - .position(CGPoint(x: flipButton.size.width + 8.0 + flashButton.size.width / 2.0 + 11.0, y: availableSize.height - flashButton.size.height / 2.0 - 8.0)) - .appear(.default(scale: true, alpha: true)) - .disappear(.default(scale: true, alpha: true)) - ) + ), + availableSize: availableSize, + transition: context.transition + ) + context.add(flashButton + .position(CGPoint(x: flipButton.size.width + 8.0 + flashButton.size.width / 2.0 + 11.0, y: availableSize.height - flashButton.size.height / 2.0 - 8.0)) + .appear(.default(scale: true, alpha: true)) + .disappear(.default(scale: true, alpha: true)) + ) + } } if showViewOnce { diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index b4bba91d9d..1a6292104e 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -413,12 +413,11 @@ public final class WebAppController: ViewController, AttachmentContainable { } func checkBotIdAndUrl(_ url: String) { - //1985737506 - if url.hasPrefix("https://walletbot.me"), let botId = self.controller?.botId.id._internalGetInt64Value(), botId != 1985737506 { - let alertController = textAlertController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, title: nil, text: "Bot id mismatch, please report steps to app developer", actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: { - })]) - self.controller?.present(alertController, in: .window(.root)) - } +// if url.hasPrefix("https://walletbot.me"), let botId = self.controller?.botId.id._internalGetInt64Value(), botId != 1985737506 { +// let alertController = textAlertController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, title: nil, text: "Bot id mismatch, please report steps to app developer", actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: { +// })]) +// self.controller?.present(alertController, in: .window(.root)) +// } } @objc fileprivate func mainButtonPressed() { From 81cbb58312fdf537135e1897b679a02d2fc6552b Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Sat, 27 Jul 2024 00:04:05 +0800 Subject: [PATCH 32/41] Various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 2 +- .../Sources/PeerInfoStoryPaneNode.swift | 6 ++++ .../Sources/LanguageSelectionScreenNode.swift | 30 +++++++------------ .../SpaceWarpView/Sources/SpaceWarpView.swift | 4 +-- .../Sources/LocalizationListItem.swift | 5 +++- 5 files changed, 24 insertions(+), 23 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index f85d7b974b..7352c39bd2 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -12627,7 +12627,7 @@ Sorry for the inconvenience."; "BotPreviews.SubtitleLoading" = "loading"; "BotPreviews.SubtitleEmpty" = "no preview added"; "BotPreviews.SubtitleCount_1" = "1 preview"; -"BotPreviews.SubtitleCount_any" = "1 previews"; +"BotPreviews.SubtitleCount_any" = "%d previews"; "BotPreviews.SheetDeleteTitle_1" = "Delete 1 Preview?"; "BotPreviews.SheetDeleteTitle_any" = "Delete %d Previews?"; "BotPreviews.LanguageTab.Main" = "Main"; diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift index 15327d43df..738ef1a489 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift @@ -3614,11 +3614,15 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr effectiveScrollingOffset = self.itemGrid.scrollingOffset botPreviewLanguageTabFrame.origin.y -= effectiveScrollingOffset + let isSelectingOrReordering = self.isReordering || self.itemInteraction.selectedIds != nil + if let botPreviewLanguageTabView = botPreviewLanguageTab.view { if botPreviewLanguageTabView.superview == nil { self.view.addSubview(botPreviewLanguageTabView) } transition.updateFrame(view: botPreviewLanguageTabView, frame: botPreviewLanguageTabFrame) + transition.updateAlpha(layer: botPreviewLanguageTabView.layer, alpha: isSelectingOrReordering ? 0.5 : 1.0) + botPreviewLanguageTabView.isUserInteractionEnabled = !isSelectingOrReordering } } @@ -4405,6 +4409,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr let _ = self.context.engine.messages.updatePinnedToTopStories(peerId: id, ids: updatedPinnedIds).startStandalone() } } + + self.update(transition: animated ? .animated(duration: 0.4, curve: .spring) : .immediate) } } } diff --git a/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/Sources/LanguageSelectionScreenNode.swift b/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/Sources/LanguageSelectionScreenNode.swift index b31bb91475..cd14148e2e 100644 --- a/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/Sources/LanguageSelectionScreenNode.swift +++ b/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/Sources/LanguageSelectionScreenNode.swift @@ -32,13 +32,13 @@ private enum LanguageListEntryType { private enum LanguageListEntry: Comparable, Identifiable { case localizationTitle(text: String, section: ItemListSectionId) - case localization(index: Int, info: LocalizationInfo?, type: LanguageListEntryType) + case localization(index: Int, info: LocalizationInfo?, type: LanguageListEntryType, isEnabled: Bool) var stableId: LanguageListEntryId { switch self { case .localizationTitle: return .localizationTitle - case let .localization(index, info, _): + case let .localization(index, info, _, _): return .localization(info?.languageCode ?? "\(index)") } } @@ -47,7 +47,7 @@ private enum LanguageListEntry: Comparable, Identifiable { switch self { case .localizationTitle: return 1000 - case let .localization(index, _, _): + case let .localization(index, _, _, _): return 1001 + index } } @@ -60,8 +60,8 @@ private enum LanguageListEntry: Comparable, Identifiable { switch self { case let .localizationTitle(text, section): return ItemListSectionHeaderItem(presentationData: ItemListPresentationData(presentationData), text: text, sectionId: section) - case let .localization(_, info, type): - return LocalizationListItem(presentationData: ItemListPresentationData(presentationData), id: info?.languageCode ?? "", title: info?.title ?? " ", subtitle: info?.localizedTitle ?? " ", checked: false, activity: false, loading: info == nil, editing: LocalizationListItemEditing(editable: false, editing: false, revealed: false, reorderable: false), sectionId: type == .official ? LanguageListSection.official.rawValue : LanguageListSection.unofficial.rawValue, alwaysPlain: searchMode, action: { + case let .localization(_, info, type, isEnabled): + return LocalizationListItem(presentationData: ItemListPresentationData(presentationData), id: info?.languageCode ?? "", title: info?.title ?? " ", subtitle: info?.localizedTitle ?? " ", checked: false, activity: false, loading: info == nil, editing: LocalizationListItemEditing(editable: false, editing: false, revealed: false, reorderable: false), enabled: isEnabled, sectionId: type == .official ? LanguageListSection.official.rawValue : LanguageListSection.unofficial.rawValue, alwaysPlain: searchMode, action: { if let info { selectLocalization(info) } @@ -106,7 +106,7 @@ private final class LocalizationListSearchContainerNode: SearchDisplayController return true } - init(context: AccountContext, listState: LocalizationListState, selectLocalization: @escaping (LocalizationInfo) -> Void) { + init(context: AccountContext, listState: LocalizationListState, excludedIds: [String], selectLocalization: @escaping (LocalizationInfo) -> Void) { let presentationData = context.sharedContext.currentPresentationData.with { $0 } self.presentationData = presentationData @@ -157,7 +157,7 @@ private final class LocalizationListSearchContainerNode: SearchDisplayController var entries: [LanguageListEntry] = [] if let items = items { for item in items { - entries.append(.localization(index: entries.count, info: item, type: .official)) + entries.append(.localization(index: entries.count, info: item, type: .official, isEnabled: !excludedIds.contains(item.languageCode))) } } let previousEntriesAndPresentationData = previousEntriesHolder.swap((entries, presentationData.theme, presentationData.strings)) @@ -364,38 +364,29 @@ final class LanguageSelectionScreenNode: ViewControllerTracingNode { var entries: [LanguageListEntry] = [] var existingIds = Set() - var localizationListState = localizationListState - localizationListState.availableOfficialLocalizations = localizationListState.availableOfficialLocalizations.filter { - !strongSelf.excludeIds.contains($0.languageCode) - } - localizationListState.availableSavedLocalizations = [] - if !localizationListState.availableOfficialLocalizations.isEmpty { strongSelf.currentListState = localizationListState let availableSavedLocalizations = localizationListState.availableSavedLocalizations.filter({ info in !localizationListState.availableOfficialLocalizations.contains(where: { $0.languageCode == info.languageCode }) }) if !availableSavedLocalizations.isEmpty { - //entries.append(.localizationTitle(text: presentationData.strings.Localization_InterfaceLanguage.uppercased(), section: LanguageListSection.unofficial.rawValue)) for info in availableSavedLocalizations { if existingIds.contains(info.languageCode) { continue } existingIds.insert(info.languageCode) - entries.append(.localization(index: entries.count, info: info, type: .unofficial)) + entries.append(.localization(index: entries.count, info: info, type: .unofficial, isEnabled: !strongSelf.excludeIds.contains(info.languageCode))) } - } else { - //entries.append(.localizationTitle(text: presentationData.strings.Localization_InterfaceLanguage.uppercased(), section: LanguageListSection.official.rawValue)) } for info in localizationListState.availableOfficialLocalizations { if existingIds.contains(info.languageCode) { continue } existingIds.insert(info.languageCode) - entries.append(.localization(index: entries.count, info: info, type: .official)) + entries.append(.localization(index: entries.count, info: info, type: .official, isEnabled: !strongSelf.excludeIds.contains(info.languageCode))) } } else { for _ in 0 ..< 15 { - entries.append(.localization(index: entries.count, info: nil, type: .official)) + entries.append(.localization(index: entries.count, info: nil, type: .official, isEnabled: true)) } } @@ -542,6 +533,7 @@ final class LanguageSelectionScreenNode: ViewControllerTracingNode { contentNode: LocalizationListSearchContainerNode( context: self.context, listState: self.currentListState ?? LocalizationListState.defaultSettings, + excludedIds: self.excludeIds, selectLocalization: { [weak self] info in self?.selectLocalization(info) }), diff --git a/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift b/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift index 7c3bc20527..4d77d7cc97 100644 --- a/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift +++ b/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift @@ -124,7 +124,7 @@ private func rippleOffset( } if distance <= 60.0 { - rippleAmount *= 0.4 + rippleAmount = 0.4 * rippleAmount } // A vector of length `amplitude` that points away from position. @@ -368,7 +368,7 @@ open class SpaceWarpNodeImpl: ASDisplayNode, SpaceWarpNode { transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size)) - let params = RippleParams(amplitude: 26.0, frequency: 15.0, decay: 8.0, speed: 1400.0) + let params = RippleParams(amplitude: 20.0, frequency: 15.0, decay: 8.0, speed: 1400.0) if let currentCloneView = self.currentCloneView { currentCloneView.removeFromSuperview() diff --git a/submodules/TranslateUI/Sources/LocalizationListItem.swift b/submodules/TranslateUI/Sources/LocalizationListItem.swift index dc60b5f59d..6723f2bdc5 100644 --- a/submodules/TranslateUI/Sources/LocalizationListItem.swift +++ b/submodules/TranslateUI/Sources/LocalizationListItem.swift @@ -138,7 +138,7 @@ class LocalizationListItemNode: ItemListRevealOptionsItemNode { if self.editableControlNode != nil { return false } - if let _ = self.layoutParams?.0, let item = self.item, !item.loading { + if let _ = self.layoutParams?.0, let item = self.item, !item.loading, item.enabled { return super.canBeSelected } else { return false @@ -334,6 +334,9 @@ class LocalizationListItemNode: ItemListRevealOptionsItemNode { transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: editingOffset + revealOffset + leftInset, y: 8.0), size: titleLayout.size)) transition.updateFrame(node: strongSelf.subtitleNode, frame: CGRect(origin: CGPoint(x: editingOffset + revealOffset + leftInset, y: strongSelf.titleNode.frame.maxY + 1.0), size: subtitleLayout.size)) + strongSelf.titleNode.alpha = item.enabled ? 1.0 : 0.5 + strongSelf.subtitleNode.alpha = item.enabled ? 1.0 : 0.5 + if let editableControlSizeAndApply = editableControlSizeAndApply { let editableControlFrame = CGRect(origin: CGPoint(x: params.leftInset + revealOffset, y: 0.0), size: CGSize(width: editableControlSizeAndApply.0, height: layout.contentSize.height)) if strongSelf.editableControlNode == nil { From ce874ed51b0dd8045d33de41d4d995e4dec3f315 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Sat, 27 Jul 2024 00:12:15 +0800 Subject: [PATCH 33/41] Fix build --- .../MediaEditorScreen/Sources/MediaEditorScreen.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 49f044c0b2..3ef0f1aff9 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -6293,7 +6293,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate let content: UndoOverlayContent = .info( title: nil, - text: presentationData.strings.Story_Editor_TooltipWeatherLimitText.string, + text: presentationData.strings.Story_Editor_TooltipWeatherLimitText, timeout: nil, customUndoText: nil ) From 40d40dd8e76576d801b8f1da23c1d7c18b75f043 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 26 Jul 2024 20:59:48 +0200 Subject: [PATCH 34/41] Various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 12 +- .../Sources/BrowserAddressBarComponent.swift | 3 +- .../Sources/BrowserAddressListComponent.swift | 2 +- .../BrowserUI/Sources/BrowserScreen.swift | 47 +- .../BrowserUI/Sources/BrowserWebContent.swift | 7 +- .../WebBrowserSettingsController.swift | 73 ++- submodules/TelegramApi/Sources/Api0.swift | 3 +- submodules/TelegramApi/Sources/Api10.swift | 280 ++++------ submodules/TelegramApi/Sources/Api11.swift | 486 ++++++----------- submodules/TelegramApi/Sources/Api12.swift | 492 +++++++++++------- submodules/TelegramApi/Sources/Api13.swift | 310 ++++++----- submodules/TelegramApi/Sources/Api14.swift | 136 +++++ submodules/TelegramApi/Sources/Api9.swift | 134 +---- .../Sources/State/ApplyUpdateMessage.swift | 14 +- .../TelegramEngine/Messages/Stories.swift | 6 +- .../Components/Chat/ChatEmptyNode/BUILD | 1 + .../ChatEmptyNode/Sources/ChatEmptyNode.swift | 32 +- .../ChatMessageWebpageBubbleContentNode.swift | 6 +- .../Sources/EditStories.swift | 13 +- .../Sources/MediaEditorScreen.swift | 8 +- .../Resources/Animations/PremiumRequired.tgs | Bin 0 -> 1676 bytes .../Chat/ChatControllerOpenWebApp.swift | 10 +- .../Sources/TelegramRootController.swift | 4 +- .../WebUI/Sources/WebAppController.swift | 8 + 24 files changed, 1117 insertions(+), 970 deletions(-) create mode 100644 submodules/TelegramUI/Resources/Animations/PremiumRequired.tgs diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index da0a67d6af..a5f2b09caa 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -12571,7 +12571,7 @@ Sorry for the inconvenience."; "WebBrowser.AutoLogin.Info" = "Use your Telegram account to automatically log in to websites opened in the in-app browser."; "WebBrowser.ClearCookies" = "Clear Cookies"; -"WebBrowser.ClearCookies.Info" = "Delete all cookies in the Telegram in-app browser. This action will sign you out of most websites."; +"WebBrowser.ClearCookies.Info" = "Delete all cookies and cache in the Telegram in-app browser. This action will sign you out of most websites."; "WebBrowser.ClearCookies.Succeed" = "Cookies cleared."; "WebBrowser.Exceptions.Title" = "NEVER OPEN IN THE IN-APP BROWSER"; @@ -12589,6 +12589,12 @@ Sorry for the inconvenience."; "WebBrowser.ClearCookies.ClearConfirmation.Text" = "Are you sure you want to clear cookies?"; "WebBrowser.ClearCookies.ClearConfirmation.Clear" = "Clear"; +"WebBrowser.ClearCache" = "Clear Cache"; +"WebBrowser.ClearCache.ClearConfirmation.Text" = "Are you sure you want to clear cookies?"; +"WebBrowser.ClearCache.ClearConfirmation.Clear" = "Clear"; +"WebBrowser.ClearCache.Succeed" = "Cookies cleared."; + + "WebBrowser.Done" = "Done"; "AccessDenied.LocationWeather" = "Telegram needs access to your location so that you can add the weather widget to your stories.\n\nPlease go to Settings > Privacy > Location Services and set Telegram to ON."; @@ -12658,3 +12664,7 @@ Sorry for the inconvenience."; "Stars.Intro.Transaction.Gift.Title" = "Received Gift"; "Stars.Intro.Transaction.Gift.UnknownUser" = "Unknown User"; + +"WebApp.PrivacyPolicy" = "Privacy Policy"; + +"Conversation.OpenProfile" = "OPEN PROFILE"; diff --git a/submodules/BrowserUI/Sources/BrowserAddressBarComponent.swift b/submodules/BrowserUI/Sources/BrowserAddressBarComponent.swift index cb5f0d373c..dcb1cd88c8 100644 --- a/submodules/BrowserUI/Sources/BrowserAddressBarComponent.swift +++ b/submodules/BrowserUI/Sources/BrowserAddressBarComponent.swift @@ -249,8 +249,7 @@ final class AddressBarContentComponent: Component { public func textFieldShouldReturn(_ textField: UITextField) -> Bool { if let component = self.component { let finalUrl = explicitUrl(textField.text ?? "") -// finalUrl = finalUrl.addingPercentEncoding(withAllowedCharacters: .urlFragmentAllowed) ?? finalUrl - component.performAction.invoke(.navigateTo(finalUrl)) + component.performAction.invoke(.navigateTo(finalUrl, true)) } textField.endEditing(true) return false diff --git a/submodules/BrowserUI/Sources/BrowserAddressListComponent.swift b/submodules/BrowserUI/Sources/BrowserAddressListComponent.swift index b07f068e49..5675dd5a46 100644 --- a/submodules/BrowserUI/Sources/BrowserAddressListComponent.swift +++ b/submodules/BrowserUI/Sources/BrowserAddressListComponent.swift @@ -390,7 +390,7 @@ final class BrowserAddressListComponent: Component { insets: component.insets, action: { if let url = webPage?.content.url { - performAction.invoke(.navigateTo(url)) + performAction.invoke(.navigateTo(url, false)) } }, contextAction: { [weak self] webPage, message, sourceView, gesture in diff --git a/submodules/BrowserUI/Sources/BrowserScreen.swift b/submodules/BrowserUI/Sources/BrowserScreen.swift index 2b0df2a489..c9ce6c8ed8 100644 --- a/submodules/BrowserUI/Sources/BrowserScreen.swift +++ b/submodules/BrowserUI/Sources/BrowserScreen.swift @@ -153,24 +153,26 @@ private final class BrowserScreenComponent: CombinedComponent { ] if isTablet { -// navigationLeftItems.append( -// AnyComponentWithIdentity( -// id: "minimize", -// component: AnyComponent( -// Button( -// content: AnyComponent( -// BundleIconComponent( -// name: "Media Gallery/PictureInPictureButton", -// tintColor: environment.theme.rootController.navigationBar.accentTextColor -// ) -// ), -// action: { -// performAction.invoke(.close) -// } -// ) -// ) -// ) -// ) + #if DEBUG + navigationLeftItems.append( + AnyComponentWithIdentity( + id: "minimize", + component: AnyComponent( + Button( + content: AnyComponent( + BundleIconComponent( + name: "Media Gallery/PictureInPictureButton", + tintColor: environment.theme.rootController.navigationBar.accentTextColor + ) + ), + action: { + performAction.invoke(.close) + } + ) + ) + ) + ) + #endif let canGoBack = context.component.contentState?.canGoBack ?? false let canGoForward = context.component.contentState?.canGoForward ?? false @@ -483,7 +485,7 @@ public class BrowserScreen: ViewController, MinimizableController { case openBookmarks case openAddressBar case closeAddressBar - case navigateTo(String) + case navigateTo(String, Bool) case expand } @@ -730,9 +732,12 @@ public class BrowserScreen: ViewController, MinimizableController { updatedState.addressFocused = false return updatedState }) - case let .navigateTo(address): + case let .navigateTo(address, addToRecent): if let content = self.content.last as? BrowserWebContent { content.navigateTo(address: address) + if addToRecent { + content.addToRecentlyVisited() + } } self.updatePresentationState(transition: .spring(duration: 0.4), { state in var updatedState = state @@ -988,7 +993,7 @@ public class BrowserScreen: ViewController, MinimizableController { } let controller = BrowserBookmarksScreen(context: self.context, url: url, openUrl: { [weak self] url in if let self { - self.performAction.invoke(.navigateTo(url)) + self.performAction.invoke(.navigateTo(url, true)) } }, addBookmark: { [weak self] in self?.addBookmark(url, showArrow: false) diff --git a/submodules/BrowserUI/Sources/BrowserWebContent.swift b/submodules/BrowserUI/Sources/BrowserWebContent.swift index 438236dd96..7efff4450f 100644 --- a/submodules/BrowserUI/Sources/BrowserWebContent.swift +++ b/submodules/BrowserUI/Sources/BrowserWebContent.swift @@ -752,7 +752,12 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? { if navigationAction.targetFrame == nil { if let url = navigationAction.request.url?.absoluteString { - self.open(url: url, new: true) + if isTelegramMeLink(url) || isTelegraPhLink(url) { + self.minimize() + self.openAppUrl(url) + } else { + self.open(url: url, new: true) + } } } return nil diff --git a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift index 8a303cdf76..455b8d430a 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift @@ -21,6 +21,7 @@ private final class WebBrowserSettingsControllerArguments { let context: AccountContext let updateDefaultBrowser: (String?) -> Void let clearCookies: () -> Void + let clearCache: () -> Void let addException: () -> Void let removeException: (String) -> Void let clearExceptions: () -> Void @@ -29,6 +30,7 @@ private final class WebBrowserSettingsControllerArguments { context: AccountContext, updateDefaultBrowser: @escaping (String?) -> Void, clearCookies: @escaping () -> Void, + clearCache: @escaping () -> Void, addException: @escaping () -> Void, removeException: @escaping (String) -> Void, clearExceptions: @escaping () -> Void @@ -36,6 +38,7 @@ private final class WebBrowserSettingsControllerArguments { self.context = context self.updateDefaultBrowser = updateDefaultBrowser self.clearCookies = clearCookies + self.clearCache = clearCache self.addException = addException self.removeException = removeException self.clearExceptions = clearExceptions @@ -53,6 +56,7 @@ private enum WebBrowserSettingsControllerEntry: ItemListNodeEntry { case browser(PresentationTheme, String, OpenInApplication?, String?, Bool, Int32) case clearCookies(PresentationTheme, String) + case clearCache(PresentationTheme, String) case clearCookiesInfo(PresentationTheme, String) case exceptionsHeader(PresentationTheme, String) @@ -65,7 +69,7 @@ private enum WebBrowserSettingsControllerEntry: ItemListNodeEntry { switch self { case .browserHeader, .browser: return WebBrowserSettingsSection.browsers.rawValue - case .clearCookies, .clearCookiesInfo: + case .clearCookies, .clearCache, .clearCookiesInfo: return WebBrowserSettingsSection.clearCookies.rawValue case .exceptionsHeader, .exceptionsAdd, .exception, .exceptionsClear, .exceptionsInfo: return WebBrowserSettingsSection.exceptions.rawValue @@ -80,12 +84,14 @@ private enum WebBrowserSettingsControllerEntry: ItemListNodeEntry { return UInt64(1 + index) case .clearCookies: return 102 - case .clearCookiesInfo: + case .clearCache: return 103 - case .exceptionsHeader: + case .clearCookiesInfo: return 104 - case .exceptionsAdd: + case .exceptionsHeader: return 105 + case .exceptionsAdd: + return 106 case let .exception(_, _, exception): return 2000 + exception.domain.persistentHashValue case .exceptionsClear: @@ -103,14 +109,16 @@ private enum WebBrowserSettingsControllerEntry: ItemListNodeEntry { return 1 + index case .clearCookies: return 102 - case .clearCookiesInfo: + case .clearCache: return 103 - case .exceptionsHeader: + case .clearCookiesInfo: return 104 - case .exceptionsAdd: + case .exceptionsHeader: return 105 + case .exceptionsAdd: + return 106 case let .exception(index, _, _): - return 106 + index + return 107 + index case .exceptionsClear: return 1000 case .exceptionsInfo: @@ -138,6 +146,12 @@ private enum WebBrowserSettingsControllerEntry: ItemListNodeEntry { } else { return false } + case let .clearCache(lhsTheme, lhsText): + if case let .clearCache(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } case let .clearCookiesInfo(lhsTheme, lhsText): if case let .clearCookiesInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true @@ -194,6 +208,10 @@ private enum WebBrowserSettingsControllerEntry: ItemListNodeEntry { return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.accentDeleteIconImage(presentationData.theme), title: text, sectionId: self.section, height: .generic, color: .accent, editing: false, action: { arguments.clearCookies() }) + case let .clearCache(_, text): + return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.accentDeleteIconImage(presentationData.theme), title: text, sectionId: self.section, height: .generic, color: .accent, editing: false, action: { + arguments.clearCache() + }) case let .clearCookiesInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .exceptionsHeader(_, text): @@ -232,6 +250,7 @@ private func webBrowserSettingsControllerEntries(context: AccountContext, presen if settings.defaultWebBrowser == nil { entries.append(.clearCookies(presentationData.theme, presentationData.strings.WebBrowser_ClearCookies)) + entries.append(.clearCache(presentationData.theme, presentationData.strings.WebBrowser_ClearCache)) entries.append(.clearCookiesInfo(presentationData.theme, presentationData.strings.WebBrowser_ClearCookies_Info)) entries.append(.exceptionsHeader(presentationData.theme, presentationData.strings.WebBrowser_Exceptions_Title)) @@ -255,6 +274,7 @@ private func webBrowserSettingsControllerEntries(context: AccountContext, presen public func webBrowserSettingsController(context: AccountContext) -> ViewController { var clearCookiesImpl: (() -> Void)? + var clearCacheImpl: (() -> Void)? var addExceptionImpl: (() -> Void)? var removeExceptionImpl: ((String) -> Void)? var clearExceptionsImpl: (() -> Void)? @@ -269,6 +289,9 @@ public func webBrowserSettingsController(context: AccountContext) -> ViewControl clearCookies: { clearCookiesImpl?() }, + clearCache: { + clearCacheImpl?() + }, addException: { addExceptionImpl?() }, @@ -320,7 +343,7 @@ public func webBrowserSettingsController(context: AccountContext) -> ViewControl actions: [ TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.WebBrowser_ClearCookies_ClearConfirmation_Clear, action: { - WKWebsiteDataStore.default().removeData(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(), modifiedSince: Date(timeIntervalSince1970: 0), completionHandler:{}) + WKWebsiteDataStore.default().removeData(ofTypes: [WKWebsiteDataTypeCookies, WKWebsiteDataTypeLocalStorage, WKWebsiteDataTypeSessionStorage], modifiedSince: Date(timeIntervalSince1970: 0), completionHandler:{}) let presentationData = context.sharedContext.currentPresentationData.with { $0 } controller?.present(UndoOverlayController( @@ -341,6 +364,38 @@ public func webBrowserSettingsController(context: AccountContext) -> ViewControl controller?.present(alertController, in: .window(.root)) } + clearCacheImpl = { [weak controller] in + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + let alertController = textAlertController( + context: context, + updatedPresentationData: nil, + title: nil, + text: presentationData.strings.WebBrowser_ClearCache_ClearConfirmation_Text, + actions: [ + TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), + TextAlertAction(type: .defaultAction, title: presentationData.strings.WebBrowser_ClearCache_ClearConfirmation_Clear, action: { + WKWebsiteDataStore.default().removeData(ofTypes: [WKWebsiteDataTypeDiskCache, WKWebsiteDataTypeMemoryCache], modifiedSince: Date(timeIntervalSince1970: 0), completionHandler:{}) + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + controller?.present(UndoOverlayController( + presentationData: presentationData, + content: .info( + title: nil, + text: presentationData.strings.WebBrowser_ClearCache_Succeed, + timeout: nil, + customUndoText: nil + ), + elevatedLayout: false, + position: .bottom, + action: { _ in return false }), in: .current + ) + }) + ] + ) + controller?.present(alertController, in: .window(.root)) + } + addExceptionImpl = { [weak controller] in var dismissImpl: (() -> Void)? let linkController = webBrowserDomainController(context: context, apply: { url in diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index ff4d7a0245..11b0f8515c 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -355,6 +355,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1690108678] = { return Api.InputEncryptedFile.parse_inputEncryptedFileUploaded($0) } dict[-181407105] = { return Api.InputFile.parse_inputFile($0) } dict[-95482955] = { return Api.InputFile.parse_inputFileBig($0) } + dict[1658620744] = { return Api.InputFile.parse_inputFileStoryDocument($0) } dict[-1160743548] = { return Api.InputFileLocation.parse_inputDocumentFileLocation($0) } dict[-182231723] = { return Api.InputFileLocation.parse_inputEncryptedFileLocation($0) } dict[-539317279] = { return Api.InputFileLocation.parse_inputFileLocation($0) } @@ -1401,7 +1402,7 @@ public extension Api { return parser(reader) } else { - telegramApiLog("Type constructor \(String(UInt32(bitPattern: signature), radix: 16, uppercase: false)) not found") + telegramApiLog("Type constructor \(String(signature, radix: 16, uppercase: false)) not found") return nil } } diff --git a/submodules/TelegramApi/Sources/Api10.swift b/submodules/TelegramApi/Sources/Api10.swift index eefc46b743..79acdb0d0a 100644 --- a/submodules/TelegramApi/Sources/Api10.swift +++ b/submodules/TelegramApi/Sources/Api10.swift @@ -1,3 +1,115 @@ +public extension Api { + indirect enum InputInvoice: TypeConstructorDescription { + case inputInvoiceMessage(peer: Api.InputPeer, msgId: Int32) + case inputInvoicePremiumGiftCode(purpose: Api.InputStorePaymentPurpose, option: Api.PremiumGiftCodeOption) + case inputInvoiceSlug(slug: String) + case inputInvoiceStars(purpose: Api.InputStorePaymentPurpose) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputInvoiceMessage(let peer, let msgId): + if boxed { + buffer.appendInt32(-977967015) + } + peer.serialize(buffer, true) + serializeInt32(msgId, buffer: buffer, boxed: false) + break + case .inputInvoicePremiumGiftCode(let purpose, let option): + if boxed { + buffer.appendInt32(-1734841331) + } + purpose.serialize(buffer, true) + option.serialize(buffer, true) + break + case .inputInvoiceSlug(let slug): + if boxed { + buffer.appendInt32(-1020867857) + } + serializeString(slug, buffer: buffer, boxed: false) + break + case .inputInvoiceStars(let purpose): + if boxed { + buffer.appendInt32(1710230755) + } + purpose.serialize(buffer, true) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputInvoiceMessage(let peer, let msgId): + return ("inputInvoiceMessage", [("peer", peer as Any), ("msgId", msgId as Any)]) + case .inputInvoicePremiumGiftCode(let purpose, let option): + return ("inputInvoicePremiumGiftCode", [("purpose", purpose as Any), ("option", option as Any)]) + case .inputInvoiceSlug(let slug): + return ("inputInvoiceSlug", [("slug", slug as Any)]) + case .inputInvoiceStars(let purpose): + return ("inputInvoiceStars", [("purpose", purpose as Any)]) + } + } + + public static func parse_inputInvoiceMessage(_ reader: BufferReader) -> InputInvoice? { + var _1: Api.InputPeer? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.InputPeer + } + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.InputInvoice.inputInvoiceMessage(peer: _1!, msgId: _2!) + } + else { + return nil + } + } + public static func parse_inputInvoicePremiumGiftCode(_ reader: BufferReader) -> InputInvoice? { + var _1: Api.InputStorePaymentPurpose? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.InputStorePaymentPurpose + } + var _2: Api.PremiumGiftCodeOption? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.PremiumGiftCodeOption + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.InputInvoice.inputInvoicePremiumGiftCode(purpose: _1!, option: _2!) + } + else { + return nil + } + } + public static func parse_inputInvoiceSlug(_ reader: BufferReader) -> InputInvoice? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return Api.InputInvoice.inputInvoiceSlug(slug: _1!) + } + else { + return nil + } + } + public static func parse_inputInvoiceStars(_ reader: BufferReader) -> InputInvoice? { + var _1: Api.InputStorePaymentPurpose? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.InputStorePaymentPurpose + } + let _c1 = _1 != nil + if _c1 { + return Api.InputInvoice.inputInvoiceStars(purpose: _1!) + } + else { + return nil + } + } + + } +} public extension Api { indirect enum InputMedia: TypeConstructorDescription { case inputMediaContact(phoneNumber: String, firstName: String, lastName: String, vcard: String) @@ -918,171 +1030,3 @@ public extension Api { } } -public extension Api { - indirect enum InputPeer: TypeConstructorDescription { - case inputPeerChannel(channelId: Int64, accessHash: Int64) - case inputPeerChannelFromMessage(peer: Api.InputPeer, msgId: Int32, channelId: Int64) - case inputPeerChat(chatId: Int64) - case inputPeerEmpty - case inputPeerSelf - case inputPeerUser(userId: Int64, accessHash: Int64) - case inputPeerUserFromMessage(peer: Api.InputPeer, msgId: Int32, userId: Int64) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inputPeerChannel(let channelId, let accessHash): - if boxed { - buffer.appendInt32(666680316) - } - serializeInt64(channelId, buffer: buffer, boxed: false) - serializeInt64(accessHash, buffer: buffer, boxed: false) - break - case .inputPeerChannelFromMessage(let peer, let msgId, let channelId): - if boxed { - buffer.appendInt32(-1121318848) - } - peer.serialize(buffer, true) - serializeInt32(msgId, buffer: buffer, boxed: false) - serializeInt64(channelId, buffer: buffer, boxed: false) - break - case .inputPeerChat(let chatId): - if boxed { - buffer.appendInt32(900291769) - } - serializeInt64(chatId, buffer: buffer, boxed: false) - break - case .inputPeerEmpty: - if boxed { - buffer.appendInt32(2134579434) - } - - break - case .inputPeerSelf: - if boxed { - buffer.appendInt32(2107670217) - } - - break - case .inputPeerUser(let userId, let accessHash): - if boxed { - buffer.appendInt32(-571955892) - } - serializeInt64(userId, buffer: buffer, boxed: false) - serializeInt64(accessHash, buffer: buffer, boxed: false) - break - case .inputPeerUserFromMessage(let peer, let msgId, let userId): - if boxed { - buffer.appendInt32(-1468331492) - } - peer.serialize(buffer, true) - serializeInt32(msgId, buffer: buffer, boxed: false) - serializeInt64(userId, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .inputPeerChannel(let channelId, let accessHash): - return ("inputPeerChannel", [("channelId", channelId as Any), ("accessHash", accessHash as Any)]) - case .inputPeerChannelFromMessage(let peer, let msgId, let channelId): - return ("inputPeerChannelFromMessage", [("peer", peer as Any), ("msgId", msgId as Any), ("channelId", channelId as Any)]) - case .inputPeerChat(let chatId): - return ("inputPeerChat", [("chatId", chatId as Any)]) - case .inputPeerEmpty: - return ("inputPeerEmpty", []) - case .inputPeerSelf: - return ("inputPeerSelf", []) - case .inputPeerUser(let userId, let accessHash): - return ("inputPeerUser", [("userId", userId as Any), ("accessHash", accessHash as Any)]) - case .inputPeerUserFromMessage(let peer, let msgId, let userId): - return ("inputPeerUserFromMessage", [("peer", peer as Any), ("msgId", msgId as Any), ("userId", userId as Any)]) - } - } - - public static func parse_inputPeerChannel(_ reader: BufferReader) -> InputPeer? { - var _1: Int64? - _1 = reader.readInt64() - var _2: Int64? - _2 = reader.readInt64() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.InputPeer.inputPeerChannel(channelId: _1!, accessHash: _2!) - } - else { - return nil - } - } - public static func parse_inputPeerChannelFromMessage(_ reader: BufferReader) -> InputPeer? { - var _1: Api.InputPeer? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.InputPeer - } - var _2: Int32? - _2 = reader.readInt32() - var _3: Int64? - _3 = reader.readInt64() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.InputPeer.inputPeerChannelFromMessage(peer: _1!, msgId: _2!, channelId: _3!) - } - else { - return nil - } - } - public static func parse_inputPeerChat(_ reader: BufferReader) -> InputPeer? { - var _1: Int64? - _1 = reader.readInt64() - let _c1 = _1 != nil - if _c1 { - return Api.InputPeer.inputPeerChat(chatId: _1!) - } - else { - return nil - } - } - public static func parse_inputPeerEmpty(_ reader: BufferReader) -> InputPeer? { - return Api.InputPeer.inputPeerEmpty - } - public static func parse_inputPeerSelf(_ reader: BufferReader) -> InputPeer? { - return Api.InputPeer.inputPeerSelf - } - public static func parse_inputPeerUser(_ reader: BufferReader) -> InputPeer? { - var _1: Int64? - _1 = reader.readInt64() - var _2: Int64? - _2 = reader.readInt64() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.InputPeer.inputPeerUser(userId: _1!, accessHash: _2!) - } - else { - return nil - } - } - public static func parse_inputPeerUserFromMessage(_ reader: BufferReader) -> InputPeer? { - var _1: Api.InputPeer? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.InputPeer - } - var _2: Int32? - _2 = reader.readInt32() - var _3: Int64? - _3 = reader.readInt64() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.InputPeer.inputPeerUserFromMessage(peer: _1!, msgId: _2!, userId: _3!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api11.swift b/submodules/TelegramApi/Sources/Api11.swift index 8a502fcddf..8845ce3ec6 100644 --- a/submodules/TelegramApi/Sources/Api11.swift +++ b/submodules/TelegramApi/Sources/Api11.swift @@ -1,3 +1,171 @@ +public extension Api { + indirect enum InputPeer: TypeConstructorDescription { + case inputPeerChannel(channelId: Int64, accessHash: Int64) + case inputPeerChannelFromMessage(peer: Api.InputPeer, msgId: Int32, channelId: Int64) + case inputPeerChat(chatId: Int64) + case inputPeerEmpty + case inputPeerSelf + case inputPeerUser(userId: Int64, accessHash: Int64) + case inputPeerUserFromMessage(peer: Api.InputPeer, msgId: Int32, userId: Int64) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputPeerChannel(let channelId, let accessHash): + if boxed { + buffer.appendInt32(666680316) + } + serializeInt64(channelId, buffer: buffer, boxed: false) + serializeInt64(accessHash, buffer: buffer, boxed: false) + break + case .inputPeerChannelFromMessage(let peer, let msgId, let channelId): + if boxed { + buffer.appendInt32(-1121318848) + } + peer.serialize(buffer, true) + serializeInt32(msgId, buffer: buffer, boxed: false) + serializeInt64(channelId, buffer: buffer, boxed: false) + break + case .inputPeerChat(let chatId): + if boxed { + buffer.appendInt32(900291769) + } + serializeInt64(chatId, buffer: buffer, boxed: false) + break + case .inputPeerEmpty: + if boxed { + buffer.appendInt32(2134579434) + } + + break + case .inputPeerSelf: + if boxed { + buffer.appendInt32(2107670217) + } + + break + case .inputPeerUser(let userId, let accessHash): + if boxed { + buffer.appendInt32(-571955892) + } + serializeInt64(userId, buffer: buffer, boxed: false) + serializeInt64(accessHash, buffer: buffer, boxed: false) + break + case .inputPeerUserFromMessage(let peer, let msgId, let userId): + if boxed { + buffer.appendInt32(-1468331492) + } + peer.serialize(buffer, true) + serializeInt32(msgId, buffer: buffer, boxed: false) + serializeInt64(userId, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputPeerChannel(let channelId, let accessHash): + return ("inputPeerChannel", [("channelId", channelId as Any), ("accessHash", accessHash as Any)]) + case .inputPeerChannelFromMessage(let peer, let msgId, let channelId): + return ("inputPeerChannelFromMessage", [("peer", peer as Any), ("msgId", msgId as Any), ("channelId", channelId as Any)]) + case .inputPeerChat(let chatId): + return ("inputPeerChat", [("chatId", chatId as Any)]) + case .inputPeerEmpty: + return ("inputPeerEmpty", []) + case .inputPeerSelf: + return ("inputPeerSelf", []) + case .inputPeerUser(let userId, let accessHash): + return ("inputPeerUser", [("userId", userId as Any), ("accessHash", accessHash as Any)]) + case .inputPeerUserFromMessage(let peer, let msgId, let userId): + return ("inputPeerUserFromMessage", [("peer", peer as Any), ("msgId", msgId as Any), ("userId", userId as Any)]) + } + } + + public static func parse_inputPeerChannel(_ reader: BufferReader) -> InputPeer? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int64? + _2 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.InputPeer.inputPeerChannel(channelId: _1!, accessHash: _2!) + } + else { + return nil + } + } + public static func parse_inputPeerChannelFromMessage(_ reader: BufferReader) -> InputPeer? { + var _1: Api.InputPeer? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.InputPeer + } + var _2: Int32? + _2 = reader.readInt32() + var _3: Int64? + _3 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.InputPeer.inputPeerChannelFromMessage(peer: _1!, msgId: _2!, channelId: _3!) + } + else { + return nil + } + } + public static func parse_inputPeerChat(_ reader: BufferReader) -> InputPeer? { + var _1: Int64? + _1 = reader.readInt64() + let _c1 = _1 != nil + if _c1 { + return Api.InputPeer.inputPeerChat(chatId: _1!) + } + else { + return nil + } + } + public static func parse_inputPeerEmpty(_ reader: BufferReader) -> InputPeer? { + return Api.InputPeer.inputPeerEmpty + } + public static func parse_inputPeerSelf(_ reader: BufferReader) -> InputPeer? { + return Api.InputPeer.inputPeerSelf + } + public static func parse_inputPeerUser(_ reader: BufferReader) -> InputPeer? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int64? + _2 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.InputPeer.inputPeerUser(userId: _1!, accessHash: _2!) + } + else { + return nil + } + } + public static func parse_inputPeerUserFromMessage(_ reader: BufferReader) -> InputPeer? { + var _1: Api.InputPeer? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.InputPeer + } + var _2: Int32? + _2 = reader.readInt32() + var _3: Int64? + _3 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.InputPeer.inputPeerUserFromMessage(peer: _1!, msgId: _2!, userId: _3!) + } + else { + return nil + } + } + + } +} public extension Api { enum InputPeerNotifySettings: TypeConstructorDescription { case inputPeerNotifySettings(flags: Int32, showPreviews: Api.Bool?, silent: Api.Bool?, muteUntil: Int32?, sound: Api.NotificationSound?, storiesMuted: Api.Bool?, storiesHideSender: Api.Bool?, storiesSound: Api.NotificationSound?) @@ -510,321 +678,3 @@ public extension Api { } } -public extension Api { - enum InputQuickReplyShortcut: TypeConstructorDescription { - case inputQuickReplyShortcut(shortcut: String) - case inputQuickReplyShortcutId(shortcutId: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inputQuickReplyShortcut(let shortcut): - if boxed { - buffer.appendInt32(609840449) - } - serializeString(shortcut, buffer: buffer, boxed: false) - break - case .inputQuickReplyShortcutId(let shortcutId): - if boxed { - buffer.appendInt32(18418929) - } - serializeInt32(shortcutId, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .inputQuickReplyShortcut(let shortcut): - return ("inputQuickReplyShortcut", [("shortcut", shortcut as Any)]) - case .inputQuickReplyShortcutId(let shortcutId): - return ("inputQuickReplyShortcutId", [("shortcutId", shortcutId as Any)]) - } - } - - public static func parse_inputQuickReplyShortcut(_ reader: BufferReader) -> InputQuickReplyShortcut? { - var _1: String? - _1 = parseString(reader) - let _c1 = _1 != nil - if _c1 { - return Api.InputQuickReplyShortcut.inputQuickReplyShortcut(shortcut: _1!) - } - else { - return nil - } - } - public static func parse_inputQuickReplyShortcutId(_ reader: BufferReader) -> InputQuickReplyShortcut? { - var _1: Int32? - _1 = reader.readInt32() - let _c1 = _1 != nil - if _c1 { - return Api.InputQuickReplyShortcut.inputQuickReplyShortcutId(shortcutId: _1!) - } - else { - return nil - } - } - - } -} -public extension Api { - indirect enum InputReplyTo: TypeConstructorDescription { - case inputReplyToMessage(flags: Int32, replyToMsgId: Int32, topMsgId: Int32?, replyToPeerId: Api.InputPeer?, quoteText: String?, quoteEntities: [Api.MessageEntity]?, quoteOffset: Int32?) - case inputReplyToStory(peer: Api.InputPeer, storyId: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inputReplyToMessage(let flags, let replyToMsgId, let topMsgId, let replyToPeerId, let quoteText, let quoteEntities, let quoteOffset): - if boxed { - buffer.appendInt32(583071445) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt32(replyToMsgId, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 0) != 0 {serializeInt32(topMsgId!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 1) != 0 {replyToPeerId!.serialize(buffer, true)} - if Int(flags) & Int(1 << 2) != 0 {serializeString(quoteText!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 3) != 0 {buffer.appendInt32(481674261) - buffer.appendInt32(Int32(quoteEntities!.count)) - for item in quoteEntities! { - item.serialize(buffer, true) - }} - if Int(flags) & Int(1 << 4) != 0 {serializeInt32(quoteOffset!, buffer: buffer, boxed: false)} - break - case .inputReplyToStory(let peer, let storyId): - if boxed { - buffer.appendInt32(1484862010) - } - peer.serialize(buffer, true) - serializeInt32(storyId, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .inputReplyToMessage(let flags, let replyToMsgId, let topMsgId, let replyToPeerId, let quoteText, let quoteEntities, let quoteOffset): - return ("inputReplyToMessage", [("flags", flags as Any), ("replyToMsgId", replyToMsgId as Any), ("topMsgId", topMsgId as Any), ("replyToPeerId", replyToPeerId as Any), ("quoteText", quoteText as Any), ("quoteEntities", quoteEntities as Any), ("quoteOffset", quoteOffset as Any)]) - case .inputReplyToStory(let peer, let storyId): - return ("inputReplyToStory", [("peer", peer as Any), ("storyId", storyId as Any)]) - } - } - - public static func parse_inputReplyToMessage(_ reader: BufferReader) -> InputReplyTo? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: Int32? - if Int(_1!) & Int(1 << 0) != 0 {_3 = reader.readInt32() } - var _4: Api.InputPeer? - if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() { - _4 = Api.parse(reader, signature: signature) as? Api.InputPeer - } } - var _5: String? - if Int(_1!) & Int(1 << 2) != 0 {_5 = parseString(reader) } - var _6: [Api.MessageEntity]? - if Int(_1!) & Int(1 << 3) != 0 {if let _ = reader.readInt32() { - _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) - } } - var _7: Int32? - if Int(_1!) & Int(1 << 4) != 0 {_7 = reader.readInt32() } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 3) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 4) == 0) || _7 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { - return Api.InputReplyTo.inputReplyToMessage(flags: _1!, replyToMsgId: _2!, topMsgId: _3, replyToPeerId: _4, quoteText: _5, quoteEntities: _6, quoteOffset: _7) - } - else { - return nil - } - } - public static func parse_inputReplyToStory(_ reader: BufferReader) -> InputReplyTo? { - var _1: Api.InputPeer? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.InputPeer - } - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.InputReplyTo.inputReplyToStory(peer: _1!, storyId: _2!) - } - else { - return nil - } - } - - } -} -public extension Api { - enum InputSecureFile: TypeConstructorDescription { - case inputSecureFile(id: Int64, accessHash: Int64) - case inputSecureFileUploaded(id: Int64, parts: Int32, md5Checksum: String, fileHash: Buffer, secret: Buffer) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inputSecureFile(let id, let accessHash): - if boxed { - buffer.appendInt32(1399317950) - } - serializeInt64(id, buffer: buffer, boxed: false) - serializeInt64(accessHash, buffer: buffer, boxed: false) - break - case .inputSecureFileUploaded(let id, let parts, let md5Checksum, let fileHash, let secret): - if boxed { - buffer.appendInt32(859091184) - } - serializeInt64(id, buffer: buffer, boxed: false) - serializeInt32(parts, buffer: buffer, boxed: false) - serializeString(md5Checksum, buffer: buffer, boxed: false) - serializeBytes(fileHash, buffer: buffer, boxed: false) - serializeBytes(secret, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .inputSecureFile(let id, let accessHash): - return ("inputSecureFile", [("id", id as Any), ("accessHash", accessHash as Any)]) - case .inputSecureFileUploaded(let id, let parts, let md5Checksum, let fileHash, let secret): - return ("inputSecureFileUploaded", [("id", id as Any), ("parts", parts as Any), ("md5Checksum", md5Checksum as Any), ("fileHash", fileHash as Any), ("secret", secret as Any)]) - } - } - - public static func parse_inputSecureFile(_ reader: BufferReader) -> InputSecureFile? { - var _1: Int64? - _1 = reader.readInt64() - var _2: Int64? - _2 = reader.readInt64() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.InputSecureFile.inputSecureFile(id: _1!, accessHash: _2!) - } - else { - return nil - } - } - public static func parse_inputSecureFileUploaded(_ reader: BufferReader) -> InputSecureFile? { - var _1: Int64? - _1 = reader.readInt64() - var _2: Int32? - _2 = reader.readInt32() - var _3: String? - _3 = parseString(reader) - var _4: Buffer? - _4 = parseBytes(reader) - var _5: Buffer? - _5 = parseBytes(reader) - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return Api.InputSecureFile.inputSecureFileUploaded(id: _1!, parts: _2!, md5Checksum: _3!, fileHash: _4!, secret: _5!) - } - else { - return nil - } - } - - } -} -public extension Api { - enum InputSecureValue: TypeConstructorDescription { - case inputSecureValue(flags: Int32, type: Api.SecureValueType, data: Api.SecureData?, frontSide: Api.InputSecureFile?, reverseSide: Api.InputSecureFile?, selfie: Api.InputSecureFile?, translation: [Api.InputSecureFile]?, files: [Api.InputSecureFile]?, plainData: Api.SecurePlainData?) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inputSecureValue(let flags, let type, let data, let frontSide, let reverseSide, let selfie, let translation, let files, let plainData): - if boxed { - buffer.appendInt32(-618540889) - } - serializeInt32(flags, buffer: buffer, boxed: false) - type.serialize(buffer, true) - if Int(flags) & Int(1 << 0) != 0 {data!.serialize(buffer, true)} - if Int(flags) & Int(1 << 1) != 0 {frontSide!.serialize(buffer, true)} - if Int(flags) & Int(1 << 2) != 0 {reverseSide!.serialize(buffer, true)} - if Int(flags) & Int(1 << 3) != 0 {selfie!.serialize(buffer, true)} - if Int(flags) & Int(1 << 6) != 0 {buffer.appendInt32(481674261) - buffer.appendInt32(Int32(translation!.count)) - for item in translation! { - item.serialize(buffer, true) - }} - if Int(flags) & Int(1 << 4) != 0 {buffer.appendInt32(481674261) - buffer.appendInt32(Int32(files!.count)) - for item in files! { - item.serialize(buffer, true) - }} - if Int(flags) & Int(1 << 5) != 0 {plainData!.serialize(buffer, true)} - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .inputSecureValue(let flags, let type, let data, let frontSide, let reverseSide, let selfie, let translation, let files, let plainData): - return ("inputSecureValue", [("flags", flags as Any), ("type", type as Any), ("data", data as Any), ("frontSide", frontSide as Any), ("reverseSide", reverseSide as Any), ("selfie", selfie as Any), ("translation", translation as Any), ("files", files as Any), ("plainData", plainData as Any)]) - } - } - - public static func parse_inputSecureValue(_ reader: BufferReader) -> InputSecureValue? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Api.SecureValueType? - if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.SecureValueType - } - var _3: Api.SecureData? - if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() { - _3 = Api.parse(reader, signature: signature) as? Api.SecureData - } } - var _4: Api.InputSecureFile? - if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() { - _4 = Api.parse(reader, signature: signature) as? Api.InputSecureFile - } } - var _5: Api.InputSecureFile? - if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { - _5 = Api.parse(reader, signature: signature) as? Api.InputSecureFile - } } - var _6: Api.InputSecureFile? - if Int(_1!) & Int(1 << 3) != 0 {if let signature = reader.readInt32() { - _6 = Api.parse(reader, signature: signature) as? Api.InputSecureFile - } } - var _7: [Api.InputSecureFile]? - if Int(_1!) & Int(1 << 6) != 0 {if let _ = reader.readInt32() { - _7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputSecureFile.self) - } } - var _8: [Api.InputSecureFile]? - if Int(_1!) & Int(1 << 4) != 0 {if let _ = reader.readInt32() { - _8 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputSecureFile.self) - } } - var _9: Api.SecurePlainData? - if Int(_1!) & Int(1 << 5) != 0 {if let signature = reader.readInt32() { - _9 = Api.parse(reader, signature: signature) as? Api.SecurePlainData - } } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 3) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 6) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 4) == 0) || _8 != nil - let _c9 = (Int(_1!) & Int(1 << 5) == 0) || _9 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { - return Api.InputSecureValue.inputSecureValue(flags: _1!, type: _2!, data: _3, frontSide: _4, reverseSide: _5, selfie: _6, translation: _7, files: _8, plainData: _9) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api12.swift b/submodules/TelegramApi/Sources/Api12.swift index 91a4fdefa6..ac16a87240 100644 --- a/submodules/TelegramApi/Sources/Api12.swift +++ b/submodules/TelegramApi/Sources/Api12.swift @@ -1,3 +1,321 @@ +public extension Api { + enum InputQuickReplyShortcut: TypeConstructorDescription { + case inputQuickReplyShortcut(shortcut: String) + case inputQuickReplyShortcutId(shortcutId: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputQuickReplyShortcut(let shortcut): + if boxed { + buffer.appendInt32(609840449) + } + serializeString(shortcut, buffer: buffer, boxed: false) + break + case .inputQuickReplyShortcutId(let shortcutId): + if boxed { + buffer.appendInt32(18418929) + } + serializeInt32(shortcutId, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputQuickReplyShortcut(let shortcut): + return ("inputQuickReplyShortcut", [("shortcut", shortcut as Any)]) + case .inputQuickReplyShortcutId(let shortcutId): + return ("inputQuickReplyShortcutId", [("shortcutId", shortcutId as Any)]) + } + } + + public static func parse_inputQuickReplyShortcut(_ reader: BufferReader) -> InputQuickReplyShortcut? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return Api.InputQuickReplyShortcut.inputQuickReplyShortcut(shortcut: _1!) + } + else { + return nil + } + } + public static func parse_inputQuickReplyShortcutId(_ reader: BufferReader) -> InputQuickReplyShortcut? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return Api.InputQuickReplyShortcut.inputQuickReplyShortcutId(shortcutId: _1!) + } + else { + return nil + } + } + + } +} +public extension Api { + indirect enum InputReplyTo: TypeConstructorDescription { + case inputReplyToMessage(flags: Int32, replyToMsgId: Int32, topMsgId: Int32?, replyToPeerId: Api.InputPeer?, quoteText: String?, quoteEntities: [Api.MessageEntity]?, quoteOffset: Int32?) + case inputReplyToStory(peer: Api.InputPeer, storyId: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputReplyToMessage(let flags, let replyToMsgId, let topMsgId, let replyToPeerId, let quoteText, let quoteEntities, let quoteOffset): + if boxed { + buffer.appendInt32(583071445) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(replyToMsgId, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeInt32(topMsgId!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 1) != 0 {replyToPeerId!.serialize(buffer, true)} + if Int(flags) & Int(1 << 2) != 0 {serializeString(quoteText!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 3) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(quoteEntities!.count)) + for item in quoteEntities! { + item.serialize(buffer, true) + }} + if Int(flags) & Int(1 << 4) != 0 {serializeInt32(quoteOffset!, buffer: buffer, boxed: false)} + break + case .inputReplyToStory(let peer, let storyId): + if boxed { + buffer.appendInt32(1484862010) + } + peer.serialize(buffer, true) + serializeInt32(storyId, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputReplyToMessage(let flags, let replyToMsgId, let topMsgId, let replyToPeerId, let quoteText, let quoteEntities, let quoteOffset): + return ("inputReplyToMessage", [("flags", flags as Any), ("replyToMsgId", replyToMsgId as Any), ("topMsgId", topMsgId as Any), ("replyToPeerId", replyToPeerId as Any), ("quoteText", quoteText as Any), ("quoteEntities", quoteEntities as Any), ("quoteOffset", quoteOffset as Any)]) + case .inputReplyToStory(let peer, let storyId): + return ("inputReplyToStory", [("peer", peer as Any), ("storyId", storyId as Any)]) + } + } + + public static func parse_inputReplyToMessage(_ reader: BufferReader) -> InputReplyTo? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + if Int(_1!) & Int(1 << 0) != 0 {_3 = reader.readInt32() } + var _4: Api.InputPeer? + if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() { + _4 = Api.parse(reader, signature: signature) as? Api.InputPeer + } } + var _5: String? + if Int(_1!) & Int(1 << 2) != 0 {_5 = parseString(reader) } + var _6: [Api.MessageEntity]? + if Int(_1!) & Int(1 << 3) != 0 {if let _ = reader.readInt32() { + _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) + } } + var _7: Int32? + if Int(_1!) & Int(1 << 4) != 0 {_7 = reader.readInt32() } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil + let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil + let _c6 = (Int(_1!) & Int(1 << 3) == 0) || _6 != nil + let _c7 = (Int(_1!) & Int(1 << 4) == 0) || _7 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { + return Api.InputReplyTo.inputReplyToMessage(flags: _1!, replyToMsgId: _2!, topMsgId: _3, replyToPeerId: _4, quoteText: _5, quoteEntities: _6, quoteOffset: _7) + } + else { + return nil + } + } + public static func parse_inputReplyToStory(_ reader: BufferReader) -> InputReplyTo? { + var _1: Api.InputPeer? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.InputPeer + } + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.InputReplyTo.inputReplyToStory(peer: _1!, storyId: _2!) + } + else { + return nil + } + } + + } +} +public extension Api { + enum InputSecureFile: TypeConstructorDescription { + case inputSecureFile(id: Int64, accessHash: Int64) + case inputSecureFileUploaded(id: Int64, parts: Int32, md5Checksum: String, fileHash: Buffer, secret: Buffer) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputSecureFile(let id, let accessHash): + if boxed { + buffer.appendInt32(1399317950) + } + serializeInt64(id, buffer: buffer, boxed: false) + serializeInt64(accessHash, buffer: buffer, boxed: false) + break + case .inputSecureFileUploaded(let id, let parts, let md5Checksum, let fileHash, let secret): + if boxed { + buffer.appendInt32(859091184) + } + serializeInt64(id, buffer: buffer, boxed: false) + serializeInt32(parts, buffer: buffer, boxed: false) + serializeString(md5Checksum, buffer: buffer, boxed: false) + serializeBytes(fileHash, buffer: buffer, boxed: false) + serializeBytes(secret, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputSecureFile(let id, let accessHash): + return ("inputSecureFile", [("id", id as Any), ("accessHash", accessHash as Any)]) + case .inputSecureFileUploaded(let id, let parts, let md5Checksum, let fileHash, let secret): + return ("inputSecureFileUploaded", [("id", id as Any), ("parts", parts as Any), ("md5Checksum", md5Checksum as Any), ("fileHash", fileHash as Any), ("secret", secret as Any)]) + } + } + + public static func parse_inputSecureFile(_ reader: BufferReader) -> InputSecureFile? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int64? + _2 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.InputSecureFile.inputSecureFile(id: _1!, accessHash: _2!) + } + else { + return nil + } + } + public static func parse_inputSecureFileUploaded(_ reader: BufferReader) -> InputSecureFile? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int32? + _2 = reader.readInt32() + var _3: String? + _3 = parseString(reader) + var _4: Buffer? + _4 = parseBytes(reader) + var _5: Buffer? + _5 = parseBytes(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.InputSecureFile.inputSecureFileUploaded(id: _1!, parts: _2!, md5Checksum: _3!, fileHash: _4!, secret: _5!) + } + else { + return nil + } + } + + } +} +public extension Api { + enum InputSecureValue: TypeConstructorDescription { + case inputSecureValue(flags: Int32, type: Api.SecureValueType, data: Api.SecureData?, frontSide: Api.InputSecureFile?, reverseSide: Api.InputSecureFile?, selfie: Api.InputSecureFile?, translation: [Api.InputSecureFile]?, files: [Api.InputSecureFile]?, plainData: Api.SecurePlainData?) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputSecureValue(let flags, let type, let data, let frontSide, let reverseSide, let selfie, let translation, let files, let plainData): + if boxed { + buffer.appendInt32(-618540889) + } + serializeInt32(flags, buffer: buffer, boxed: false) + type.serialize(buffer, true) + if Int(flags) & Int(1 << 0) != 0 {data!.serialize(buffer, true)} + if Int(flags) & Int(1 << 1) != 0 {frontSide!.serialize(buffer, true)} + if Int(flags) & Int(1 << 2) != 0 {reverseSide!.serialize(buffer, true)} + if Int(flags) & Int(1 << 3) != 0 {selfie!.serialize(buffer, true)} + if Int(flags) & Int(1 << 6) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(translation!.count)) + for item in translation! { + item.serialize(buffer, true) + }} + if Int(flags) & Int(1 << 4) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(files!.count)) + for item in files! { + item.serialize(buffer, true) + }} + if Int(flags) & Int(1 << 5) != 0 {plainData!.serialize(buffer, true)} + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputSecureValue(let flags, let type, let data, let frontSide, let reverseSide, let selfie, let translation, let files, let plainData): + return ("inputSecureValue", [("flags", flags as Any), ("type", type as Any), ("data", data as Any), ("frontSide", frontSide as Any), ("reverseSide", reverseSide as Any), ("selfie", selfie as Any), ("translation", translation as Any), ("files", files as Any), ("plainData", plainData as Any)]) + } + } + + public static func parse_inputSecureValue(_ reader: BufferReader) -> InputSecureValue? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.SecureValueType? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.SecureValueType + } + var _3: Api.SecureData? + if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() { + _3 = Api.parse(reader, signature: signature) as? Api.SecureData + } } + var _4: Api.InputSecureFile? + if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() { + _4 = Api.parse(reader, signature: signature) as? Api.InputSecureFile + } } + var _5: Api.InputSecureFile? + if Int(_1!) & Int(1 << 2) != 0 {if let signature = reader.readInt32() { + _5 = Api.parse(reader, signature: signature) as? Api.InputSecureFile + } } + var _6: Api.InputSecureFile? + if Int(_1!) & Int(1 << 3) != 0 {if let signature = reader.readInt32() { + _6 = Api.parse(reader, signature: signature) as? Api.InputSecureFile + } } + var _7: [Api.InputSecureFile]? + if Int(_1!) & Int(1 << 6) != 0 {if let _ = reader.readInt32() { + _7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputSecureFile.self) + } } + var _8: [Api.InputSecureFile]? + if Int(_1!) & Int(1 << 4) != 0 {if let _ = reader.readInt32() { + _8 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputSecureFile.self) + } } + var _9: Api.SecurePlainData? + if Int(_1!) & Int(1 << 5) != 0 {if let signature = reader.readInt32() { + _9 = Api.parse(reader, signature: signature) as? Api.SecurePlainData + } } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil + let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil + let _c6 = (Int(_1!) & Int(1 << 3) == 0) || _6 != nil + let _c7 = (Int(_1!) & Int(1 << 6) == 0) || _7 != nil + let _c8 = (Int(_1!) & Int(1 << 4) == 0) || _8 != nil + let _c9 = (Int(_1!) & Int(1 << 5) == 0) || _9 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { + return Api.InputSecureValue.inputSecureValue(flags: _1!, type: _2!, data: _3, frontSide: _4, reverseSide: _5, selfie: _6, translation: _7, files: _8, plainData: _9) + } + else { + return nil + } + } + + } +} public extension Api { indirect enum InputSingleMedia: TypeConstructorDescription { case inputSingleMedia(flags: Int32, media: Api.InputMedia, randomId: Int64, message: String, entities: [Api.MessageEntity]?) @@ -760,177 +1078,3 @@ public extension Api { } } -public extension Api { - indirect enum InputUser: TypeConstructorDescription { - case inputUser(userId: Int64, accessHash: Int64) - case inputUserEmpty - case inputUserFromMessage(peer: Api.InputPeer, msgId: Int32, userId: Int64) - case inputUserSelf - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inputUser(let userId, let accessHash): - if boxed { - buffer.appendInt32(-233744186) - } - serializeInt64(userId, buffer: buffer, boxed: false) - serializeInt64(accessHash, buffer: buffer, boxed: false) - break - case .inputUserEmpty: - if boxed { - buffer.appendInt32(-1182234929) - } - - break - case .inputUserFromMessage(let peer, let msgId, let userId): - if boxed { - buffer.appendInt32(497305826) - } - peer.serialize(buffer, true) - serializeInt32(msgId, buffer: buffer, boxed: false) - serializeInt64(userId, buffer: buffer, boxed: false) - break - case .inputUserSelf: - if boxed { - buffer.appendInt32(-138301121) - } - - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .inputUser(let userId, let accessHash): - return ("inputUser", [("userId", userId as Any), ("accessHash", accessHash as Any)]) - case .inputUserEmpty: - return ("inputUserEmpty", []) - case .inputUserFromMessage(let peer, let msgId, let userId): - return ("inputUserFromMessage", [("peer", peer as Any), ("msgId", msgId as Any), ("userId", userId as Any)]) - case .inputUserSelf: - return ("inputUserSelf", []) - } - } - - public static func parse_inputUser(_ reader: BufferReader) -> InputUser? { - var _1: Int64? - _1 = reader.readInt64() - var _2: Int64? - _2 = reader.readInt64() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.InputUser.inputUser(userId: _1!, accessHash: _2!) - } - else { - return nil - } - } - public static func parse_inputUserEmpty(_ reader: BufferReader) -> InputUser? { - return Api.InputUser.inputUserEmpty - } - public static func parse_inputUserFromMessage(_ reader: BufferReader) -> InputUser? { - var _1: Api.InputPeer? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.InputPeer - } - var _2: Int32? - _2 = reader.readInt32() - var _3: Int64? - _3 = reader.readInt64() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.InputUser.inputUserFromMessage(peer: _1!, msgId: _2!, userId: _3!) - } - else { - return nil - } - } - public static func parse_inputUserSelf(_ reader: BufferReader) -> InputUser? { - return Api.InputUser.inputUserSelf - } - - } -} -public extension Api { - enum InputWallPaper: TypeConstructorDescription { - case inputWallPaper(id: Int64, accessHash: Int64) - case inputWallPaperNoFile(id: Int64) - case inputWallPaperSlug(slug: String) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inputWallPaper(let id, let accessHash): - if boxed { - buffer.appendInt32(-433014407) - } - serializeInt64(id, buffer: buffer, boxed: false) - serializeInt64(accessHash, buffer: buffer, boxed: false) - break - case .inputWallPaperNoFile(let id): - if boxed { - buffer.appendInt32(-1770371538) - } - serializeInt64(id, buffer: buffer, boxed: false) - break - case .inputWallPaperSlug(let slug): - if boxed { - buffer.appendInt32(1913199744) - } - serializeString(slug, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .inputWallPaper(let id, let accessHash): - return ("inputWallPaper", [("id", id as Any), ("accessHash", accessHash as Any)]) - case .inputWallPaperNoFile(let id): - return ("inputWallPaperNoFile", [("id", id as Any)]) - case .inputWallPaperSlug(let slug): - return ("inputWallPaperSlug", [("slug", slug as Any)]) - } - } - - public static func parse_inputWallPaper(_ reader: BufferReader) -> InputWallPaper? { - var _1: Int64? - _1 = reader.readInt64() - var _2: Int64? - _2 = reader.readInt64() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.InputWallPaper.inputWallPaper(id: _1!, accessHash: _2!) - } - else { - return nil - } - } - public static func parse_inputWallPaperNoFile(_ reader: BufferReader) -> InputWallPaper? { - var _1: Int64? - _1 = reader.readInt64() - let _c1 = _1 != nil - if _c1 { - return Api.InputWallPaper.inputWallPaperNoFile(id: _1!) - } - else { - return nil - } - } - public static func parse_inputWallPaperSlug(_ reader: BufferReader) -> InputWallPaper? { - var _1: String? - _1 = parseString(reader) - let _c1 = _1 != nil - if _c1 { - return Api.InputWallPaper.inputWallPaperSlug(slug: _1!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api13.swift b/submodules/TelegramApi/Sources/Api13.swift index 81b087af3a..c66b7ed17b 100644 --- a/submodules/TelegramApi/Sources/Api13.swift +++ b/submodules/TelegramApi/Sources/Api13.swift @@ -1,3 +1,177 @@ +public extension Api { + indirect enum InputUser: TypeConstructorDescription { + case inputUser(userId: Int64, accessHash: Int64) + case inputUserEmpty + case inputUserFromMessage(peer: Api.InputPeer, msgId: Int32, userId: Int64) + case inputUserSelf + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputUser(let userId, let accessHash): + if boxed { + buffer.appendInt32(-233744186) + } + serializeInt64(userId, buffer: buffer, boxed: false) + serializeInt64(accessHash, buffer: buffer, boxed: false) + break + case .inputUserEmpty: + if boxed { + buffer.appendInt32(-1182234929) + } + + break + case .inputUserFromMessage(let peer, let msgId, let userId): + if boxed { + buffer.appendInt32(497305826) + } + peer.serialize(buffer, true) + serializeInt32(msgId, buffer: buffer, boxed: false) + serializeInt64(userId, buffer: buffer, boxed: false) + break + case .inputUserSelf: + if boxed { + buffer.appendInt32(-138301121) + } + + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputUser(let userId, let accessHash): + return ("inputUser", [("userId", userId as Any), ("accessHash", accessHash as Any)]) + case .inputUserEmpty: + return ("inputUserEmpty", []) + case .inputUserFromMessage(let peer, let msgId, let userId): + return ("inputUserFromMessage", [("peer", peer as Any), ("msgId", msgId as Any), ("userId", userId as Any)]) + case .inputUserSelf: + return ("inputUserSelf", []) + } + } + + public static func parse_inputUser(_ reader: BufferReader) -> InputUser? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int64? + _2 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.InputUser.inputUser(userId: _1!, accessHash: _2!) + } + else { + return nil + } + } + public static func parse_inputUserEmpty(_ reader: BufferReader) -> InputUser? { + return Api.InputUser.inputUserEmpty + } + public static func parse_inputUserFromMessage(_ reader: BufferReader) -> InputUser? { + var _1: Api.InputPeer? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.InputPeer + } + var _2: Int32? + _2 = reader.readInt32() + var _3: Int64? + _3 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.InputUser.inputUserFromMessage(peer: _1!, msgId: _2!, userId: _3!) + } + else { + return nil + } + } + public static func parse_inputUserSelf(_ reader: BufferReader) -> InputUser? { + return Api.InputUser.inputUserSelf + } + + } +} +public extension Api { + enum InputWallPaper: TypeConstructorDescription { + case inputWallPaper(id: Int64, accessHash: Int64) + case inputWallPaperNoFile(id: Int64) + case inputWallPaperSlug(slug: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputWallPaper(let id, let accessHash): + if boxed { + buffer.appendInt32(-433014407) + } + serializeInt64(id, buffer: buffer, boxed: false) + serializeInt64(accessHash, buffer: buffer, boxed: false) + break + case .inputWallPaperNoFile(let id): + if boxed { + buffer.appendInt32(-1770371538) + } + serializeInt64(id, buffer: buffer, boxed: false) + break + case .inputWallPaperSlug(let slug): + if boxed { + buffer.appendInt32(1913199744) + } + serializeString(slug, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputWallPaper(let id, let accessHash): + return ("inputWallPaper", [("id", id as Any), ("accessHash", accessHash as Any)]) + case .inputWallPaperNoFile(let id): + return ("inputWallPaperNoFile", [("id", id as Any)]) + case .inputWallPaperSlug(let slug): + return ("inputWallPaperSlug", [("slug", slug as Any)]) + } + } + + public static func parse_inputWallPaper(_ reader: BufferReader) -> InputWallPaper? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int64? + _2 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.InputWallPaper.inputWallPaper(id: _1!, accessHash: _2!) + } + else { + return nil + } + } + public static func parse_inputWallPaperNoFile(_ reader: BufferReader) -> InputWallPaper? { + var _1: Int64? + _1 = reader.readInt64() + let _c1 = _1 != nil + if _c1 { + return Api.InputWallPaper.inputWallPaperNoFile(id: _1!) + } + else { + return nil + } + } + public static func parse_inputWallPaperSlug(_ reader: BufferReader) -> InputWallPaper? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return Api.InputWallPaper.inputWallPaperSlug(slug: _1!) + } + else { + return nil + } + } + + } +} public extension Api { enum InputWebDocument: TypeConstructorDescription { case inputWebDocument(url: String, size: Int32, mimeType: String, attributes: [Api.DocumentAttribute]) @@ -900,139 +1074,3 @@ public extension Api { } } -public extension Api { - enum KeyboardButtonRow: TypeConstructorDescription { - case keyboardButtonRow(buttons: [Api.KeyboardButton]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .keyboardButtonRow(let buttons): - if boxed { - buffer.appendInt32(2002815875) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(buttons.count)) - for item in buttons { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .keyboardButtonRow(let buttons): - return ("keyboardButtonRow", [("buttons", buttons as Any)]) - } - } - - public static func parse_keyboardButtonRow(_ reader: BufferReader) -> KeyboardButtonRow? { - var _1: [Api.KeyboardButton]? - if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.KeyboardButton.self) - } - let _c1 = _1 != nil - if _c1 { - return Api.KeyboardButtonRow.keyboardButtonRow(buttons: _1!) - } - else { - return nil - } - } - - } -} -public extension Api { - enum LabeledPrice: TypeConstructorDescription { - case labeledPrice(label: String, amount: Int64) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .labeledPrice(let label, let amount): - if boxed { - buffer.appendInt32(-886477832) - } - serializeString(label, buffer: buffer, boxed: false) - serializeInt64(amount, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .labeledPrice(let label, let amount): - return ("labeledPrice", [("label", label as Any), ("amount", amount as Any)]) - } - } - - public static func parse_labeledPrice(_ reader: BufferReader) -> LabeledPrice? { - var _1: String? - _1 = parseString(reader) - var _2: Int64? - _2 = reader.readInt64() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.LabeledPrice.labeledPrice(label: _1!, amount: _2!) - } - else { - return nil - } - } - - } -} -public extension Api { - enum LangPackDifference: TypeConstructorDescription { - case langPackDifference(langCode: String, fromVersion: Int32, version: Int32, strings: [Api.LangPackString]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .langPackDifference(let langCode, let fromVersion, let version, let strings): - if boxed { - buffer.appendInt32(-209337866) - } - serializeString(langCode, buffer: buffer, boxed: false) - serializeInt32(fromVersion, buffer: buffer, boxed: false) - serializeInt32(version, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(strings.count)) - for item in strings { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .langPackDifference(let langCode, let fromVersion, let version, let strings): - return ("langPackDifference", [("langCode", langCode as Any), ("fromVersion", fromVersion as Any), ("version", version as Any), ("strings", strings as Any)]) - } - } - - public static func parse_langPackDifference(_ reader: BufferReader) -> LangPackDifference? { - var _1: String? - _1 = parseString(reader) - var _2: Int32? - _2 = reader.readInt32() - var _3: Int32? - _3 = reader.readInt32() - var _4: [Api.LangPackString]? - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.LangPackString.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.LangPackDifference.langPackDifference(langCode: _1!, fromVersion: _2!, version: _3!, strings: _4!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api14.swift b/submodules/TelegramApi/Sources/Api14.swift index d7f64bdd25..6bdd0446fe 100644 --- a/submodules/TelegramApi/Sources/Api14.swift +++ b/submodules/TelegramApi/Sources/Api14.swift @@ -1,3 +1,139 @@ +public extension Api { + enum KeyboardButtonRow: TypeConstructorDescription { + case keyboardButtonRow(buttons: [Api.KeyboardButton]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .keyboardButtonRow(let buttons): + if boxed { + buffer.appendInt32(2002815875) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(buttons.count)) + for item in buttons { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .keyboardButtonRow(let buttons): + return ("keyboardButtonRow", [("buttons", buttons as Any)]) + } + } + + public static func parse_keyboardButtonRow(_ reader: BufferReader) -> KeyboardButtonRow? { + var _1: [Api.KeyboardButton]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.KeyboardButton.self) + } + let _c1 = _1 != nil + if _c1 { + return Api.KeyboardButtonRow.keyboardButtonRow(buttons: _1!) + } + else { + return nil + } + } + + } +} +public extension Api { + enum LabeledPrice: TypeConstructorDescription { + case labeledPrice(label: String, amount: Int64) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .labeledPrice(let label, let amount): + if boxed { + buffer.appendInt32(-886477832) + } + serializeString(label, buffer: buffer, boxed: false) + serializeInt64(amount, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .labeledPrice(let label, let amount): + return ("labeledPrice", [("label", label as Any), ("amount", amount as Any)]) + } + } + + public static func parse_labeledPrice(_ reader: BufferReader) -> LabeledPrice? { + var _1: String? + _1 = parseString(reader) + var _2: Int64? + _2 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.LabeledPrice.labeledPrice(label: _1!, amount: _2!) + } + else { + return nil + } + } + + } +} +public extension Api { + enum LangPackDifference: TypeConstructorDescription { + case langPackDifference(langCode: String, fromVersion: Int32, version: Int32, strings: [Api.LangPackString]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .langPackDifference(let langCode, let fromVersion, let version, let strings): + if boxed { + buffer.appendInt32(-209337866) + } + serializeString(langCode, buffer: buffer, boxed: false) + serializeInt32(fromVersion, buffer: buffer, boxed: false) + serializeInt32(version, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(strings.count)) + for item in strings { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .langPackDifference(let langCode, let fromVersion, let version, let strings): + return ("langPackDifference", [("langCode", langCode as Any), ("fromVersion", fromVersion as Any), ("version", version as Any), ("strings", strings as Any)]) + } + } + + public static func parse_langPackDifference(_ reader: BufferReader) -> LangPackDifference? { + var _1: String? + _1 = parseString(reader) + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: [Api.LangPackString]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.LangPackString.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.LangPackDifference.langPackDifference(langCode: _1!, fromVersion: _2!, version: _3!, strings: _4!) + } + else { + return nil + } + } + + } +} public extension Api { enum LangPackLanguage: TypeConstructorDescription { case langPackLanguage(flags: Int32, name: String, nativeName: String, langCode: String, baseLangCode: String?, pluralCode: String, stringsCount: Int32, translatedCount: Int32, translationsUrl: String) diff --git a/submodules/TelegramApi/Sources/Api9.swift b/submodules/TelegramApi/Sources/Api9.swift index 85225552db..4ef5bacfdd 100644 --- a/submodules/TelegramApi/Sources/Api9.swift +++ b/submodules/TelegramApi/Sources/Api9.swift @@ -412,6 +412,7 @@ public extension Api { enum InputFile: TypeConstructorDescription { case inputFile(id: Int64, parts: Int32, name: String, md5Checksum: String) case inputFileBig(id: Int64, parts: Int32, name: String) + case inputFileStoryDocument(id: Api.InputDocument) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -432,6 +433,12 @@ public extension Api { serializeInt32(parts, buffer: buffer, boxed: false) serializeString(name, buffer: buffer, boxed: false) break + case .inputFileStoryDocument(let id): + if boxed { + buffer.appendInt32(1658620744) + } + id.serialize(buffer, true) + break } } @@ -441,6 +448,8 @@ public extension Api { return ("inputFile", [("id", id as Any), ("parts", parts as Any), ("name", name as Any), ("md5Checksum", md5Checksum as Any)]) case .inputFileBig(let id, let parts, let name): return ("inputFileBig", [("id", id as Any), ("parts", parts as Any), ("name", name as Any)]) + case .inputFileStoryDocument(let id): + return ("inputFileStoryDocument", [("id", id as Any)]) } } @@ -481,6 +490,19 @@ public extension Api { return nil } } + public static func parse_inputFileStoryDocument(_ reader: BufferReader) -> InputFile? { + var _1: Api.InputDocument? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.InputDocument + } + let _c1 = _1 != nil + if _c1 { + return Api.InputFile.inputFileStoryDocument(id: _1!) + } + else { + return nil + } + } } } @@ -1002,115 +1024,3 @@ public extension Api { } } -public extension Api { - indirect enum InputInvoice: TypeConstructorDescription { - case inputInvoiceMessage(peer: Api.InputPeer, msgId: Int32) - case inputInvoicePremiumGiftCode(purpose: Api.InputStorePaymentPurpose, option: Api.PremiumGiftCodeOption) - case inputInvoiceSlug(slug: String) - case inputInvoiceStars(purpose: Api.InputStorePaymentPurpose) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inputInvoiceMessage(let peer, let msgId): - if boxed { - buffer.appendInt32(-977967015) - } - peer.serialize(buffer, true) - serializeInt32(msgId, buffer: buffer, boxed: false) - break - case .inputInvoicePremiumGiftCode(let purpose, let option): - if boxed { - buffer.appendInt32(-1734841331) - } - purpose.serialize(buffer, true) - option.serialize(buffer, true) - break - case .inputInvoiceSlug(let slug): - if boxed { - buffer.appendInt32(-1020867857) - } - serializeString(slug, buffer: buffer, boxed: false) - break - case .inputInvoiceStars(let purpose): - if boxed { - buffer.appendInt32(1710230755) - } - purpose.serialize(buffer, true) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .inputInvoiceMessage(let peer, let msgId): - return ("inputInvoiceMessage", [("peer", peer as Any), ("msgId", msgId as Any)]) - case .inputInvoicePremiumGiftCode(let purpose, let option): - return ("inputInvoicePremiumGiftCode", [("purpose", purpose as Any), ("option", option as Any)]) - case .inputInvoiceSlug(let slug): - return ("inputInvoiceSlug", [("slug", slug as Any)]) - case .inputInvoiceStars(let purpose): - return ("inputInvoiceStars", [("purpose", purpose as Any)]) - } - } - - public static func parse_inputInvoiceMessage(_ reader: BufferReader) -> InputInvoice? { - var _1: Api.InputPeer? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.InputPeer - } - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.InputInvoice.inputInvoiceMessage(peer: _1!, msgId: _2!) - } - else { - return nil - } - } - public static func parse_inputInvoicePremiumGiftCode(_ reader: BufferReader) -> InputInvoice? { - var _1: Api.InputStorePaymentPurpose? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.InputStorePaymentPurpose - } - var _2: Api.PremiumGiftCodeOption? - if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.PremiumGiftCodeOption - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.InputInvoice.inputInvoicePremiumGiftCode(purpose: _1!, option: _2!) - } - else { - return nil - } - } - public static func parse_inputInvoiceSlug(_ reader: BufferReader) -> InputInvoice? { - var _1: String? - _1 = parseString(reader) - let _c1 = _1 != nil - if _c1 { - return Api.InputInvoice.inputInvoiceSlug(slug: _1!) - } - else { - return nil - } - } - public static func parse_inputInvoiceStars(_ reader: BufferReader) -> InputInvoice? { - var _1: Api.InputStorePaymentPurpose? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.InputStorePaymentPurpose - } - let _c1 = _1 != nil - if _c1 { - return Api.InputInvoice.inputInvoiceStars(purpose: _1!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift b/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift index edf64213f0..be44796999 100644 --- a/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift +++ b/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift @@ -14,7 +14,7 @@ private func copyOrMoveResourceData(from fromResource: MediaResource, to toResou } } -func applyMediaResourceChanges(from: Media, to: Media, postbox: Postbox, force: Bool) { +func applyMediaResourceChanges(from: Media, to: Media, postbox: Postbox, force: Bool, skipPreviews: Bool = false) { if let fromImage = from as? TelegramMediaImage, let toImage = to as? TelegramMediaImage { let fromSmallestRepresentation = smallestImageRepresentation(fromImage.representations) if let fromSmallestRepresentation = fromSmallestRepresentation, let toSmallestRepresentation = smallestImageRepresentation(toImage.representations) { @@ -32,11 +32,13 @@ func applyMediaResourceChanges(from: Media, to: Media, postbox: Postbox, force: } } } else if let fromFile = from as? TelegramMediaFile, let toFile = to as? TelegramMediaFile { - if let fromPreview = smallestImageRepresentation(fromFile.previewRepresentations), let toPreview = smallestImageRepresentation(toFile.previewRepresentations) { - copyOrMoveResourceData(from: fromPreview.resource, to: toPreview.resource, mediaBox: postbox.mediaBox) - } - if let fromVideoThumbnail = fromFile.videoThumbnails.first, let toVideoThumbnail = toFile.videoThumbnails.first, fromVideoThumbnail.resource.id != toVideoThumbnail.resource.id { - copyOrMoveResourceData(from: fromVideoThumbnail.resource, to: toVideoThumbnail.resource, mediaBox: postbox.mediaBox) + if !skipPreviews { + if let fromPreview = smallestImageRepresentation(fromFile.previewRepresentations), let toPreview = smallestImageRepresentation(toFile.previewRepresentations) { + copyOrMoveResourceData(from: fromPreview.resource, to: toPreview.resource, mediaBox: postbox.mediaBox) + } + if let fromVideoThumbnail = fromFile.videoThumbnails.first, let toVideoThumbnail = toFile.videoThumbnails.first, fromVideoThumbnail.resource.id != toVideoThumbnail.resource.id { + copyOrMoveResourceData(from: fromVideoThumbnail.resource, to: toVideoThumbnail.resource, mediaBox: postbox.mediaBox) + } } let videoFirstFrameFromPath = postbox.mediaBox.cachedRepresentationCompletePath(fromFile.resource.id, keepDuration: .general, representationId: "first-frame") let videoFirstFrameToPath = postbox.mediaBox.cachedRepresentationCompletePath(toFile.resource.id, keepDuration: .general, representationId: "first-frame") diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index 4c0b7329cb..0ff890c31b 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -1498,9 +1498,13 @@ func _internal_editStory(account: Account, peerId: PeerId, id: Int32, media: Eng return .single(.progress(progress.progress)) } + var updatingCoverTime = false let inputMedia: Api.InputMedia? if let result = result, case let .content(uploadedContent) = result, case let .media(media, _) = uploadedContent.content { inputMedia = media + } else if case let .existing(media) = media, let file = media as? TelegramMediaFile, let resource = file.resource as? CloudDocumentMediaResource { + inputMedia = .inputMediaUploadedDocument(flags: 0, file: .inputFileStoryDocument(id: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference))), thumb: nil, mimeType: file.mimeType, attributes: inputDocumentAttributesFromFileAttributes(file.attributes), stickers: nil, ttlSeconds: nil) + updatingCoverTime = true } else { inputMedia = nil } @@ -1566,7 +1570,7 @@ func _internal_editStory(account: Account, peerId: PeerId, id: Int32, media: Eng case let .storyItem(_, _, _, _, _, _, _, _, media, _, _, _, _): let (parsedMedia, _, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, account.peerId) if let parsedMedia = parsedMedia, let originalMedia = originalMedia { - applyMediaResourceChanges(from: originalMedia, to: parsedMedia, postbox: account.postbox, force: false) + applyMediaResourceChanges(from: originalMedia, to: parsedMedia, postbox: account.postbox, force: false, skipPreviews: updatingCoverTime) } default: break diff --git a/submodules/TelegramUI/Components/Chat/ChatEmptyNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatEmptyNode/BUILD index 405bbcd734..b374c14152 100644 --- a/submodules/TelegramUI/Components/Chat/ChatEmptyNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatEmptyNode/BUILD @@ -31,6 +31,7 @@ swift_library( "//submodules/ReactionSelectionNode", "//submodules/TelegramUI/Components/Chat/ChatMediaInputStickerGridItem", "//submodules/PremiumUI", + "//submodules/TelegramUI/Components/LottieComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift b/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift index d4388e4be2..7eccdcf765 100644 --- a/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift @@ -22,6 +22,7 @@ import ReactionSelectionNode import ChatMediaInputStickerGridItem import UndoUI import PremiumUI +import LottieComponent private protocol ChatEmptyNodeContent { func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize @@ -1203,7 +1204,7 @@ public final class ChatEmptyNodePremiumRequiredChatContent: ASDisplayNode, ChatE private let interaction: ChatPanelInterfaceInteraction? private let iconBackground: SimpleLayer - private let icon: UIImageView + private let icon = ComponentView() private let text = ComponentView() private let buttonTitle = ComponentView() private let button: HighlightTrackingButton @@ -1219,8 +1220,7 @@ public final class ChatEmptyNodePremiumRequiredChatContent: ASDisplayNode, ChatE self.interaction = interaction self.iconBackground = SimpleLayer() - self.icon = UIImageView(image: UIImage(bundleImageName: "Chat/Empty Chat/PremiumRequiredIcon")?.withRenderingMode(.alwaysTemplate)) - + self.button = HighlightTrackingButton() self.button.clipsToBounds = true @@ -1230,7 +1230,6 @@ public final class ChatEmptyNodePremiumRequiredChatContent: ASDisplayNode, ChatE super.init() self.layer.addSublayer(self.iconBackground) - self.view.addSubview(self.icon) if !self.isPremiumDisabled { self.view.addSubview(self.button) @@ -1330,11 +1329,28 @@ public final class ChatEmptyNodePremiumRequiredChatContent: ASDisplayNode, ChatE contentsHeight += iconBackgroundSize contentsHeight += iconTextSpacing - self.icon.tintColor = serviceColor.primaryText - if let image = self.icon.image { - transition.updateFrame(view: self.icon, frame: CGRect(origin: CGPoint(x: iconBackgroundFrame.minX + floor((iconBackgroundFrame.width - image.size.width) * 0.5), y: iconBackgroundFrame.minY + floor((iconBackgroundFrame.height - image.size.height) * 0.5)), size: image.size)) + let iconSize = self.icon.update( + transition: .immediate, + component: AnyComponent( + LottieComponent( + content: LottieComponent.AppBundleContent(name: "PremiumRequired"), + color: serviceColor.primaryText, + size: CGSize(width: 120.0, height: 120.0), + loop: true + ) + ), + environment: {}, + containerSize: CGSize(width: maxWidth - sideInset * 2.0, height: 500.0) + ) + let iconFrame = CGRect(origin: CGPoint(x: iconBackgroundFrame.minX + floor((iconBackgroundFrame.width - iconSize.width) * 0.5), y: iconBackgroundFrame.minY + floor((iconBackgroundFrame.height - iconSize.height) * 0.5)), size: iconSize) + if let iconView = self.icon.view { + if iconView.superview == nil { + iconView.isUserInteractionEnabled = false + self.view.addSubview(iconView) + } + iconView.frame = iconFrame } - + let textFrame = CGRect(origin: CGPoint(x: floor((contentsWidth - textSize.width) * 0.5), y: contentsHeight), size: textSize) if let textView = self.text.view { if textView.superview == nil { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift index 9d73bee8eb..a869fe1543 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift @@ -395,7 +395,11 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent actionTitle = item.presentationData.strings.Conversation_ViewMessage } case "telegram_user": - actionTitle = item.presentationData.strings.Conversation_UserSendMessage + if webpage.displayUrl.contains("?profile") { + actionTitle = item.presentationData.strings.Conversation_OpenProfile + } else { + actionTitle = item.presentationData.strings.Conversation_UserSendMessage + } case "telegram_channel_request": actionTitle = item.presentationData.strings.Conversation_RequestToJoinChannel case "telegram_chat_request", "telegram_megagroup_request": diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift index bd7d76ca41..9b259b362c 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift @@ -83,6 +83,17 @@ public extension MediaEditorScreen { transitionOut: nil ) + var videoPlaybackPosition = videoPlaybackPosition + if cover, case let .file(file) = storyItem.media { + videoPlaybackPosition = 0.0 + for attribute in file.attributes { + if case let .Video(_, _, _, _, coverTime) = attribute { + videoPlaybackPosition = coverTime + break + } + } + } + var updateProgressImpl: ((Float) -> Void)? let controller = MediaEditorScreen( context: context, @@ -248,7 +259,7 @@ public extension MediaEditorScreen { var updatedAttributes: [TelegramMediaFileAttribute] = [] for attribute in file.attributes { if case let .Video(duration, size, flags, preloadSize, _) = attribute { - updatedAttributes.append(.Video(duration: duration, size: size, flags: flags, preloadSize: preloadSize, coverTime: updatedCoverTimestamp)) + updatedAttributes.append(.Video(duration: duration, size: size, flags: flags, preloadSize: preloadSize, coverTime: min(duration, updatedCoverTimestamp))) } else { updatedAttributes.append(attribute) } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 49f044c0b2..49c5a40cff 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -2945,7 +2945,11 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate let mediaEditor = MediaEditor(context: self.context, mode: isStickerEditor ? .sticker : .default, subject: effectiveSubject.editorSubject, values: initialValues, hasHistogram: true) if let initialVideoPosition = controller.initialVideoPosition { - mediaEditor.seek(initialVideoPosition, andPlay: true) + if controller.isEditingStoryCover { + mediaEditor.setCoverImageTimestamp(initialVideoPosition) + } else { + mediaEditor.seek(initialVideoPosition, andPlay: true) + } } if case .message = subject, self.context.sharedContext.currentPresentationData.with({$0}).autoNightModeTriggered { mediaEditor.setNightTheme(true) @@ -6293,7 +6297,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate let content: UndoOverlayContent = .info( title: nil, - text: presentationData.strings.Story_Editor_TooltipWeatherLimitText.string, + text: presentationData.strings.Story_Editor_TooltipWeatherLimitText, timeout: nil, customUndoText: nil ) diff --git a/submodules/TelegramUI/Resources/Animations/PremiumRequired.tgs b/submodules/TelegramUI/Resources/Animations/PremiumRequired.tgs new file mode 100644 index 0000000000000000000000000000000000000000..036e8ac4d375fecd954e2748401363c87fd3de19 GIT binary patch literal 1676 zcmV;726OoziwFpHH>G6&18rq9s4B`b!4xCM$DLBu6FoEgsX4M~5gK312N6G{tJ z)zxiVU3ytpn|*bOW`{;aeEd~iI;HDsUD}5&R{gZw?k_Y}-Q3(T`LgR)hpxK3TGZA3 z^7E>t?=Qpj<^ip$tHbB&(iR@?KDS(~JDShq^%zE<17-6Iz8TJFv;W~Teaq(Dx zlX2DDx{Vc4W3y36XU5G25lyHu8N41wne>RVDWp@%xwX$-RiCBHVErhR!>AUG-&1+gka6lvCrK@#GnUW?Om4#%3dlN{ zYM_uMzoLcC@bZM@87_Tj97!+A^}6*cPlRWdJ;zKim)#W2nqIZDxD0^<6^zH~V1-sD zqh?TWS|Z2zTp*o=L_lx!M3IDqSK(aEP>wQ~0s_gSC=z?^xCUFqhh)40BMT2Wg0%u! z%7CGgs2oz4xbOi&1LA^ZQ0$-&kT)?$7b*o65KBdu_pOphi6R=UaW8nx86zjANbdBJ z_X*V!J1?YJ=y-~8nU5jVQdWu4pGY+2QK&>@a}c~{|D_bpVAF@rvG}51uU%m?6`xtk z>G|kbcvF;XdhO0(GlGsp&O&J%sB&?EKNCD1J;d0IDH_aC{DUJeDmn#N!W6|-9iifk zs~wnY4mE@lA_Ip3ttc*VP-210{QQrKwj*?t1IHwt(W`qAVrUI)t@bnyPHFK zp~|mzJkhPI;a%)*{vuZ53VVis988t7Lq|8cPK11E3K{s2OM$^(QA#2Aw9VGDnkr6I zHe-HBiu%T6+X0l3z9|)0QGjx|g43^|yEM_NycQ;;JVD<~pb4yzU&B3!lTg9qKGmB5Ht*Y?ah!yB{$LaE@wLmxz7@^ zRnT`R$<&Yej*d^w+Kj4BLcToN_ObhRd01c2L-qCny&r!5@t2D~R@aAJd-0A!`J%aa z)9ya(L9o6lS2>13is9S)iuul7b8G&$7*pgcY31^(Gs6GuAf#KKnsL2@ zes7l#-R-X3GQP~7__5oEJwLx+muL85%7QB=Y Date: Fri, 26 Jul 2024 21:41:03 +0200 Subject: [PATCH 35/41] Various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 2 + .../AccountContext/Sources/Premium.swift | 5 ++ .../TelegramEngine/Data/PeersData.swift | 82 ++++++++++++------ .../Sources/StarsTransactionsScreen.swift | 6 +- .../Privacy.imageset/Contents.json | 12 +++ .../Context Menu/Privacy.imageset/privacy.pdf | Bin 0 -> 2542 bytes .../WebUI/Sources/WebAppController.swift | 46 +++++++--- 7 files changed, 110 insertions(+), 43 deletions(-) create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Privacy.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Privacy.imageset/privacy.pdf diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index f55b0b7fea..becb070a68 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -12668,3 +12668,5 @@ Sorry for the inconvenience."; "WebApp.PrivacyPolicy" = "Privacy Policy"; "Conversation.OpenProfile" = "OPEN PROFILE"; + +"Stars.Intro.GiftStars" = "Gift Stars to Friends"; diff --git a/submodules/AccountContext/Sources/Premium.swift b/submodules/AccountContext/Sources/Premium.swift index 821887ec3f..52ee4ce8d4 100644 --- a/submodules/AccountContext/Sources/Premium.swift +++ b/submodules/AccountContext/Sources/Premium.swift @@ -139,6 +139,7 @@ public struct PremiumConfiguration { showPremiumGiftInAttachMenu: false, showPremiumGiftInTextField: false, giveawayGiftsPurchaseAvailable: false, + starsGiftsPurchaseAvailable: false, boostsPerGiftCount: 3, audioTransciptionTrialMaxDuration: 300, audioTransciptionTrialCount: 2, @@ -165,6 +166,7 @@ public struct PremiumConfiguration { public let showPremiumGiftInAttachMenu: Bool public let showPremiumGiftInTextField: Bool public let giveawayGiftsPurchaseAvailable: Bool + public let starsGiftsPurchaseAvailable: Bool public let boostsPerGiftCount: Int32 public let audioTransciptionTrialMaxDuration: Int32 public let audioTransciptionTrialCount: Int32 @@ -190,6 +192,7 @@ public struct PremiumConfiguration { showPremiumGiftInAttachMenu: Bool, showPremiumGiftInTextField: Bool, giveawayGiftsPurchaseAvailable: Bool, + starsGiftsPurchaseAvailable: Bool, boostsPerGiftCount: Int32, audioTransciptionTrialMaxDuration: Int32, audioTransciptionTrialCount: Int32, @@ -214,6 +217,7 @@ public struct PremiumConfiguration { self.showPremiumGiftInAttachMenu = showPremiumGiftInAttachMenu self.showPremiumGiftInTextField = showPremiumGiftInTextField self.giveawayGiftsPurchaseAvailable = giveawayGiftsPurchaseAvailable + self.starsGiftsPurchaseAvailable = starsGiftsPurchaseAvailable self.boostsPerGiftCount = boostsPerGiftCount self.audioTransciptionTrialMaxDuration = audioTransciptionTrialMaxDuration self.audioTransciptionTrialCount = audioTransciptionTrialCount @@ -246,6 +250,7 @@ public struct PremiumConfiguration { showPremiumGiftInAttachMenu: data["premium_gift_attach_menu_icon"] as? Bool ?? defaultValue.showPremiumGiftInAttachMenu, showPremiumGiftInTextField: data["premium_gift_text_field_icon"] as? Bool ?? defaultValue.showPremiumGiftInTextField, giveawayGiftsPurchaseAvailable: data["giveaway_gifts_purchase_available"] as? Bool ?? defaultValue.giveawayGiftsPurchaseAvailable, + starsGiftsPurchaseAvailable: data["stars_gifts_enabled"] as? Bool ?? defaultValue.starsGiftsPurchaseAvailable, boostsPerGiftCount: get(data["boosts_per_sent_gift"]) ?? defaultValue.boostsPerGiftCount, audioTransciptionTrialMaxDuration: get(data["transcribe_audio_trial_duration_max"]) ?? defaultValue.audioTransciptionTrialMaxDuration, audioTransciptionTrialCount: get(data["transcribe_audio_trial_weekly_number"]) ?? defaultValue.audioTransciptionTrialCount, diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift index 74c34c1c0b..4c6c5e094d 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift @@ -2076,32 +2076,60 @@ public extension TelegramEngine.EngineData.Item { } } -public struct BotMenu: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { -public typealias Result = Optional - -fileprivate var id: EnginePeer.Id -public var mapKey: EnginePeer.Id { -return self.id -} - -public init(id: EnginePeer.Id) { -self.id = id -} - -var key: PostboxViewKey { -return .cachedPeerData(peerId: self.id) -} - -func extract(view: PostboxView) -> Result { -guard let view = view as? CachedPeerDataView else { -preconditionFailure() -} -if let cachedData = view.cachedPeerData as? CachedUserData { -return cachedData.botInfo?.menuButton -} else { -return nil -} -} -} + public struct BotMenu: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { + public typealias Result = Optional + + fileprivate var id: EnginePeer.Id + public var mapKey: EnginePeer.Id { + return self.id + } + + public init(id: EnginePeer.Id) { + self.id = id + } + + var key: PostboxViewKey { + return .cachedPeerData(peerId: self.id) + } + + func extract(view: PostboxView) -> Result { + guard let view = view as? CachedPeerDataView else { + preconditionFailure() + } + if let cachedData = view.cachedPeerData as? CachedUserData { + return cachedData.botInfo?.menuButton + } else { + return nil + } + } + } + + public struct BotCommands: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { + public typealias Result = Optional<[BotCommand]> + + fileprivate var id: EnginePeer.Id + public var mapKey: EnginePeer.Id { + return self.id + } + + public init(id: EnginePeer.Id) { + self.id = id + } + + var key: PostboxViewKey { + return .cachedPeerData(peerId: self.id) + } + + func extract(view: PostboxView) -> Result { + guard let view = view as? CachedPeerDataView else { + preconditionFailure() + } + if let cachedData = view.cachedPeerData as? CachedUserData { + return cachedData.botInfo?.commands + } else { + return nil + } + } + } } } diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift index 04df414332..e6329ee119 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift @@ -538,7 +538,7 @@ final class StarsTransactionsScreenComponent: Component { component.buy() }, buyAds: nil, - additionalAction: AnyComponent( + additionalAction: premiumConfiguration.starsGiftsPurchaseAvailable ? AnyComponent( Button( content: AnyComponent( HStack([ @@ -548,7 +548,7 @@ final class StarsTransactionsScreenComponent: Component { ), AnyComponentWithIdentity( id: "label", - component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: "Gift Stars to Friends", font: Font.regular(17.0), textColor: environment.theme.list.itemAccentColor)))) + component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Stars_Intro_GiftStars, font: Font.regular(17.0), textColor: environment.theme.list.itemAccentColor)))) ) ], spacing: 6.0) @@ -557,7 +557,7 @@ final class StarsTransactionsScreenComponent: Component { component.gift() } ) - ) + ) : nil ) ))] )), diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Privacy.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Privacy.imageset/Contents.json new file mode 100644 index 0000000000..638a59cc88 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Privacy.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "privacy.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Privacy.imageset/privacy.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Privacy.imageset/privacy.pdf new file mode 100644 index 0000000000000000000000000000000000000000..e6d3dcbb39d0d4bc05755e10dcaa6c548e4c92d7 GIT binary patch literal 2542 zcmZWrXH-+^76qh8#Lyk8T!B!8kV2E*TPP9&h~TIJE)a!8lhCUa8Nft}R4GbPP#`o# zY;;8t1cr`RlD zDFh_?bABP{f8q#RLNFC10|?t-JQcJC{Yd^GJA-WqCy*FO4Fa^;;cje*U4cR+gZL0g z1UD_;nJ%j@lJd^b<`59FBf~#yM%jGafe!4k=c1kHFdr;iYxIHByt$vHIJYl-WJGzq zo?l>m`yBe^;|#(~%j`?shxPYQ-+r23%cvuK-9Pf>Rrf2yxN+9$n^*O02m9a7kz>N* zV%pA3BkB9j>tk1u=r?2O=B z^z-sbz@#-f7&^@8W-*r;^o-_0+Ba@DqJwZRU_wN@w&Y>gTq zXo4_83T9GFWg-I-$0)6d@(?6ytB&pOTe#5=JD0l{XyYSq)(jY{kH+hpaf)F016B+RWOpa$fZ z*)h3H^#qgMub$8g8A?q`U*(pg-(#6b~v$Y^C$N9g%1 zKHrEX4auej{-=Vq#z6t)a9w*75ow*<;#f4w*!oP~v4C_Lc!FBDi@1AaL7$@Z$)#$s zsU9c#bG z@Cnts@dwb(e1H2^*S9t7D1IW9vVY4Fs-QKY<)EU23sk!2AZFia50_xfvsKx-RFJIE zFjT1{uQXFW(z-SOiR?{_F1J7yUniZm3#U~2IcFwj_}l~a!DRjy>RSCYqFrQn#+?Im zN|A}5Rjoh2PvBZDza*de@xRbJcVNDrioGqvq5{>Iu%psx5E@GHWVUn=aQ`l^bFAt& zz1Fn==nVNr;ZYF>BmYsGu>2q1z47Q08Cdzo#$@=P(IiV#S)n``m za2zUGMzgrxOzSi4FEAuvqTG;Ya$F6b6@>E(lZ&w1UG!KwU8w_$;5pCh4WSO1^$ zK?0{W#Uvbx^BQ+yy;EHc=A@PNwkGkm?6V5w4SzQpX0Gh6Zudrg-l%$AM2kkZOzH2f zKqV?45xlVFRB4H-LcHI;zAu^*@Z;tQnX_LaYIPLuRa)Oon@Y=wO6V|}k-m-{mY-s( zT!m{htw&n8UcBz;i9e7X!Ji>{xsN)l;%SQRm=X85L6S@fd!Gi99V|L-*SMo3Pw^U`3c^q7Ita{|ohuhJ`to3mEf?TVUDoY?JT_Cv=79U-)*K?4*5>S!a#zoN2 z%?R*UbyafFglg!?T1AeB*+3irxV)ISQ`%$at!}vSWVR4mcrsb{-kdJh_3k}H+uf56LI-4+?(-9U#B8f@?eE$5&*zS?XKmg&258R+| zXV9O3wm0b(ESPhLuhcIO#y;%B>1yO1vneH0ULd+NTKXp)c?&-`5`zquA%o^CC-AB&4!%8 zek^W|JU47xn)bHz{wb4ij3}&;}_J5%~8_@s& literal 0 HcmV?d00001 diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index f6ecc633db..9b32c4a186 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -1987,7 +1987,12 @@ public final class WebAppController: ViewController, AttachmentContainable { @objc private func morePressed(node: ContextReferenceContentNode, gesture: ContextGesture?) { let context = self.context - let presentationData = self.presentationData + var presentationData = self.presentationData + if !presentationData.theme.overallDarkAppearance, let headerColor = self.controllerNode.headerColor { + if headerColor.lightness < 0.5 { + presentationData = presentationData.withUpdated(theme: defaultDarkPresentationTheme) + } + } let peerId = self.peerId let botId = self.botId @@ -1998,10 +2003,11 @@ public final class WebAppController: ViewController, AttachmentContainable { let items = combineLatest(queue: Queue.mainQueue(), context.engine.messages.attachMenuBots(), - context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.botId)) + context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.botId)), + context.engine.data.get(TelegramEngine.EngineData.Item.Peer.BotCommands(id: self.botId)) ) |> take(1) - |> map { [weak self] attachMenuBots, botPeer -> ContextController.Items in + |> map { [weak self] attachMenuBots, botPeer, botCommands -> ContextController.Items in var items: [ContextMenuItem] = [] let attachMenuBot = attachMenuBots.first(where: { $0.peer.id == botId && !$0.flags.contains(.notActivated) }) @@ -2068,15 +2074,7 @@ public final class WebAppController: ViewController, AttachmentContainable { self?.controllerNode.webView?.reload() }))) - -// items.append(.action(ContextMenuActionItem(text: presentationData.strings.WebApp_PrivacyPolicy, icon: { theme in -// return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Reload"), color: theme.contextMenu.primaryColor) -// }, action: { [weak self] c, _ in -// c?.dismiss(completion: nil) -// -// self?.controllerNode.webView?.reload() -// }))) - + items.append(.action(ContextMenuActionItem(text: presentationData.strings.WebApp_TermsOfUse, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Info"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in @@ -2096,6 +2094,28 @@ public final class WebAppController: ViewController, AttachmentContainable { }) }))) + if let botCommands { + for command in botCommands { + if command.text == "privacy" { + items.append(.action(ContextMenuActionItem(text: presentationData.strings.WebApp_PrivacyPolicy, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Privacy"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] c, _ in + c?.dismiss(completion: nil) + + guard let self else { + return + } + let _ = enqueueMessages(account: self.context.account, peerId: self.botId, messages: [.message(text: "/privacy", attributes: [], inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]).startStandalone() + + if let botPeer, let navigationController = self.getNavigationController() { + (self.parentController() as? AttachmentController)?.minimizeIfNeeded() + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(botPeer))) + } + }))) + } + } + } + if let _ = attachMenuBot, [.attachMenu, .settings, .generic].contains(source) { items.append(.action(ContextMenuActionItem(text: presentationData.strings.WebApp_RemoveBot, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) @@ -2117,7 +2137,7 @@ public final class WebAppController: ViewController, AttachmentContainable { return ContextController.Items(content: .list(items)) } - let contextController = ContextController(presentationData: self.presentationData, source: .reference(WebAppContextReferenceContentSource(controller: self, sourceNode: node)), items: items, gesture: gesture) + let contextController = ContextController(presentationData: presentationData, source: .reference(WebAppContextReferenceContentSource(controller: self, sourceNode: node)), items: items, gesture: gesture) self.presentInGlobalOverlay(contextController) } From 9bb789dc7587b5dbed2291cc60be127d45b59b07 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 26 Jul 2024 22:07:41 +0200 Subject: [PATCH 36/41] Various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 2 +- .../WebBrowserSettingsController.swift | 2 +- .../Sources/MonetizationBalanceItem.swift | 4 ++-- .../Sources/StatsOverviewItem.swift | 24 +++++++++---------- .../Sources/TonFormat.swift | 16 +++++++++++-- .../Sources/StarsBalanceComponent.swift | 2 +- .../Sources/StarsOverviewItemComponent.swift | 3 ++- .../Sources/StarsUtils.swift | 6 ----- versions.json | 2 +- 9 files changed, 34 insertions(+), 27 deletions(-) delete mode 100644 submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsUtils.swift diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index becb070a68..ae9452a0bf 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -12571,7 +12571,7 @@ Sorry for the inconvenience."; "WebBrowser.AutoLogin.Info" = "Use your Telegram account to automatically log in to websites opened in the in-app browser."; "WebBrowser.ClearCookies" = "Clear Cookies"; -"WebBrowser.ClearCookies.Info" = "Delete all cookies and cache in the Telegram in-app browser. This action will sign you out of most websites."; +"WebBrowser.ClearCookies.Info" = "Delete all cookies in the Telegram in-app browser. This action will sign you out of most websites."; "WebBrowser.ClearCookies.Succeed" = "Cookies cleared."; "WebBrowser.Exceptions.Title" = "NEVER OPEN IN THE IN-APP BROWSER"; diff --git a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift index 455b8d430a..903f4697ab 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift @@ -250,7 +250,7 @@ private func webBrowserSettingsControllerEntries(context: AccountContext, presen if settings.defaultWebBrowser == nil { entries.append(.clearCookies(presentationData.theme, presentationData.strings.WebBrowser_ClearCookies)) - entries.append(.clearCache(presentationData.theme, presentationData.strings.WebBrowser_ClearCache)) +// entries.append(.clearCache(presentationData.theme, presentationData.strings.WebBrowser_ClearCache)) entries.append(.clearCookiesInfo(presentationData.theme, presentationData.strings.WebBrowser_ClearCookies_Info)) entries.append(.exceptionsHeader(presentationData.theme, presentationData.strings.WebBrowser_Exceptions_Title)) diff --git a/submodules/StatisticsUI/Sources/MonetizationBalanceItem.swift b/submodules/StatisticsUI/Sources/MonetizationBalanceItem.swift index 2686024892..01839afd73 100644 --- a/submodules/StatisticsUI/Sources/MonetizationBalanceItem.swift +++ b/submodules/StatisticsUI/Sources/MonetizationBalanceItem.swift @@ -178,10 +178,10 @@ final class MonetizationBalanceItemNode: ListViewItemNode, ItemListItemNode { if let stats = item.stats as? RevenueStats { let cryptoValue = formatTonAmountText(stats.balances.availableBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator) 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))" + value = stats.balances.availableBalance == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.availableBalance, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))" } else if let stats = item.stats as? StarsRevenueStats { amountString = NSAttributedString(string: presentationStringsFormattedNumber(Int32(stats.balances.availableBalance), item.presentationData.dateTimeFormat.groupingSeparator), font: integralFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor) - value = stats.balances.availableBalance == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.availableBalance, divide: false, rate: stats.usdRate))" + value = stats.balances.availableBalance == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.availableBalance, divide: false, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))" isStars = true } else { fatalError() diff --git a/submodules/StatisticsUI/Sources/StatsOverviewItem.swift b/submodules/StatisticsUI/Sources/StatsOverviewItem.swift index a635f7fe6b..7df6ea96f5 100644 --- a/submodules/StatisticsUI/Sources/StatsOverviewItem.swift +++ b/submodules/StatisticsUI/Sources/StatsOverviewItem.swift @@ -774,7 +774,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.presentationData, formatTonAmountText(stats.balances.availableBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator), item.presentationData.strings.Monetization_StarsProceeds_Available, - (stats.balances.availableBalance == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.availableBalance, rate: stats.usdRate))", .generic), + (stats.balances.availableBalance == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.availableBalance, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .ton ) @@ -784,7 +784,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.presentationData, formatTonAmountText(stats.balances.currentBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator), item.presentationData.strings.Monetization_StarsProceeds_Current, - (stats.balances.currentBalance == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.currentBalance, rate: stats.usdRate))", .generic), + (stats.balances.currentBalance == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.currentBalance, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .ton ) @@ -794,7 +794,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.presentationData, formatTonAmountText(stats.balances.overallRevenue, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator), item.presentationData.strings.Monetization_StarsProceeds_Total, - (stats.balances.overallRevenue == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.overallRevenue, rate: stats.usdRate))", .generic), + (stats.balances.overallRevenue == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.overallRevenue, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .ton ) @@ -804,7 +804,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.presentationData, presentationStringsFormattedNumber(Int32(additionalStats.balances.availableBalance), item.presentationData.dateTimeFormat.groupingSeparator), " ", - (additionalStats.balances.availableBalance == 0 ? "" : "≈\(formatTonUsdValue(additionalStats.balances.availableBalance, divide: false, rate: additionalStats.usdRate))", .generic), + (additionalStats.balances.availableBalance == 0 ? "" : "≈\(formatTonUsdValue(additionalStats.balances.availableBalance, divide: false, rate: additionalStats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .stars ) @@ -814,7 +814,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.presentationData, presentationStringsFormattedNumber(Int32(additionalStats.balances.currentBalance), item.presentationData.dateTimeFormat.groupingSeparator), " ", - (additionalStats.balances.currentBalance == 0 ? "" : "≈\(formatTonUsdValue(additionalStats.balances.currentBalance, divide: false, rate: additionalStats.usdRate))", .generic), + (additionalStats.balances.currentBalance == 0 ? "" : "≈\(formatTonUsdValue(additionalStats.balances.currentBalance, divide: false, rate: additionalStats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .stars ) @@ -824,7 +824,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.presentationData, presentationStringsFormattedNumber(Int32(additionalStats.balances.overallRevenue), item.presentationData.dateTimeFormat.groupingSeparator), " ", - (additionalStats.balances.overallRevenue == 0 ? "" : "≈\(formatTonUsdValue(additionalStats.balances.overallRevenue, divide: false, rate: additionalStats.usdRate))", .generic), + (additionalStats.balances.overallRevenue == 0 ? "" : "≈\(formatTonUsdValue(additionalStats.balances.overallRevenue, divide: false, rate: additionalStats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .stars ) @@ -838,7 +838,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.presentationData, formatTonAmountText(stats.balances.availableBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator), item.presentationData.strings.Monetization_Overview_Available, - (stats.balances.availableBalance == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.availableBalance, rate: stats.usdRate))", .generic), + (stats.balances.availableBalance == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.availableBalance, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .ton ) @@ -848,7 +848,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.presentationData, formatTonAmountText(stats.balances.currentBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator), item.presentationData.strings.Monetization_Overview_Current, - (stats.balances.currentBalance == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.currentBalance, rate: stats.usdRate))", .generic), + (stats.balances.currentBalance == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.currentBalance, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .ton ) @@ -858,7 +858,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.presentationData, formatTonAmountText(stats.balances.overallRevenue, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator), item.presentationData.strings.Monetization_Overview_Total, - (stats.balances.overallRevenue == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.overallRevenue, rate: stats.usdRate))", .generic), + (stats.balances.overallRevenue == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.overallRevenue, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .ton ) @@ -873,7 +873,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.presentationData, presentationStringsFormattedNumber(Int32(stats.balances.availableBalance), item.presentationData.dateTimeFormat.groupingSeparator), item.presentationData.strings.Monetization_StarsProceeds_Available, - (stats.balances.availableBalance == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.availableBalance, rate: stats.usdRate))", .generic), + (stats.balances.availableBalance == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.availableBalance, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .stars ) @@ -883,7 +883,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.presentationData, presentationStringsFormattedNumber(Int32(stats.balances.currentBalance), item.presentationData.dateTimeFormat.groupingSeparator), item.presentationData.strings.Monetization_StarsProceeds_Current, - (stats.balances.currentBalance == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.currentBalance, rate: stats.usdRate))", .generic), + (stats.balances.currentBalance == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.currentBalance, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .stars ) @@ -893,7 +893,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.presentationData, presentationStringsFormattedNumber(Int32(stats.balances.overallRevenue), item.presentationData.dateTimeFormat.groupingSeparator), item.presentationData.strings.Monetization_StarsProceeds_Total, - (stats.balances.overallRevenue == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.overallRevenue, rate: stats.usdRate))", .generic), + (stats.balances.overallRevenue == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.overallRevenue, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .stars ) diff --git a/submodules/TelegramStringFormatting/Sources/TonFormat.swift b/submodules/TelegramStringFormatting/Sources/TonFormat.swift index c925081d93..c337430784 100644 --- a/submodules/TelegramStringFormatting/Sources/TonFormat.swift +++ b/submodules/TelegramStringFormatting/Sources/TonFormat.swift @@ -1,5 +1,6 @@ import Foundation import UIKit +import TelegramPresentationData let walletAddressLength: Int = 48 @@ -9,9 +10,20 @@ public func formatTonAddress(_ address: String) -> String { return address } -public func formatTonUsdValue(_ value: Int64, divide: Bool = true, rate: Double) -> String { +public func formatTonUsdValue(_ value: Int64, divide: Bool = true, rate: Double, dateTimeFormat: PresentationDateTimeFormat) -> String { + let decimalSeparator = dateTimeFormat.decimalSeparator let normalizedValue: Double = divide ? Double(value) / 1000000000 : Double(value) - let formattedValue = String(format: "%0.2f", normalizedValue * rate) + var formattedValue = String(format: "%0.2f", normalizedValue * rate) + formattedValue = formattedValue.replacingOccurrences(of: ".", with: decimalSeparator) + if let dotIndex = formattedValue.firstIndex(of: decimalSeparator.first!) { + let integerPartString = formattedValue[.. String { - let formattedValue = String(format: "%0.2f", (Double(value)) * rate) - return "$\(formattedValue)" -} diff --git a/versions.json b/versions.json index 2f50425e88..394e4986a7 100644 --- a/versions.json +++ b/versions.json @@ -1,5 +1,5 @@ { - "app": "10.14.3", + "app": "10.15", "xcode": "15.2", "bazel": "7.1.1", "macos": "13.0" From 0450f35da82f054d29ff9982e57d7c0c7c9d0d13 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 26 Jul 2024 22:18:13 +0200 Subject: [PATCH 37/41] Various fixes --- .../Sources/PhotoResources.swift | 33 ++++++++++++------- .../WebBrowserDomainExceptionItem.swift | 2 +- .../WebBrowserSettingsController.swift | 2 +- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/submodules/PhotoResources/Sources/PhotoResources.swift b/submodules/PhotoResources/Sources/PhotoResources.swift index 84a72e4897..c3b5f6000d 100644 --- a/submodules/PhotoResources/Sources/PhotoResources.swift +++ b/submodules/PhotoResources/Sources/PhotoResources.swift @@ -56,6 +56,10 @@ public func representationFetchRangeForDisplayAtSize(representation: TelegramMed } public func chatMessagePhotoDatas(postbox: Postbox, userLocation: MediaResourceUserLocation, customUserContentType: MediaResourceUserContentType? = nil, photoReference: ImageMediaReference, fullRepresentationSize: CGSize = CGSize(width: 1280.0, height: 1280.0), autoFetchFullSize: Bool = false, tryAdditionalRepresentations: Bool = false, synchronousLoad: Bool = false, useMiniThumbnailIfAvailable: Bool = false, forceThumbnail: Bool = false, automaticFetch: Bool = true) -> Signal, NoError> { + return chatMessagePhotoDatas(mediaBox: postbox.mediaBox, userLocation: userLocation, customUserContentType: customUserContentType, photoReference: photoReference, fullRepresentationSize: fullRepresentationSize, autoFetchFullSize: autoFetchFullSize, tryAdditionalRepresentations: tryAdditionalRepresentations, synchronousLoad: synchronousLoad, useMiniThumbnailIfAvailable: useMiniThumbnailIfAvailable, forceThumbnail: forceThumbnail, automaticFetch: automaticFetch) +} + +func chatMessagePhotoDatas(mediaBox: MediaBox, userLocation: MediaResourceUserLocation, customUserContentType: MediaResourceUserContentType? = nil, photoReference: ImageMediaReference, fullRepresentationSize: CGSize = CGSize(width: 1280.0, height: 1280.0), autoFetchFullSize: Bool = false, tryAdditionalRepresentations: Bool = false, synchronousLoad: Bool = false, useMiniThumbnailIfAvailable: Bool = false, forceThumbnail: Bool = false, automaticFetch: Bool = true) -> Signal, NoError> { if !forceThumbnail, let progressiveRepresentation = progressiveImageRepresentation(photoReference.media.representations), progressiveRepresentation.progressiveSizes.count > 1 { enum SizeSource { case miniThumbnail(data: Data) @@ -93,7 +97,7 @@ public func chatMessagePhotoDatas(postbox: Postbox, userLocation: MediaResourceU case let .miniThumbnail(data): return .single((source, data)) case let .image(size): - return postbox.mediaBox.resourceData(progressiveRepresentation.resource, size: Int64(progressiveRepresentation.progressiveSizes.last!), in: 0 ..< size, mode: .incremental, notifyAboutIncomplete: true, attemptSynchronously: synchronousLoad) + return mediaBox.resourceData(progressiveRepresentation.resource, size: Int64(progressiveRepresentation.progressiveSizes.last!), in: 0 ..< size, mode: .incremental, notifyAboutIncomplete: true, attemptSynchronously: synchronousLoad) |> map { (data, _) -> (SizeSource, Data?) in return (source, data) } @@ -131,9 +135,9 @@ public func chatMessagePhotoDatas(postbox: Postbox, userLocation: MediaResourceU var fetchDisposable: Disposable? if automaticFetch { if autoFetchFullSize { - fetchDisposable = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: customUserContentType ?? .image, reference: photoReference.resourceReference(progressiveRepresentation.resource), range: (0 ..< Int64(largestByteSize), .default), statsCategory: .image).start() + fetchDisposable = fetchedMediaResource(mediaBox: mediaBox, userLocation: userLocation, userContentType: customUserContentType ?? .image, reference: photoReference.resourceReference(progressiveRepresentation.resource), range: (0 ..< Int64(largestByteSize), .default), statsCategory: .image).start() } else if useMiniThumbnailIfAvailable { - fetchDisposable = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: customUserContentType ?? .image, reference: photoReference.resourceReference(progressiveRepresentation.resource), range: (0 ..< Int64(thumbnailByteSize), .default), statsCategory: .image).start() + fetchDisposable = fetchedMediaResource(mediaBox: mediaBox, userLocation: userLocation, userContentType: customUserContentType ?? .image, reference: photoReference.resourceReference(progressiveRepresentation.resource), range: (0 ..< Int64(thumbnailByteSize), .default), statsCategory: .image).start() } } @@ -145,8 +149,8 @@ public func chatMessagePhotoDatas(postbox: Postbox, userLocation: MediaResourceU } if !forceThumbnail || photoReference.media.immediateThumbnailData == nil, let smallestRepresentation = smallestImageRepresentation(photoReference.media.representations), let largestRepresentation = photoReference.media.representationForDisplayAtSize(PixelDimensions(width: Int32(fullRepresentationSize.width), height: Int32(fullRepresentationSize.height))), let fullRepresentation = largestImageRepresentation(photoReference.media.representations) { - let maybeFullSize = postbox.mediaBox.resourceData(largestRepresentation.resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: synchronousLoad) - let maybeLargestSize = postbox.mediaBox.resourceData(fullRepresentation.resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: synchronousLoad) + let maybeFullSize = mediaBox.resourceData(largestRepresentation.resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: synchronousLoad) + let maybeLargestSize = mediaBox.resourceData(fullRepresentation.resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: synchronousLoad) let signal = combineLatest(maybeFullSize, maybeLargestSize) |> take(1) @@ -163,16 +167,16 @@ public func chatMessagePhotoDatas(postbox: Postbox, userLocation: MediaResourceU if let _ = decodedThumbnailData { fetchedThumbnail = .complete() } else { - fetchedThumbnail = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: customUserContentType ?? .image, reference: photoReference.resourceReference(smallestRepresentation.resource), statsCategory: .image) + fetchedThumbnail = fetchedMediaResource(mediaBox: mediaBox, userLocation: userLocation, userContentType: customUserContentType ?? .image, reference: photoReference.resourceReference(smallestRepresentation.resource), statsCategory: .image) } - let fetchedFullSize = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: customUserContentType ?? .image, reference: photoReference.resourceReference(largestRepresentation.resource), statsCategory: .image) + let fetchedFullSize = fetchedMediaResource(mediaBox: mediaBox, userLocation: userLocation, userContentType: customUserContentType ?? .image, reference: photoReference.resourceReference(largestRepresentation.resource), statsCategory: .image) let anyThumbnail: [Signal<(MediaResourceData, ChatMessagePhotoQuality), NoError>] if tryAdditionalRepresentations { anyThumbnail = photoReference.media.representations.filter({ representation in return representation != largestRepresentation }).map({ representation -> Signal<(MediaResourceData, ChatMessagePhotoQuality), NoError> in - return postbox.mediaBox.resourceData(representation.resource) + return mediaBox.resourceData(representation.resource) |> take(1) |> map { data -> (MediaResourceData, ChatMessagePhotoQuality) in if representation.dimensions.width > 200 || representation.dimensions.height > 200 { @@ -193,7 +197,7 @@ public func chatMessagePhotoDatas(postbox: Postbox, userLocation: MediaResourceU return EmptyDisposable } else { let fetchedDisposable = fetchedThumbnail.start() - let thumbnailDisposable = postbox.mediaBox.resourceData(smallestRepresentation.resource, attemptSynchronously: synchronousLoad).start(next: { next in + let thumbnailDisposable = mediaBox.resourceData(smallestRepresentation.resource, attemptSynchronously: synchronousLoad).start(next: { next in subscriber.putNext(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: [])) }, error: subscriber.putError, completed: subscriber.putCompletion) @@ -222,7 +226,7 @@ public func chatMessagePhotoDatas(postbox: Postbox, userLocation: MediaResourceU if autoFetchFullSize && !useMiniThumbnailIfAvailable { fullSizeData = Signal, NoError> { subscriber in let fetchedFullSizeDisposable = fetchedFullSize.start() - let fullSizeDisposable = postbox.mediaBox.resourceData(largestRepresentation.resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: synchronousLoad).start(next: { next in + let fullSizeDisposable = mediaBox.resourceData(largestRepresentation.resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: synchronousLoad).start(next: { next in subscriber.putNext(Tuple(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete)) }, error: subscriber.putError, completed: subscriber.putCompletion) @@ -232,7 +236,7 @@ public func chatMessagePhotoDatas(postbox: Postbox, userLocation: MediaResourceU } } } else { - fullSizeData = postbox.mediaBox.resourceData(largestRepresentation.resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: synchronousLoad) + fullSizeData = mediaBox.resourceData(largestRepresentation.resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: synchronousLoad) |> map { next -> Tuple2 in return Tuple(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete) } @@ -600,6 +604,13 @@ public func chatMessagePhoto(postbox: Postbox, userLocation: MediaResourceUserLo } } +public func chatMessagePhoto(mediaBox: MediaBox, userLocation: MediaResourceUserLocation, userContentType customUserContentType: MediaResourceUserContentType? = nil, photoReference: ImageMediaReference, synchronousLoad: Bool = false, highQuality: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + return chatMessagePhotoInternal(photoData: chatMessagePhotoDatas(mediaBox: mediaBox, userLocation: userLocation, customUserContentType: customUserContentType, photoReference: photoReference, tryAdditionalRepresentations: true, synchronousLoad: synchronousLoad), synchronousLoad: synchronousLoad) + |> map { _, _, generate in + return generate + } +} + public enum ChatMessagePhotoQuality { case none case blurred diff --git a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserDomainExceptionItem.swift b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserDomainExceptionItem.swift index 3e66f1ff29..869532b3a8 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserDomainExceptionItem.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserDomainExceptionItem.swift @@ -208,7 +208,7 @@ final class WebBrowserDomainExceptionItemNode: ItemListRevealOptionsItemNode, It let iconSize = CGSize(width: 40.0, height: 40.0) var imageSize = iconSize if currentItem?.icon?.id != item.icon?.id, let icon = item.icon { - strongSelf.iconNode.setSignal(chatMessagePhoto(postbox: item.context.account.postbox, userLocation: .other, photoReference: .standalone(media: icon))) + strongSelf.iconNode.setSignal(chatMessagePhoto(mediaBox: item.context.sharedContext.accountManager.mediaBox, userLocation: .other, photoReference: .standalone(media: icon))) } if let icon = item.icon, let dimensions = largestImageRepresentation(icon.representations)?.dimensions.cgSize { imageSize = dimensions.aspectFilled(imageSize) diff --git a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift index 903f4697ab..53c073eae2 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift @@ -479,7 +479,7 @@ private func fetchDomainExceptionInfo(context: AccountContext, url: String) -> S var image: TelegramMediaImage? if let imageData, let parsedImage = UIImage(data: imageData) { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - context.account.postbox.mediaBox.storeResourceData(resource.id, data: imageData) + context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: imageData) image = TelegramMediaImage( imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: Int64.random(in: Int64.min ... Int64.max)), representations: [ From 32ace10858501b150860771727f5e876437c972e Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 26 Jul 2024 22:57:42 +0200 Subject: [PATCH 38/41] Various fixes --- .../Telegram-iOS/en.lproj/Localizable.strings | 2 + .../Sources/MediaPickerScreen.swift | 6 ++- .../Sources/MediaPickerTitleView.swift | 44 ++++++++++++++++-- .../Privacy.imageset/Contents.json | 2 +- .../Privacy.imageset/privacy (2).pdf | Bin 0 -> 2500 bytes .../Context Menu/Privacy.imageset/privacy.pdf | Bin 2542 -> 0 bytes 6 files changed, 48 insertions(+), 6 deletions(-) create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Privacy.imageset/privacy (2).pdf delete mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Privacy.imageset/privacy.pdf diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index ae9452a0bf..6c71dc1d97 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -12670,3 +12670,5 @@ Sorry for the inconvenience."; "Conversation.OpenProfile" = "OPEN PROFILE"; "Stars.Intro.GiftStars" = "Gift Stars to Friends"; + +"MediaPicker.CreateSticker" = "Create a sticker from a photo"; diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index d56dcdd359..40cd313cd8 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -1799,9 +1799,13 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { self.titleView.title = collection.localizedTitle ?? presentationData.strings.Attachment_Gallery } else { switch mode { - case .default, .createSticker: + case .default: self.titleView.title = presentationData.strings.MediaPicker_Recents self.titleView.isEnabled = true + case .createSticker: + self.titleView.title = presentationData.strings.MediaPicker_Recents + self.titleView.subtitle = presentationData.strings.MediaPicker_CreateSticker + self.titleView.isEnabled = true case .story: self.titleView.title = presentationData.strings.MediaPicker_Recents self.titleView.isEnabled = true diff --git a/submodules/MediaPickerUI/Sources/MediaPickerTitleView.swift b/submodules/MediaPickerUI/Sources/MediaPickerTitleView.swift index bd44af71bb..a9d360915b 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerTitleView.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerTitleView.swift @@ -10,12 +10,14 @@ final class MediaPickerTitleView: UIView { let contextSourceNode: ContextReferenceContentNode private let buttonNode: HighlightTrackingButtonNode private let titleNode: ImmediateTextNode + private let subtitleNode: ImmediateTextNode private let arrowNode: ASImageNode private let segmentedControlNode: SegmentedControlNode public var theme: PresentationTheme { didSet { self.titleNode.attributedText = NSAttributedString(string: self.title, font: NavigationBar.titleFont, textColor: theme.rootController.navigationBar.primaryTextColor) + self.subtitleNode.attributedText = NSAttributedString(string: self.subtitle, font: Font.regular(12.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) self.segmentedControlNode.updateTheme(SegmentedControlTheme(theme: self.theme)) } } @@ -23,7 +25,16 @@ final class MediaPickerTitleView: UIView { public var title: String = "" { didSet { if self.title != oldValue { - self.titleNode.attributedText = NSAttributedString(string: self.title, font: NavigationBar.titleFont, textColor: theme.rootController.navigationBar.primaryTextColor) + self.titleNode.attributedText = NSAttributedString(string: self.title, font: NavigationBar.titleFont, textColor: self.theme.rootController.navigationBar.primaryTextColor) + self.setNeedsLayout() + } + } + } + + public var subtitle: String = "" { + didSet { + if self.subtitle != oldValue { + self.subtitleNode.attributedText = NSAttributedString(string: self.subtitle, font: Font.regular(12.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) self.setNeedsLayout() } } @@ -36,7 +47,7 @@ final class MediaPickerTitleView: UIView { } } - public func updateTitle(title: String, isEnabled: Bool, animated: Bool) { + public func updateTitle(title: String, subtitle: String = "", isEnabled: Bool, animated: Bool) { if animated { if self.title != title { if let snapshotView = self.titleNode.view.snapshotContentTree() { @@ -49,6 +60,17 @@ final class MediaPickerTitleView: UIView { self.titleNode.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } } + if self.subtitle != subtitle { + if let snapshotView = self.subtitleNode.view.snapshotContentTree() { + snapshotView.frame = self.subtitleNode.frame + self.addSubview(snapshotView) + + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + snapshotView.removeFromSuperview() + }) + self.subtitleNode.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } if self.isEnabled != isEnabled { if let snapshotView = self.arrowNode.view.snapshotContentTree() { snapshotView.frame = self.arrowNode.frame @@ -62,6 +84,7 @@ final class MediaPickerTitleView: UIView { } } self.title = title + self.subtitle = subtitle self.isEnabled = isEnabled } @@ -76,6 +99,7 @@ final class MediaPickerTitleView: UIView { if self.segmentsHidden != oldValue { let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) transition.updateAlpha(node: self.titleNode, alpha: self.segmentsHidden ? 1.0 : 0.0) + transition.updateAlpha(node: self.subtitleNode, alpha: self.segmentsHidden ? 1.0 : 0.0) transition.updateAlpha(node: self.arrowNode, alpha: self.segmentsHidden ? 1.0 : 0.0) transition.updateAlpha(node: self.segmentedControlNode, alpha: self.segmentsHidden ? 0.0 : 1.0) self.segmentedControlNode.isUserInteractionEnabled = !self.segmentsHidden @@ -115,6 +139,9 @@ final class MediaPickerTitleView: UIView { self.titleNode = ImmediateTextNode() self.titleNode.displaysAsynchronously = false + self.subtitleNode = ImmediateTextNode() + self.subtitleNode.displaysAsynchronously = false + self.arrowNode = ASImageNode() self.arrowNode.displaysAsynchronously = false self.arrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/DownArrow"), color: theme.rootController.navigationBar.secondaryTextColor) @@ -145,6 +172,7 @@ final class MediaPickerTitleView: UIView { self.addSubnode(self.contextSourceNode) self.addSubnode(self.titleNode) + self.addSubnode(self.subtitleNode) self.addSubnode(self.arrowNode) self.addSubnode(self.buttonNode) self.addSubnode(self.segmentedControlNode) @@ -166,10 +194,18 @@ final class MediaPickerTitleView: UIView { self.segmentedControlNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - controlSize.width) / 2.0), y: floorToScreenPixels((size.height - controlSize.height) / 2.0)), size: controlSize) let titleSize = self.titleNode.updateLayout(CGSize(width: 210.0, height: 44.0)) - self.titleNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: floorToScreenPixels((size.height - titleSize.height) / 2.0)), size: titleSize) + let subtitleSize = self.subtitleNode.updateLayout(CGSize(width: 210.0, height: 44.0)) + + var totalHeight: CGFloat = titleSize.height + if subtitleSize.height > 0.0 { + totalHeight += subtitleSize.height + } + + self.titleNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: floorToScreenPixels((size.height - totalHeight) / 2.0)), size: titleSize) + self.subtitleNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - subtitleSize.width) / 2.0), y: floorToScreenPixels((size.height - totalHeight) / 2.0) + subtitleSize.height + 7.0), size: subtitleSize) if let arrowSize = self.arrowNode.image?.size { - self.arrowNode.frame = CGRect(origin: CGPoint(x: self.titleNode.frame.maxX + 5.0, y: floorToScreenPixels((size.height - arrowSize.height) / 2.0) + 1.0 - UIScreenPixel), size: arrowSize) + self.arrowNode.frame = CGRect(origin: CGPoint(x: self.titleNode.frame.maxX + 5.0, y: floorToScreenPixels((size.height - totalHeight) / 2.0) + titleSize.height / 2.0 - arrowSize.height / 2.0 + 1.0 - UIScreenPixel), size: arrowSize) } self.buttonNode.frame = CGRect(origin: .zero, size: size) } diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Privacy.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Privacy.imageset/Contents.json index 638a59cc88..6e7b34e21d 100644 --- a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Privacy.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Privacy.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "privacy.pdf", + "filename" : "privacy (2).pdf", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Privacy.imageset/privacy (2).pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Privacy.imageset/privacy (2).pdf new file mode 100644 index 0000000000000000000000000000000000000000..badb2fabc505b01ea105299eda4038ada4089b0d GIT binary patch literal 2500 zcmZXWdpy(a8^@gzL(UY@H_;|(#^!uBR!eP0t56w66Psrn=I~TfOXUc8IVd(6;+B3lM20@{QR<eP-_towqiM?R%EePF z20fSbQj^EG+1xnEU+FnE*weJSIJVd~pNZ)?b)@Mizws{?Z!}d~-tK>xFO;QsDF-jX zs7EVnJNZM)uVnRb8l zqc<->z ziH}OLor@jMrs8BI)b8AewB!COGDzKE>=C?WutWAjWEOOsB@Lmv+niujD9H_RraUL zP6Ow5&~D3i+&a7AGf#44cwopKRL}6I$aGkJ=-TNOk1@IXIlaP< z%WbQNWtpLmw}&Rm7@S>r_43GW{j;tk+dFlYEPdaGcvPI3a42Gwd&Tcg>2O+4QcY&1 z5ae1cQNzWShGFUK63@xhF3!dQHqm3oJc-kHK_6X#J8o+;`#JzQZNQnc zFeKj8SHCF#EX^zV@xfXy=f0w1r9}v+x|g|btlc(B^h6%MD8WBKwd9;zyE)wYV24cc z9;muEX$J4b6!RXxiHqDzR<6XkhZHR&VKLQmtl20B_|b>S;(y_jgSmFtiR1y@p&<=b zIpk>XEqfO+mWHI!^V`N!&E@OXKl21^RT%TVx`WKVl}!a6JthsInS0!sMrCmHB->3T zZNL%P7-sl5JIvuiM<;sTU>VKPZq~j+pHIq~O-SSWmCQd~U1@*6B5HLgD#wmhWTG-8XsSgswHWwGwNkkkF0gv!= zR|`T*RDDP-DR>tE@ZgbhxqLuj8gQ$_b0RYFhRl%yiN>#y~|R zoi9%5p!XH*i~r=6e?hyf$_b@EU7(KTBDy?T6BX&M2f^IvQQYwYK9-WF$# z)aY((Ox0zqf5B=YEl6K^)MI}|TQ4RT&4-BB{mJrB`dZR;+&3dR3_BOv#j>xG9@2@t z{~xFO^P*G^uY+g0O($fI8z84uf5cK_1Zx}ngqS}l*Wg-2-Fgl$lOIl~e{HH2-s9ui z{?$(f9miRbO^pSHBn>-1x@e`8B_EZ`&wZF_-NI z399fE_ru|rwl|Q|l_IVnww= zzR01cFzHt3TN+Nbn@$*1R442q)bV!uvdyLFFAw9HFaqeNl=Y%4(^{{3W1>S#87%&C z(3Q#F$_AwSq!udeff*k&H#g-F8TopS@0C*gC9-#QMbXCd#VvD z535Tt=}P*385zVh--)YkF!@4+g0rFV^y%JIS@y!=DK&DM!qO|0Qi9gCJimhLg*!;P z#!pFe4%K%k2p*s@qVFBWWn;^czM(0as$0wZI(G%QdS6Ve>KHH3DkaSYw(q#-!hlO3 z6El%0s^J-fOM@!KWe0MtMWO?P#%`Hp={F=a&85Ie{(#}HpOM4gW%DMcQuLDvz~?$R^buI zciXny3|JKD&sU3K(W*BXwsI3yQ+L2gif>N$;3uo5;ywz* z{s`RG=wZ1>)y$!GD@Sq7bmv|>L`Q7ib$1+GRTddfwU}od6#VFVy zu)X>&G0Cj|-ZN)dK>fri5w2md=Up-C`=}S*OBj4z=W>mk`g_&5;RSn4zbC^-)8Sww zLk~1OfwRfB-VS-%Pzv`!bA0yVf zVfx!mLV>iO4+;zWF5HPgCj?Uhe@|sli2)!ZzzpRlD zDFh_?bABP{f8q#RLNFC10|?t-JQcJC{Yd^GJA-WqCy*FO4Fa^;;cje*U4cR+gZL0g z1UD_;nJ%j@lJd^b<`59FBf~#yM%jGafe!4k=c1kHFdr;iYxIHByt$vHIJYl-WJGzq zo?l>m`yBe^;|#(~%j`?shxPYQ-+r23%cvuK-9Pf>Rrf2yxN+9$n^*O02m9a7kz>N* zV%pA3BkB9j>tk1u=r?2O=B z^z-sbz@#-f7&^@8W-*r;^o-_0+Ba@DqJwZRU_wN@w&Y>gTq zXo4_83T9GFWg-I-$0)6d@(?6ytB&pOTe#5=JD0l{XyYSq)(jY{kH+hpaf)F016B+RWOpa$fZ z*)h3H^#qgMub$8g8A?q`U*(pg-(#6b~v$Y^C$N9g%1 zKHrEX4auej{-=Vq#z6t)a9w*75ow*<;#f4w*!oP~v4C_Lc!FBDi@1AaL7$@Z$)#$s zsU9c#bG z@Cnts@dwb(e1H2^*S9t7D1IW9vVY4Fs-QKY<)EU23sk!2AZFia50_xfvsKx-RFJIE zFjT1{uQXFW(z-SOiR?{_F1J7yUniZm3#U~2IcFwj_}l~a!DRjy>RSCYqFrQn#+?Im zN|A}5Rjoh2PvBZDza*de@xRbJcVNDrioGqvq5{>Iu%psx5E@GHWVUn=aQ`l^bFAt& zz1Fn==nVNr;ZYF>BmYsGu>2q1z47Q08Cdzo#$@=P(IiV#S)n``m za2zUGMzgrxOzSi4FEAuvqTG;Ya$F6b6@>E(lZ&w1UG!KwU8w_$;5pCh4WSO1^$ zK?0{W#Uvbx^BQ+yy;EHc=A@PNwkGkm?6V5w4SzQpX0Gh6Zudrg-l%$AM2kkZOzH2f zKqV?45xlVFRB4H-LcHI;zAu^*@Z;tQnX_LaYIPLuRa)Oon@Y=wO6V|}k-m-{mY-s( zT!m{htw&n8UcBz;i9e7X!Ji>{xsN)l;%SQRm=X85L6S@fd!Gi99V|L-*SMo3Pw^U`3c^q7Ita{|ohuhJ`to3mEf?TVUDoY?JT_Cv=79U-)*K?4*5>S!a#zoN2 z%?R*UbyafFglg!?T1AeB*+3irxV)ISQ`%$at!}vSWVR4mcrsb{-kdJh_3k}H+uf56LI-4+?(-9U#B8f@?eE$5&*zS?XKmg&258R+| zXV9O3wm0b(ESPhLuhcIO#y;%B>1yO1vneH0ULd+NTKXp)c?&-`5`zquA%o^CC-AB&4!%8 zek^W|JU47xn)bHz{wb4ij3}&;}_J5%~8_@s& From 9df433937e5b4fb1db3ed9a9885ca0bd32071155 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sat, 27 Jul 2024 01:01:11 +0200 Subject: [PATCH 39/41] Fix browser t.me handling --- submodules/BrowserUI/Sources/BrowserWebContent.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/submodules/BrowserUI/Sources/BrowserWebContent.swift b/submodules/BrowserUI/Sources/BrowserWebContent.swift index 7efff4450f..0c0f883e0d 100644 --- a/submodules/BrowserUI/Sources/BrowserWebContent.swift +++ b/submodules/BrowserUI/Sources/BrowserWebContent.swift @@ -681,7 +681,7 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU // }) // } else { if let url = navigationAction.request.url?.absoluteString { - if isTelegramMeLink(url) || isTelegraPhLink(url) { + if (navigationAction.targetFrame == nil || navigationAction.targetFrame?.isMainFrame == true) && (isTelegramMeLink(url) || isTelegraPhLink(url)) { decisionHandler(.cancel, preferences) self.minimize() self.openAppUrl(url) @@ -712,7 +712,7 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { if let url = navigationAction.request.url?.absoluteString { - if isTelegramMeLink(url) || isTelegraPhLink(url) { + if (navigationAction.targetFrame == nil || navigationAction.targetFrame?.isMainFrame == true) && (isTelegramMeLink(url) || isTelegraPhLink(url)) { decisionHandler(.cancel) self.minimize() self.openAppUrl(url) From 60196075a6cac2d8aaffeca086e030a9ca974340 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sat, 27 Jul 2024 01:21:33 +0200 Subject: [PATCH 40/41] Fix video message layout when minimized container is present --- .../Display/Source/ContainerViewLayout.swift | 4 ++++ .../Navigation/NavigationController.swift | 21 ++++++++++++------- .../Sources/VideoMessageCameraScreen.swift | 2 +- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/submodules/Display/Source/ContainerViewLayout.swift b/submodules/Display/Source/ContainerViewLayout.swift index 8325f28258..ba1894abd4 100644 --- a/submodules/Display/Source/ContainerViewLayout.swift +++ b/submodules/Display/Source/ContainerViewLayout.swift @@ -90,6 +90,10 @@ public struct ContainerViewLayout: Equatable { return ContainerViewLayout(size: self.size, metrics: self.metrics, deviceMetrics: self.deviceMetrics, intrinsicInsets: intrinsicInsets, safeInsets: self.safeInsets, additionalInsets: self.additionalInsets, statusBarHeight: self.statusBarHeight, inputHeight: self.inputHeight, inputHeightIsInteractivellyChanging: self.inputHeightIsInteractivellyChanging, inVoiceOver: self.inVoiceOver) } + public func withUpdatedAdditionalInsets(_ additionalInsets: UIEdgeInsets) -> ContainerViewLayout { + return ContainerViewLayout(size: self.size, metrics: self.metrics, deviceMetrics: self.deviceMetrics, intrinsicInsets: self.intrinsicInsets, safeInsets: self.safeInsets, additionalInsets: additionalInsets, statusBarHeight: self.statusBarHeight, inputHeight: self.inputHeight, inputHeightIsInteractivellyChanging: self.inputHeightIsInteractivellyChanging, inVoiceOver: self.inVoiceOver) + } + public func withUpdatedInputHeight(_ inputHeight: CGFloat?) -> ContainerViewLayout { return ContainerViewLayout(size: self.size, metrics: self.metrics, deviceMetrics: self.deviceMetrics, intrinsicInsets: self.intrinsicInsets, safeInsets: self.safeInsets, additionalInsets: self.additionalInsets, statusBarHeight: self.statusBarHeight, inputHeight: inputHeight, inputHeightIsInteractivellyChanging: self.inputHeightIsInteractivellyChanging, inVoiceOver: self.inVoiceOver) } diff --git a/submodules/Display/Source/Navigation/NavigationController.swift b/submodules/Display/Source/Navigation/NavigationController.swift index 7f7156e25d..dd7c033a3a 100644 --- a/submodules/Display/Source/Navigation/NavigationController.swift +++ b/submodules/Display/Source/Navigation/NavigationController.swift @@ -443,7 +443,20 @@ open class NavigationController: UINavigationController, ContainableController, globalScrollToTopNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -1.0), size: CGSize(width: layout.size.width, height: 1.0)) } - let overlayContainerLayout = layout + var overlayContainerLayout = layout + + var updatedSize = layout.size + var updatedIntrinsicInsets = layout.intrinsicInsets + var updatedAdditionalInsets = layout.additionalInsets + if let minimizedContainer = self.minimizedContainer { + if (layout.inputHeight ?? 0.0).isZero { + let minimizedContainerHeight = minimizedContainer.collapsedHeight(layout: layout) + updatedSize.height -= minimizedContainerHeight + updatedIntrinsicInsets.bottom = 0.0 + updatedAdditionalInsets.bottom += minimizedContainerHeight + } + } + overlayContainerLayout = overlayContainerLayout.withUpdatedAdditionalInsets(updatedAdditionalInsets) if let inCallStatusBar = self.inCallStatusBar { let isLandscape = layout.size.width > layout.size.height @@ -843,8 +856,6 @@ open class NavigationController: UINavigationController, ContainableController, layout.additionalInsets.left = max(layout.intrinsicInsets.left, additionalSideInsets.left) layout.additionalInsets.right = max(layout.intrinsicInsets.right, additionalSideInsets.right) - var updatedSize = layout.size - var updatedIntrinsicInsets = layout.intrinsicInsets if case .flat = navigationLayout.root, let minimizedContainer = self.minimizedContainer { if minimizedContainer.supernode !== self.displayNode { if let rootContainer = self.rootContainer, case let .flat(flatContainer) = rootContainer { @@ -857,10 +868,6 @@ open class NavigationController: UINavigationController, ContainableController, self.displayNode.insertSubnode(minimizedContainer, at: 0) } } - if (layout.inputHeight ?? 0.0).isZero { - updatedSize.height -= minimizedContainer.collapsedHeight(layout: layout) - updatedIntrinsicInsets.bottom = 0.0 - } } switch navigationLayout.root { diff --git a/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift b/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift index 2676399bd6..cd5491a460 100644 --- a/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift +++ b/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift @@ -1389,7 +1389,7 @@ public class VideoMessageCameraScreen: ViewController { } var backgroundFrame = CGRect(origin: .zero, size: CGSize(width: layout.size.width, height: controller.inputPanelFrame.0.minY)) - if backgroundFrame.maxY < layout.size.height - 100.0 && (layout.inputHeight ?? 0.0).isZero && !controller.inputPanelFrame.1 { + if backgroundFrame.maxY < layout.size.height - 100.0 && (layout.inputHeight ?? 0.0).isZero && !controller.inputPanelFrame.1 && layout.additionalInsets.bottom.isZero { backgroundFrame = CGRect(origin: .zero, size: CGSize(width: layout.size.width, height: layout.size.height - layout.intrinsicInsets.bottom - controller.inputPanelFrame.0.height)) } From d21d943d8d1780deb582ff26f1fc178b59a49794 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Mon, 29 Jul 2024 00:53:17 +0200 Subject: [PATCH 41/41] Various improvements --- .../DrawingUI/Sources/DrawingLinkEntityView.swift | 2 +- .../Sources/TwoFactorAuthDataInputScreen.swift | 4 ++-- .../MediaEditorScreen/Sources/CreateLinkScreen.swift | 11 +++++++++-- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/submodules/DrawingUI/Sources/DrawingLinkEntityView.swift b/submodules/DrawingUI/Sources/DrawingLinkEntityView.swift index 22d81cd41d..19eeba7632 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() + string = self.linkEntity.url.uppercased().replacingOccurrences(of: "http://", with: "").replacingOccurrences(of: "https://", with: "") } let text = NSMutableAttributedString(string: string) let range = NSMakeRange(0, text.length) diff --git a/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift b/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift index 539b78af18..9ec039326d 100644 --- a/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift +++ b/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift @@ -1104,7 +1104,7 @@ private final class TwoFactorDataInputTextNode: ASDisplayNode, UITextFieldDelega self.hideButtonNode.isHidden = confirmation case .email: self.inputNode.textField.keyboardType = .emailAddress - self.inputNode.textField.returnKeyType = .done + self.inputNode.textField.returnKeyType = .next self.hideButtonNode.isHidden = true if #available(iOS 12.0, *) { @@ -1134,7 +1134,7 @@ private final class TwoFactorDataInputTextNode: ASDisplayNode, UITextFieldDelega } case .hint: self.inputNode.textField.keyboardType = .asciiCapable - self.inputNode.textField.returnKeyType = .done + self.inputNode.textField.returnKeyType = .next self.hideButtonNode.isHidden = true self.inputNode.textField.autocorrectionType = .no diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CreateLinkScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CreateLinkScreen.swift index b8afdb5081..8b64da17ae 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CreateLinkScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CreateLinkScreen.swift @@ -466,14 +466,21 @@ private final class CreateLinkSheetComponent: CombinedComponent { let text = !self.name.isEmpty ? self.name : self.link var effectiveMedia: TelegramMediaWebpage? - if let webpage = self.webpage, case .Loaded = webpage.content, !self.dismissed { + var webpageHasLargeMedia = false + if let webpage = self.webpage, case let .Loaded(content) = webpage.content, !self.dismissed { effectiveMedia = webpage + + if let isMediaLargeByDefault = content.isMediaLargeByDefault, isMediaLargeByDefault { + webpageHasLargeMedia = true + } else { + webpageHasLargeMedia = true + } } var attributes: [MessageAttribute] = [] attributes.append(TextEntitiesMessageAttribute(entities: [.init(range: 0 ..< (text as NSString).length, type: .Url)])) if !self.dismissed { - attributes.append(WebpagePreviewMessageAttribute(leadingPreview: !self.positionBelowText, forceLargeMedia: self.largeMedia, isManuallyAdded: false, isSafe: true)) + attributes.append(WebpagePreviewMessageAttribute(leadingPreview: !self.positionBelowText, forceLargeMedia: self.largeMedia ?? webpageHasLargeMedia, isManuallyAdded: false, isSafe: true)) } let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(1))