From d83734eb461fe98aebfcf07cb31a3e4620eb477b Mon Sep 17 00:00:00 2001 From: isaac <> Date: Tue, 28 Apr 2026 19:00:04 +0400 Subject: [PATCH] Various improvements --- .../Sources/DebugController.swift | 54 +- .../Sources/InstantPageLayout.swift | 4 +- .../Sources/InstantPageTileNode.swift | 9 +- .../Chat/ChatMessageBubbleItemNode/BUILD | 1 + .../Sources/ChatMessageBubbleItemNode.swift | 27 +- .../BUILD | 27 + ...ChatMessageRichDataBubbleContentNode.swift | 478 ++++++++++++++++++ .../Sources/TextStyleEditScreen.swift | 9 +- .../Sources/ExperimentalUISettings.swift | 10 +- 9 files changed, 587 insertions(+), 32 deletions(-) create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageRichDataBubbleContentNode/BUILD create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageRichDataBubbleContentNode/Sources/ChatMessageRichDataBubbleContentNode.swift diff --git a/submodules/DebugSettingsUI/Sources/DebugController.swift b/submodules/DebugSettingsUI/Sources/DebugController.swift index aa97e6e2c7..76a88cf387 100644 --- a/submodules/DebugSettingsUI/Sources/DebugController.swift +++ b/submodules/DebugSettingsUI/Sources/DebugController.swift @@ -98,6 +98,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { case fakeGlass(Bool) case forceClearGlass(Bool) case debugRipple(Bool) + case debugRichText(Bool) case browserExperiment(Bool) case allForumsHaveTabs(Bool) case enableReactionOverrides(Bool) @@ -137,7 +138,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { return DebugControllerSection.web.rawValue case .keepChatNavigationStack, .skipReadHistory, .alwaysDisplayTyping, .debugRatingLayout, .crashOnSlowQueries, .crashOnMemoryPressure: return DebugControllerSection.experiments.rawValue - case .clearTips, .resetNotifications, .crash, .fillLocalSavedMessageCache, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .resetTagHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .compressedEmojiCache, .storiesJpegExperiment, .checkSerializedData, .enableQuickReactionSwitch, .experimentalCompatibility, .enableDebugDataDisplay, .fakeGlass, .forceClearGlass, .debugRipple, .browserExperiment, .allForumsHaveTabs, .enableReactionOverrides, .restorePurchases, .disableReloginTokens, .liveStreamV2, .experimentalCallMute, .playerV2, .devRequests, .enableUpdates, .pwa, .enableLocalTranslation: + case .clearTips, .resetNotifications, .crash, .fillLocalSavedMessageCache, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .resetTagHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .compressedEmojiCache, .storiesJpegExperiment, .checkSerializedData, .enableQuickReactionSwitch, .experimentalCompatibility, .enableDebugDataDisplay, .fakeGlass, .forceClearGlass, .debugRipple, .debugRichText, .browserExperiment, .allForumsHaveTabs, .enableReactionOverrides, .restorePurchases, .disableReloginTokens, .liveStreamV2, .experimentalCallMute, .playerV2, .devRequests, .enableUpdates, .pwa, .enableLocalTranslation: return DebugControllerSection.experiments.rawValue case .logTranslationRecognition, .resetTranslationStates: return DebugControllerSection.translation.rawValue @@ -234,44 +235,46 @@ private enum DebugControllerEntry: ItemListNodeEntry { return 40 case .debugRipple: return 41 - case .browserExperiment: + case .debugRichText: return 42 - case .allForumsHaveTabs: + case .browserExperiment: return 43 - case .enableReactionOverrides: + case .allForumsHaveTabs: return 44 - case .restorePurchases: + case .enableReactionOverrides: return 45 - case .logTranslationRecognition: + case .restorePurchases: return 46 - case .resetTranslationStates: + case .logTranslationRecognition: return 47 - case .compressedEmojiCache: + case .resetTranslationStates: return 48 - case .storiesJpegExperiment: + case .compressedEmojiCache: return 49 - case .disableReloginTokens: + case .storiesJpegExperiment: return 50 - case .checkSerializedData: + case .disableReloginTokens: return 51 - case .enableQuickReactionSwitch: + case .checkSerializedData: return 52 - case .liveStreamV2: + case .enableQuickReactionSwitch: return 53 - case .experimentalCallMute: + case .liveStreamV2: return 54 - case .playerV2: + case .experimentalCallMute: return 55 - case .devRequests: + case .playerV2: return 56 - case .pwa: + case .devRequests: return 57 - case .enableLocalTranslation: + case .pwa: return 58 - case .enableUpdates: + case .enableLocalTranslation: return 59 + case .enableUpdates: + return 60 case let .preferredVideoCodec(index, _, _, _): - return 60 + index + return 61 + index case .disableVideoAspectScaling: return 100 case .enableNetworkFramework: @@ -1305,6 +1308,16 @@ private enum DebugControllerEntry: ItemListNodeEntry { }) }).start() }) + case let .debugRichText(value): + return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: "Debug Text", 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.debugRichText = value + return PreferencesEntry(settings) + }) + }).start() + }) case let .browserExperiment(value): return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: "Inline UI", value: value, sectionId: self.section, style: .blocks, updated: { value in let _ = arguments.sharedContext.accountManager.transaction ({ transaction in @@ -1583,6 +1596,7 @@ private func debugControllerEntries(context: AccountContext?, sharedContext: Sha entries.append(.fakeGlass(experimentalSettings.fakeGlass)) entries.append(.forceClearGlass(experimentalSettings.forceClearGlass)) entries.append(.debugRipple(experimentalSettings.debugRipple)) + entries.append(.debugRichText(experimentalSettings.debugRichText)) #if DEBUG entries.append(.browserExperiment(experimentalSettings.browserExperiment)) #else diff --git a/submodules/InstantPageUI/Sources/InstantPageLayout.swift b/submodules/InstantPageUI/Sources/InstantPageLayout.swift index 937d04c6b4..39baa17a74 100644 --- a/submodules/InstantPageUI/Sources/InstantPageLayout.swift +++ b/submodules/InstantPageUI/Sources/InstantPageLayout.swift @@ -1046,7 +1046,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation: } } -public func instantPageLayoutForWebPage(_ webPage: TelegramMediaWebpage, instantPage: InstantPage?, userLocation: MediaResourceUserLocation, boundingWidth: CGFloat, safeInset: CGFloat, strings: PresentationStrings, theme: InstantPageTheme, dateTimeFormat: PresentationDateTimeFormat, webEmbedHeights: [Int : CGFloat] = [:], cachedMessageSyntaxHighlight: CachedMessageSyntaxHighlight? = nil) -> InstantPageLayout { +public func instantPageLayoutForWebPage(_ webPage: TelegramMediaWebpage, instantPage: InstantPage?, userLocation: MediaResourceUserLocation, boundingWidth: CGFloat, safeInset: CGFloat, strings: PresentationStrings, theme: InstantPageTheme, dateTimeFormat: PresentationDateTimeFormat, webEmbedHeights: [Int : CGFloat] = [:], cachedMessageSyntaxHighlight: CachedMessageSyntaxHighlight? = nil, addFeedback: Bool = true) -> InstantPageLayout { var maybeLoadedContent: TelegramMediaWebpageLoadedContent? if case let .Loaded(content) = webPage.content { maybeLoadedContent = content @@ -1088,7 +1088,7 @@ public func instantPageLayoutForWebPage(_ webPage: TelegramMediaWebpage, instant let closingSpacing = spacingBetweenBlocks(upper: previousBlock, lower: nil) contentSize.height += closingSpacing - if webPage.webpageId.id != 0 { + if webPage.webpageId.id != 0 && addFeedback { let feedbackItem = InstantPageFeedbackItem(frame: CGRect(x: 0.0, y: contentSize.height, width: boundingWidth, height: 40.0), webPage: webPage) contentSize.height += feedbackItem.frame.height items.append(feedbackItem) diff --git a/submodules/InstantPageUI/Sources/InstantPageTileNode.swift b/submodules/InstantPageUI/Sources/InstantPageTileNode.swift index 3ed1a30426..2da701bd59 100644 --- a/submodules/InstantPageUI/Sources/InstantPageTileNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageTileNode.swift @@ -1,6 +1,7 @@ import Foundation import UIKit import AsyncDisplayKit +import Display private final class InstantPageTileNodeParameters: NSObject { let tile: InstantPageTile @@ -45,9 +46,11 @@ public final class InstantPageTileNode: ASDisplayNode { if let parameters = parameters as? InstantPageTileNodeParameters { if !isRasterizing { - context.setBlendMode(.copy) - context.setFillColor(parameters.backgroundColor.cgColor) - context.fill(bounds) + if !parameters.backgroundColor.alpha.isZero { + context.setBlendMode(.copy) + context.setFillColor(parameters.backgroundColor.cgColor) + context.fill(bounds) + } } parameters.tile.draw(context: context) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/BUILD index 21cd7ce809..a739a9a4e9 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/BUILD @@ -96,6 +96,7 @@ swift_library( "//submodules/AvatarNode", "//submodules/TelegramUI/Components/Chat/ChatMessageSuggestedPostInfoNode", "//submodules/TelegramUI/Components/PremiumAlertController", + "//submodules/TelegramUI/Components/Chat/ChatMessageRichDataBubbleContentNode", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index 8fb75e72b5..60c7f868a5 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -35,6 +35,7 @@ import ChatMessageDateAndStatusNode import ChatMessageBubbleContentNode import ChatHistoryEntry import ChatMessageTextBubbleContentNode +import ChatMessageRichDataBubbleContentNode import ChatMessageItemCommon import ChatMessageReplyInfoNode import ChatMessageCallBubbleContentNode @@ -382,7 +383,11 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ if let attribute = message.attributes.first(where: { $0 is WebpagePreviewMessageAttribute }) as? WebpagePreviewMessageAttribute, attribute.leadingPreview { result.insert((message, ChatMessageWebpageBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)), at: addedPriceInfo ? 1 : 0) } else { - result.append((message, ChatMessageWebpageBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) + if content.instantPage != nil && item.context.sharedContext.immediateExperimentalUISettings.debugRichText { + result.append((message, ChatMessageRichDataBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) + } else { + result.append((message, ChatMessageWebpageBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) + } } needReactions = false } @@ -1620,6 +1625,12 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI var allowFullWidth = false let chatLocationPeerId: PeerId = item.chatLocation.peerId ?? item.content.firstMessage.id.peerId + + var isInlinePage = false + if item.context.sharedContext.immediateExperimentalUISettings.debugRichText, let webpage = item.message.media.first(where: { $0 is TelegramMediaWebpage }) as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content, content.instantPage != nil { + allowFullWidth = true + isInlinePage = true + } do { let peerId = chatLocationPeerId @@ -1888,6 +1899,10 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI if let subject = item.associatedData.subject, case .messageOptions = subject { needsShareButton = false } + + if isInlinePage { + needsShareButton = false + } var tmpWidth: CGFloat if allowFullWidth { @@ -1895,7 +1910,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI if (needsShareButton && !isSidePanelOpen) || isAd { tmpWidth -= 45.0 } else { - tmpWidth -= 4.0 + tmpWidth -= 3.0 } } else { tmpWidth = layoutConstants.bubble.maximumWidthFill.widthFor(baseWidth) @@ -2262,7 +2277,11 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI bubbleReactions = ReactionsMessageAttribute(canViewList: false, isTags: false, reactions: [], recentPeers: [], topPeers: []) } if !bubbleReactions.reactions.isEmpty && !item.presentationData.isPreview { - bottomNodeMergeStatus = .Right + if incoming { + bottomNodeMergeStatus = .Both + } else { + bottomNodeMergeStatus = .Right + } } var currentCredibilityIcon: (EmojiStatusComponent.Content, UIColor?)? @@ -7348,7 +7367,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } for contentNode in self.contentNodes { - if contentNode is ChatMessageMediaBubbleContentNode || contentNode is ChatMessageGiftBubbleContentNode || contentNode is ChatMessageWebpageBubbleContentNode || contentNode is ChatMessageInvoiceBubbleContentNode || contentNode is ChatMessageGameBubbleContentNode || contentNode is ChatMessageInstantVideoBubbleContentNode { + if contentNode is ChatMessageMediaBubbleContentNode || contentNode is ChatMessageGiftBubbleContentNode || contentNode is ChatMessageWebpageBubbleContentNode || contentNode is ChatMessageInvoiceBubbleContentNode || contentNode is ChatMessageGameBubbleContentNode || contentNode is ChatMessageInstantVideoBubbleContentNode || contentNode is ChatMessageRichDataBubbleContentNode { contentNode.visibility = mapVisibility(effectiveMediaVisibility, boundsSize: self.bounds.size, insets: self.insets, to: contentNode) } else { contentNode.visibility = mapVisibility(effectiveVisibility, boundsSize: self.bounds.size, insets: self.insets, to: contentNode) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageRichDataBubbleContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageRichDataBubbleContentNode/BUILD new file mode 100644 index 0000000000..7c9d63aaa9 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageRichDataBubbleContentNode/BUILD @@ -0,0 +1,27 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageRichDataBubbleContentNode", + module_name = "ChatMessageRichDataBubbleContentNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/TelegramCore", + "//submodules/Postbox", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/AccountContext", + "//submodules/InstantPageUI", + "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + "//submodules/TelegramUIPreferences", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageRichDataBubbleContentNode/Sources/ChatMessageRichDataBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageRichDataBubbleContentNode/Sources/ChatMessageRichDataBubbleContentNode.swift new file mode 100644 index 0000000000..dbb3525d44 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageRichDataBubbleContentNode/Sources/ChatMessageRichDataBubbleContentNode.swift @@ -0,0 +1,478 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import TelegramCore +import Postbox +import SwiftSignalKit +import AccountContext +import ChatMessageBubbleContentNode +import ChatMessageItemCommon +import InstantPageUI +import TelegramUIPreferences + +public class ChatMessageRichDataBubbleContentNode: ChatMessageBubbleContentNode { + public final class ContainerNode: ASDisplayNode { + } + + private let containerNode: ContainerNode + private var currentLayoutTiles: [InstantPageTile] = [] + private var visibleTiles: [Int: InstantPageTileNode] = [:] + private var visibleItemsWithNodes: [Int: InstantPageNode] = [:] + private var currentPageLayout: (boundingWidth: CGFloat, layout: InstantPageLayout)? + private var distanceThresholdGroupCount: [Int: Int] = [:] + private var currentLayoutItemsWithNodes: [InstantPageItem] = [] + private var currentExpandedDetails: [Int : Bool]? + + override public var visibility: ListViewItemNodeVisibility { + didSet { + if oldValue != self.visibility { + self.updateVisibility() + } + } + } + + required public init() { + self.containerNode = ContainerNode() + self.containerNode.clipsToBounds = true + + super.init() + + self.addSubnode(self.containerNode) + } + + required public init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { + let currentPageLayout = self.currentPageLayout + let previousCurrentLayoutTiles = self.currentLayoutTiles + + return { [weak self] item, layoutConstants, _, _, _, _ in + let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none) + + return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in + let suggestedBoundingWidth: CGFloat = constrainedSize.width + + return (suggestedBoundingWidth, { boundingWidth in + var boundingSize = CGSize(width: boundingWidth, height: 0.0) + + var pageLayout: InstantPageLayout? + var currentLayoutTiles: [InstantPageTile] = [] + + if let webpage = item.message.media.first(where: { $0 is TelegramMediaWebpage }) as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content, let instantPage = content.instantPage { + if let current = currentPageLayout, current.boundingWidth == boundingSize.width { + pageLayout = current.layout + currentLayoutTiles = previousCurrentLayoutTiles + } else { + let pageTheme = instantPageThemeForType(item.presentationData.theme.theme.overallDarkAppearance ? .dark : .light, settings: InstantPagePresentationSettings( + themeType: item.presentationData.theme.theme.overallDarkAppearance ? .dark : .light, + fontSize: .standard, + forceSerif: false, + autoNightMode: false, + ignoreAutoNightModeUntil: 0 + )) + pageLayout = instantPageLayoutForWebPage(webpage, instantPage: instantPage._parse(), userLocation: .other, boundingWidth: boundingWidth - 2.0, safeInset: 0.0, strings: item.presentationData.strings, theme: pageTheme, dateTimeFormat: item.presentationData.dateTimeFormat, webEmbedHeights: [:], addFeedback: false) + if let pageLayout { + currentLayoutTiles = instantPageTilesFromLayout(pageLayout, boundingWidth: boundingWidth) + } + } + } + + if let pageLayout { + boundingSize.height = pageLayout.contentSize.height + 2.0 + } + + return (boundingSize, { animation, synchronousLoads, itemApply in + guard let self else { + return + } + self.item = item + + self.containerNode.frame = CGRect(origin: CGPoint(x: 1.0, y: 1.0), size: CGSize(width: boundingSize.width - 2.0, height: boundingSize.height - 2.0)) + + if let pageLayout { + self.currentPageLayout = (boundingSize.width, pageLayout) + self.currentLayoutTiles = currentLayoutTiles + + var distanceThresholdGroupCount: [Int : Int] = [:] + + for item in pageLayout.items { + if item.wantsNode { + self.currentLayoutItemsWithNodes.append(item) + + if let group = item.distanceThresholdGroup() { + let count: Int + if let currentCount = distanceThresholdGroupCount[Int(group)] { + count = currentCount + } else { + count = 0 + } + distanceThresholdGroupCount[Int(group)] = count + 1 + } + } + } + + self.distanceThresholdGroupCount = distanceThresholdGroupCount + } else { + self.currentPageLayout = nil + self.currentLayoutTiles = [] + self.distanceThresholdGroupCount = [:] + } + + self.updateVisibility() + }) + }) + }) + } + } + + private func effectiveFrameForTile(_ tile: InstantPageTile) -> CGRect { + let layoutOrigin = tile.frame.origin + let origin = layoutOrigin + return CGRect(origin: origin, size: tile.frame.size) + } + + private func updateVisibility() { + switch self.visibility { + case .none: + self.updateVisibleItems(visibleBounds: CGRect(), animated: false) + case let .visible(_, subRect): + self.updateVisibleItems(visibleBounds: subRect, animated: false) + } + } + + private func updateVisibleItems(visibleBounds: CGRect, animated: Bool = false) { + guard let messageItem = self.item else { + return + } + let pageTheme = instantPageThemeForType(messageItem.presentationData.theme.theme.overallDarkAppearance ? .dark : .light, settings: InstantPagePresentationSettings( + themeType: messageItem.presentationData.theme.theme.overallDarkAppearance ? .dark : .light, + fontSize: .standard, + forceSerif: false, + autoNightMode: false, + ignoreAutoNightModeUntil: 0 + )) + let sourceLocation = InstantPageSourceLocation(userLocation: .other, peerType: .otherPrivate) + + var visibleTileIndices = Set() + var visibleItemIndices = Set() + + var topNode: ASDisplayNode? + let topTileNode = topNode + if let containerSubnodes = self.containerNode.subnodes { + for node in containerSubnodes.reversed() { + if let node = node as? InstantPageTileNode { + topNode = node + break + } + } + } + + var collapseOffset: CGFloat = 0.0 + collapseOffset = 0.0 + let transition: ContainedViewLayoutTransition + if animated { + transition = .animated(duration: 0.3, curve: .spring) + } else { + transition = .immediate + } + + var itemIndex = -1 + var embedIndex = -1 + var detailsIndex = -1 + + var previousDetailsNode: InstantPageDetailsNode? + + for item in self.currentLayoutItemsWithNodes { + itemIndex += 1 + if item is InstantPageWebEmbedItem { + embedIndex += 1 + } + if let imageItem = item as? InstantPageImageItem, case .webpage = imageItem.media.media { + embedIndex += 1 + } + if item is InstantPageDetailsItem { + detailsIndex += 1 + } + + var itemThreshold: CGFloat = 0.0 + if let group = item.distanceThresholdGroup() { + var count: Int = 0 + if let currentCount = self.distanceThresholdGroupCount[group] { + count = currentCount + } + itemThreshold = item.distanceThresholdWithGroupCount(count) + } + + let itemFrame = item.frame.offsetBy(dx: 0.0, dy: -collapseOffset) + var thresholdedItemFrame = itemFrame + thresholdedItemFrame.origin.y -= itemThreshold + thresholdedItemFrame.size.height += itemThreshold * 2.0 + + if visibleBounds.intersects(thresholdedItemFrame) { + visibleItemIndices.insert(itemIndex) + + var itemNode = self.visibleItemsWithNodes[itemIndex] + if let currentItemNode = itemNode { + if !item.matchesNode(currentItemNode) { + currentItemNode.removeFromSupernode() + self.visibleItemsWithNodes.removeValue(forKey: itemIndex) + itemNode = nil + } + } + + if itemNode == nil { + let itemIndex = itemIndex + //let embedIndex = embedIndex + //let detailsIndex = detailsIndex + if let newNode = item.node(context: messageItem.context, strings: messageItem.presentationData.strings, nameDisplayOrder: messageItem.presentationData.nameDisplayOrder, theme: pageTheme, sourceLocation: sourceLocation, openMedia: { [weak self] media in + let _ = self + //self?.openMedia(media) + }, longPressMedia: { [weak self] media in + //self?.longPressMedia(media) + let _ = self + }, activatePinchPreview: { [weak self] sourceNode in + /*guard let strongSelf = self, let controller = strongSelf.controller else { + return + } + let pinchController = makePinchController(sourceNode: sourceNode, getContentAreaInScreenSpace: { + guard let strongSelf = self else { + return CGRect() + } + + let localRect = CGRect(origin: CGPoint(x: 0.0, y: strongSelf.navigationBar.frame.maxY), size: CGSize(width: strongSelf.bounds.width, height: strongSelf.bounds.height - strongSelf.navigationBar.frame.maxY)) + return strongSelf.view.convert(localRect, to: nil) + }) + controller.window?.presentInGlobalOverlay(pinchController)*/ + let _ = self + }, pinchPreviewFinished: { [weak self] itemNode in + /*guard let strongSelf = self else { + return + } + for (_, listItemNode) in strongSelf.visibleItemsWithNodes { + if let listItemNode = listItemNode as? InstantPagePeerReferenceNode { + if listItemNode.frame.intersects(itemNode.frame) && listItemNode.frame.maxY <= itemNode.frame.maxY + 2.0 { + listItemNode.layer.animateAlpha(from: 0.0, to: listItemNode.alpha, duration: 0.25) + break + } + } + }*/ + let _ = self + }, openPeer: { [weak self] peerId in + let _ = self + //self?.openPeer(peerId) + }, openUrl: { [weak self] url in + let _ = self + //self?.openUrl(url) + }, updateWebEmbedHeight: { [weak self] height in + let _ = self + //self?.updateWebEmbedHeight(embedIndex, height) + }, updateDetailsExpanded: { [weak self] expanded in + let _ = self + //self?.updateDetailsExpanded(detailsIndex, expanded) + }, currentExpandedDetails: self.currentExpandedDetails, getPreloadedResource: { _ in return nil }) { + newNode.frame = itemFrame + newNode.updateLayout(size: itemFrame.size, transition: transition) + if let topNode = topNode { + self.containerNode.insertSubnode(newNode, aboveSubnode: topNode) + } else { + self.containerNode.insertSubnode(newNode, at: 0) + } + topNode = newNode + self.visibleItemsWithNodes[itemIndex] = newNode + itemNode = newNode + + if let itemNode = itemNode as? InstantPageDetailsNode { + itemNode.requestLayoutUpdate = { [weak self] animated in + let _ = self + /*if let strongSelf = self { + strongSelf.updateVisibleItems(visibleBounds: strongSelf.scrollNode.view.bounds, animated: animated) + }*/ + } + + if let previousDetailsNode = previousDetailsNode { + if itemNode.frame.minY - previousDetailsNode.frame.maxY < 1.0 { + itemNode.previousNode = previousDetailsNode + } + } + previousDetailsNode = itemNode + } + } + } else { + if let itemNode = itemNode, itemNode.frame != itemFrame { + transition.updateFrame(node: itemNode, frame: itemFrame) + itemNode.updateLayout(size: itemFrame.size, transition: transition) + } + } + + if let itemNode = itemNode as? InstantPageDetailsNode { + itemNode.updateVisibleItems(visibleBounds: visibleBounds.offsetBy(dx: -itemNode.frame.minX, dy: -itemNode.frame.minY), animated: animated) + } + } + } + + topNode = topTileNode + + var tileIndex = -1 + for tile in self.currentLayoutTiles { + tileIndex += 1 + + let tileFrame = effectiveFrameForTile(tile) + var tileVisibleFrame = tileFrame + tileVisibleFrame.origin.y -= 400.0 + tileVisibleFrame.size.height += 400.0 * 2.0 + if tileVisibleFrame.intersects(visibleBounds) { + visibleTileIndices.insert(tileIndex) + + if self.visibleTiles[tileIndex] == nil { + let tileNode = InstantPageTileNode(tile: tile, backgroundColor: .clear) + tileNode.frame = tileFrame + if let topNode = topNode { + self.containerNode.insertSubnode(tileNode, aboveSubnode: topNode) + } else { + self.containerNode.insertSubnode(tileNode, at: 0) + } + topNode = tileNode + self.visibleTiles[tileIndex] = tileNode + } else { + if let tileNode = self.visibleTiles[tileIndex] { + tileNode.update(tile: tile, backgroundColor: .clear) + if tileNode.frame != tileFrame { + transition.updateFrame(node: tileNode, frame: tileFrame) + } + } + } + } + } + + var removeTileIndices: [Int] = [] + for (index, tileNode) in self.visibleTiles { + if !visibleTileIndices.contains(index) { + removeTileIndices.append(index) + tileNode.removeFromSupernode() + } + } + for index in removeTileIndices { + self.visibleTiles.removeValue(forKey: index) + } + + var removeItemIndices: [Int] = [] + for (index, itemNode) in self.visibleItemsWithNodes { + if !visibleItemIndices.contains(index) { + removeItemIndices.append(index) + itemNode.removeFromSupernode() + } else { + var itemFrame = itemNode.frame + let itemThreshold: CGFloat = 200.0 + itemFrame.origin.y -= itemThreshold + itemFrame.size.height += itemThreshold * 2.0 + itemNode.updateIsVisible(visibleBounds.intersects(itemFrame)) + } + } + for index in removeItemIndices { + self.visibleItemsWithNodes.removeValue(forKey: index) + } + } + + override public func animateInsertion(_ currentTimestamp: Double, duration: Double) { + /*self.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + if let statusNode = self.statusNode, statusNode.alpha != 0.0 { + statusNode.layer.animateAlpha(from: 0.0, to: statusNode.alpha, duration: 0.2) + }*/ + } + + override public func animateAdded(_ currentTimestamp: Double, duration: Double) { + /*self.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + if let statusNode = self.statusNode, statusNode.alpha != 0.0 { + statusNode.layer.animateAlpha(from: 0.0, to: statusNode.alpha, duration: 0.2) + }*/ + } + + override public func animateRemoved(_ currentTimestamp: Double, duration: Double) { + /*self.textNode.textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + if let statusNode = self.statusNode, statusNode.alpha != 0.0 { + statusNode.layer.animateAlpha(from: statusNode.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false) + }*/ + } + + override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { + if case .tap = gesture { + } else { + if let item = self.item, let subject = item.associatedData.subject, case .messageOptions = subject { + return ChatMessageBubbleContentTapAction(content: .none) + } + } + + /*func makeActivate(_ urlRange: NSRange?) -> (() -> Promise?)? { + return { [weak self] in + guard let self else { + return nil + } + + let promise = Promise() + + self.linkProgressDisposable?.dispose() + + if self.linkProgressRange != nil { + self.linkProgressRange = nil + self.updateLinkProgressState() + } + + self.linkProgressDisposable = (promise.get() |> deliverOnMainQueue).startStrict(next: { [weak self] value in + guard let self else { + return + } + let updatedRange: NSRange? = value ? urlRange : nil + if self.linkProgressRange != updatedRange { + self.linkProgressRange = updatedRange + self.updateLinkProgressState() + } + }) + + return promise + } + }*/ + + return ChatMessageBubbleContentTapAction(content: .none) + } + + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + return super.hitTest(point, with: event) + } + + override public func updateTouchesAtPoint(_ point: CGPoint?) { + } + + override public func updateSearchTextHighlightState(text: String?, messages: [MessageIndex]?) { + } + + override public func willUpdateIsExtractedToContextPreview(_ value: Bool) { + } + + override public func updateIsExtractedToContextPreview(_ value: Bool) { + } + + override public func reactionTargetView(value: MessageReaction.Reaction) -> UIView? { + /*if let statusNode = self.statusNode, !statusNode.isHidden { + return statusNode.reactionView(value: value) + }*/ + return nil + } + + override public func messageEffectTargetView() -> UIView? { + /*if let statusNode = self.statusNode, !statusNode.isHidden { + return statusNode.messageEffectTargetView() + }*/ + return nil + } + + override public func getStatusNode() -> ASDisplayNode? { + return nil + //return self.statusNode + } +} diff --git a/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextStyleEditScreen.swift b/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextStyleEditScreen.swift index 785818e083..3c1fbb09f3 100644 --- a/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextStyleEditScreen.swift +++ b/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextStyleEditScreen.swift @@ -822,6 +822,7 @@ private final class TextStyleEditSheetComponent: Component { theme: theme, strings: environmentValue.strings, actionTitle: actionButtonTitle, + displayProgress: self.isActionInProgress, action: isMainActionEnabled ? performMainAction : nil ) ), @@ -998,17 +999,20 @@ private final class ActionButtonsComponent: Component { let theme: PresentationTheme let strings: PresentationStrings let actionTitle: String + let displayProgress: Bool let action: (() -> Void)? init( theme: PresentationTheme, strings: PresentationStrings, actionTitle: String, + displayProgress: Bool, action: (() -> Void)? ) { self.theme = theme self.strings = strings self.actionTitle = actionTitle + self.displayProgress = displayProgress self.action = action } @@ -1022,6 +1026,9 @@ private final class ActionButtonsComponent: Component { if lhs.actionTitle != rhs.actionTitle { return false } + if lhs.displayProgress != rhs.displayProgress { + return false + } if (lhs.action == nil) != (rhs.action == nil) { return false } @@ -1070,7 +1077,7 @@ private final class ActionButtonsComponent: Component { )) ), isEnabled: component.action != nil, - displaysProgress: false, + displaysProgress: component.displayProgress, action: { [weak self] in guard let self, let component = self.component else { return diff --git a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift index 90c5e4f0d2..1274aaf1fc 100644 --- a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift +++ b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift @@ -72,6 +72,7 @@ public struct ExperimentalUISettings: Codable, Equatable { public var enablePWA: Bool public var forceClearGlass: Bool public var debugRipple: Bool + public var debugRichText: Bool public static var defaultSettings: ExperimentalUISettings { return ExperimentalUISettings( @@ -121,7 +122,8 @@ public struct ExperimentalUISettings: Codable, Equatable { enableUpdates: false, enablePWA: false, forceClearGlass: false, - debugRipple: false + debugRipple: false, + debugRichText: false ) } @@ -172,7 +174,8 @@ public struct ExperimentalUISettings: Codable, Equatable { enableUpdates: Bool, enablePWA: Bool, forceClearGlass: Bool, - debugRipple: Bool + debugRipple: Bool, + debugRichText: Bool ) { self.keepChatNavigationStack = keepChatNavigationStack self.skipReadHistory = skipReadHistory @@ -221,6 +224,7 @@ public struct ExperimentalUISettings: Codable, Equatable { self.enablePWA = enablePWA self.forceClearGlass = forceClearGlass self.debugRipple = debugRipple + self.debugRichText = debugRichText } public init(from decoder: Decoder) throws { @@ -273,6 +277,7 @@ public struct ExperimentalUISettings: Codable, Equatable { self.enablePWA = try container.decodeIfPresent(Bool.self, forKey: "enablePWA") ?? false self.forceClearGlass = try container.decodeIfPresent(Bool.self, forKey: "forceClearGlass") ?? false self.debugRipple = try container.decodeIfPresent(Bool.self, forKey: "debugRipple") ?? false + self.debugRichText = try container.decodeIfPresent(Bool.self, forKey: "debugRichText") ?? false } public func encode(to encoder: Encoder) throws { @@ -325,6 +330,7 @@ public struct ExperimentalUISettings: Codable, Equatable { try container.encodeIfPresent(self.enablePWA, forKey: "enablePWA") try container.encodeIfPresent(self.forceClearGlass, forKey: "forceClearGlass") try container.encodeIfPresent(self.debugRipple, forKey: "debugRipple") + try container.encodeIfPresent(self.debugRichText, forKey: "debugRichText") } }