From 858e00c99194d780aba949cad80ad3d3492a7688 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 11 Nov 2025 21:58:05 +0800 Subject: [PATCH] Various improvements --- .../Sources/ChatTextInputPanelComponent.swift | 9 ++- .../Sources/ChatTextInputPanelNode.swift | 36 +++++++++ .../Sources/StoryContainerScreen.swift | 37 ++++++++++ .../Sources/StoryItemContentComponent.swift | 51 +++++++++++-- .../StoryItemSetContainerComponent.swift | 74 ++++++++++++++++++- 5 files changed, 195 insertions(+), 12 deletions(-) diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelComponent.swift b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelComponent.swift index b07c97b471..3405cb4852 100644 --- a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelComponent.swift +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelComponent.swift @@ -277,6 +277,7 @@ public final class ChatTextInputPanelComponent: Component { private var panelNode: ChatTextInputPanelNode? private var interfaceInteraction: ChatPanelInterfaceInteraction? + private var hasPendingInputTextRefresh: Bool = false private var component: ChatTextInputPanelComponent? private weak var state: EmptyComponentState? @@ -406,7 +407,10 @@ public final class ChatTextInputPanelComponent: Component { if let component = self.component { let currentMode = inputModeFromComponent(component) let (updatedTextInputState, updatedMode) = f(component.externalState.textInputState, currentMode) - component.externalState.textInputState = updatedTextInputState + if component.externalState.textInputState != updatedTextInputState { + component.externalState.textInputState = updatedTextInputState + self.hasPendingInputTextRefresh = true + } if !self.isUpdating { self.state?.updated(transition: .spring(duration: 0.4)) } @@ -944,7 +948,10 @@ public final class ChatTextInputPanelComponent: Component { component.externalState.resetInputState = nil let _ = resetInputState panelNode.text = "" + } else if self.hasPendingInputTextRefresh { + panelNode.updateInputTextState(component.externalState.textInputState) } + self.hasPendingInputTextRefresh = false let panelHeight = panelNode.updateLayout( width: availableSize.width, diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift index 934142d182..0b4427f8ea 100644 --- a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift @@ -529,6 +529,42 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg } } + public func updateInputTextState(_ state: ChatTextInputState) { + if self.ignoreInputStateUpdates { + return + } + if state.inputText.length != 0 && self.textInputNode == nil { + self.loadTextInputNode() + } + + if let textInputNode = self.textInputNode, let _ = self.presentationInterfaceState, let context = self.context { + self.updatingInputState = true + + var textColor: UIColor = .black + var accentTextColor: UIColor = .blue + var baseFontSize: CGFloat = 17.0 + if let presentationInterfaceState = self.presentationInterfaceState { + textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor + accentTextColor = presentationInterfaceState.theme.chat.inputPanel.panelControlAccentColor + baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) + } + textInputNode.attributedText = textAttributedStringForStateText(context: context, stateText: state.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed, availableEmojis: (self.context?.animatedEmojiStickersValue.keys).flatMap(Set.init) ?? Set(), emojiViewProvider: self.emojiViewProvider, makeCollapsedQuoteAttachment: { text, attributes in + return ChatInputTextCollapsedQuoteAttachmentImpl(text: text, attributes: attributes) + }) + textInputNode.selectedRange = NSMakeRange(state.selectionRange.lowerBound, state.selectionRange.count) + + if let presentationInterfaceState = self.presentationInterfaceState { + refreshChatTextInputAttributes(context: context, textView: textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: (self.context?.animatedEmojiStickersValue.keys).flatMap(Set.init) ?? Set(), emojiViewProvider: self.emojiViewProvider, makeCollapsedQuoteAttachment: { text, attributes in + return ChatInputTextCollapsedQuoteAttachmentImpl(text: text, attributes: attributes) + }) + } + + self.updatingInputState = false + self.updateTextNodeText(animated: false) + self.updateSpoiler() + } + } + public func updateKeepSendButtonEnabled(keepSendButtonEnabled: Bool, extendedSearchLayout: Bool, animated: Bool) { if keepSendButtonEnabled != self.keepSendButtonEnabled || extendedSearchLayout != self.extendedSearchLayout { self.keepSendButtonEnabled = keepSendButtonEnabled diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift index 0647e7b5d2..1cb2b467c9 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift @@ -1007,6 +1007,9 @@ private final class StoryContainerScreenComponent: Component { } func animateIn() { + self.isAnimatingOut = false + self.didAnimateOut = false + if let component = self.component { component.focusedItemPromise.set(self.focusedItem.get()) } @@ -2158,6 +2161,40 @@ public class StoryContainerScreen: ViewControllerComponentContainer { self.dismiss(completion: completion) } + func dismissForPictureInPicture() { + if !self.isDismissed { + self.isDismissed = true + self.didAnimateIn = false + + /*if let componentView = self.node.hostView.componentView as? StoryContainerScreenComponent.View { + componentView.endEditing(true) + + componentView.animateOut(completion: { [weak self] in + self?.dismiss(animated: false) + }) + } else { + self.dismiss(animated: false) + }*/ + self.dismiss(animated: false) + } + } + + func restoreForPictureInPicture(navigationController: NavigationController, completion: @escaping () -> Void) { + if self.isDismissed { + self.isDismissed = false + + navigationController.pushViewController(self, animated: false, completion: completion) + + if !self.didAnimateIn { + self.didAnimateIn = true + + if let componentView = self.node.hostView.componentView as? StoryContainerScreenComponent.View { + componentView.animateIn() + } + } + } + } + override public func dismiss(completion: (() -> Void)? = nil) { if !self.isDismissed { self.isDismissed = true diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift index 00f47e3a18..75bfda1137 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift @@ -166,6 +166,10 @@ final class StoryItemContentComponent: Component { private var liveCallStateDisposable: Disposable? private var liveCallStatsDisposable: Disposable? private var mediaStream: ComponentView? + private let activatePictureInPictureAction = ActionSlot>() + private let deactivatePictureInPictureAction = ActionSlot() + private var restorePictureInPicture: ((@escaping () -> Void) -> Void)? + private var dismissWhileInPictureInPicture: (() -> Void)? private var loadingEffectView: StoryItemLoadingEffectView? private var loadingEffectAppearanceTimer: SwiftSignalKit.Timer? @@ -534,7 +538,7 @@ final class StoryItemContentComponent: Component { if let mediaStreamCall = self.mediaStreamCall { //print("call progressMode: \(self.progressMode)") var canPlay = true - if case .pause = self.progressMode.mode, (!self.progressMode.isCentral || !self.hierarchyTrackingLayer.isInHierarchy) { + if case .pause = self.progressMode.mode, (!self.progressMode.isCentral || (!self.hierarchyTrackingLayer.isInHierarchy && self.restorePictureInPicture == nil)) { canPlay = false } if !canPlay { @@ -782,6 +786,22 @@ final class StoryItemContentComponent: Component { func seekEnded() { self.isSeeking = false } + + func beginPictureInPicture(dismissController: @escaping () -> (restore: (@escaping () -> Void) -> Void, dismissWhilePictureInPicture: () -> Void)) { + self.activatePictureInPictureAction.invoke(Action { [weak self] in + guard let self else { + return + } + var restorePictureInPictureImpl: ((restore: (@escaping () -> Void) -> Void, dismissWhilePictureInPicture: () -> Void))? + self.restorePictureInPicture = { f in + restorePictureInPictureImpl?.restore(f) + } + self.dismissWhileInPictureInPicture = { + restorePictureInPictureImpl?.dismissWhilePictureInPicture() + } + restorePictureInPictureImpl = dismissController() + }) + } func update(component: StoryItemContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true @@ -1000,13 +1020,30 @@ final class StoryItemContentComponent: Component { isFullscreen: false, videoLoading: false, callPeer: nil, - enablePictureInPicture: false, - activatePictureInPicture: ActionSlot(), - deactivatePictureInPicture: ActionSlot(), - bringBackControllerForPictureInPictureDeactivation: { f in - f() + enablePictureInPicture: true, + activatePictureInPicture: self.activatePictureInPictureAction, + deactivatePictureInPicture: self.deactivatePictureInPictureAction, + bringBackControllerForPictureInPictureDeactivation: { [weak self] f in + guard let self else { + return + } + self.dismissWhileInPictureInPicture = nil + if let restorePictureInPicture = self.restorePictureInPicture { + self.restorePictureInPicture = nil + restorePictureInPicture(f) + } else { + f() + } }, - pictureInPictureClosed: { + pictureInPictureClosed: { [weak self] in + guard let self else { + return + } + self.restorePictureInPicture = nil + if let dismissWhileInPictureInPicture = self.dismissWhileInPictureInPicture { + self.dismissWhileInPictureInPicture = nil + dismissWhileInPictureInPicture() + } }, onVideoSizeRetrieved: { _ in }, diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 3ffe7d142f..d45d14325c 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -1426,7 +1426,10 @@ public final class StoryItemSetContainerComponent: Component { if self.verticalPanState != nil { return .pause } - if self.inputPanelExternalState.isEditing || component.isProgressPaused || self.sendMessageContext.actionSheet != nil || self.sendMessageContext.isViewingAttachedStickers || self.contextController != nil || self.sendMessageContext.audioRecorderValue != nil || self.sendMessageContext.videoRecorderValue != nil || self.viewListDisplayState != .hidden { + if self.contextController != nil { + return .blurred + } + if self.inputPanelExternalState.isEditing || component.isProgressPaused || self.sendMessageContext.actionSheet != nil || self.sendMessageContext.isViewingAttachedStickers || self.sendMessageContext.audioRecorderValue != nil || self.sendMessageContext.videoRecorderValue != nil || self.viewListDisplayState != .hidden { return .pause } if let reactionContextNode = self.reactionContextNode, reactionContextNode.isReactionSearchActive { @@ -6505,7 +6508,19 @@ public final class StoryItemSetContainerComponent: Component { self.openItemPrivacySettings() }))) - if !isLiveStream { + if isLiveStream { + //TODO:localize + items.append(.action(ContextMenuActionItem(text: "Minimize", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Call/pip"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + a(.default) + + guard let self else { + return + } + self.beginPictureInPicture() + }))) + } else { items.append(.action(ContextMenuActionItem(text: component.strings.Story_Context_Edit, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, a in @@ -6827,7 +6842,19 @@ public final class StoryItemSetContainerComponent: Component { }))) } - if !isLiveStream { + if isLiveStream { + //TODO:localize + items.append(.action(ContextMenuActionItem(text: "Minimize", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Call/pip"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + a(.default) + + guard let self else { + return + } + self.beginPictureInPicture() + }))) + } else { let saveText: String = component.strings.Story_Context_SaveToGallery items.append(.action(ContextMenuActionItem(text: saveText, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.contextMenu.primaryColor) @@ -7300,7 +7327,19 @@ public final class StoryItemSetContainerComponent: Component { }))) } - if !component.slice.item.storyItem.isForwardingDisabled && !isLiveStream { + if isLiveStream { + //TODO:localize + items.append(.action(ContextMenuActionItem(text: "Minimize", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Call/pip"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + a(.default) + + guard let self else { + return + } + self.beginPictureInPicture() + }))) + } else if !component.slice.item.storyItem.isForwardingDisabled { let saveText: String = component.strings.Story_Context_SaveToGallery items.append(.action(ContextMenuActionItem(text: saveText, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: accountUser.isPremium ? "Chat/Context Menu/Download" : "Chat/Context Menu/DownloadLocked"), color: theme.contextMenu.primaryColor) @@ -7425,6 +7464,33 @@ public final class StoryItemSetContainerComponent: Component { }) } + private func beginPictureInPicture() { + guard let component = self.component, let visibleItem = self.visibleItems[component.slice.item.id] else { + return + } + guard let itemView = visibleItem.view.view as? StoryItemContentComponent.View else { + return + } + itemView.beginPictureInPicture(dismissController: { [weak self] in + guard let self, let component = self.component, let controller = component.controller() as? StoryContainerScreen, let navigationController = controller.navigationController as? NavigationController else { + return ({ completion in + completion() + }, {}) + } + + controller.dismissForPictureInPicture() + + return ({ [weak navigationController] completion in + guard let navigationController else { + completion() + return + } + controller.restoreForPictureInPicture(navigationController: navigationController, completion: completion) + }, { + }) + }) + } + private func presentAddStoryFolder(addItems: [EngineStoryItem] = []) { guard let component = self.component else { return