Various improvements

This commit is contained in:
Isaac
2025-11-11 21:58:05 +08:00
parent 7acf2b0b41
commit 858e00c991
5 changed files with 195 additions and 12 deletions

View File

@@ -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)
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,

View File

@@ -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

View File

@@ -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

View File

@@ -166,6 +166,10 @@ final class StoryItemContentComponent: Component {
private var liveCallStateDisposable: Disposable?
private var liveCallStatsDisposable: Disposable?
private var mediaStream: ComponentView<Empty>?
private let activatePictureInPictureAction = ActionSlot<Action<Void>>()
private let deactivatePictureInPictureAction = ActionSlot<Void>()
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 {
@@ -783,6 +787,22 @@ final class StoryItemContentComponent: Component {
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<StoryContentItem.Environment>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
@@ -1000,13 +1020,30 @@ final class StoryItemContentComponent: Component {
isFullscreen: false,
videoLoading: false,
callPeer: nil,
enablePictureInPicture: false,
activatePictureInPicture: ActionSlot(),
deactivatePictureInPicture: ActionSlot(),
bringBackControllerForPictureInPictureDeactivation: { f in
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
},

View File

@@ -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