From be6c32ef9c2594248da00f0510803c936d256a3a Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 7 Apr 2026 21:44:48 +0200 Subject: [PATCH 1/4] Trigger build --- Random.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Random.txt b/Random.txt index c407a4cb13..66824569ca 100644 --- a/Random.txt +++ b/Random.txt @@ -1 +1 @@ -06de25b179c80e59 +c27f02bf6e413fdc From d2293b8e0944bf436d25f6a6fd5fdab6b7ea4286 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Thu, 9 Apr 2026 15:02:17 +0200 Subject: [PATCH 2/4] Xcode 26.4 build crashes on iOS 15/16 --- versions.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/versions.json b/versions.json index 3fe2d79551..002ba133f1 100644 --- a/versions.json +++ b/versions.json @@ -1,6 +1,6 @@ { - "app": "12.6.2", - "xcode": "26.4", + "app": "12.6.3", + "xcode": "26.2", "deploy_xcode": "26.2", "bazel": "8.4.2:45e9388abf21d1107e146ea366ad080eb93cb6a5f3a4a3b048f78de0bc3faffa", "macos": "26" From d575824ab438ed2327ccae2e5431671ca48f2113 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Fri, 10 Apr 2026 14:30:23 +0200 Subject: [PATCH 3/4] Various improvements --- .../AuthorizationSequenceController.swift | 2 +- submodules/Display/Source/ListView.swift | 31 +- submodules/Display/Source/NavigationBar.swift | 8 +- .../GalleryUI/Sources/GalleryController.swift | 2 +- .../SetupTwoStepVerificationController.swift | 2 +- .../TwoFactorAuthDataInputScreen.swift | 2 +- .../Sources/TwoFactorAuthSplashScreen.swift | 2 +- .../QrCodeUI/Sources/QrCodeScanScreen.swift | 2 +- .../ApiUtils/StoreMessage_Telegram.swift | 2 + .../State/AccountStateManagementUtils.swift | 42 +- .../Sources/ComponentsThemes.swift | 2 +- .../Sources/ChatSideTopicsPanel.swift | 22 +- ...tControllerExtractedPresentationNode.swift | 2 +- .../Sources/ForumCreateTopicScreen.swift | 6 +- .../Sources/NavigationBarImpl.swift | 22 ++ .../Sources/NavigationButtonNode.swift | 24 +- .../Sources/PeerInfoScreen.swift | 1 + .../Sources/Chat/ChatControllerOpenPeer.swift | 58 +-- .../TelegramUI/Sources/ChatController.swift | 370 +++++++++++++----- .../Sources/ChatControllerNode.swift | 1 + .../ChatInterfaceStateContextMenus.swift | 9 +- .../Sources/ChatMessageTransitionNode.swift | 4 +- .../Sources/WebSearchGalleryController.swift | 2 +- .../WebUI/Sources/WebAppController.swift | 1 + 24 files changed, 443 insertions(+), 176 deletions(-) diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift index 70780df037..da66b26129 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift @@ -34,7 +34,7 @@ private enum InnerState: Equatable { public final class AuthorizationSequenceController: NavigationController, ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding { static func navigationBarTheme(_ theme: PresentationTheme) -> NavigationBarTheme { - return NavigationBarTheme(overallDarkAppearance: theme.overallDarkAppearance, buttonColor: theme.chat.inputPanel.panelControlColor, disabledButtonColor: theme.intro.disabledTextColor, primaryTextColor: theme.intro.primaryTextColor, backgroundColor: .clear, opaqueBackgroundColor: .clear, enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: theme.rootController.navigationBar.badgeBackgroundColor, badgeStrokeColor: theme.rootController.navigationBar.badgeStrokeColor, badgeTextColor: theme.rootController.navigationBar.badgeTextColor, edgeEffectColor: .clear, accentButtonColor: theme.list.itemCheckColors.fillColor, accentForegroundColor: theme.list.itemCheckColors.foregroundColor, style: .glass) + return NavigationBarTheme(overallDarkAppearance: theme.overallDarkAppearance, buttonColor: theme.chat.inputPanel.panelControlColor, disabledButtonColor: theme.intro.disabledTextColor, primaryTextColor: theme.intro.primaryTextColor, backgroundColor: .clear, opaqueBackgroundColor: .clear, enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: theme.rootController.navigationBar.badgeBackgroundColor, badgeStrokeColor: theme.rootController.navigationBar.badgeStrokeColor, badgeTextColor: theme.rootController.navigationBar.badgeTextColor, edgeEffectColor: .clear, accentButtonColor: theme.list.itemCheckColors.fillColor, accentDisabledButtonColor: theme.chat.inputPanel.panelControlDisabledColor, accentForegroundColor: theme.list.itemCheckColors.foregroundColor, style: .glass) } private let sharedContext: SharedAccountContext diff --git a/submodules/Display/Source/ListView.swift b/submodules/Display/Source/ListView.swift index 0a78640967..76187edacc 100644 --- a/submodules/Display/Source/ListView.swift +++ b/submodules/Display/Source/ListView.swift @@ -3136,20 +3136,11 @@ open class ListViewImpl: ASDisplayNode, ListView, ASScrollViewDelegate, ASGestur } else if self.snapToBottomInsetUntilFirstInteraction { offsetFix = -updateSizeAndInsets.insets.bottom + self.insets.bottom } else { - /*if let visualInsets = self.visualInsets, animated, (visualInsets.top == updateSizeAndInsets.insets.top || visualInsets.top == self.insets.top) { - offsetFix = 0.0 - } else {*/ - offsetFix = updateSizeAndInsets.insets.top - self.insets.top - //} + offsetFix = updateSizeAndInsets.insets.top - self.insets.top } offsetFix += additionalScrollDistance - /*if let topItemNode = self.itemNodes.first(where: { $0.index == 0 }) { - let topEdge = self.scroller.contentOffset.y + updateSizeAndInsets.insets.top - offsetFix = -(topEdge - topItemNode.apparentFrame.minY) - }*/ - self.insets = updateSizeAndInsets.insets self.headerInsets = updateSizeAndInsets.headerInsets ?? self.insets self.scrollIndicatorInsets = updateSizeAndInsets.scrollIndicatorInsets ?? self.insets @@ -3239,16 +3230,18 @@ open class ListViewImpl: ASDisplayNode, ListView, ASScrollViewDelegate, ASGestur animation = basicAnimation } - deferredUpdateVisible = true - animation.completion = { [weak self] _ in - self?.updateItemNodesVisibilities(onlyPositive: false) - } - self.layer.add(animation, forKey: nil) - if !completeOffset.isZero { - for itemNode in self.itemNodes { - itemNode.applyAbsoluteOffset(value: CGPoint(x: 0.0, y: -completeOffset), animationCurve: animationCurve, duration: animationDuration) + if customAnimationTransition == nil { + deferredUpdateVisible = true + animation.completion = { [weak self] _ in + self?.updateItemNodesVisibilities(onlyPositive: false) + } + self.layer.add(animation, forKey: nil) + if !completeOffset.isZero { + for itemNode in self.itemNodes { + itemNode.applyAbsoluteOffset(value: CGPoint(x: 0.0, y: -completeOffset), animationCurve: animationCurve, duration: animationDuration) + } + self.didScrollWithOffset?(-completeOffset, ContainedViewLayoutTransition.animated(duration: animationDuration, curve: animationCurve), nil, self.isTrackingOrDecelerating) } - self.didScrollWithOffset?(-completeOffset, ContainedViewLayoutTransition.animated(duration: animationDuration, curve: animationCurve), nil, self.isTrackingOrDecelerating) } } else { self.didScrollWithOffset?(-completeOffset, .immediate, nil, self.isTrackingOrDecelerating) diff --git a/submodules/Display/Source/NavigationBar.swift b/submodules/Display/Source/NavigationBar.swift index 37137b665e..bdac308c32 100644 --- a/submodules/Display/Source/NavigationBar.swift +++ b/submodules/Display/Source/NavigationBar.swift @@ -29,11 +29,12 @@ public final class NavigationBarTheme { public let badgeTextColor: UIColor public let edgeEffectColor: UIColor? public let accentButtonColor: UIColor + public let accentDisabledButtonColor: UIColor public let accentForegroundColor: UIColor public let style: NavigationBar.Style public let glassStyle: NavigationBar.GlassStyle - public init(overallDarkAppearance: Bool, buttonColor: UIColor, disabledButtonColor: UIColor, primaryTextColor: UIColor, backgroundColor: UIColor, opaqueBackgroundColor: UIColor? = nil, enableBackgroundBlur: Bool, separatorColor: UIColor, badgeBackgroundColor: UIColor, badgeStrokeColor: UIColor, badgeTextColor: UIColor, edgeEffectColor: UIColor? = nil, accentButtonColor: UIColor, accentForegroundColor: UIColor, style: NavigationBar.Style = .legacy, glassStyle: NavigationBar.GlassStyle = .default) { + public init(overallDarkAppearance: Bool, buttonColor: UIColor, disabledButtonColor: UIColor, primaryTextColor: UIColor, backgroundColor: UIColor, opaqueBackgroundColor: UIColor? = nil, enableBackgroundBlur: Bool, separatorColor: UIColor, badgeBackgroundColor: UIColor, badgeStrokeColor: UIColor, badgeTextColor: UIColor, edgeEffectColor: UIColor? = nil, accentButtonColor: UIColor, accentDisabledButtonColor: UIColor, accentForegroundColor: UIColor, style: NavigationBar.Style = .legacy, glassStyle: NavigationBar.GlassStyle = .default) { self.overallDarkAppearance = overallDarkAppearance self.buttonColor = buttonColor self.disabledButtonColor = disabledButtonColor @@ -47,17 +48,18 @@ public final class NavigationBarTheme { self.badgeTextColor = badgeTextColor self.edgeEffectColor = edgeEffectColor self.accentButtonColor = accentButtonColor + self.accentDisabledButtonColor = accentDisabledButtonColor self.accentForegroundColor = accentForegroundColor self.style = style self.glassStyle = glassStyle } public func withUpdatedBackgroundColor(_ color: UIColor) -> NavigationBarTheme { - return NavigationBarTheme(overallDarkAppearance: self.overallDarkAppearance, buttonColor: self.buttonColor, disabledButtonColor: self.disabledButtonColor, primaryTextColor: self.primaryTextColor, backgroundColor: color, opaqueBackgroundColor: self.opaqueBackgroundColor, enableBackgroundBlur: false, separatorColor: self.separatorColor, badgeBackgroundColor: self.badgeBackgroundColor, badgeStrokeColor: self.badgeStrokeColor, badgeTextColor: self.badgeTextColor, edgeEffectColor: self.edgeEffectColor, accentButtonColor: self.accentButtonColor, accentForegroundColor: self.accentForegroundColor, style: self.style, glassStyle: self.glassStyle) + return NavigationBarTheme(overallDarkAppearance: self.overallDarkAppearance, buttonColor: self.buttonColor, disabledButtonColor: self.disabledButtonColor, primaryTextColor: self.primaryTextColor, backgroundColor: color, opaqueBackgroundColor: self.opaqueBackgroundColor, enableBackgroundBlur: false, separatorColor: self.separatorColor, badgeBackgroundColor: self.badgeBackgroundColor, badgeStrokeColor: self.badgeStrokeColor, badgeTextColor: self.badgeTextColor, edgeEffectColor: self.edgeEffectColor, accentButtonColor: self.accentButtonColor, accentDisabledButtonColor: self.accentDisabledButtonColor, accentForegroundColor: self.accentForegroundColor, style: self.style, glassStyle: self.glassStyle) } public func withUpdatedSeparatorColor(_ color: UIColor) -> NavigationBarTheme { - return NavigationBarTheme(overallDarkAppearance: self.overallDarkAppearance, buttonColor: self.buttonColor, disabledButtonColor: self.disabledButtonColor, primaryTextColor: self.primaryTextColor, backgroundColor: self.backgroundColor, opaqueBackgroundColor: self.opaqueBackgroundColor, enableBackgroundBlur: self.enableBackgroundBlur, separatorColor: color, badgeBackgroundColor: self.badgeBackgroundColor, badgeStrokeColor: self.badgeStrokeColor, badgeTextColor: self.badgeTextColor, edgeEffectColor: self.edgeEffectColor, accentButtonColor: self.accentButtonColor, accentForegroundColor: self.accentForegroundColor, style: self.style, glassStyle: self.glassStyle) + return NavigationBarTheme(overallDarkAppearance: self.overallDarkAppearance, buttonColor: self.buttonColor, disabledButtonColor: self.disabledButtonColor, primaryTextColor: self.primaryTextColor, backgroundColor: self.backgroundColor, opaqueBackgroundColor: self.opaqueBackgroundColor, enableBackgroundBlur: self.enableBackgroundBlur, separatorColor: color, badgeBackgroundColor: self.badgeBackgroundColor, badgeStrokeColor: self.badgeStrokeColor, badgeTextColor: self.badgeTextColor, edgeEffectColor: self.edgeEffectColor, accentButtonColor: self.accentButtonColor, accentDisabledButtonColor: self.accentDisabledButtonColor, accentForegroundColor: self.accentForegroundColor, style: self.style, glassStyle: self.glassStyle) } } diff --git a/submodules/GalleryUI/Sources/GalleryController.swift b/submodules/GalleryUI/Sources/GalleryController.swift index 769144c9a8..10399d446e 100644 --- a/submodules/GalleryUI/Sources/GalleryController.swift +++ b/submodules/GalleryUI/Sources/GalleryController.swift @@ -690,7 +690,7 @@ private func galleryEntriesForMessageHistoryEntries(_ entries: [MessageHistoryEn } public class GalleryController: ViewController, StandalonePresentableController, KeyShortcutResponder, GalleryControllerProtocol { - public static let darkNavigationTheme = NavigationBarTheme(overallDarkAppearance: true, buttonColor: .white, disabledButtonColor: UIColor(rgb: 0x525252), primaryTextColor: .white, backgroundColor: UIColor(white: 0.0, alpha: 0.6), enableBackgroundBlur: false, separatorColor: UIColor(white: 0.0, alpha: 0.8), badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear, edgeEffectColor: .clear, accentButtonColor: .white, accentForegroundColor: .black, style: .glass) + public static let darkNavigationTheme = NavigationBarTheme(overallDarkAppearance: true, buttonColor: .white, disabledButtonColor: UIColor(rgb: 0x525252), primaryTextColor: .white, backgroundColor: UIColor(white: 0.0, alpha: 0.6), enableBackgroundBlur: false, separatorColor: UIColor(white: 0.0, alpha: 0.8), badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear, edgeEffectColor: .clear, accentButtonColor: .white, accentDisabledButtonColor: .white, accentForegroundColor: .black, style: .glass) private var galleryNode: GalleryControllerNode { return self.displayNode as! GalleryControllerNode diff --git a/submodules/PasswordSetupUI/Sources/SetupTwoStepVerificationController.swift b/submodules/PasswordSetupUI/Sources/SetupTwoStepVerificationController.swift index 56f4938f09..01405a99a2 100644 --- a/submodules/PasswordSetupUI/Sources/SetupTwoStepVerificationController.swift +++ b/submodules/PasswordSetupUI/Sources/SetupTwoStepVerificationController.swift @@ -39,7 +39,7 @@ public class SetupTwoStepVerificationController: ViewController { self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 } - super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: NavigationBarTheme(overallDarkAppearance: self.presentationData.theme.overallDarkAppearance, buttonColor: self.presentationData.theme.rootController.navigationBar.accentTextColor, disabledButtonColor: self.presentationData.theme.rootController.navigationBar.disabledButtonColor, primaryTextColor: self.presentationData.theme.rootController.navigationBar.primaryTextColor, backgroundColor: .clear, enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear, accentButtonColor: self.presentationData.theme.list.itemCheckColors.fillColor, accentForegroundColor: self.presentationData.theme.list.itemCheckColors.foregroundColor), strings: NavigationBarStrings(presentationStrings: self.presentationData.strings))) + super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: NavigationBarTheme(overallDarkAppearance: self.presentationData.theme.overallDarkAppearance, buttonColor: self.presentationData.theme.rootController.navigationBar.accentTextColor, disabledButtonColor: self.presentationData.theme.rootController.navigationBar.disabledButtonColor, primaryTextColor: self.presentationData.theme.rootController.navigationBar.primaryTextColor, backgroundColor: .clear, enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear, accentButtonColor: self.presentationData.theme.list.itemCheckColors.fillColor, accentDisabledButtonColor: self.presentationData.theme.chat.inputPanel.panelControlDisabledColor, accentForegroundColor: self.presentationData.theme.list.itemCheckColors.foregroundColor), strings: NavigationBarStrings(presentationStrings: self.presentationData.strings))) self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style diff --git a/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift b/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift index d42a2103b9..78bd4bfa78 100644 --- a/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift +++ b/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift @@ -71,7 +71,7 @@ public final class TwoFactorDataInputScreen: ViewController { self.presentationData = self.sharedContext.currentPresentationData.with { $0 } let defaultTheme = NavigationBarTheme(rootControllerTheme: self.presentationData.theme) - let navigationBarTheme = NavigationBarTheme(overallDarkAppearance: defaultTheme.overallDarkAppearance, buttonColor: defaultTheme.buttonColor, disabledButtonColor: defaultTheme.disabledButtonColor, primaryTextColor: defaultTheme.primaryTextColor, backgroundColor: .clear, enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: defaultTheme.badgeBackgroundColor, badgeStrokeColor: defaultTheme.badgeStrokeColor, badgeTextColor: defaultTheme.badgeTextColor, accentButtonColor: defaultTheme.accentButtonColor, accentForegroundColor: defaultTheme.accentForegroundColor) + let navigationBarTheme = NavigationBarTheme(overallDarkAppearance: defaultTheme.overallDarkAppearance, buttonColor: defaultTheme.buttonColor, disabledButtonColor: defaultTheme.disabledButtonColor, primaryTextColor: defaultTheme.primaryTextColor, backgroundColor: .clear, enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: defaultTheme.badgeBackgroundColor, badgeStrokeColor: defaultTheme.badgeStrokeColor, badgeTextColor: defaultTheme.badgeTextColor, accentButtonColor: defaultTheme.accentButtonColor, accentDisabledButtonColor: defaultTheme.accentDisabledButtonColor, accentForegroundColor: defaultTheme.accentForegroundColor) super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: navigationBarTheme, strings: NavigationBarStrings(back: self.presentationData.strings.Common_Back, close: self.presentationData.strings.Common_Close))) diff --git a/submodules/PasswordSetupUI/Sources/TwoFactorAuthSplashScreen.swift b/submodules/PasswordSetupUI/Sources/TwoFactorAuthSplashScreen.swift index f94f1ee2cf..670043340b 100644 --- a/submodules/PasswordSetupUI/Sources/TwoFactorAuthSplashScreen.swift +++ b/submodules/PasswordSetupUI/Sources/TwoFactorAuthSplashScreen.swift @@ -57,7 +57,7 @@ public final class TwoFactorAuthSplashScreen: ViewController { self.presentationData = self.sharedContext.currentPresentationData.with { $0 } let defaultTheme = NavigationBarTheme(rootControllerTheme: self.presentationData.theme) - let navigationBarTheme = NavigationBarTheme(overallDarkAppearance: defaultTheme.overallDarkAppearance, buttonColor: defaultTheme.buttonColor, disabledButtonColor: defaultTheme.disabledButtonColor, primaryTextColor: defaultTheme.primaryTextColor, backgroundColor: .clear, enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: defaultTheme.badgeBackgroundColor, badgeStrokeColor: defaultTheme.badgeStrokeColor, badgeTextColor: defaultTheme.badgeTextColor, accentButtonColor: defaultTheme.accentButtonColor, accentForegroundColor: defaultTheme.accentForegroundColor) + let navigationBarTheme = NavigationBarTheme(overallDarkAppearance: defaultTheme.overallDarkAppearance, buttonColor: defaultTheme.buttonColor, disabledButtonColor: defaultTheme.disabledButtonColor, primaryTextColor: defaultTheme.primaryTextColor, backgroundColor: .clear, enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: defaultTheme.badgeBackgroundColor, badgeStrokeColor: defaultTheme.badgeStrokeColor, badgeTextColor: defaultTheme.badgeTextColor, accentButtonColor: defaultTheme.accentButtonColor, accentDisabledButtonColor: defaultTheme.accentDisabledButtonColor, accentForegroundColor: defaultTheme.accentForegroundColor) super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: navigationBarTheme, strings: NavigationBarStrings(back: self.presentationData.strings.Common_Back, close: self.presentationData.strings.Common_Close))) diff --git a/submodules/QrCodeUI/Sources/QrCodeScanScreen.swift b/submodules/QrCodeUI/Sources/QrCodeScanScreen.swift index 7ede3135e3..3a54062f87 100644 --- a/submodules/QrCodeUI/Sources/QrCodeScanScreen.swift +++ b/submodules/QrCodeUI/Sources/QrCodeScanScreen.swift @@ -77,7 +77,7 @@ public final class QrCodeScanScreen: ViewController { self.presentationData = context.sharedContext.currentPresentationData.with { $0 } - let navigationBarTheme = NavigationBarTheme(overallDarkAppearance: self.presentationData.theme.overallDarkAppearance, buttonColor: .white, disabledButtonColor: .white, primaryTextColor: .white, backgroundColor: .clear, enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear, accentButtonColor: .white, accentForegroundColor: .black) + let navigationBarTheme = NavigationBarTheme(overallDarkAppearance: self.presentationData.theme.overallDarkAppearance, buttonColor: .white, disabledButtonColor: .white, primaryTextColor: .white, backgroundColor: .clear, enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear, accentButtonColor: .white, accentDisabledButtonColor: .white, accentForegroundColor: .black) super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: navigationBarTheme, strings: NavigationBarStrings(back: self.presentationData.strings.Common_Back, close: self.presentationData.strings.Common_Close))) diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index e36aff5d0c..db7cd5ebd2 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -1341,6 +1341,8 @@ extension StoreMessage { attributes.append(ReplyMessageAttribute(messageId: MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId), threadMessageId: threadMessageId, quote: quote, isQuote: isQuote, innerSubject: innerSubject)) } else if let replyHeader = replyHeader { attributes.append(QuotedReplyMessageAttribute(apiHeader: replyHeader, quote: quote, isQuote: isQuote)) + } else if let replyToTopId, peerIsForum { + threadId = Int64(replyToTopId) } case let .messageReplyStoryHeader(messageReplyStoryHeaderData): let (peer, storyId) = (messageReplyStoryHeaderData.peer, messageReplyStoryHeaderData.storyId) diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index 555b9d62cb..8afe3cfffc 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -4011,7 +4011,7 @@ func replayFinalState( case cancel } - var liveTypingDraftUpdates: [PeerAndThreadId: LiveTypingDraftUpdate] = [:] + var liveTypingDraftUpdates: [PeerAndThreadId: [LiveTypingDraftUpdate]] = [:] for operation in finalState.state.operations { switch operation { @@ -4226,11 +4226,11 @@ func replayFinalState( let allKey = PeerAndThreadId(peerId: chatPeerId, threadId: nil) if liveTypingDraftUpdates[key] != nil { - liveTypingDraftUpdates[key] = .cancel - liveTypingDraftUpdates[allKey] = .cancel + liveTypingDraftUpdates[key] = [.cancel] + liveTypingDraftUpdates[allKey] = [.cancel] } else if let currentDraft = transaction.getCurrentTypingDraft(location: key) { - liveTypingDraftUpdates[key] = .cancel - liveTypingDraftUpdates[allKey] = .cancel + liveTypingDraftUpdates[key] = [.cancel] + liveTypingDraftUpdates[allKey] = [.cancel] messages[i] = messages[i].withUpdatedCustomStableId(currentDraft.stableId) } } @@ -4930,23 +4930,30 @@ func replayFinalState( updatedSecretChatTypingActivities.insert(chatPeerId.peerId) } case let .AddPeerLiveTypingDraftUpdate(peerAndThreadId, id, timestamp, authorId, text, entities): - liveTypingDraftUpdates[peerAndThreadId] = .update(LiveTypingDraftUpdate.Update( + if liveTypingDraftUpdates[peerAndThreadId] == nil { + liveTypingDraftUpdates[peerAndThreadId] = [] + } + liveTypingDraftUpdates[peerAndThreadId]?.append(.update(LiveTypingDraftUpdate.Update( id: id, threadId: peerAndThreadId.threadId, authorId: authorId, timestamp: timestamp, text: text, entities: entities - )) + ))) if peerAndThreadId.threadId != nil { - liveTypingDraftUpdates[PeerAndThreadId(peerId: peerAndThreadId.peerId, threadId: nil)] = .update(LiveTypingDraftUpdate.Update( + let allKey = PeerAndThreadId(peerId: peerAndThreadId.peerId, threadId: nil) + if liveTypingDraftUpdates[allKey] == nil { + liveTypingDraftUpdates[allKey] = [] + } + liveTypingDraftUpdates[allKey]?.append(.update(LiveTypingDraftUpdate.Update( id: id, threadId: peerAndThreadId.threadId, authorId: authorId, timestamp: timestamp, text: text, entities: entities - )) + ))) } case let .UpdatePinnedItemIds(groupId, pinnedOperation): switch pinnedOperation { @@ -6049,11 +6056,26 @@ func replayFinalState( if !liveTypingDraftUpdates.isEmpty { transaction.combineTypingDrafts(locations: Set(liveTypingDraftUpdates.keys), update: { key, current in - guard let update = liveTypingDraftUpdates[key] else { + guard let update = liveTypingDraftUpdates[key]?.max(by: { lhs, rhs in + switch lhs { + case .cancel: + return false + case let .update(lhsUpdate): + switch rhs { + case .cancel: + return true + case let .update(rhsUpdate): + return lhsUpdate.timestamp < rhsUpdate.timestamp + } + } + }) else { return current } switch update { case let .update(update): + if let current, current.id > update.id { + return current + } return ( update.id, Namespaces.Message.Cloud, diff --git a/submodules/TelegramPresentationData/Sources/ComponentsThemes.swift b/submodules/TelegramPresentationData/Sources/ComponentsThemes.swift index 8dbd3e82b6..4f6c9b29b1 100644 --- a/submodules/TelegramPresentationData/Sources/ComponentsThemes.swift +++ b/submodules/TelegramPresentationData/Sources/ComponentsThemes.swift @@ -69,7 +69,7 @@ public extension NavigationBarTheme { badgeTextColor = theme.badgeTextColor } - self.init(overallDarkAppearance: rootControllerTheme.overallDarkAppearance, buttonColor: buttonColor, disabledButtonColor: disabledButtonColor, primaryTextColor: theme.primaryTextColor, backgroundColor: hideBackground ? .clear : theme.blurredBackgroundColor, opaqueBackgroundColor: hideBackground ? .clear : theme.opaqueBackgroundColor, enableBackgroundBlur: enableBackgroundBlur, separatorColor: hideBackground || hideSeparator ? .clear : theme.separatorColor, badgeBackgroundColor: hideBadge ? .clear : badgeBackgroundColor, badgeStrokeColor: .clear, badgeTextColor: hideBadge ? .clear : badgeTextColor, edgeEffectColor: edgeEffectColor, accentButtonColor: rootControllerTheme.list.itemCheckColors.fillColor, accentForegroundColor: rootControllerTheme.list.itemCheckColors.foregroundColor, style: style, glassStyle: glassStyle) + self.init(overallDarkAppearance: rootControllerTheme.overallDarkAppearance, buttonColor: buttonColor, disabledButtonColor: disabledButtonColor, primaryTextColor: theme.primaryTextColor, backgroundColor: hideBackground ? .clear : theme.blurredBackgroundColor, opaqueBackgroundColor: hideBackground ? .clear : theme.opaqueBackgroundColor, enableBackgroundBlur: enableBackgroundBlur, separatorColor: hideBackground || hideSeparator ? .clear : theme.separatorColor, badgeBackgroundColor: hideBadge ? .clear : badgeBackgroundColor, badgeStrokeColor: .clear, badgeTextColor: hideBadge ? .clear : badgeTextColor, edgeEffectColor: edgeEffectColor, accentButtonColor: rootControllerTheme.list.itemCheckColors.fillColor, accentDisabledButtonColor: rootControllerTheme.chat.inputPanel.panelControlDisabledColor, accentForegroundColor: rootControllerTheme.list.itemCheckColors.foregroundColor, style: style, glassStyle: glassStyle) } } diff --git a/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatSideTopicsPanel.swift b/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatSideTopicsPanel.swift index 19fa39ba65..b431c8ff21 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatSideTopicsPanel.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatSideTopicsPanel.swift @@ -941,8 +941,8 @@ public final class ChatSideTopicsPanel: Component { } let size = CGSize(width: contentSize, height: availableSize.height) - let iconFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((size.height - iconSize.height) * 0.5)), size: iconSize) - let titleFrame = CGRect(origin: CGPoint(x: iconFrame.maxX + spacing, y: floor((size.height - titleSize.height) * 0.5)), size: titleSize) + let iconFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((size.height - iconSize.height) * 0.5) - 1.0), size: iconSize) + let titleFrame = CGRect(origin: CGPoint(x: iconFrame.maxX + spacing, y: floor((size.height - titleSize.height) * 0.5) - 1.0), size: titleSize) if let icon = self.icon { if let avatarNode = self.avatarNode { @@ -1422,7 +1422,7 @@ public final class ChatSideTopicsPanel: Component { let contentSize: CGFloat = leftInset + rightInset + titleSize.width let size = CGSize(width: contentSize, height: availableSize.height) - let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((size.height - titleSize.height) * 0.5)), size: titleSize) + let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((size.height - titleSize.height) * 0.5) - 1.0), size: titleSize) if let titleView = self.title.view { if titleView.superview == nil { @@ -1599,6 +1599,20 @@ public final class ChatSideTopicsPanel: Component { } } if isPinned { + if !seenPinnedItems { + switch component.location { + case .side: + beforePinnedItemsPosition = item.frame.minY + case .top, .bottom: + beforePinnedItemsPosition = item.frame.minX + } + switch component.location { + case .side: + afterPinnedItemsPosition = item.frame.maxY + case .top, .bottom: + afterPinnedItemsPosition = item.frame.maxX + } + } seenPinnedItems = true } else { if !seenPinnedItems { @@ -1885,7 +1899,7 @@ public final class ChatSideTopicsPanel: Component { itemFrame = CGRect(origin: CGPoint(x: 8.0 + 4.0, y: directionContainerInset + 6.0), size: itemSize) directionContainerInset += itemSize.height case .top, .bottom: - itemFrame = CGRect(origin: CGPoint(x: 12.0, y: 6.0), size: itemSize) + itemFrame = CGRect(origin: CGPoint(x: 12.0, y: 5.0), size: itemSize) directionContainerInset += itemSize.width - 20.0 } diff --git a/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerExtractedPresentationNode.swift b/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerExtractedPresentationNode.swift index 7c1fa64281..546500c6f4 100644 --- a/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerExtractedPresentationNode.swift +++ b/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerExtractedPresentationNode.swift @@ -818,7 +818,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo if let transitionInfo = reference.transitionInfo() { if let referenceView = transitionInfo.referenceView as? ContextExtractableContainer { if #available(iOS 26.2, *) { - if transitionInfo.referenceView.bounds.width == transitionInfo.referenceView.bounds.height { + if !"".isEmpty, transitionInfo.referenceView.bounds.width == transitionInfo.referenceView.bounds.height { contextExtractableContainer = (referenceView, convertFrame(transitionInfo.referenceView.bounds.inset(by: transitionInfo.insets), from: transitionInfo.referenceView, to: self.view)) } } diff --git a/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift b/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift index 0a0b5a4028..56160337cd 100644 --- a/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift +++ b/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift @@ -1020,10 +1020,10 @@ public class ForumCreateTopicScreen: ViewControllerComponentContainer { switch mode { case .create: title = presentationData.strings.CreateTopic_CreateTitle - doneTitle = presentationData.strings.CreateTopic_Create + doneTitle = "___done" case let .edit(threadId, topic, isHidden): title = presentationData.strings.CreateTopic_EditTitle - doneTitle = presentationData.strings.Common_Done + doneTitle = "___done" self.state = (topic.title, topic.icon, topic.iconColor, threadId == 1 ? isHidden : nil) } @@ -1032,7 +1032,7 @@ public class ForumCreateTopicScreen: ViewControllerComponentContainer { self.readyValue.set(componentReady.get() |> timeout(0.3, queue: .mainQueue(), alternate: .single(true))) - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "___close", style: .plain, target: self, action: #selector(self.cancelPressed)) self.doneBarItem = UIBarButtonItem(title: doneTitle, style: .done, target: self, action: #selector(self.createPressed)) self.navigationItem.rightBarButtonItem = self.doneBarItem diff --git a/submodules/TelegramUI/Components/NavigationBarImpl/Sources/NavigationBarImpl.swift b/submodules/TelegramUI/Components/NavigationBarImpl/Sources/NavigationBarImpl.swift index 8639fa514d..f06aa9ef82 100644 --- a/submodules/TelegramUI/Components/NavigationBarImpl/Sources/NavigationBarImpl.swift +++ b/submodules/TelegramUI/Components/NavigationBarImpl/Sources/NavigationBarImpl.swift @@ -721,6 +721,12 @@ public final class NavigationBarImpl: ASDisplayNode, NavigationBar { } } } + self.leftButtonNodeImpl.requestUpdate = { [weak self] in + guard let self else { + return + } + self.requestLayout() + } self.rightButtonNodeImpl.pressed = { [weak self] index in if let item = self?.item { @@ -733,6 +739,12 @@ public final class NavigationBarImpl: ASDisplayNode, NavigationBar { } } } + self.rightButtonNodeImpl.requestUpdate = { [weak self] in + guard let self else { + return + } + self.requestLayout() + } } public var isBackgroundVisible: Bool { @@ -906,6 +918,9 @@ public final class NavigationBarImpl: ASDisplayNode, NavigationBar { case .accent: self.leftButtonNodeImpl.color = self.presentationData.theme.accentForegroundColor self.leftButtonNodeImpl.disabledColor = self.presentationData.theme.accentForegroundColor.withMultipliedAlpha(0.5) + case .accentDisabled: + self.leftButtonNodeImpl.color = self.presentationData.theme.accentForegroundColor + self.leftButtonNodeImpl.disabledColor = self.presentationData.theme.accentForegroundColor.withMultipliedAlpha(0.5) case .generic: self.leftButtonNodeImpl.color = self.presentationData.theme.buttonColor self.leftButtonNodeImpl.disabledColor = self.presentationData.theme.disabledButtonColor @@ -946,6 +961,9 @@ public final class NavigationBarImpl: ASDisplayNode, NavigationBar { case .accent: self.rightButtonNodeImpl.color = self.presentationData.theme.accentForegroundColor self.rightButtonNodeImpl.disabledColor = self.presentationData.theme.accentForegroundColor.withMultipliedAlpha(0.5) + case .accentDisabled: + self.rightButtonNodeImpl.color = self.presentationData.theme.accentForegroundColor + self.rightButtonNodeImpl.disabledColor = self.presentationData.theme.accentForegroundColor.withMultipliedAlpha(0.5) case .generic: self.rightButtonNodeImpl.color = self.presentationData.theme.buttonColor self.rightButtonNodeImpl.disabledColor = self.presentationData.theme.disabledButtonColor @@ -994,6 +1012,8 @@ public final class NavigationBarImpl: ASDisplayNode, NavigationBar { switch self.leftButtonNodeImpl.commonContentType { case .accent: leftButtonsColor = .init(kind: .custom(style: self.presentationData.theme.glassStyle == .clear ? .clear : .default, color: self.presentationData.theme.accentButtonColor)) + case .accentDisabled: + leftButtonsColor = .init(kind: .custom(style: self.presentationData.theme.glassStyle == .clear ? .clear : .default, color: self.presentationData.theme.accentDisabledButtonColor)) case .generic: break } @@ -1028,6 +1048,8 @@ public final class NavigationBarImpl: ASDisplayNode, NavigationBar { switch self.rightButtonNodeImpl.commonContentType { case .accent: rightButtonsColor = .init(kind: .custom(style: self.presentationData.theme.glassStyle == .clear ? .clear : .default, color: self.presentationData.theme.accentButtonColor)) + case .accentDisabled: + rightButtonsColor = .init(kind: .custom(style: self.presentationData.theme.glassStyle == .clear ? .clear : .default, color: self.presentationData.theme.accentDisabledButtonColor)) case .generic: break } diff --git a/submodules/TelegramUI/Components/NavigationBarImpl/Sources/NavigationButtonNode.swift b/submodules/TelegramUI/Components/NavigationBarImpl/Sources/NavigationButtonNode.swift index 5c5982341f..9970050187 100644 --- a/submodules/TelegramUI/Components/NavigationBarImpl/Sources/NavigationButtonNode.swift +++ b/submodules/TelegramUI/Components/NavigationBarImpl/Sources/NavigationButtonNode.swift @@ -211,6 +211,7 @@ private final class ItemComponent: Component { private final class NavigationButtonItemNode: ImmediateTextNode { private let isGlass: Bool + var requestUpdate: (() -> Void)? private func fontForCurrentState() -> UIFont { return self.bold ? Font.semibold(17.0) : Font.medium(17.0) @@ -235,7 +236,13 @@ private final class NavigationButtonItemNode: ImmediateTextNode { if let item = self.item { self.setEnabledListener = item.addSetEnabledListener { [weak self] value in - self?.isEnabled = value + guard let self else { + return + } + if self.isEnabled != value { + self.isEnabled = value + self.requestUpdate?() + } } self.accessibilityHint = item.accessibilityHint self.accessibilityLabel = item.accessibilityLabel @@ -528,6 +535,7 @@ private final class NavigationButtonItemNode: ImmediateTextNode { public final class NavigationButtonNodeImpl: ContextControllerSourceNode, NavigationButtonNode { enum ContentType { case accent + case accentDisabled case generic } @@ -537,6 +545,8 @@ public final class NavigationButtonNodeImpl: ContextControllerSourceNode, Naviga private var items: [UIBarButtonItem] = [] private var nodes: [NavigationButtonItemNode] = [] + var requestUpdate: (() -> Void)? + private var disappearingNodes: [(frame: CGRect, size: CGSize, node: NavigationButtonItemNode)] = [] public var singleCustomNode: ASDisplayNode? { @@ -633,6 +643,9 @@ public final class NavigationButtonNodeImpl: ContextControllerSourceNode, Naviga } } } + node.requestUpdate = { [weak self] in + self?.requestUpdate?() + } self.nodes.append(node) self.addSubnode(node) } @@ -680,6 +693,9 @@ public final class NavigationButtonNodeImpl: ContextControllerSourceNode, Naviga } } } + node.requestUpdate = { [weak self] in + self?.requestUpdate?() + } self.nodes.append(node) self.addSubnode(node) } @@ -812,7 +828,11 @@ public final class NavigationButtonNodeImpl: ContextControllerSourceNode, Naviga if item.title == "___close" { } else if item.title == "___clear" { } else if item.title == "___done" { - nodeContentType = .accent + if item.isEnabled { + nodeContentType = .accent + } else { + nodeContentType = .accentDisabled + } } if commonType == nil { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index e7c811e0b7..eb882686f9 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -6459,6 +6459,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc badgeStrokeColor: baseNavigationBarPresentationData.theme.badgeStrokeColor, badgeTextColor: baseNavigationBarPresentationData.theme.badgeTextColor, accentButtonColor: baseNavigationBarPresentationData.theme.accentButtonColor, + accentDisabledButtonColor: baseNavigationBarPresentationData.theme.accentDisabledButtonColor, accentForegroundColor: baseNavigationBarPresentationData.theme.accentForegroundColor ), strings: baseNavigationBarPresentationData.strings)) diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPeer.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPeer.swift index 4a364bda5a..d4e0f3fca8 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPeer.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPeer.swift @@ -409,39 +409,45 @@ extension ChatControllerImpl { } }))) } else { - items.append(.action(ContextMenuActionItem(text: strings.Chat_CreateTopic, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] action in - guard let self else { - return - } - - action.dismissWithResult(.default) - - let controller = ForumCreateTopicScreen(context: self.context, peerId: peerId, mode: .create) - controller.navigationPresentation = .modal - - controller.completion = { [weak self, weak controller] title, fileId, iconColor, _ in - controller?.isInProgress = true - controller?.view.endEditing(true) - + var canCreateTopics = false + if let peer = self.presentationInterfaceState.renderedPeer?.chatMainPeer as? TelegramUser, let botInfo = peer.botInfo, botInfo.flags.contains(.forumManagedByUser) { + canCreateTopics = true + } + if canCreateTopics { + items.append(.action(ContextMenuActionItem(text: strings.Chat_CreateTopic, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] action in guard let self else { return } - let _ = (self.context.engine.peers.createForumChannelTopic(id: peerId, title: title, iconColor: iconColor, iconFileId: fileId) - |> deliverOnMainQueue).startStandalone(next: { [weak self, weak controller] topicId in + action.dismissWithResult(.default) + + let controller = ForumCreateTopicScreen(context: self.context, peerId: peerId, mode: .create) + controller.navigationPresentation = .modal + + controller.completion = { [weak self, weak controller] title, fileId, iconColor, _ in + controller?.isInProgress = true + controller?.view.endEditing(true) + guard let self else { return } - self.updateChatLocationThread(threadId: topicId) - controller?.dismiss() - }, error: { _ in - controller?.isInProgress = false - }) - } - self.push(controller) - }))) + + let _ = (self.context.engine.peers.createForumChannelTopic(id: peerId, title: title, iconColor: iconColor, iconFileId: fileId) + |> deliverOnMainQueue).startStandalone(next: { [weak self, weak controller] topicId in + guard let self else { + return + } + self.updateChatLocationThread(threadId: topicId) + controller?.dismiss() + }, error: { _ in + controller?.isInProgress = false + }) + } + self.push(controller) + }))) + } } let presentationData = self.presentationData diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index a36a485982..87c6a95c98 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -2281,78 +2281,112 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let strongSelf else { return } - strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ - if let strongSelf = self { - strongSelf.chatDisplayNode.collapseInput() - - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { current in - var current = current - current = current.updatedInterfaceState { interfaceState in - var interfaceState = interfaceState - interfaceState = interfaceState.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil).withUpdatedPostSuggestionState(nil) - if clearInput { - interfaceState = interfaceState.withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString())) - } - return interfaceState - }.updatedInputMode { current in - if case let .media(mode, maybeExpanded, focused) = current, maybeExpanded != nil { - return .media(mode: mode, expanded: nil, focused: focused) - } - return current - } - - return current - }) - } - }, shouldAnimateMessageTransition ? correlationId : nil) - if shouldAnimateMessageTransition { - if let sourceNode = sourceView?.asyncdisplaykit_node as? ChatMediaInputStickerGridItemNode { - strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .stickerMediaInput(input: .inputPanel(itemNode: sourceNode), replyPanel: replyPanel), initiated: { - guard let strongSelf = self else { - return - } - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { current in - var current = current - current = current.updatedInputMode { current in - if case let .media(mode, maybeExpanded, focused) = current, maybeExpanded != nil { - return .media(mode: mode, expanded: nil, focused: focused) - } - return current + let addToTransitionNodeIfNeeded: () -> Void = { + guard let strongSelf = self else { + return + } + if shouldAnimateMessageTransition { + if let sourceNode = sourceView?.asyncdisplaykit_node as? ChatMediaInputStickerGridItemNode { + strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .stickerMediaInput(input: .inputPanel(itemNode: sourceNode), replyPanel: replyPanel), initiated: { + guard let strongSelf = self else { + return } - - return current - }) - }) - } else if let sourceNode = sourceView?.asyncdisplaykit_node as? HorizontalStickerGridItemNode { - strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .stickerMediaInput(input: .mediaPanel(itemNode: sourceNode), replyPanel: replyPanel), initiated: {}) - } else if let sourceNode = sourceView?.asyncdisplaykit_node as? ChatEmptyNodeStickerContentNode { - strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .stickerMediaInput(input: .emptyPanel(itemNode: sourceNode), replyPanel: nil), initiated: {}) - } else if let sourceLayer, let sourceView, let sourceRect { - strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .stickerMediaInput(input: .universal(sourceContainerView: sourceView, sourceRect: sourceRect, sourceLayer: sourceLayer), replyPanel: replyPanel), initiated: { - guard let strongSelf = self else { - return - } - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { current in - var current = current - current = current.updatedInputMode { current in - if case let .media(mode, maybeExpanded, focused) = current, maybeExpanded != nil { - return .media(mode: mode, expanded: nil, focused: focused) + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { current in + var current = current + current = current.updatedInputMode { current in + if case let .media(mode, maybeExpanded, focused) = current, maybeExpanded != nil { + return .media(mode: mode, expanded: nil, focused: focused) + } + return current } + return current - } - - return current + }) }) - }) + } else if let sourceNode = sourceView?.asyncdisplaykit_node as? HorizontalStickerGridItemNode { + strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .stickerMediaInput(input: .mediaPanel(itemNode: sourceNode), replyPanel: replyPanel), initiated: {}) + } else if let sourceNode = sourceView?.asyncdisplaykit_node as? ChatEmptyNodeStickerContentNode { + strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .stickerMediaInput(input: .emptyPanel(itemNode: sourceNode), replyPanel: nil), initiated: {}) + } else if let sourceLayer, let sourceView, let sourceRect { + strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .stickerMediaInput(input: .universal(sourceContainerView: sourceView, sourceRect: sourceRect, sourceLayer: sourceLayer), replyPanel: replyPanel), initiated: { + guard let strongSelf = self else { + return + } + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { current in + var current = current + current = current.updatedInputMode { current in + if case let .media(mode, maybeExpanded, focused) = current, maybeExpanded != nil { + return .media(mode: mode, expanded: nil, focused: focused) + } + return current + } + + return current + }) + }) + } } } - let messages: [EnqueueMessage] = [.message(text: "", attributes: attributes, inlineStickers: [:], mediaReference: fileReference.abstract, threadId: strongSelf.chatLocation.threadId, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets)] + let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject + + let messages: [EnqueueMessage] = [.message(text: "", attributes: attributes, inlineStickers: [:], mediaReference: fileReference.abstract, threadId: strongSelf.chatLocation.threadId, replyToMessageId: replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets)] if silentPosting { + strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ + if let strongSelf = self { + strongSelf.chatDisplayNode.collapseInput() + + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { current in + var current = current + current = current.updatedInterfaceState { interfaceState in + var interfaceState = interfaceState + interfaceState = interfaceState.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil).withUpdatedPostSuggestionState(nil) + if clearInput { + interfaceState = interfaceState.withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString())) + } + return interfaceState + }.updatedInputMode { current in + if case let .media(mode, maybeExpanded, focused) = current, maybeExpanded != nil { + return .media(mode: mode, expanded: nil, focused: focused) + } + return current + } + + return current + }) + } + }, shouldAnimateMessageTransition ? correlationId : nil) + + addToTransitionNodeIfNeeded() let transformedMessages = strongSelf.transformEnqueueMessages(messages, silentPosting: silentPosting, postpone: postpone) strongSelf.sendMessages(transformedMessages) } else if schedule { + strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ + if let strongSelf = self { + strongSelf.chatDisplayNode.collapseInput() + + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { current in + var current = current + current = current.updatedInterfaceState { interfaceState in + var interfaceState = interfaceState + interfaceState = interfaceState.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil).withUpdatedPostSuggestionState(nil) + if clearInput { + interfaceState = interfaceState.withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString())) + } + return interfaceState + }.updatedInputMode { current in + if case let .media(mode, maybeExpanded, focused) = current, maybeExpanded != nil { + return .media(mode: mode, expanded: nil, focused: focused) + } + return current + } + + return current + }) + } + }, shouldAnimateMessageTransition ? correlationId : nil) + strongSelf.presentScheduleTimePicker(completion: { [weak self] scheduleTime, repeatPeriod in if let strongSelf = self { let transformedMessages = strongSelf.transformEnqueueMessages(messages, silentPosting: false, scheduleTime: scheduleTime, repeatPeriod: repeatPeriod, postpone: postpone) @@ -2360,8 +2394,90 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }) } else { - let transformedMessages = strongSelf.transformEnqueueMessages(messages, postpone: postpone) - strongSelf.sendMessages(transformedMessages) + let messages = strongSelf.transformEnqueueMessages(messages, postpone: postpone) + + var targetThreadId: Int64? + var clearMainThreadForward = false + if strongSelf.chatLocation.threadId == nil, let user = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramUser, let botInfo = user.botInfo, botInfo.flags.contains(.hasForum), botInfo.flags.contains(.forumManagedByUser) { + if let message = messages.first { + switch message { + case let .message(_, _, _, _, _, replyToMessageId, _, _, _, _): + if let _ = replyToMessageId { + if let replyMessage = strongSelf.presentationInterfaceState.replyMessage { + targetThreadId = replyMessage.threadId + } + } else { + targetThreadId = EngineMessage.newTopicThreadId + } + case let .forward(_, threadId, _, _, _): + if let threadId { + targetThreadId = threadId + } else { + targetThreadId = EngineMessage.newTopicThreadId + clearMainThreadForward = true + } + } + } + } + + let doSend: (Int64?) -> Void = { [weak self] overrideThreadId in + guard let strongSelf = self else { + return + } + + var messages = messages + if let overrideThreadId { + messages = messages.map { message in + return message.withUpdatedThreadId(overrideThreadId) + } + } + + strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ + if let strongSelf = self { + strongSelf.chatDisplayNode.collapseInput() + + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { current in + var current = current + current = current.updatedInterfaceState { interfaceState in + var interfaceState = interfaceState + interfaceState = interfaceState.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil).withUpdatedPostSuggestionState(nil) + if clearInput { + interfaceState = interfaceState.withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString())) + } + return interfaceState + }.updatedInputMode { current in + if case let .media(mode, maybeExpanded, focused) = current, maybeExpanded != nil { + return .media(mode: mode, expanded: nil, focused: focused) + } + return current + } + + return current + }) + } + }, shouldAnimateMessageTransition ? correlationId : nil) + + addToTransitionNodeIfNeeded() + + strongSelf.sendMessages(messages.map { $0.withUpdatedReplyToMessageId(replyMessageSubject?.subjectModel) }) + } + + if let targetThreadId { + strongSelf.chatDisplayNode.historyNode.stopHistoryUpdates() + strongSelf.updateChatLocationThread(threadId: targetThreadId, animationDirection: .right, transferInputState: true, completion: { [weak strongSelf] in + guard let strongSelf else { + return + } + doSend(targetThreadId) + if clearMainThreadForward, let peerId = strongSelf.chatLocation.peerId { + let _ = ChatInterfaceState.update(engine: strongSelf.context.engine, peerId: peerId, threadId: nil, { current in + return current.withUpdatedForwardMessageIds(nil) + }).startStandalone() + } + }) + } else { + doSend(nil) + } } }) return true @@ -8689,10 +8805,58 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G mappedMessages.append(message) } + + let messages = strongSelf.transformEnqueueMessages(mappedMessages, silentPosting: silentPosting, scheduleTime: scheduleTime, postpone: postpone) + let replyMessageSubject = replyToSubject ?? strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject + + var targetThreadId: Int64? + var clearMainThreadForward = false + if strongSelf.chatLocation.threadId == nil, let user = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramUser, let botInfo = user.botInfo, botInfo.flags.contains(.hasForum), botInfo.flags.contains(.forumManagedByUser) { + if let message = messages.first { + switch message { + case let .message(_, _, _, _, _, replyToMessageId, _, _, _, _): + if let _ = replyToMessageId { + if let replyMessage = strongSelf.presentationInterfaceState.replyMessage { + targetThreadId = replyMessage.threadId + } + } else { + targetThreadId = EngineMessage.newTopicThreadId + } + case let .forward(_, threadId, _, _, _): + if let threadId { + targetThreadId = threadId + } else { + targetThreadId = EngineMessage.newTopicThreadId + clearMainThreadForward = true + } + } + } + } - if addedTransitions.count > 1 { - var transitions: [(Int64, ChatMessageTransitionNodeImpl.Source, () -> Void)] = [] - for (correlationId, uniqueIds, initiated) in addedTransitions { + let addTransitionNodes: () -> Void = { + guard let strongSelf = self else { + return + } + + if addedTransitions.count > 1 { + var transitions: [(Int64, ChatMessageTransitionNodeImpl.Source, () -> Void)] = [] + for (correlationId, uniqueIds, initiated) in addedTransitions { + var source: ChatMessageTransitionNodeImpl.Source? + if uniqueIds.count > 1 { + source = .groupedMediaInput(ChatMessageTransitionNodeImpl.Source.GroupedMediaInput(extractSnapshots: { + return uniqueIds.compactMap({ getAnimatedTransitionSource?($0) }) + })) + } else if let uniqueId = uniqueIds.first { + source = .mediaInput(ChatMessageTransitionNodeImpl.Source.MediaInput(extractSnapshot: { + return getAnimatedTransitionSource?(uniqueId) + })) + } + if let source = source { + transitions.append((correlationId, source, initiated)) + } + } + strongSelf.chatDisplayNode.messageTransitionNode.add(grouped: transitions) + } else if let (correlationId, uniqueIds, initiated) = addedTransitions.first { var source: ChatMessageTransitionNodeImpl.Source? if uniqueIds.count > 1 { source = .groupedMediaInput(ChatMessageTransitionNodeImpl.Source.GroupedMediaInput(extractSnapshots: { @@ -8704,26 +8868,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G })) } if let source = source { - transitions.append((correlationId, source, initiated)) + strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: source, initiated: { + initiated() + }) } } - strongSelf.chatDisplayNode.messageTransitionNode.add(grouped: transitions) - } else if let (correlationId, uniqueIds, initiated) = addedTransitions.first { - var source: ChatMessageTransitionNodeImpl.Source? - if uniqueIds.count > 1 { - source = .groupedMediaInput(ChatMessageTransitionNodeImpl.Source.GroupedMediaInput(extractSnapshots: { - return uniqueIds.compactMap({ getAnimatedTransitionSource?($0) }) - })) - } else if let uniqueId = uniqueIds.first { - source = .mediaInput(ChatMessageTransitionNodeImpl.Source.MediaInput(extractSnapshot: { - return getAnimatedTransitionSource?(uniqueId) - })) - } - if let source = source { - strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: source, initiated: { - initiated() - }) - } } if case let .customChatContents(customChatContents) = strongSelf.presentationInterfaceState.subject, let messageLimit = customChatContents.messageLimit { @@ -8740,21 +8889,50 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } } - - let messages = strongSelf.transformEnqueueMessages(mappedMessages, silentPosting: silentPosting, scheduleTime: scheduleTime, postpone: postpone) - let replyMessageSubject = replyToSubject ?? strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject - strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ - if let strongSelf = self { - strongSelf.chatDisplayNode.collapseInput() - - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil).withUpdatedPostSuggestionState(nil) } - }) + + let doSend: (Int64?) -> Void = { [weak strongSelf] overrideThreadId in + guard let strongSelf else { + return } - completionImpl?() - }, usedCorrelationId) + + var messages = messages + if let overrideThreadId { + messages = messages.map { message in + return message.withUpdatedThreadId(overrideThreadId) + } + } + + strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ + if let strongSelf = self { + strongSelf.chatDisplayNode.collapseInput() - strongSelf.sendMessages(messages.map { $0.withUpdatedReplyToMessageId(replyMessageSubject?.subjectModel) }, media: true) + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil).withUpdatedPostSuggestionState(nil) } + }) + } + completionImpl?() + }, usedCorrelationId) + + addTransitionNodes() + strongSelf.sendMessages(messages.map { $0.withUpdatedReplyToMessageId(replyMessageSubject?.subjectModel) }, media: true) + } + + if let targetThreadId { + strongSelf.chatDisplayNode.historyNode.stopHistoryUpdates() + strongSelf.updateChatLocationThread(threadId: targetThreadId, animationDirection: .right, transferInputState: true, completion: { [weak strongSelf] in + guard let strongSelf else { + return + } + doSend(targetThreadId) + if clearMainThreadForward, let peerId = strongSelf.chatLocation.peerId { + let _ = ChatInterfaceState.update(engine: strongSelf.context.engine, peerId: peerId, threadId: nil, { current in + return current.withUpdatedForwardMessageIds(nil) + }).startStandalone() + } + }) + } else { + doSend(nil) + } if let _ = scheduleTime { completion() diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index fb25659d4f..cf3f0612e7 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -2051,6 +2051,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { messageTransitionNode.transform = previousMessageTransitionNode.transform previousMessageTransitionNode.supernode?.insertSubnode(self.messageTransitionNode, aboveSubnode: previousMessageTransitionNode) + previousMessageTransitionNode.overlayContainerNode.supernode?.insertSubnode( self.messageTransitionNode.overlayContainerNode, aboveSubnode: previousMessageTransitionNode.overlayContainerNode) self.emptyType = nil self.isLoadingValue = false diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 798844b9d7..0d3099e97e 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -484,8 +484,13 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState guard let interfaceInteraction = interfaceInteraction, let controllerInteraction = controllerInteraction else { return .single(ContextController.Items(content: .list([]))) } - if let message = messages.first, message.id.namespace < 0 { - return .single(ContextController.Items(content: .list([]))) + if let message = messages.first { + if message.id.namespace < 0 { + return .single(ContextController.Items(content: .list([]))) + } + if message.id.namespace == Namespaces.Message.Local && message.attributes.contains(where: { $0 is TypingDraftMessageAttribute }) { + return .single(ContextController.Items(content: .list([]))) + } } var isEmbeddedMode = false diff --git a/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift b/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift index 82217835b6..b56451a7dd 100644 --- a/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift @@ -388,9 +388,9 @@ public final class ChatMessageTransitionNodeImpl: ASDisplayNode, ChatMessageTran func beginAnimation() { if let portalTargetView = self.portalTargetView { portalTargetView.view.alpha = 0.0 - portalTargetView.view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15) + portalTargetView.view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, delay: 0.15) - self.portalSourceView.layer.animateAlpha(from: 0.01, to: 1.0, duration: 0.12) + self.portalSourceView.layer.animateAlpha(from: 0.01, to: 1.0, duration: 0.12, delay: 0.15) } let verticalDuration: Double = ChatMessageTransitionNodeImpl.animationDuration diff --git a/submodules/WebSearchUI/Sources/WebSearchGalleryController.swift b/submodules/WebSearchUI/Sources/WebSearchGalleryController.swift index 48266cf949..dcf02efb5f 100644 --- a/submodules/WebSearchUI/Sources/WebSearchGalleryController.swift +++ b/submodules/WebSearchUI/Sources/WebSearchGalleryController.swift @@ -60,7 +60,7 @@ final class WebSearchGalleryControllerPresentationArguments { } class WebSearchGalleryController: ViewController { - private static let navigationTheme = NavigationBarTheme(overallDarkAppearance: false, buttonColor: .white, disabledButtonColor: UIColor(rgb: 0x525252), primaryTextColor: .white, backgroundColor: .clear, enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear, accentButtonColor: .white, accentForegroundColor: .black) + private static let navigationTheme = NavigationBarTheme(overallDarkAppearance: false, buttonColor: .white, disabledButtonColor: UIColor(rgb: 0x525252), primaryTextColor: .white, backgroundColor: .clear, enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear, accentButtonColor: .white, accentDisabledButtonColor: .white, accentForegroundColor: .black) private var galleryNode: GalleryControllerNode { return self.displayNode as! GalleryControllerNode diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index a20596acf5..0c267441ed 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -3801,6 +3801,7 @@ public final class WebAppController: ViewController, AttachmentContainable { badgeStrokeColor: .clear, badgeTextColor: .clear, accentButtonColor: self.presentationData.theme.list.itemCheckColors.fillColor, + accentDisabledButtonColor: self.presentationData.theme.chat.inputPanel.panelControlDisabledColor, accentForegroundColor: self.presentationData.theme.list.itemCheckColors.foregroundColor ), strings: NavigationBarStrings(back: "", close: "") From 41d6f727e060b2514d08efdb3dd126576ec93beb Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Fri, 10 Apr 2026 17:21:36 +0200 Subject: [PATCH 4/4] Various improvements --- .../Sources/ChatMessageItemImpl.swift | 2 +- .../ChatMessageTextBubbleContentNode.swift | 28 +-- .../Sources/ChatNewThreadInfoItem.swift | 24 +- .../Sources/ChatSideTopicsPanel.swift | 1 - .../Sources/InteractiveTextComponent.swift | 212 ++++++++++++++---- .../ChatInterfaceStateNavigationButtons.swift | 3 + .../ChatPinnedMessageTitlePanelNode.swift | 2 +- 7 files changed, 207 insertions(+), 65 deletions(-) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift index 881982906f..5907b5360f 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift @@ -518,7 +518,7 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible } } - if viewClassName == ChatMessageBubbleItemNode.self && self.presentationData.largeEmoji && self.message.media.isEmpty { + if viewClassName == ChatMessageBubbleItemNode.self && self.presentationData.largeEmoji && self.message.media.isEmpty && !self.message.attributes.contains(where: { $0 is TypingDraftMessageAttribute }) { if case let .message(_, _, _, attributes, _) = self.content { switch attributes.contentTypeHint { case .largeEmoji: diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift index af00e36bc7..000d153ca8 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift @@ -249,7 +249,6 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { let currentMaxGlyphCount: Int? if let textRevealAnimationState = self.textRevealAnimationState { currentMaxGlyphCount = textRevealAnimationState.glyphCount(timestamp: CACurrentMediaTime()) - //print("currentMaxGlyphCount(\(textRevealAnimationState.fromCount) -> \(textRevealAnimationState.toCount)) fraction: \(textRevealAnimationState.fraction(timestamp: CACurrentMediaTime()))") } else { currentMaxGlyphCount = nil } @@ -792,7 +791,6 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { if let clippedGlyphCountLayout { textFrame.size = clippedGlyphCountLayout.size - //print("currentMaxGlyphCount: \(currentMaxGlyphCount), size: \(textFrame.size.height)") textFrameWithoutInsets.size = CGSize(width: textFrame.width - textInsets.left - textInsets.right, height: textFrame.height - textInsets.top - textInsets.bottom) } @@ -1053,7 +1051,10 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { codeHighlightState.disposable.dispose() } - if previousAnimateGlyphCount != nil || strongSelf.textRevealAnimationState != nil || hadDraft { + if previousAnimateGlyphCount != nil || strongSelf.textRevealAnimationState != nil || hasDraft || hadDraft { + if strongSelf.textNode.textNode.revealGlyphCount == nil { + strongSelf.textNode.textNode.updateRevealGlyphCount(count: previousAnimateGlyphCount ?? 0) + } strongSelf.updateTextRevealAnimation(previousGlyphCount: previousAnimateGlyphCount ?? 0) } } @@ -1092,7 +1093,6 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { duration: duration ) if self.textRevealLink == nil, self.textRevealAnimationState != nil { - var lastLineUpdateTimestamp = timestamp self.textRevealLink = SharedDisplayLinkDriver.shared.add { [weak self] _ in guard let self else { return @@ -1109,22 +1109,22 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { self.textNode.textNode.updateRevealGlyphCount(count: nil) self.requestFullUpdate?() } else { - let lineUpdateTimeout = timestamp - lastLineUpdateTimestamp - var requestUpdate = false let glyphCount = textRevealAnimationState.glyphCount(timestamp: timestamp) - if let revealGlyphCount = self.textNode.textNode.revealGlyphCount, let cachedLayout = self.textNode.textNode.cachedLayout { - let previousLayout = cachedLayout.layoutForGlyphCount(glyphCount: revealGlyphCount) - let updatedLayout = cachedLayout.layoutForGlyphCount(glyphCount: glyphCount) - - if updatedLayout.size.height != previousLayout.size.height || abs(updatedLayout.size.width - previousLayout.size.width) > 8.0 { - if lineUpdateTimeout >= 0.0 { - lastLineUpdateTimestamp = timestamp + if let previousRevealGlyphCount = self.textNode.textNode.revealGlyphCount, previousRevealGlyphCount != glyphCount { + if let cachedLayout = self.textNode.textNode.cachedLayout { + if cachedLayout.sizeForGlyphCount(glyphCount: previousRevealGlyphCount) != cachedLayout.sizeForGlyphCount(glyphCount: glyphCount) { requestUpdate = true } + } else { + requestUpdate = true } + if requestUpdate { + //print("glyphCount: request update") + } + + self.textNode.textNode.updateRevealGlyphCount(count: glyphCount) } - self.textNode.textNode.updateRevealGlyphCount(count: glyphCount) if requestUpdate { self.requestFullUpdate?() diff --git a/submodules/TelegramUI/Components/Chat/ChatNewThreadInfoItem/Sources/ChatNewThreadInfoItem.swift b/submodules/TelegramUI/Components/Chat/ChatNewThreadInfoItem/Sources/ChatNewThreadInfoItem.swift index 9870951c4e..c198b36a82 100644 --- a/submodules/TelegramUI/Components/Chat/ChatNewThreadInfoItem/Sources/ChatNewThreadInfoItem.swift +++ b/submodules/TelegramUI/Components/Chat/ChatNewThreadInfoItem/Sources/ChatNewThreadInfoItem.swift @@ -80,7 +80,7 @@ public final class ChatNewThreadInfoItemNode: ListViewItemNode, ASGestureRecogni public let offsetContainer: ASDisplayNode public let titleNode: TextNode public let subtitleNode: TextNode - let arrowView: UIImageView + var arrowView: UIImageView? let iconBackground: SimpleLayer var icon = ComponentView() @@ -106,8 +106,6 @@ public final class ChatNewThreadInfoItemNode: ListViewItemNode, ASGestureRecogni self.subtitleNode.isUserInteractionEnabled = false self.subtitleNode.displaysAsynchronously = false - self.arrowView = UIImageView() - super.init(layerBacked: false, rotated: true) self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) @@ -231,8 +229,14 @@ public final class ChatNewThreadInfoItemNode: ListViewItemNode, ASGestureRecogni if strongSelf.iconBackground.superlayer == nil { strongSelf.offsetContainer.layer.addSublayer(strongSelf.iconBackground) } - if strongSelf.arrowView.superview == nil { - strongSelf.offsetContainer.view.addSubview(strongSelf.arrowView) + + let arrowView: UIImageView + if let current = self?.arrowView { + arrowView = current + } else { + arrowView = UIImageView() + strongSelf.arrowView = arrowView + strongSelf.offsetContainer.view.addSubview(arrowView) } let iconComponent = AnyComponent(BundleIconComponent( @@ -265,15 +269,15 @@ public final class ChatNewThreadInfoItemNode: ListViewItemNode, ASGestureRecogni contentOriginY += subtitleLayout.size.height contentOriginY += 20.0 - if strongSelf.arrowView.image == nil { - strongSelf.arrowView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Search/DownButton"), color: .white)?.withRenderingMode(.alwaysTemplate) + if arrowView.image == nil { + arrowView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Search/DownButton"), color: .white)?.withRenderingMode(.alwaysTemplate) } - strongSelf.arrowView.tintColor = primaryTextColor.withMultipliedAlpha(0.5) - if let image = strongSelf.arrowView.image { + arrowView.tintColor = primaryTextColor.withMultipliedAlpha(0.5) + if let image = arrowView.image { let scaleFactor: CGFloat = 0.8 let imageSize = CGSize(width: floor(image.size.width * scaleFactor), height: floor(image.size.height * scaleFactor)) let arrowFrame = CGRect(origin: CGPoint(x: backgroundFrame.origin.x + floor((backgroundSize.width - imageSize.width) / 2.0), y: backgroundFrame.minY + backgroundFrame.height - 8.0 - imageSize.height), size: imageSize) - strongSelf.arrowView.frame = arrowFrame + arrowView.frame = arrowFrame } if strongSelf.backgroundContent == nil, let backgroundContent = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) { diff --git a/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatSideTopicsPanel.swift b/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatSideTopicsPanel.swift index b431c8ff21..6de9dc2e22 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatSideTopicsPanel.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatSideTopicsPanel.swift @@ -1502,7 +1502,6 @@ public final class ChatSideTopicsPanel: Component { self.scrollContainerView = UIView() self.scrollViewMask = UIImageView() self.scrollContainerView.mask = self.scrollViewMask - //self.scrollContainerView.addSubview(self.scrollViewMask) super.init(frame: frame) diff --git a/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift b/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift index c4ffa9609a..1c0eef8cd2 100644 --- a/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift +++ b/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift @@ -2390,14 +2390,12 @@ final class TextContentItemLayer: SimpleLayer { let size: CGSize let item: TextContentItem let mask: RenderMask? - let maxGlyphDrawCount: Int? - - init(size: CGSize, item: TextContentItem, mask: RenderMask?, maxGlyphDrawCount: Int?) { + + init(size: CGSize, item: TextContentItem, mask: RenderMask?) { self.size = size self.item = item self.mask = mask - self.maxGlyphDrawCount = maxGlyphDrawCount - + super.init() } } @@ -2476,8 +2474,6 @@ final class TextContentItemLayer: SimpleLayer { let offset = params.item.contentOffset let alignment: NSTextAlignment = .left - var drawnGlyphCount = 0 - for i in 0 ..< params.item.segment.lines.count { let line = params.item.segment.lines[i] @@ -2508,14 +2504,6 @@ final class TextContentItemLayer: SimpleLayer { let run = run as! CTRun let glyphCount = CTRunGetGlyphCount(run) - var runDrawGlyphCount = glyphCount - if let maxGlyphDrawCount = params.maxGlyphDrawCount { - if drawnGlyphCount >= maxGlyphDrawCount { - break - } - runDrawGlyphCount = CFIndex(max(0, min(Int(glyphCount), maxGlyphDrawCount - drawnGlyphCount))) - } - let attributes = CTRunGetAttributes(run) as NSDictionary if attributes["Attribute__EmbeddedItem"] != nil { continue @@ -2565,14 +2553,12 @@ final class TextContentItemLayer: SimpleLayer { let stringRange = CTRunGetStringRange(run) if line.attachments.contains(where: { $0.range.contains(stringRange.location) }) { } else { - CTRunDraw(run, context, CFRangeMake(0, runDrawGlyphCount)) + CTRunDraw(run, context, CFRangeMake(0, glyphCount)) } } else { - CTRunDraw(run, context, CFRangeMake(0, runDrawGlyphCount)) + CTRunDraw(run, context, CFRangeMake(0, glyphCount)) } - - drawnGlyphCount += Int(glyphCount) - + if fixDoubleEmoji { context.setBlendMode(.normal) } @@ -2740,8 +2726,9 @@ final class TextContentItemLayer: SimpleLayer { } private(set) var params: Params? - + let renderNode: RenderNode + private let renderNodeContainer: SimpleLayer private var contentMaskNode: ASImageNode? private var overlayContentLayer: SimpleLayer? @@ -2755,21 +2742,26 @@ final class TextContentItemLayer: SimpleLayer { private var currentAnimationId: Int = 0 private var isAnimating: Bool = false private var currentContentMask: RenderMask? - + private var revealMaskLayer: SimpleLayer? + private var revealLineMaskLayers: [SimpleLayer] = [] + private var maxGlyphDrawCount: Int? init(displaysAsynchronously: Bool) { self.renderNode = RenderNode() self.renderNode.displaysAsynchronously = displaysAsynchronously - + self.renderNodeContainer = SimpleLayer() + super.init() - - self.addSublayer(self.renderNode.layer) + + self.renderNodeContainer.addSublayer(self.renderNode.layer) + self.addSublayer(self.renderNodeContainer) } - + override init(layer: Any) { self.renderNode = RenderNode() - + self.renderNodeContainer = SimpleLayer() + super.init(layer: layer) } @@ -2778,13 +2770,155 @@ final class TextContentItemLayer: SimpleLayer { } func updateMaxGlyphDrawCount(value: Int?) { - if self.maxGlyphDrawCount != value { - self.maxGlyphDrawCount = value - - if let renderParams = self.renderNode.params { - self.renderNode.params = RenderParams(size: renderParams.size, item: renderParams.item, mask: renderParams.mask, maxGlyphDrawCount: self.maxGlyphDrawCount) - self.renderNode.displayImmediately() + if self.maxGlyphDrawCount == value { + return + } + self.maxGlyphDrawCount = value + self.updateRevealMask() + } + + private func updateRevealMask() { + guard let params = self.params else { + return + } + + guard let maxGlyphDrawCount = self.maxGlyphDrawCount else { + if let _ = self.revealMaskLayer { + self.renderNodeContainer.mask = nil + self.revealMaskLayer = nil + self.revealLineMaskLayers.removeAll() } + return + } + + let item = params.item + let lines = item.segment.lines + let layerSize = item.size + let offset = item.contentOffset + + let revealMaskLayer: SimpleLayer + if let existing = self.revealMaskLayer { + revealMaskLayer = existing + } else { + revealMaskLayer = SimpleLayer() + revealMaskLayer.backgroundColor = UIColor.clear.cgColor + self.revealMaskLayer = revealMaskLayer + self.renderNodeContainer.mask = revealMaskLayer + } + revealMaskLayer.frame = CGRect(origin: CGPoint(), size: layerSize) + + // First pass: compute per-line state + struct LineInfo { + let minY: CGFloat + let maxY: CGFloat + let maskFrame: CGRect // only meaningful for partial lines + let isFull: Bool + } + + var lineInfos: [LineInfo] = [] + var remainingGlyphs = maxGlyphDrawCount + + for i in 0 ..< lines.count { + let line = lines[i] + + var lineFrame = line.frame + lineFrame.origin.y += offset.y + + if line.isRTL { + lineFrame.origin.x = offset.x + floor(layerSize.width - lineFrame.width) + lineFrame = displayLineFrame(frame: lineFrame, isRTL: true, boundingRect: CGRect(origin: CGPoint(), size: layerSize), cutout: nil) + } else { + lineFrame.origin.x += offset.x + } + + let lineHeight = line.ascent + line.descent + + if remainingGlyphs <= 0 { + // Empty line — skip, no mask rect needed + continue + } + + var lineGlyphCount = 0 + var revealedWidth: CGFloat = 0.0 + let glyphRuns = CTLineGetGlyphRuns(line.line) as NSArray + + for run in glyphRuns { + let run = run as! CTRun + let glyphCount = Int(CTRunGetGlyphCount(run)) + lineGlyphCount += glyphCount + + if remainingGlyphs > 0 { + let revealCount = min(glyphCount, remainingGlyphs) + + var advances = [CGSize](repeating: CGSize(), count: revealCount) + CTRunGetAdvances(run, CFRangeMake(0, revealCount), &advances) + + for j in 0 ..< revealCount { + revealedWidth += advances[j].width + } + + remainingGlyphs -= glyphCount + } + } + + let isFull = remainingGlyphs >= 0 + revealedWidth = ceil(revealedWidth) + + let maskFrame: CGRect + if isFull { + maskFrame = CGRect(x: 0.0, y: lineFrame.minY, width: layerSize.width, height: lineHeight) + } else if line.isRTL { + maskFrame = CGRect(x: lineFrame.maxX - revealedWidth, y: lineFrame.minY, width: revealedWidth, height: lineHeight) + } else { + maskFrame = CGRect(x: lineFrame.minX, y: lineFrame.minY, width: revealedWidth, height: lineHeight) + } + + lineInfos.append(LineInfo(minY: lineFrame.minY, maxY: lineFrame.minY + lineHeight, maskFrame: maskFrame, isFull: isFull)) + } + + // Second pass: merge consecutive fully-filled lines into single rects + var maskRects: [CGRect] = [] + var mergeStartY: CGFloat? + var mergeEndY: CGFloat? + + for info in lineInfos { + if info.isFull { + if mergeStartY != nil { + mergeEndY = info.maxY + } else { + mergeStartY = info.minY + mergeEndY = info.maxY + } + } else { + // Flush any pending merged full-lines + if let startY = mergeStartY, let endY = mergeEndY { + maskRects.append(CGRect(x: 0.0, y: startY, width: layerSize.width, height: endY - startY)) + mergeStartY = nil + mergeEndY = nil + } + // Add the partial line + maskRects.append(info.maskFrame) + } + } + // Flush trailing merged full-lines + if let startY = mergeStartY, let endY = mergeEndY { + maskRects.append(CGRect(x: 0.0, y: startY, width: layerSize.width, height: endY - startY)) + } + + // Update mask child layers to match the number of rects + while self.revealLineMaskLayers.count < maskRects.count { + let childLayer = SimpleLayer() + childLayer.backgroundColor = UIColor.white.cgColor + revealMaskLayer.addSublayer(childLayer) + self.revealLineMaskLayers.append(childLayer) + } + while self.revealLineMaskLayers.count > maskRects.count { + let removed = self.revealLineMaskLayers.removeLast() + removed.removeFromSuperlayer() + } + + for i in 0 ..< maskRects.count { + self.revealLineMaskLayers[i].frame = maskRects[i] } } @@ -2923,7 +3057,8 @@ final class TextContentItemLayer: SimpleLayer { } } - animation.animator.updateFrame(layer: self.renderNode.layer, frame: effectiveContentFrame, completion: nil) + animation.animator.updateFrame(layer: self.renderNodeContainer, frame: effectiveContentFrame, completion: nil) + animation.animator.updateFrame(layer: self.renderNode.layer, frame: CGRect(origin: CGPoint(), size: effectiveContentFrame.size), completion: nil) var staticContentMask = contentMask if let contentMask, self.isAnimating { @@ -3040,10 +3175,11 @@ final class TextContentItemLayer: SimpleLayer { self.currentContentMask = contentMask - self.renderNode.params = RenderParams(size: contentFrame.size, item: params.item, mask: staticContentMask, maxGlyphDrawCount: self.maxGlyphDrawCount) + self.renderNode.params = RenderParams(size: contentFrame.size, item: params.item, mask: staticContentMask) + self.updateRevealMask() if synchronously { if let spoilerExpandRect, animation.isAnimated { - let localSpoilerExpandRect = spoilerExpandRect.offsetBy(dx: -self.renderNode.frame.minX, dy: -self.renderNode.frame.minY) + let localSpoilerExpandRect = spoilerExpandRect.offsetBy(dx: -self.renderNodeContainer.frame.minX, dy: -self.renderNodeContainer.frame.minY) let revealAnimationDuration: CGFloat = 0.55 @@ -3051,7 +3187,7 @@ final class TextContentItemLayer: SimpleLayer { let previousContents = self.renderNode.layer.contents let copyContentsLayer = SimpleLayer() - copyContentsLayer.frame = self.renderNode.frame + copyContentsLayer.frame = self.renderNodeContainer.frame copyContentsLayer.contents = previousContents copyContentsLayer.masksToBounds = self.renderNode.layer.masksToBounds copyContentsLayer.contentsGravity = self.renderNode.layer.contentsGravity @@ -3073,7 +3209,7 @@ final class TextContentItemLayer: SimpleLayer { copyContentsLayer.addSublayer(copySublayer) } - self.renderNode.layer.superlayer?.insertSublayer(copyContentsLayer, below: self.renderNode.layer) + self.renderNodeContainer.superlayer?.insertSublayer(copyContentsLayer, below: self.renderNodeContainer) self.renderNode.displayImmediately() diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift index 803f089285..29e1aaa190 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift @@ -78,6 +78,9 @@ func leftNavigationButtonForChatInterfaceState(_ presentationInterfaceState: Cha } func rightNavigationButtonForChatInterfaceState(context: AccountContext, presentationInterfaceState: ChatPresentationInterfaceState, strings: PresentationStrings, currentButton: ChatNavigationButton?, target: Any?, selector: Selector?, chatInfoNavigationButton: ChatNavigationButton?, moreInfoNavigationButton: ChatNavigationButton?) -> ChatNavigationButton? { + if case .standard(.previewing) = presentationInterfaceState.mode { + return nil + } var hasMessages = false if let chatHistoryState = presentationInterfaceState.chatHistoryState { if case .loaded(false, _) = chatHistoryState { diff --git a/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift index 534654c109..6fc1835df1 100644 --- a/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift @@ -797,7 +797,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { messageText = NSAttributedString(string: foldLineBreaks(textString.string), font: textFont, textColor: message.media.isEmpty || message.media.first is TelegramMediaWebpage ? theme.chat.inputPanel.primaryTextColor : theme.chat.inputPanel.secondaryTextColor) } - let textConstrainedSize = CGSize(width: width - textLineInset - contentLeftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude) + let textConstrainedSize = CGSize(width: width - textLineInset - contentLeftInset - rightInset - textRightInset - 10.0, height: CGFloat.greatestFiniteMagnitude) let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: messageText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 0.0, bottom: 2.0, right: 0.0))) let spoilerTextLayoutAndApply: (TextNodeLayout, (TextNodeWithEntities.Arguments?) -> TextNodeWithEntities)?