From 6cadbd6cc395f1db7a8eeea2fe077a0a0bda08d0 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 25 Apr 2023 21:26:26 +0400 Subject: [PATCH] [WIP] Stories --- .../Sources/HierarchyTrackingLayer.swift | 11 ++ .../Sources/DebugController.swift | 4 +- .../QrCodeUI/Sources/QrCodeScreen.swift | 6 +- .../TelegramEngine/Peers/Communities.swift | 4 +- .../Sources/MessageInputPanelComponent.swift | 2 + .../Stories/StoryContainerScreen/BUILD | 1 + .../MediaNavigationStripComponent.swift | 5 +- .../Sources/StoryContainerScreen.swift | 69 +++++++-- .../Sources/StoryContent.swift | 2 +- .../Stories/StoryContentComponent/BUILD | 1 + .../StoryMessageContentComponent.swift | 140 +++++++++++++++--- .../Sources/TextFieldComponent.swift | 12 +- 12 files changed, 213 insertions(+), 44 deletions(-) diff --git a/submodules/Components/HierarchyTrackingLayer/Sources/HierarchyTrackingLayer.swift b/submodules/Components/HierarchyTrackingLayer/Sources/HierarchyTrackingLayer.swift index 7b643425f4..91fccdf7d1 100644 --- a/submodules/Components/HierarchyTrackingLayer/Sources/HierarchyTrackingLayer.swift +++ b/submodules/Components/HierarchyTrackingLayer/Sources/HierarchyTrackingLayer.swift @@ -10,12 +10,23 @@ private let nullAction = NullActionClass() open class HierarchyTrackingLayer: CALayer { public var didEnterHierarchy: (() -> Void)? public var didExitHierarchy: (() -> Void)? + public var isInHierarchyUpdated: ((Bool) -> Void)? + + public private(set) var isInHierarchy: Bool = false { + didSet { + if self.isInHierarchy != oldValue { + self.isInHierarchyUpdated?(self.isInHierarchy) + } + } + } override open func action(forKey event: String) -> CAAction? { if event == kCAOnOrderIn { self.didEnterHierarchy?() + self.isInHierarchy = true } else if event == kCAOnOrderOut { self.didExitHierarchy?() + self.isInHierarchy = false } return nullAction } diff --git a/submodules/DebugSettingsUI/Sources/DebugController.swift b/submodules/DebugSettingsUI/Sources/DebugController.swift index 2620357f47..1f6af3e940 100644 --- a/submodules/DebugSettingsUI/Sources/DebugController.swift +++ b/submodules/DebugSettingsUI/Sources/DebugController.swift @@ -1384,7 +1384,9 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present entries.append(.logTranslationRecognition(experimentalSettings.logLanguageRecognition)) entries.append(.resetTranslationStates) - entries.append(.storiesExperiment(experimentalSettings.storiesExperiment)) + if case .internal = sharedContext.applicationBindings.appBuildType { + entries.append(.storiesExperiment(experimentalSettings.storiesExperiment)) + } entries.append(.playlistPlayback(experimentalSettings.playlistPlayback)) entries.append(.enableQuickReactionSwitch(!experimentalSettings.disableQuickReaction)) } diff --git a/submodules/QrCodeUI/Sources/QrCodeScreen.swift b/submodules/QrCodeUI/Sources/QrCodeScreen.swift index c0853f568e..ef4567a4a5 100644 --- a/submodules/QrCodeUI/Sources/QrCodeScreen.swift +++ b/submodules/QrCodeUI/Sources/QrCodeScreen.swift @@ -47,7 +47,11 @@ public final class QrCodeScreen: ViewController { case let .invite(invite, _): return invite.link ?? "" case let .chatFolder(slug): - return slug + if slug.hasPrefix("https://") { + return slug + } else { + return "https://t.me/addlist/\(slug)" + } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift index ba2c227add..4410b438cb 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift @@ -59,8 +59,8 @@ public struct ExportedChatFolderLink: Equatable { public extension ExportedChatFolderLink { var slug: String { var slug = self.link - if slug.hasPrefix("https://t.me/folder/") { - slug = String(slug[slug.index(slug.startIndex, offsetBy: "https://t.me/folder/".count)...]) + if slug.hasPrefix("https://t.me/addlist/") { + slug = String(slug[slug.index(slug.startIndex, offsetBy: "https://t.me/addlist/".count)...]) } return slug } diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift index 4297831d39..e2596bb7d9 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift @@ -8,6 +8,7 @@ import BundleIconComponent public final class MessageInputPanelComponent: Component { public final class ExternalState { + public fileprivate(set) var isEditing: Bool = false public fileprivate(set) var hasText: Bool = false public init() { @@ -195,6 +196,7 @@ public final class MessageInputPanelComponent: Component { transition.setScale(view: self.stickerIconView, scale: self.textFieldExternalState.hasText ? 0.1 : 1.0) } + component.externalState.isEditing = self.textFieldExternalState.isEditing component.externalState.hasText = self.textFieldExternalState.hasText return size diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD index 33840888b0..bc6f9d4dd2 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD @@ -41,6 +41,7 @@ swift_library( "//submodules/LegacyComponents", "//submodules/TelegramUI/Components/LegacyCamera", "//submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent", + "//submodules/TelegramPresentationData", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/MediaNavigationStripComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/MediaNavigationStripComponent.swift index 21ab7c5442..008785d354 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/MediaNavigationStripComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/MediaNavigationStripComponent.swift @@ -120,7 +120,10 @@ final class MediaNavigationStripComponent: Component { } let potentiallyVisibleCount = Int(ceil((availableSize.width + spacing) / (itemWidth + spacing))) - for i in (component.index - potentiallyVisibleCount) ... (component.index + potentiallyVisibleCount) { + let overflowDistance: CGFloat = 24.0 + let potentialOverflowCount = 10 + let _ = overflowDistance + for i in (component.index - potentiallyVisibleCount) ... (component.index + potentiallyVisibleCount + potentialOverflowCount) { if i < 0 { continue } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift index bdc677f629..39294a21ec 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift @@ -30,6 +30,7 @@ import ICloudResources import LegacyComponents import LegacyCamera import StoryFooterPanelComponent +import TelegramPresentationData private func hasFirstResponder(_ view: UIView) -> Bool { if view.isFirstResponder { @@ -149,7 +150,6 @@ private final class StoryContainerScreenComponent: Component { self.contentContainerView = UIView() self.contentContainerView.clipsToBounds = true - self.contentContainerView.isUserInteractionEnabled = false self.topContentGradientLayer = SimpleGradientLayer() self.bottomContentGradientLayer = SimpleGradientLayer() @@ -188,7 +188,8 @@ private final class StoryContainerScreenComponent: Component { self.addSubview(self.closeButton) self.closeButton.addTarget(self, action: #selector(self.closePressed), for: .touchUpInside) - self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + self.contentContainerView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + self.contentContainerView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))) } required init?(coder: NSCoder) { @@ -209,7 +210,7 @@ private final class StoryContainerScreenComponent: Component { let point = recognizer.location(in: self) var nextIndex: Int - if point.x < itemLayout.size.width * 0.5 { + if point.x < itemLayout.size.width * 0.25 { nextIndex = currentIndex + 1 } else { nextIndex = currentIndex - 1 @@ -237,6 +238,19 @@ private final class StoryContainerScreenComponent: Component { } } + @objc private func panGesture(_ recognizer: UIPanGestureRecognizer) { + switch recognizer.state { + case .began: + break + case .changed: + break + case .cancelled, .ended: + break + default: + break + } + } + @objc private func closePressed() { guard let environment = self.environment, let controller = environment.controller() else { return @@ -329,9 +343,14 @@ private final class StoryContainerScreenComponent: Component { ) if let view = visibleItem.view.view { if view.superview == nil { + view.isUserInteractionEnabled = false self.contentContainerView.addSubview(view) } itemTransition.setFrame(view: view, frame: CGRect(origin: CGPoint(), size: itemLayout.size)) + + if let view = view as? StoryContentItem.View { + view.setIsProgressPaused(self.inputPanelExternalState.isEditing || self.attachmentController != nil) + } } } @@ -349,6 +368,16 @@ private final class StoryContainerScreenComponent: Component { } } + private func updateIsProgressPaused() { + for (_, visibleItem) in self.visibleItems { + if let view = visibleItem.view.view { + if let view = view as? StoryContentItem.View { + view.setIsProgressPaused(self.inputPanelExternalState.isEditing || self.attachmentController?.window != nil) + } + } + } + } + func animateIn() { self.layer.allowsGroupOpacity = true self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, completion: { [weak self] _ in @@ -389,6 +418,7 @@ private final class StoryContainerScreenComponent: Component { content: .text(text) ) inputPanelView.clearSendMessageInput() + self.endEditing(true) if let controller = self.environment?.controller() { let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } @@ -686,7 +716,7 @@ private final class StoryContainerScreenComponent: Component { let attachmentController = AttachmentController( context: component.context, - updatedPresentationData: nil, + updatedPresentationData: (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: defaultDarkPresentationTheme) }), chatLocation: .peer(id: peer.id), buttons: buttons, initialButton: initialButton, @@ -706,6 +736,7 @@ private final class StoryContainerScreenComponent: Component { return } self.attachmentController = nil + self.updateIsProgressPaused() } attachmentController.getSourceRect = { [weak self] in guard let self else { @@ -764,7 +795,7 @@ private final class StoryContainerScreenComponent: Component { controller.prepareForReuse() return } - let controller = component.context.sharedContext.makeAttachmentFileController(context: component.context, updatedPresentationData: nil, bannedSendMedia: bannedSendFiles, presentGallery: { [weak self, weak attachmentController] in + let controller = component.context.sharedContext.makeAttachmentFileController(context: component.context, updatedPresentationData: (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: defaultDarkPresentationTheme) }), bannedSendMedia: bannedSendFiles, presentGallery: { [weak self, weak attachmentController] in guard let self else { return } @@ -821,7 +852,7 @@ private final class StoryContainerScreenComponent: Component { return } let hasLiveLocation = peer.id.namespace != Namespaces.Peer.SecretChat && peer.id != component.context.account.peerId - let controller = LocationPickerController(context: component.context, updatedPresentationData: nil, mode: .share(peer: peer, selfPeer: selfPeer, hasLiveLocation: hasLiveLocation), completion: { [weak self] location, _ in + let controller = LocationPickerController(context: component.context, updatedPresentationData: (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: defaultDarkPresentationTheme) }), mode: .share(peer: peer, selfPeer: selfPeer, hasLiveLocation: hasLiveLocation), completion: { [weak self] location, _ in guard let self else { return } @@ -833,7 +864,7 @@ private final class StoryContainerScreenComponent: Component { let _ = currentLocationController.swap(controller) }) case .contact: - let contactsController = component.context.sharedContext.makeContactSelectionController(ContactSelectionControllerParams(context: component.context, updatedPresentationData: nil, title: { $0.Contacts_Title }, displayDeviceContacts: true, multipleSelection: true)) + let contactsController = component.context.sharedContext.makeContactSelectionController(ContactSelectionControllerParams(context: component.context, updatedPresentationData: (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: defaultDarkPresentationTheme) }), title: { $0.Contacts_Title }, displayDeviceContacts: true, multipleSelection: true)) contactsController.presentScheduleTimePicker = { [weak self] completion in guard let self else { return @@ -1041,7 +1072,7 @@ private final class StoryContainerScreenComponent: Component { fromAttachMenu = true let params = WebAppParameters(peerId: peer.id, botId: bot.id, botName: botName, url: nil, queryId: nil, payload: payload, buttonText: nil, keepAliveSignal: nil, fromMenu: false, fromAttachMenu: fromAttachMenu, isInline: false, isSimple: false) let replyMessageId = targetMessageId - let controller = WebAppController(context: component.context, updatedPresentationData: nil, params: params, replyToMessageId: replyMessageId, threadId: nil) + let controller = WebAppController(context: component.context, updatedPresentationData: (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: defaultDarkPresentationTheme) }), params: params, replyToMessageId: replyMessageId, threadId: nil) controller.openUrl = { [weak self] url in guard let self else { return @@ -1080,6 +1111,7 @@ private final class StoryContainerScreenComponent: Component { attachmentController.navigationPresentation = .flatModal controller.push(attachmentController) self.attachmentController = attachmentController + self.updateIsProgressPaused() } if inputIsActive { @@ -1107,7 +1139,7 @@ private final class StoryContainerScreenComponent: Component { guard let component = self.component else { return } - let controller = MediaPickerScreen(context: component.context, updatedPresentationData: nil, peer: peer, threadTitle: nil, chatLocation: .peer(id: peer.id), bannedSendPhotos: bannedSendPhotos, bannedSendVideos: bannedSendVideos, subject: subject, saveEditedPhotos: saveEditedPhotos) + let controller = MediaPickerScreen(context: component.context, updatedPresentationData: (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: defaultDarkPresentationTheme) }), peer: peer, threadTitle: nil, chatLocation: .peer(id: peer.id), bannedSendPhotos: bannedSendPhotos, bannedSendVideos: bannedSendVideos, subject: subject, saveEditedPhotos: saveEditedPhotos) let mediaPickerContext = controller.mediaPickerContext controller.openCamera = { [weak self] cameraView in guard let self else { @@ -1220,7 +1252,7 @@ private final class StoryContainerScreenComponent: Component { configureLegacyAssetPicker(controller, context: component.context, peer: peer._asPeer(), chatLocation: .peer(id: peer.id), initialCaption: inputText, hasSchedule: peer.id.namespace != Namespaces.Peer.SecretChat, presentWebSearch: editingMedia ? nil : { [weak legacyController] in if let strongSelf = self, let component = strongSelf.component { - let controller = WebSearchController(context: component.context, updatedPresentationData: nil, peer: peer, chatLocation: .peer(id: peer.id), configuration: searchBotsConfiguration, mode: .media(attachment: false, completion: { results, selectionState, editingState, silentPosting in + let controller = WebSearchController(context: component.context, updatedPresentationData: (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: defaultDarkPresentationTheme) }), peer: peer, chatLocation: .peer(id: peer.id), configuration: searchBotsConfiguration, mode: .media(attachment: false, completion: { results, selectionState, editingState, silentPosting in if let legacyController = legacyController { legacyController.dismiss() } @@ -1420,6 +1452,10 @@ private final class StoryContainerScreenComponent: Component { } if component.context.engine.messages.enqueueOutgoingMessageWithChatContextResult(to: peer.id, threadId: nil, botId: results.botId, result: result, replyToMessageId: replyMessageId, hideVia: hideVia, silentPosting: silentPosting, scheduleTime: scheduleTime) { } + + if let attachmentController = self.attachmentController { + attachmentController.dismiss(animated: true) + } } sendMessage(nil) @@ -1689,7 +1725,7 @@ private final class StoryContainerScreenComponent: Component { } else { mode = .scheduledMessages(sendWhenOnlineAvailable: sendWhenOnlineAvailable) } - let controller = ChatScheduleTimeController(context: component.context, updatedPresentationData: nil, peerId: peer.id, mode: mode, style: style, currentTime: selectedTime, minimalTime: nil, dismissByTapOutside: dismissByTapOutside, completion: { time in + let controller = ChatScheduleTimeController(context: component.context, updatedPresentationData: (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: defaultDarkPresentationTheme) }), peerId: peer.id, mode: mode, style: style, currentTime: selectedTime, minimalTime: nil, dismissByTapOutside: dismissByTapOutside, completion: { time in completion(time) }) self.endEditing(true) @@ -1701,7 +1737,7 @@ private final class StoryContainerScreenComponent: Component { guard let component = self.component else { return } - let controller = ChatTimerScreen(context: component.context, updatedPresentationData: nil, style: style, currentTime: selectedTime, dismissByTapOutside: dismissByTapOutside, completion: { time in + let controller = ChatTimerScreen(context: component.context, updatedPresentationData: (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: defaultDarkPresentationTheme) }), style: style, currentTime: selectedTime, dismissByTapOutside: dismissByTapOutside, completion: { time in completion(time) }) self.endEditing(true) @@ -1712,7 +1748,7 @@ private final class StoryContainerScreenComponent: Component { guard let component = self.component else { return nil } - return createPollController(context: component.context, updatedPresentationData: nil, peer: peer, isQuiz: isQuiz, completion: { [weak self] poll in + return createPollController(context: component.context, updatedPresentationData: (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: defaultDarkPresentationTheme) }), peer: peer, isQuiz: isQuiz, completion: { [weak self] poll in guard let self else { return } @@ -1805,6 +1841,10 @@ private final class StoryContainerScreenComponent: Component { donateSendMessageIntent(account: component.context.account, sharedContext: component.context.sharedContext, intentContext: .chat, peerIds: [peer.id]) + if let attachmentController = self.attachmentController { + attachmentController.dismiss(animated: true) + } + if let controller = self.environment?.controller() { let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } controller.present(UndoOverlayController( @@ -2182,6 +2222,7 @@ private final class StoryContainerScreenComponent: Component { ) if let navigationStripView = self.navigationStrip.view { if navigationStripView.superview == nil { + navigationStripView.isUserInteractionEnabled = false self.addSubview(navigationStripView) } transition.setFrame(view: navigationStripView, frame: CGRect(origin: CGPoint(x: contentFrame.minX + navigationStripSideInset, y: contentFrame.minY + navigationStripTopInset), size: CGSize(width: availableSize.width - navigationStripSideInset * 2.0, height: 2.0))) @@ -2269,7 +2310,7 @@ private final class StoryContainerScreenComponent: Component { } transition.setFrame(layer: self.contentDimLayer, frame: contentFrame) - transition.setAlpha(layer: self.contentDimLayer, alpha: inputPanelIsOverlay ? 1.0 : 0.0) + transition.setAlpha(layer: self.contentDimLayer, alpha: (inputPanelIsOverlay || self.inputPanelExternalState.isEditing) ? 1.0 : 0.0) self.ignoreScrolling = true transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height))) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift index 652110f6d1..628e6d12cd 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift @@ -12,7 +12,7 @@ public final class StoryContentItem { } open class View: UIView { - func setIsProgressPaused(_ isProgressPaused: Bool) { + open func setIsProgressPaused(_ isProgressPaused: Bool) { } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContentComponent/BUILD b/submodules/TelegramUI/Components/Stories/StoryContentComponent/BUILD index 5b7d77123e..f31c881253 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContentComponent/BUILD +++ b/submodules/TelegramUI/Components/Stories/StoryContentComponent/BUILD @@ -21,6 +21,7 @@ swift_library( "//submodules/MediaPlayer:UniversalMediaPlayer", "//submodules/TelegramUniversalVideoContent", "//submodules/AvatarNode", + "//submodules/Components/HierarchyTrackingLayer", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryMessageContentComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryMessageContentComponent.swift index 76bb610ebf..7cedad7d43 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryMessageContentComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryMessageContentComponent.swift @@ -10,6 +10,7 @@ import SwiftSignalKit import UniversalMediaPlayer import TelegramUniversalVideoContent import StoryContainerScreen +import HierarchyTrackingLayer final class StoryMessageContentComponent: Component { typealias EnvironmentType = StoryContentItem.Environment @@ -93,16 +94,31 @@ final class StoryMessageContentComponent: Component { private weak var state: EmptyComponentState? private var environment: StoryContentItem.Environment? - private var currentProgressStart: Double? + private var isProgressPaused: Bool = false private var currentProgressTimer: SwiftSignalKit.Timer? + private var currentProgressTimerValue: Double = 0.0 private var videoProgressDisposable: Disposable? + private var videoPlaybackStatus: MediaPlayerStatus? + + private let hierarchyTrackingLayer: HierarchyTrackingLayer + override init(frame: CGRect) { + self.hierarchyTrackingLayer = HierarchyTrackingLayer() self.imageNode = TransformImageNode() super.init(frame: frame) + self.layer.addSublayer(self.hierarchyTrackingLayer) + self.addSubnode(self.imageNode) + + self.hierarchyTrackingLayer.isInHierarchyUpdated = { [weak self] value in + guard let self else { + return + } + self.updateIsProgressPaused() + } } required init?(coder: NSCoder) { @@ -150,7 +166,7 @@ final class StoryMessageContentComponent: Component { } if value { self.videoNode?.seek(0.0) - self.videoNode?.play() + self.videoNode?.playOnceWithSound(playAndRecord: false) } } videoNode.canAttachContent = true @@ -161,7 +177,100 @@ final class StoryMessageContentComponent: Component { } } - func setIsProgressPaused(_ isProgressPaused: Bool) { + override func setIsProgressPaused(_ isProgressPaused: Bool) { + if self.isProgressPaused != isProgressPaused { + self.isProgressPaused = isProgressPaused + self.updateIsProgressPaused() + } + } + + private func updateIsProgressPaused() { + if let videoNode = self.videoNode { + if !self.isProgressPaused && self.hierarchyTrackingLayer.isInHierarchy { + videoNode.play() + } else { + videoNode.pause() + } + } + + self.updateVideoPlaybackProgress() + self.updateProgressTimer() + } + + private func updateProgressTimer() { + let needsTimer = !self.isProgressPaused && self.hierarchyTrackingLayer.isInHierarchy + + if needsTimer { + if self.currentProgressTimer == nil { + self.currentProgressTimer = SwiftSignalKit.Timer( + timeout: 1.0 / 60.0, + repeat: true, + completion: { [weak self] in + guard let self, !self.isProgressPaused, self.hierarchyTrackingLayer.isInHierarchy else { + return + } + + if self.videoNode != nil { + self.updateVideoPlaybackProgress() + } else { + let currentProgressTimerLimit: Double = 5.0 + var currentProgressTimerValue = self.currentProgressTimerValue + 1.0 / 60.0 + currentProgressTimerValue = max(0.0, min(currentProgressTimerLimit, currentProgressTimerValue)) + self.currentProgressTimerValue = currentProgressTimerValue + + self.environment?.presentationProgressUpdated(currentProgressTimerValue / currentProgressTimerLimit) + } + }, queue: .mainQueue() + ) + self.currentProgressTimer?.start() + } + } else { + if let currentProgressTimer = self.currentProgressTimer { + self.currentProgressTimer = nil + currentProgressTimer.invalidate() + } + } + } + + private func updateVideoPlaybackProgress() { + var isPlaying = false + var timestampAndDuration: (timestamp: Double?, duration: Double)? + if let videoPlaybackStatus = self.videoPlaybackStatus { + switch videoPlaybackStatus.status { + case .playing: + isPlaying = true + default: + break + } + if case .buffering(true, _, _, _) = videoPlaybackStatus.status { + timestampAndDuration = (nil, videoPlaybackStatus.duration) + } else if Double(0.0).isLess(than: videoPlaybackStatus.duration) { + timestampAndDuration = (videoPlaybackStatus.timestamp, videoPlaybackStatus.duration) + } + } + + var currentProgress: Double = 0.0 + + if let (maybeTimestamp, duration) = timestampAndDuration, let timestamp = maybeTimestamp, duration > 0.01, let videoPlaybackStatus = self.videoPlaybackStatus { + var actualTimestamp: Double + if videoPlaybackStatus.generationTimestamp.isZero || !isPlaying { + actualTimestamp = timestamp + } else { + let currentTimestamp = CACurrentMediaTime() + actualTimestamp = timestamp + (currentTimestamp - videoPlaybackStatus.generationTimestamp) * videoPlaybackStatus.baseRate + } + + var progress = CGFloat(actualTimestamp / duration) + if progress.isNaN || !progress.isFinite { + progress = 0.0 + } + progress = min(1.0, progress) + + currentProgress = progress + } + + let clippedProgress = max(0.0, min(1.0, currentProgress)) + self.environment?.presentationProgressUpdated(clippedProgress) } func update(component: StoryMessageContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { @@ -286,31 +395,16 @@ final class StoryMessageContentComponent: Component { if self.videoProgressDisposable == nil { self.videoProgressDisposable = (videoNode.status |> deliverOnMainQueue).start(next: { [weak self] status in - guard let self, let status, status.duration > 0.0 else { + guard let self, let status else { return } - let currentProgress = Double(status.timestamp / status.duration) - let clippedProgress = max(0.0, min(1.0, currentProgress)) - self.environment?.presentationProgressUpdated(clippedProgress) + + self.videoPlaybackStatus = status + self.updateVideoPlaybackProgress() }) } - } else { - if self.currentProgressTimer == nil { - self.currentProgressStart = CFAbsoluteTimeGetCurrent() - self.currentProgressTimer = SwiftSignalKit.Timer( - timeout: 1.0 / 60.0, - repeat: true, - completion: { [weak self] in - guard let self, let currentProgressStart = self.currentProgressStart else { - return - } - let currentProgress = (CFAbsoluteTimeGetCurrent() - currentProgressStart) / 5.0 - let clippedProgress = max(0.0, min(1.0, currentProgress)) - self.environment?.presentationProgressUpdated(clippedProgress) - }, queue: .mainQueue()) - self.currentProgressTimer?.start() - } } + self.updateProgressTimer() return availableSize } diff --git a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift index 8887912ac0..317bd8ec69 100644 --- a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift +++ b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift @@ -5,6 +5,7 @@ import ComponentFlow public final class TextFieldComponent: Component { public final class ExternalState { + public fileprivate(set) var isEditing: Bool = false public fileprivate(set) var hasText: Bool = false public init() { @@ -14,6 +15,7 @@ public final class TextFieldComponent: Component { public final class AnimationHint { public enum Kind { case textChanged + case textFocusChanged } public let kind: Kind @@ -102,6 +104,14 @@ public final class TextFieldComponent: Component { self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(kind: .textChanged))) } + public func textViewDidBeginEditing(_ textView: UITextView) { + self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(kind: .textFocusChanged))) + } + + public func textViewDidEndEditing(_ textView: UITextView) { + self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(kind: .textFocusChanged))) + } + public func scrollViewDidScroll(_ scrollView: UIScrollView) { //print("didScroll \(scrollView.bounds)") } @@ -128,7 +138,6 @@ public final class TextFieldComponent: Component { let refreshScrolling = self.textView.bounds.size != size self.textView.frame = CGRect(origin: CGPoint(), size: size) - //transition.setFrame(view: self.textView, frame: ) if refreshScrolling { self.textView.setContentOffset(CGPoint(x: 0.0, y: max(0.0, self.textView.contentSize.height - self.textView.bounds.height)), animated: false) @@ -155,6 +164,7 @@ public final class TextFieldComponent: Component { } component.externalState.hasText = self.textStorage.length != 0 + component.externalState.isEditing = self.textView.isFirstResponder return size }