diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 04c8b603b0..722db51454 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -9394,8 +9394,6 @@ Sorry for the inconvenience."; "Notification.Story" = "Story"; -"ChatList.StoryFeedTooltip" = "Tap above to view updates\nfrom %@"; - "StoryFeed.ContextAddStory" = "Add Story"; "StoryFeed.ContextSavedStories" = "Saved Stories"; "StoryFeed.ContextArchivedStories" = "Archived Stories"; @@ -9549,7 +9547,6 @@ Sorry for the inconvenience."; "Story.ContextDeleteStory" = "Delete Story"; -"Story.TooltipPrivacyCloseFriends" = "You are seeing this story because **%@** added you\nto their list of Close Friends."; "Story.TooltipPrivacyContacts" = "Only **%@'s** contacts can view this story."; "Story.TooltipPrivacySelectedContacts" = "Only some contacts **%@** selected can view this story."; "Story.ToastViewInChat" = "View in Chat"; @@ -9728,3 +9725,7 @@ Sorry for the inconvenience."; "Story.Editor.TooltipPremiumCaptionEntities" = "Subscribe to [Telegram Premium]() to add links and formatting in captions to your stories."; "Story.Context.TooltipPremiumSaveStories" = "Subscribe to [Telegram Premium]() to save other people's unprotected stories to your Gallery."; + +"ChatList.StoryFeedTooltipUsers" = "Tap above to view stories from %@"; + +"Story.TooltipPrivacyCloseFriends2" = "You are seeing this story because **%@** added you to their list of Close Friends."; diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 89c2e05b0b..401409aa43 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -2013,15 +2013,19 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } } - let text: String = self.presentationData.strings.ChatList_StoryFeedTooltip(itemListString).string + let text: String = self.presentationData.strings.ChatList_StoryFeedTooltipUsers(itemListString).string - let tooltipController = TooltipController(content: .text(text), baseFontSize: self.presentationData.listsFontSize.baseDisplaySize, timeout: 30.0, dismissByTapOutside: true, dismissImmediatelyOnLayoutUpdate: true, padding: 6.0, innerPadding: UIEdgeInsets(top: 2.0, left: 3.0, bottom: 2.0, right: 3.0)) - self.present(tooltipController, in: .current, with: TooltipControllerPresentationArguments(sourceNodeAndRect: { [weak self] in - guard let self else { - return nil + let tooltipScreen = TooltipScreen( + account: self.context.account, + sharedContext: self.context.sharedContext, + text: .markdown(text: text), + balancedTextLayout: true, + style: .default, + location: TooltipScreen.Location.point(self.displayNode.view.convert(absoluteFrame.insetBy(dx: 0.0, dy: 0.0).offsetBy(dx: 0.0, dy: 4.0), to: nil).offsetBy(dx: 1.0, dy: 2.0), .top), displayDuration: .infinite, shouldDismissOnTouch: { _, _ in + return .dismiss(consume: false) } - return (self.displayNode, absoluteFrame.insetBy(dx: 0.0, dy: 0.0).offsetBy(dx: 0.0, dy: 4.0)) - })) + ) + self.present(tooltipScreen, in: .current) #if !DEBUG let _ = ApplicationSpecificNotice.setDisplayChatListStoriesTooltip(accountManager: self.context.sharedContext.accountManager).start() diff --git a/submodules/Components/BalancedTextComponent/BUILD b/submodules/Components/BalancedTextComponent/BUILD new file mode 100644 index 0000000000..35ba9d1832 --- /dev/null +++ b/submodules/Components/BalancedTextComponent/BUILD @@ -0,0 +1,20 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "BalancedTextComponent", + module_name = "BalancedTextComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display:Display", + "//submodules/ComponentFlow:ComponentFlow", + "//submodules/Markdown:Markdown", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/Components/BalancedTextComponent/Sources/BalancedTextComponent.swift b/submodules/Components/BalancedTextComponent/Sources/BalancedTextComponent.swift new file mode 100644 index 0000000000..14802b61aa --- /dev/null +++ b/submodules/Components/BalancedTextComponent/Sources/BalancedTextComponent.swift @@ -0,0 +1,209 @@ +import Foundation +import UIKit +import ComponentFlow +import Display +import Markdown + +public final class BalancedTextComponent: Component { + public enum TextContent: Equatable { + case plain(NSAttributedString) + case markdown(text: String, attributes: MarkdownAttributes) + } + + public let text: TextContent + public let balanced: Bool + public let horizontalAlignment: NSTextAlignment + public let verticalAlignment: TextVerticalAlignment + public let truncationType: CTLineTruncationType + public let maximumNumberOfLines: Int + public let lineSpacing: CGFloat + public let cutout: TextNodeCutout? + public let insets: UIEdgeInsets + public let textShadowColor: UIColor? + public let textShadowBlur: CGFloat? + public let textStroke: (UIColor, CGFloat)? + public let highlightColor: UIColor? + public let highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? + public let tapAction: (([NSAttributedString.Key: Any], Int) -> Void)? + public let longTapAction: (([NSAttributedString.Key: Any], Int) -> Void)? + + public init( + text: TextContent, + balanced: Bool = true, + horizontalAlignment: NSTextAlignment = .natural, + verticalAlignment: TextVerticalAlignment = .top, + truncationType: CTLineTruncationType = .end, + maximumNumberOfLines: Int = 1, + lineSpacing: CGFloat = 0.0, + cutout: TextNodeCutout? = nil, + insets: UIEdgeInsets = UIEdgeInsets(), + textShadowColor: UIColor? = nil, + textShadowBlur: CGFloat? = nil, + textStroke: (UIColor, CGFloat)? = nil, + highlightColor: UIColor? = nil, + highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? = nil, + tapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil, + longTapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil + ) { + self.text = text + self.balanced = balanced + self.horizontalAlignment = horizontalAlignment + self.verticalAlignment = verticalAlignment + self.truncationType = truncationType + self.maximumNumberOfLines = maximumNumberOfLines + self.lineSpacing = lineSpacing + self.cutout = cutout + self.insets = insets + self.textShadowColor = textShadowColor + self.textShadowBlur = textShadowBlur + self.textStroke = textStroke + self.highlightColor = highlightColor + self.highlightAction = highlightAction + self.tapAction = tapAction + self.longTapAction = longTapAction + } + + public static func ==(lhs: BalancedTextComponent, rhs: BalancedTextComponent) -> Bool { + if lhs.text != rhs.text { + return false + } + if lhs.balanced != rhs.balanced { + return false + } + if lhs.horizontalAlignment != rhs.horizontalAlignment { + return false + } + if lhs.verticalAlignment != rhs.verticalAlignment { + return false + } + if lhs.truncationType != rhs.truncationType { + return false + } + if lhs.maximumNumberOfLines != rhs.maximumNumberOfLines { + return false + } + if lhs.lineSpacing != rhs.lineSpacing { + return false + } + if lhs.cutout != rhs.cutout { + return false + } + if lhs.insets != rhs.insets { + return false + } + + if let lhsTextShadowColor = lhs.textShadowColor, let rhsTextShadowColor = rhs.textShadowColor { + if !lhsTextShadowColor.isEqual(rhsTextShadowColor) { + return false + } + } else if (lhs.textShadowColor != nil) != (rhs.textShadowColor != nil) { + return false + } + if lhs.textShadowBlur != rhs.textShadowBlur { + return false + } + + if let lhsTextStroke = lhs.textStroke, let rhsTextStroke = rhs.textStroke { + if !lhsTextStroke.0.isEqual(rhsTextStroke.0) { + return false + } + if lhsTextStroke.1 != rhsTextStroke.1 { + return false + } + } else if (lhs.textShadowColor != nil) != (rhs.textShadowColor != nil) { + return false + } + + if let lhsHighlightColor = lhs.highlightColor, let rhsHighlightColor = rhs.highlightColor { + if !lhsHighlightColor.isEqual(rhsHighlightColor) { + return false + } + } else if (lhs.highlightColor != nil) != (rhs.highlightColor != nil) { + return false + } + + return true + } + + public final class View: UIView { + private let textView: ImmediateTextView + + override public init(frame: CGRect) { + self.textView = ImmediateTextView() + + super.init(frame: frame) + + self.addSubview(self.textView) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func attributeSubstring(name: String, index: Int) -> (String, String)? { + return self.textView.attributeSubstring(name: name, index: index) + } + + public func update(component: BalancedTextComponent, availableSize: CGSize, transition: Transition) -> CGSize { + let attributedString: NSAttributedString + switch component.text { + case let .plain(string): + attributedString = string + case let .markdown(text, attributes): + attributedString = parseMarkdownIntoAttributedString(text, attributes: attributes) + } + + self.textView.attributedText = attributedString + self.textView.maximumNumberOfLines = component.maximumNumberOfLines + self.textView.truncationType = component.truncationType + self.textView.textAlignment = component.horizontalAlignment + self.textView.verticalAlignment = component.verticalAlignment + self.textView.lineSpacing = component.lineSpacing + self.textView.cutout = component.cutout + self.textView.insets = component.insets + self.textView.textShadowColor = component.textShadowColor + self.textView.textShadowBlur = component.textShadowBlur + self.textView.textStroke = component.textStroke + self.textView.linkHighlightColor = component.highlightColor + self.textView.highlightAttributeAction = component.highlightAction + self.textView.tapAttributeAction = component.tapAction + self.textView.longTapAttributeAction = component.longTapAction + + var bestSize: (availableWidth: CGFloat, info: TextNodeLayout) + + let info = self.textView.updateLayoutFullInfo(availableSize) + bestSize = (availableSize.width, info) + + if component.balanced && info.numberOfLines > 1 { + let measureIncrement = 8.0 + var measureWidth = info.size.width + measureWidth -= measureIncrement + while measureWidth > 0.0 { + let otherInfo = self.textView.updateLayoutFullInfo(CGSize(width: measureWidth, height: availableSize.height)) + if otherInfo.numberOfLines > bestSize.info.numberOfLines { + break + } + if (otherInfo.size.width - otherInfo.trailingLineWidth) < (bestSize.info.size.width - bestSize.info.trailingLineWidth) { + bestSize = (measureWidth, otherInfo) + } + + measureWidth -= measureIncrement + } + + let bestInfo = self.textView.updateLayoutFullInfo(CGSize(width: bestSize.availableWidth, height: availableSize.height)) + bestSize = (availableSize.width, bestInfo) + } + + self.textView.frame = CGRect(origin: CGPoint(), size: bestSize.info.size) + return bestSize.info.size + } + } + + public func makeView() -> View { + return View() + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, transition: transition) + } +} diff --git a/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift b/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift index 39706976f6..ccbee0a11a 100644 --- a/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift +++ b/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift @@ -223,6 +223,8 @@ open class ViewControllerComponentContainer: ViewController { private var presentationDataDisposable: Disposable? public private(set) var validLayout: ContainerViewLayout? + public var wasDismissed: (() -> Void)? + public init(context: AccountContext, component: C, navigationBarAppearance: NavigationBarAppearance, statusBarStyle: StatusBarStyle = .default, presentationMode: PresentationMode = .default, theme: Theme = .default) where C.EnvironmentType == ViewControllerComponentContainer.Environment { self.context = context self.component = AnyComponent(component) @@ -304,7 +306,11 @@ open class ViewControllerComponentContainer: ViewController { } open override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { - super.dismiss(animated: flag, completion: completion) + let wasDismissed = self.wasDismissed + super.dismiss(animated: flag, completion: { + completion?() + wasDismissed?() + }) } fileprivate var forceNextUpdate = false diff --git a/submodules/Display/Source/TextNode.swift b/submodules/Display/Source/TextNode.swift index e1fdf727ac..32517738df 100644 --- a/submodules/Display/Source/TextNode.swift +++ b/submodules/Display/Source/TextNode.swift @@ -1279,7 +1279,27 @@ open class TextNode: ASDisplayNode { coreTextLine = originalLine } } else { - coreTextLine = CTLineCreateTruncatedLine(originalLine, max(1.0, Double(lineConstrainedSize.width) - truncationTokenWidth), truncationType, truncationToken) ?? truncationToken + if customTruncationToken != nil { + let coreTextLine1 = CTLineCreateTruncatedLine(originalLine, max(1.0, Double(lineConstrainedSize.width)), truncationType, truncationToken) ?? truncationToken + let runs = (CTLineGetGlyphRuns(coreTextLine1) as [AnyObject]) as! [CTRun] + var hasTruncationToken = false + for run in runs { + let runRange = CTRunGetStringRange(run) + if runRange.location + runRange.length >= nsString.length { + hasTruncationToken = true + break + } + } + + if hasTruncationToken { + coreTextLine = coreTextLine1 + } else { + let coreTextLine2 = CTLineCreateTruncatedLine(originalLine, max(1.0, Double(lineConstrainedSize.width) - truncationTokenWidth), truncationType, truncationToken) ?? truncationToken + coreTextLine = coreTextLine2 + } + } else { + coreTextLine = CTLineCreateTruncatedLine(originalLine, max(1.0, Double(lineConstrainedSize.width)), truncationType, truncationToken) ?? truncationToken + } let runs = (CTLineGetGlyphRuns(coreTextLine) as [AnyObject]) as! [CTRun] for run in runs { let runAttributes: NSDictionary = CTRunGetAttributes(run) diff --git a/submodules/Display/Source/TooltipController.swift b/submodules/Display/Source/TooltipController.swift index be5d52bff7..25605bcd66 100644 --- a/submodules/Display/Source/TooltipController.swift +++ b/submodules/Display/Source/TooltipController.swift @@ -100,6 +100,7 @@ open class TooltipController: ViewController, StandalonePresentableController { public private(set) var content: TooltipControllerContent private let baseFontSize: CGFloat + private let balancedTextLayout: Bool open func updateContent(_ content: TooltipControllerContent, animated: Bool, extendTimer: Bool, arrowOnBottom: Bool = true) { if self.content != content { @@ -130,9 +131,10 @@ open class TooltipController: ViewController, StandalonePresentableController { public var dismissed: ((Bool) -> Void)? - public init(content: TooltipControllerContent, baseFontSize: CGFloat, timeout: Double = 2.0, dismissByTapOutside: Bool = false, dismissByTapOutsideSource: Bool = false, dismissImmediatelyOnLayoutUpdate: Bool = false, arrowOnBottom: Bool = true, padding: CGFloat = 8.0, innerPadding: UIEdgeInsets = UIEdgeInsets()) { + public init(content: TooltipControllerContent, baseFontSize: CGFloat, balancedTextLayout: Bool = false, timeout: Double = 2.0, dismissByTapOutside: Bool = false, dismissByTapOutsideSource: Bool = false, dismissImmediatelyOnLayoutUpdate: Bool = false, arrowOnBottom: Bool = true, padding: CGFloat = 8.0, innerPadding: UIEdgeInsets = UIEdgeInsets()) { self.content = content self.baseFontSize = baseFontSize + self.balancedTextLayout = balancedTextLayout self.timeout = timeout self.dismissByTapOutside = dismissByTapOutside self.dismissByTapOutsideSource = dismissByTapOutsideSource @@ -155,7 +157,7 @@ open class TooltipController: ViewController, StandalonePresentableController { } override open func loadDisplayNode() { - self.displayNode = TooltipControllerNode(content: self.content, baseFontSize: self.baseFontSize, dismiss: { [weak self] tappedInside in + self.displayNode = TooltipControllerNode(content: self.content, baseFontSize: self.baseFontSize, balancedTextLayout: self.balancedTextLayout, dismiss: { [weak self] tappedInside in self?.dismiss(tappedInside: tappedInside) }, dismissByTapOutside: self.dismissByTapOutside, dismissByTapOutsideSource: self.dismissByTapOutsideSource) self.controllerNode.padding = self.padding diff --git a/submodules/Display/Source/TooltipControllerNode.swift b/submodules/Display/Source/TooltipControllerNode.swift index 7122720407..ae7ee4cdf2 100644 --- a/submodules/Display/Source/TooltipControllerNode.swift +++ b/submodules/Display/Source/TooltipControllerNode.swift @@ -4,6 +4,7 @@ import AsyncDisplayKit final class TooltipControllerNode: ASDisplayNode { private let baseFontSize: CGFloat + private let balancedTextLayout: Bool private let dismiss: (Bool) -> Void @@ -25,8 +26,9 @@ final class TooltipControllerNode: ASDisplayNode { private var dismissedByTouchOutside = false private var dismissByTapOutsideSource = false - init(content: TooltipControllerContent, baseFontSize: CGFloat, dismiss: @escaping (Bool) -> Void, dismissByTapOutside: Bool, dismissByTapOutsideSource: Bool) { + init(content: TooltipControllerContent, baseFontSize: CGFloat, balancedTextLayout: Bool, dismiss: @escaping (Bool) -> Void, dismissByTapOutside: Bool, dismissByTapOutsideSource: Bool) { self.baseFontSize = baseFontSize + self.balancedTextLayout = balancedTextLayout self.dismissByTapOutside = dismissByTapOutside self.dismissByTapOutsideSource = dismissByTapOutsideSource diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index aa462a0bc7..8c37d11118 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -781,9 +781,19 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { } private weak var currentGalleryController: TGModernGalleryController? + private weak var currentGalleryParentController: ViewController? fileprivate var currentAssetDownloadDisposable = MetaDisposable() + fileprivate func closeGalleryController() { + if let _ = self.currentGalleryController, let currentGalleryParentController = self.currentGalleryParentController { + self.currentGalleryController = nil + self.currentGalleryParentController = nil + + currentGalleryParentController.dismiss(completion: nil) + } + } + fileprivate func cancelAssetDownloads() { guard let downloadManager = self.controller?.downloadManager else { return @@ -868,6 +878,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { strongSelf.controller?.interaction?.sendSelected(result, silently, scheduleTime, false, completion) } }, presentSchedulePicker: controller.presentSchedulePicker, presentTimerPicker: controller.presentTimerPicker, getCaptionPanelView: controller.getCaptionPanelView, present: { [weak self] c, a in + self?.currentGalleryParentController = c self?.controller?.present(c, in: .window(.root), with: a) }, finishedTransitionIn: { [weak self] in self?.openingMedia = false @@ -906,6 +917,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { strongSelf.controller?.interaction?.sendSelected(result, silently, scheduleTime, false, completion) } }, presentSchedulePicker: controller.presentSchedulePicker, presentTimerPicker: controller.presentTimerPicker, getCaptionPanelView: controller.getCaptionPanelView, present: { [weak self] c, a in + self?.currentGalleryParentController = c self?.controller?.present(c, in: .window(.root), with: a, blockInteraction: true) }, finishedTransitionIn: { [weak self] in self?.openingMedia = false @@ -1686,6 +1698,10 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { super.displayNodeDidLoad() } + public func closeGalleryController() { + self.controllerNode.closeGalleryController() + } + private weak var undoOverlayController: UndoOverlayController? private func showSelectionUndo(item: TGMediaSelectableItem) { let scale = min(2.0, UIScreenScale) diff --git a/submodules/MediaPlayer/Sources/MediaPlayerFramePreview.swift b/submodules/MediaPlayer/Sources/MediaPlayerFramePreview.swift index b67019af30..ef00996925 100644 --- a/submodules/MediaPlayer/Sources/MediaPlayerFramePreview.swift +++ b/submodules/MediaPlayer/Sources/MediaPlayerFramePreview.swift @@ -27,7 +27,7 @@ private final class FramePreviewContext { private func initializedPreviewContext(queue: Queue, postbox: Postbox, userLocation: MediaResourceUserLocation, userContentType: MediaResourceUserContentType, fileReference: FileMediaReference) -> Signal, NoError> { return Signal { subscriber in - let source = UniversalSoftwareVideoSource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: userContentType, fileReference: fileReference) + let source = UniversalSoftwareVideoSource(mediaBox: postbox.mediaBox, source: .file(userLocation: userLocation, userContentType: userContentType, fileReference: fileReference)) let readyDisposable = (source.ready |> filter { $0 }).start(next: { _ in subscriber.putNext(QueueLocalObject(queue: queue, generate: { diff --git a/submodules/MediaPlayer/Sources/UniversalSoftwareVideoSource.swift b/submodules/MediaPlayer/Sources/UniversalSoftwareVideoSource.swift index 49b303c1b9..0605e03568 100644 --- a/submodules/MediaPlayer/Sources/UniversalSoftwareVideoSource.swift +++ b/submodules/MediaPlayer/Sources/UniversalSoftwareVideoSource.swift @@ -21,15 +21,17 @@ private func readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: Unsa context.currentReadBytes += readCount let semaphore = DispatchSemaphore(value: 0) - data = context.mediaBox.resourceData(context.fileReference.media.resource, size: context.size, in: requestRange, mode: .partial) + + data = context.mediaBox.resourceData(context.source.resource, size: context.size, in: requestRange, mode: .partial) + let requiredDataIsNotLocallyAvailable = context.requiredDataIsNotLocallyAvailable var fetchedData: Data? let fetchDisposable = MetaDisposable() let isInitialized = context.videoStream != nil || context.automaticallyFetchHeader let mediaBox = context.mediaBox - let userLocation = context.userLocation - let userContentType = context.userContentType - let reference = context.fileReference.resourceReference(context.fileReference.media.resource) + + let source = context.source + let disposable = data.start(next: { result in let (data, isComplete) = result if data.count == readCount || isComplete { @@ -37,7 +39,12 @@ private func readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: Unsa semaphore.signal() } else { if isInitialized { - fetchDisposable.set(fetchedMediaResource(mediaBox: mediaBox, userLocation: userLocation, userContentType: userContentType, reference: reference, ranges: [(requestRange, .maximum)]).start()) + switch source { + case let .file(userLocation, userContentType, fileReference): + fetchDisposable.set(fetchedMediaResource(mediaBox: mediaBox, userLocation: userLocation, userContentType: userContentType, reference: fileReference.resourceReference(fileReference.media.resource), ranges: [(requestRange, .maximum)]).start()) + case .direct: + break + } } requiredDataIsNotLocallyAvailable?() } @@ -100,9 +107,7 @@ private final class SoftwareVideoStream { private final class UniversalSoftwareVideoSourceImpl { fileprivate let mediaBox: MediaBox - fileprivate let userLocation: MediaResourceUserLocation - fileprivate let userContentType: MediaResourceUserContentType - fileprivate let fileReference: FileMediaReference + fileprivate let source: UniversalSoftwareVideoSource.Source fileprivate let size: Int64 fileprivate let automaticallyFetchHeader: Bool @@ -119,16 +124,27 @@ private final class UniversalSoftwareVideoSourceImpl { fileprivate var currentNumberOfReads: Int = 0 fileprivate var currentReadBytes: Int64 = 0 - init?(mediaBox: MediaBox, userLocation: MediaResourceUserLocation, userContentType: MediaResourceUserContentType, fileReference: FileMediaReference, state: ValuePromise, cancelInitialization: Signal, automaticallyFetchHeader: Bool, hintVP9: Bool = false) { - guard let size = fileReference.media.size else { - return nil + init?( + mediaBox: MediaBox, + source: UniversalSoftwareVideoSource.Source, + state: ValuePromise, + cancelInitialization: Signal, + automaticallyFetchHeader: Bool, + hintVP9: Bool = false + ) { + switch source { + case let .file(_, _, fileReference): + guard let size = fileReference.media.size else { + return nil + } + self.size = size + case let .direct(_, sizeValue): + self.size = sizeValue } + self.mediaBox = mediaBox - self.userLocation = userLocation - self.userContentType = userContentType - self.fileReference = fileReference - self.size = size + self.source = source self.automaticallyFetchHeader = automaticallyFetchHeader self.state = state @@ -138,7 +154,15 @@ private final class UniversalSoftwareVideoSourceImpl { let ioBufferSize = 1 * 1024 - guard let avIoContext = FFMpegAVIOContext(bufferSize: Int32(ioBufferSize), opaqueContext: Unmanaged.passUnretained(self).toOpaque(), readPacket: readPacketCallback, writePacket: nil, seek: seekCallback, isSeekable: true) else { + let isSeekable: Bool + switch source { + case .file: + isSeekable = true + case .direct: + isSeekable = false + } + + guard let avIoContext = FFMpegAVIOContext(bufferSize: Int32(ioBufferSize), opaqueContext: Unmanaged.passUnretained(self).toOpaque(), readPacket: readPacketCallback, writePacket: nil, seek: seekCallback, isSeekable: isSeekable) else { return nil } self.avIoContext = avIoContext @@ -295,9 +319,7 @@ private enum UniversalSoftwareVideoSourceState { private final class UniversalSoftwareVideoSourceThreadParams: NSObject { let mediaBox: MediaBox - let userLocation: MediaResourceUserLocation - let userContentType: MediaResourceUserContentType - let fileReference: FileMediaReference + let source: UniversalSoftwareVideoSource.Source let state: ValuePromise let cancelInitialization: Signal let automaticallyFetchHeader: Bool @@ -305,18 +327,14 @@ private final class UniversalSoftwareVideoSourceThreadParams: NSObject { init( mediaBox: MediaBox, - userLocation: MediaResourceUserLocation, - userContentType: MediaResourceUserContentType, - fileReference: FileMediaReference, + source: UniversalSoftwareVideoSource.Source, state: ValuePromise, cancelInitialization: Signal, automaticallyFetchHeader: Bool, hintVP9: Bool ) { self.mediaBox = mediaBox - self.userLocation = userLocation - self.userContentType = userContentType - self.fileReference = fileReference + self.source = source self.state = state self.cancelInitialization = cancelInitialization self.automaticallyFetchHeader = automaticallyFetchHeader @@ -345,7 +363,7 @@ private final class UniversalSoftwareVideoSourceThread: NSObject { let timer = Timer(fireAt: .distantFuture, interval: 0.0, target: UniversalSoftwareVideoSourceThread.self, selector: #selector(UniversalSoftwareVideoSourceThread.none), userInfo: nil, repeats: false) runLoop.add(timer, forMode: .common) - let source = UniversalSoftwareVideoSourceImpl(mediaBox: params.mediaBox, userLocation: params.userLocation, userContentType: params.userContentType, fileReference: params.fileReference, state: params.state, cancelInitialization: params.cancelInitialization, automaticallyFetchHeader: params.automaticallyFetchHeader) + let source = UniversalSoftwareVideoSourceImpl(mediaBox: params.mediaBox, source: params.source, state: params.state, cancelInitialization: params.cancelInitialization, automaticallyFetchHeader: params.automaticallyFetchHeader) Thread.current.threadDictionary["source"] = source while true { @@ -387,6 +405,27 @@ public enum UniversalSoftwareVideoSourceTakeFrameResult { } public final class UniversalSoftwareVideoSource { + public enum Source { + case file( + userLocation: MediaResourceUserLocation, + userContentType: MediaResourceUserContentType, + fileReference: FileMediaReference + ) + case direct( + resource: MediaResource, + size: Int64 + ) + + var resource: MediaResource { + switch self { + case let .file(_, _, fileReference): + return fileReference.media.resource + case let .direct(resource, _): + return resource + } + } + } + private let thread: Thread private let stateValue: ValuePromise = ValuePromise(.initializing, ignoreRepeated: true) private let cancelInitialization: ValuePromise = ValuePromise(false) @@ -403,8 +442,8 @@ public final class UniversalSoftwareVideoSource { } } - public init(mediaBox: MediaBox, userLocation: MediaResourceUserLocation, userContentType: MediaResourceUserContentType, fileReference: FileMediaReference, automaticallyFetchHeader: Bool = false, hintVP9: Bool = false) { - self.thread = Thread(target: UniversalSoftwareVideoSourceThread.self, selector: #selector(UniversalSoftwareVideoSourceThread.entryPoint(_:)), object: UniversalSoftwareVideoSourceThreadParams(mediaBox: mediaBox, userLocation: userLocation, userContentType: userContentType, fileReference: fileReference, state: self.stateValue, cancelInitialization: self.cancelInitialization.get(), automaticallyFetchHeader: automaticallyFetchHeader, hintVP9: hintVP9)) + public init(mediaBox: MediaBox, source: Source, automaticallyFetchHeader: Bool = false, hintVP9: Bool = false) { + self.thread = Thread(target: UniversalSoftwareVideoSourceThread.self, selector: #selector(UniversalSoftwareVideoSourceThread.entryPoint(_:)), object: UniversalSoftwareVideoSourceThreadParams(mediaBox: mediaBox, source: source, state: self.stateValue, cancelInitialization: self.cancelInitialization.get(), automaticallyFetchHeader: automaticallyFetchHeader, hintVP9: hintVP9)) self.thread.name = "UniversalSoftwareVideoSource" self.thread.start() } diff --git a/submodules/MtProtoKit/Sources/MTRequestMessageService.m b/submodules/MtProtoKit/Sources/MTRequestMessageService.m index fed278d302..d8c170f8ca 100644 --- a/submodules/MtProtoKit/Sources/MTRequestMessageService.m +++ b/submodules/MtProtoKit/Sources/MTRequestMessageService.m @@ -120,8 +120,8 @@ { if (request.requestContext != nil) { - //[_dropReponseContexts addObject:[[MTDropResponseContext alloc] initWithDropMessageId:request.requestContext.messageId]]; - //anyNewDropRequests = true; + [_dropReponseContexts addObject:[[MTDropResponseContext alloc] initWithDropMessageId:request.requestContext.messageId]]; + anyNewDropRequests = true; } if (request.requestContext.messageId != 0) { @@ -902,7 +902,7 @@ if (!requestFound) { if (MTLogEnabled()) { - MTLog(@"[MTRequestMessageService#%p response %" PRId64 " didn't match any request]", self, message.messageId); + MTLog(@"[MTRequestMessageService#%p response %" PRId64 " for % " PRId64 " didn't match any request]", self, message.messageId, rpcResultMessage.requestMessageId); } } else if (_requests.count == 0) diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index 5e66a39116..b20521e926 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -2792,6 +2792,7 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer { @objc private func cancelPressed() { self.dismiss() + self.wasDismissed?() } public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift index 7fecddfd0b..7ffbcce901 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift @@ -181,7 +181,7 @@ func telegramMediaFileFromApiDocument(_ document: Api.Document) -> TelegramMedia } } - return TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudFile, id: id), partialReference: nil, resource: CloudDocumentMediaResource(datacenterId: Int(dcId), fileId: id, accessHash: accessHash, size: size, fileReference: fileReference.makeData(), fileName: fileNameFromFileAttributes(parsedAttributes)), previewRepresentations: previewRepresentations, videoThumbnails: videoThumbnails, immediateThumbnailData: immediateThumbnail, mimeType: mimeType, size: size, attributes: parsedAttributes) + return TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudFile, id: id), partialReference: nil, resource: CloudDocumentMediaResource(datacenterId: Int(dcId), fileId: id, accessHash: accessHash, size: size, fileReference: fileReference.makeData(), fileName: fileNameFromFileAttributes(parsedAttributes)), previewRepresentations: previewRepresentations, videoThumbnails: videoThumbnails, immediateThumbnailData: immediateThumbnail, mimeType: mimeType, size: size, attributes: parsedAttributes) case .documentEmpty: return nil } diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift index 5dccba4682..ee5d65e026 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift @@ -2425,7 +2425,18 @@ public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputVi replaceImpl?(controller) }) replaceImpl = { [weak controller] c in - controller?.replace(with: c) + guard let controller else { + return + } + if controller.navigationController != nil { + controller.replace(with: c) + } else { + controller.dismiss() + + if let self { + self.presentController?(c) + } + } } strongSelf.presentController?(controller) }), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false })) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift index be2213e218..3d91718548 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift @@ -124,9 +124,11 @@ private final class FrameSequenceThumbnailNode: ASDisplayNode { let source = UniversalSoftwareVideoSource( mediaBox: self.context.account.postbox.mediaBox, - userLocation: userLocation, - userContentType: .other, - fileReference: self.file, + source: .file( + userLocation: userLocation, + userContentType: .other, + fileReference: self.file + ), automaticallyFetchHeader: true ) self.sources.append(source) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift index f60b7dbf20..55736ddeaa 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift @@ -407,7 +407,7 @@ final class StoryItemContentComponent: Component { } progress = min(1.0, progress) - if actualTimestamp < 0.1 { + if actualTimestamp < 0.3 { isBuffering = false } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index b01be680ee..677a424c0e 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -1392,6 +1392,12 @@ public final class StoryItemSetContainerComponent: Component { self.sendMessageContext.animateOut(bounds: self.bounds) + self.sendMessageContext.tooltipScreen?.dismiss() + self.sendMessageContext.tooltipScreen = nil + + self.contextController?.dismiss() + self.contextController = nil + if let inputPanelView = self.inputPanel.view { inputPanelView.layer.animatePosition( from: CGPoint(), @@ -2508,7 +2514,7 @@ public final class StoryItemSetContainerComponent: Component { let tooltipText: String switch storyPrivacyIcon { case .closeFriends: - tooltipText = component.strings.Story_TooltipPrivacyCloseFriends(component.slice.peer.compactDisplayTitle).string + tooltipText = component.strings.Story_TooltipPrivacyCloseFriends2(component.slice.peer.compactDisplayTitle).string case .contacts: tooltipText = component.strings.Story_TooltipPrivacyContacts(component.slice.peer.compactDisplayTitle).string case .selectedContacts: @@ -2520,7 +2526,10 @@ public final class StoryItemSetContainerComponent: Component { let tooltipScreen = TooltipScreen( account: component.context.account, sharedContext: component.context.sharedContext, - text: .markdown(text: tooltipText), style: .default, location: TooltipScreen.Location.point(closeFriendIconView.convert(closeFriendIconView.bounds, to: nil).offsetBy(dx: 1.0, dy: 6.0), .top), displayDuration: .infinite, shouldDismissOnTouch: { _, _ in + text: .markdown(text: tooltipText), + balancedTextLayout: true, + style: .default, + location: TooltipScreen.Location.point(closeFriendIconView.convert(closeFriendIconView.bounds, to: nil).offsetBy(dx: 1.0, dy: 6.0), .top), displayDuration: .infinite, shouldDismissOnTouch: { _, _ in return .dismiss(consume: true) } ) @@ -3376,23 +3385,41 @@ public final class StoryItemSetContainerComponent: Component { break } - if subject != nil || chat { - component.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: component.context, chatLocation: .peer(peer), subject: subject, keepStack: .always, animated: true, pushController: { [weak controller, weak navigationController] chatController, animated, completion in - guard let controller, let navigationController else { - return - } - if "".isEmpty { - navigationController.pushViewController(chatController) + if subject != nil || chat { + if let index = navigationController.viewControllers.firstIndex(where: { c in + if let c = c as? ChatController, case .peer(peer.id) = c.chatLocation { + return true } else { - var viewControllers = navigationController.viewControllers - if let index = viewControllers.firstIndex(where: { $0 === controller }) { - viewControllers.insert(chatController, at: index) - } else { - viewControllers.append(chatController) - } - navigationController.setViewControllers(viewControllers, animated: animated) + return false } - })) + }) { + var viewControllers = navigationController.viewControllers + for i in ((index + 1) ..< viewControllers.count).reversed() { + if viewControllers[i] !== controller { + viewControllers.remove(at: i) + } + } + navigationController.setViewControllers(viewControllers, animated: true) + + controller.dismissWithoutTransitionOut() + } else { + component.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: component.context, chatLocation: .peer(peer), subject: subject, keepStack: .always, animated: true, pushController: { [weak controller, weak navigationController] chatController, animated, completion in + guard let controller, let navigationController else { + return + } + if "".isEmpty { + navigationController.pushViewController(chatController) + } else { + var viewControllers = navigationController.viewControllers + if let index = viewControllers.firstIndex(where: { $0 === controller }) { + viewControllers.insert(chatController, at: index) + } else { + viewControllers.append(chatController) + } + navigationController.setViewControllers(viewControllers, animated: animated) + } + })) + } } else { var currentViewControllers = navigationController.viewControllers if let index = currentViewControllers.firstIndex(where: { c in @@ -3652,6 +3679,10 @@ public final class StoryItemSetContainerComponent: Component { } private func performMoreAction(sourceView: UIView, gesture: ContextGesture?) { + if self.isAnimatingOut { + return + } + guard let component = self.component else { return } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index 3cbf85aa86..82df7f8971 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -1684,11 +1684,11 @@ final class StoryItemSetContainerSendMessage { done(time) }) } - controller.getCaptionPanelView = { [weak self, weak view] in - guard let self, let view else { + controller.getCaptionPanelView = { [weak self, weak controller, weak view] in + guard let self, let view, let controller else { return nil } - return self.getCaptionPanelView(view: view, peer: peer) + return self.getCaptionPanelView(view: view, peer: peer, mediaPicker: controller) } controller.legacyCompletion = { signals, silently, scheduleTime, getAnimatedTransitionSource, sendCompletion in completion(signals, silently, scheduleTime, getAnimatedTransitionSource, sendCompletion) @@ -2067,7 +2067,7 @@ final class StoryItemSetContainerSendMessage { }) } - private func getCaptionPanelView(view: StoryItemSetContainerComponent.View, peer: EnginePeer) -> TGCaptionPanelView? { + private func getCaptionPanelView(view: StoryItemSetContainerComponent.View, peer: EnginePeer, mediaPicker: MediaPickerScreen? = nil) -> TGCaptionPanelView? { guard let component = view.component else { return nil } @@ -2081,7 +2081,27 @@ final class StoryItemSetContainerSendMessage { guard let view else { return } - view.component?.controller()?.presentInGlobalOverlay(c) + if let c = c as? PremiumIntroScreen { + view.endEditing(true) + if let mediaPicker { + mediaPicker.closeGalleryController() + } + if let attachmentController = self.attachmentController { + self.attachmentController = nil + attachmentController.dismiss(animated: false, completion: nil) + } + c.wasDismissed = { [weak view] in + guard let view else { + return + } + view.updateIsProgressPaused() + } + view.component?.controller()?.push(c) + + view.updateIsProgressPaused() + } else { + view.component?.controller()?.presentInGlobalOverlay(c) + } }) as? TGCaptionPanelView } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift index e7bee94391..ea9fed6c84 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift @@ -831,7 +831,7 @@ final class StoryItemSetViewListComponent: Component { transition: emptyTransition, component: AnyComponent(AnimatedStickerComponent( account: component.context.account, - animation: AnimatedStickerComponent.Animation(source: .bundle(name: "Burn"), loop: true), + animation: AnimatedStickerComponent.Animation(source: .bundle(name: "ChatListNoResults"), loop: true), size: CGSize(width: 140.0, height: 140.0) )), environment: {}, diff --git a/submodules/TelegramUI/Images.xcassets/Stories/PanelGradient.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Stories/PanelGradient.imageset/Contents.json index 5312a6642c..0d1e12bb58 100644 --- a/submodules/TelegramUI/Images.xcassets/Stories/PanelGradient.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Stories/PanelGradient.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "smoothGradient 0.6.png", + "filename" : "smoothGradient 0.4.png", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Stories/PanelGradient.imageset/smoothGradient 0.4.png b/submodules/TelegramUI/Images.xcassets/Stories/PanelGradient.imageset/smoothGradient 0.4.png new file mode 100644 index 0000000000..49a5faf1c2 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Stories/PanelGradient.imageset/smoothGradient 0.4.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Stories/PanelGradient.imageset/smoothGradient 0.6.png b/submodules/TelegramUI/Images.xcassets/Stories/PanelGradient.imageset/smoothGradient 0.6.png deleted file mode 100644 index 82f7e1820c..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Stories/PanelGradient.imageset/smoothGradient 0.6.png and /dev/null differ diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift index 623b851752..8c93bb6fd5 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift @@ -1452,7 +1452,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { } } - let state: SemanticStatusNodeState + var state: SemanticStatusNodeState var streamingState: SemanticStatusNodeState = .none let isSending = message.flags.isSending @@ -1563,6 +1563,20 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { streamingState = .none } + if isSending { + if case .progress = streamingState { + } else { + let adjustedProgress: CGFloat = 0.027 + streamingState = .progress(value: CGFloat(adjustedProgress), cancelEnabled: true, appearance: .init(inset: 1.0, lineWidth: 2.0)) + } + + if case .progress = state { + } else { + let adjustedProgress: CGFloat = 0.027 + state = .progress(value: CGFloat(adjustedProgress), cancelEnabled: true, appearance: .init(inset: 1.0, lineWidth: 2.0)) + } + } + let backgroundNodeColor: UIColor let foregroundNodeColor: UIColor if self.iconNode != nil { diff --git a/submodules/TelegramUI/Sources/FetchCachedRepresentations.swift b/submodules/TelegramUI/Sources/FetchCachedRepresentations.swift index b2170a46ff..a449e50011 100644 --- a/submodules/TelegramUI/Sources/FetchCachedRepresentations.swift +++ b/submodules/TelegramUI/Sources/FetchCachedRepresentations.swift @@ -19,6 +19,7 @@ import GZip import TelegramUniversalVideoContent import GradientBackground import Svg +import UniversalMediaPlayer public func fetchCachedResourceRepresentation(account: Account, resource: MediaResource, representation: CachedMediaResourceRepresentation) -> Signal { if let representation = representation as? CachedStickerAJpegRepresentation { @@ -38,7 +39,33 @@ public func fetchCachedResourceRepresentation(account: Account, resource: MediaR return fetchCachedScaledImageRepresentation(resource: resource, resourceData: data, representation: representation) } } else if let _ = representation as? CachedVideoFirstFrameRepresentation { - return account.postbox.mediaBox.resourceData(resource, option: .complete(waitUntilFetchStatus: false)) + return Signal { subscriber in + if let size = resource.size { + let videoSource = UniversalSoftwareVideoSource(mediaBox: account.postbox.mediaBox, source: .direct(resource: resource, size: size), automaticallyFetchHeader: false, hintVP9: false) + let disposable = videoSource.takeFrame(at: 0.0).start(next: { value in + switch value { + case let .image(image): + if let image { + if let imageData = image.jpegData(compressionQuality: 0.6) { + subscriber.putNext(.data(imageData)) + subscriber.putNext(.done) + subscriber.putCompletion() + } + } + case .waitingForData: + break + } + }) + return ActionDisposable { + // keep the reference + let _ = videoSource.takeFrame(at: 0.0) + disposable.dispose() + } + } else { + return EmptyDisposable + } + } + /*return account.postbox.mediaBox.resourceData(resource, option: .complete(waitUntilFetchStatus: false)) |> mapToSignal { data -> Signal in if data.complete { return fetchCachedVideoFirstFrameRepresentation(account: account, resource: resource, resourceData: data) @@ -50,7 +77,7 @@ public func fetchCachedResourceRepresentation(account: Account, resource: MediaR } else { return .complete() } - } + }*/ } else if let representation = representation as? CachedScaledVideoFirstFrameRepresentation { return account.postbox.mediaBox.resourceData(resource, option: .complete(waitUntilFetchStatus: false)) |> mapToSignal { data -> Signal in diff --git a/submodules/TelegramUI/Sources/PeerInfoGifPaneNode.swift b/submodules/TelegramUI/Sources/PeerInfoGifPaneNode.swift index 949e624040..0f606a494e 100644 --- a/submodules/TelegramUI/Sources/PeerInfoGifPaneNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfoGifPaneNode.swift @@ -65,9 +65,11 @@ private final class FrameSequenceThumbnailNode: ASDisplayNode { let source = UniversalSoftwareVideoSource( mediaBox: self.context.account.postbox.mediaBox, - userLocation: userLocation, - userContentType: .other, - fileReference: self.file, + source: .file( + userLocation: userLocation, + userContentType: .other, + fileReference: self.file + ), automaticallyFetchHeader: true ) self.sources.append(source) diff --git a/submodules/TooltipUI/BUILD b/submodules/TooltipUI/BUILD index 1d49fcf788..6ef6c4ed14 100644 --- a/submodules/TooltipUI/BUILD +++ b/submodules/TooltipUI/BUILD @@ -24,6 +24,7 @@ swift_library( "//submodules/ComponentFlow", "//submodules/Markdown", "//submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent", + "//submodules/Components/BalancedTextComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TooltipUI/Sources/TooltipScreen.swift b/submodules/TooltipUI/Sources/TooltipScreen.swift index 4f2b673a16..ba2501f419 100644 --- a/submodules/TooltipUI/Sources/TooltipScreen.swift +++ b/submodules/TooltipUI/Sources/TooltipScreen.swift @@ -16,6 +16,7 @@ import ComponentFlow import AvatarStoryIndicatorComponent import AccountContext import Markdown +import BalancedTextComponent public enum TooltipActiveTextItem { case url(String, Bool) @@ -107,6 +108,9 @@ private class DownArrowsIconNode: ASDisplayNode { } private final class TooltipScreenNode: ViewControllerTracingNode { + private let text: TooltipScreen.Text + private let textAlignment: TooltipScreen.Alignment + private let balancedTextLayout: Bool private let tooltipStyle: TooltipScreen.Style private let icon: TooltipScreen.Icon? private let action: TooltipScreen.Action? @@ -136,12 +140,13 @@ private final class TooltipScreenNode: ViewControllerTracingNode { private var downArrowsNode: DownArrowsIconNode? private var avatarNode: AvatarNode? private var avatarStoryIndicator: ComponentView? - private let textNode: ImmediateTextNode + private let textView = ComponentView() private var closeButtonNode: HighlightableButtonNode? private var actionButtonNode: HighlightableButtonNode? private var isArrowInverted: Bool = false + private let fontSize: CGFloat private let inset: CGFloat private var validLayout: ContainerViewLayout? @@ -152,6 +157,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode { sharedContext: SharedAccountContext, text: TooltipScreen.Text, textAlignment: TooltipScreen.Alignment, + balancedTextLayout: Bool, style: TooltipScreen.Style, icon: TooltipScreen.Icon? = nil, action: TooltipScreen.Action? = nil, @@ -337,39 +343,10 @@ private final class TooltipScreenNode: ViewControllerTracingNode { self.backgroundMaskNode.layer.rasterizationScale = UIScreen.main.scale } - self.textNode = ImmediateTextNode() - self.textNode.displaysAsynchronously = false - self.textNode.maximumNumberOfLines = 0 - - let baseFont = Font.regular(fontSize) - let boldFont = Font.semibold(14.0) - let italicFont = Font.italic(fontSize) - let boldItalicFont = Font.semiboldItalic(fontSize) - let fixedFont = Font.monospace(fontSize) - - let textColor: UIColor = .white - - let attributedText: NSAttributedString - switch text { - case let .plain(text): - attributedText = NSAttributedString(string: text, font: baseFont, textColor: textColor) - case let .entities(text, entities): - attributedText = stringWithAppliedEntities(text, entities: entities, baseColor: textColor, linkColor: textColor, baseFont: baseFont, linkFont: baseFont, boldFont: boldFont, italicFont: italicFont, boldItalicFont: boldItalicFont, fixedFont: fixedFont, blockQuoteFont: baseFont, underlineLinks: true, external: false, message: nil) - case let .markdown(text): - let linkColor = UIColor(rgb: 0x64d2ff) - let markdownAttributes = MarkdownAttributes( - body: MarkdownAttributeSet(font: baseFont, textColor: textColor), - bold: MarkdownAttributeSet(font: boldFont, textColor: textColor), - link: MarkdownAttributeSet(font: boldFont, textColor: linkColor), - linkAttribute: { _ in - return nil - } - ) - attributedText = parseMarkdownIntoAttributedString(text, attributes: markdownAttributes) - } - - self.textNode.attributedText = attributedText - self.textNode.textAlignment = textAlignment == .center ? .center : .natural + self.fontSize = fontSize + self.text = text + self.textAlignment = textAlignment + self.balancedTextLayout = balancedTextLayout self.animatedStickerNode = DefaultAnimatedStickerNodeImpl() switch icon { @@ -403,7 +380,6 @@ private final class TooltipScreenNode: ViewControllerTracingNode { self.backgroundContainerNode.addSubnode(effectNode) self.backgroundContainerNode.layer.mask = self.backgroundMaskNode.layer } - self.containerNode.addSubnode(self.textNode) self.containerNode.addSubnode(self.animatedStickerNode) if let closeButtonNode = self.closeButtonNode { @@ -428,65 +404,6 @@ private final class TooltipScreenNode: ViewControllerTracingNode { self.actionButtonNode = actionButtonNode } - self.textNode.linkHighlightColor = UIColor.white.withAlphaComponent(0.5) - self.textNode.highlightAttributeAction = { attributes in - let highlightedAttributes = [ - TelegramTextAttributes.URL, - TelegramTextAttributes.PeerMention, - TelegramTextAttributes.PeerTextMention, - TelegramTextAttributes.BotCommand, - TelegramTextAttributes.Hashtag - ] - - for attribute in highlightedAttributes { - if let _ = attributes[NSAttributedString.Key(rawValue: attribute)] { - return NSAttributedString.Key(rawValue: attribute) - } - } - return nil - } - self.textNode.tapAttributeAction = { [weak self] attributes, index in - guard let strongSelf = self else { - return - } - if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { - var concealed = true - if let (attributeText, fullText) = strongSelf.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) { - concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText) - } - openActiveTextItem?(.url(url, concealed), .tap) - } else if let mention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention { - openActiveTextItem?(.mention(mention.peerId, mention.mention), .tap) - } else if let mention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String { - openActiveTextItem?(.textMention(mention), .tap) - } else if let command = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand)] as? String { - openActiveTextItem?(.botCommand(command), .tap) - } else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag { - openActiveTextItem?(.hashtag(hashtag.hashtag), .tap) - } - } - - self.textNode.longTapAttributeAction = { [weak self] attributes, index in - guard let strongSelf = self else { - return - } - if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { - var concealed = true - if let (attributeText, fullText) = strongSelf.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) { - concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText) - } - openActiveTextItem?(.url(url, concealed), .longTap) - } else if let mention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention { - openActiveTextItem?(.mention(mention.peerId, mention.mention), .longTap) - } else if let mention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String { - openActiveTextItem?(.textMention(mention), .longTap) - } else if let command = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand)] as? String { - openActiveTextItem?(.botCommand(command), .longTap) - } else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag { - openActiveTextItem?(.hashtag(hashtag.hashtag), .longTap) - } - } - self.actionButtonNode?.addTarget(self, action: #selector(self.actionPressed), forControlEvents: .touchUpInside) self.closeButtonNode?.addTarget(self, action: #selector(self.closePressed), forControlEvents: .touchUpInside) } @@ -555,7 +472,105 @@ private final class TooltipScreenNode: ViewControllerTracingNode { buttonInset += 24.0 } - let textSize = self.textNode.updateLayout(CGSize(width: containerWidth - contentInset * 2.0 - animationSize.width - animationSpacing - buttonInset, height: .greatestFiniteMagnitude)) + let baseFont = Font.regular(self.fontSize) + let boldFont = Font.semibold(14.0) + let italicFont = Font.italic(self.fontSize) + let boldItalicFont = Font.semiboldItalic(self.fontSize) + let fixedFont = Font.monospace(self.fontSize) + + let textColor: UIColor = .white + let attributedText: NSAttributedString + switch self.text { + case let .plain(text): + attributedText = NSAttributedString(string: text, font: baseFont, textColor: textColor) + case let .entities(text, entities): + attributedText = stringWithAppliedEntities(text, entities: entities, baseColor: textColor, linkColor: textColor, baseFont: baseFont, linkFont: baseFont, boldFont: boldFont, italicFont: italicFont, boldItalicFont: boldItalicFont, fixedFont: fixedFont, blockQuoteFont: baseFont, underlineLinks: true, external: false, message: nil) + case let .markdown(text): + let linkColor = UIColor(rgb: 0x64d2ff) + let markdownAttributes = MarkdownAttributes( + body: MarkdownAttributeSet(font: baseFont, textColor: textColor), + bold: MarkdownAttributeSet(font: boldFont, textColor: textColor), + link: MarkdownAttributeSet(font: boldFont, textColor: linkColor), + linkAttribute: { _ in + return nil + } + ) + attributedText = parseMarkdownIntoAttributedString(text, attributes: markdownAttributes) + } + + let highlightColor: UIColor? = UIColor.white.withAlphaComponent(0.5) + let highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? = { attributes in + let highlightedAttributes = [ + TelegramTextAttributes.URL, + TelegramTextAttributes.PeerMention, + TelegramTextAttributes.PeerTextMention, + TelegramTextAttributes.BotCommand, + TelegramTextAttributes.Hashtag + ] + + for attribute in highlightedAttributes { + if let _ = attributes[NSAttributedString.Key(rawValue: attribute)] { + return NSAttributedString.Key(rawValue: attribute) + } + } + return nil + } + let tapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = { [weak self] attributes, index in + guard let strongSelf = self else { + return + } + if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { + var concealed = true + if let (attributeText, fullText) = (strongSelf.textView.view as? BalancedTextComponent.View)?.attributeSubstring(name: TelegramTextAttributes.URL, index: index) { + concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText) + } + strongSelf.openActiveTextItem?(.url(url, concealed), .tap) + } else if let mention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention { + strongSelf.openActiveTextItem?(.mention(mention.peerId, mention.mention), .tap) + } else if let mention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String { + strongSelf.openActiveTextItem?(.textMention(mention), .tap) + } else if let command = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand)] as? String { + strongSelf.openActiveTextItem?(.botCommand(command), .tap) + } else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag { + strongSelf.openActiveTextItem?(.hashtag(hashtag.hashtag), .tap) + } + } + let longTapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = { [weak self] attributes, index in + guard let strongSelf = self else { + return + } + if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { + var concealed = true + if let (attributeText, fullText) = (strongSelf.textView.view as? BalancedTextComponent.View)?.attributeSubstring(name: TelegramTextAttributes.URL, index: index) { + concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText) + } + strongSelf.openActiveTextItem?(.url(url, concealed), .longTap) + } else if let mention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention { + strongSelf.openActiveTextItem?(.mention(mention.peerId, mention.mention), .longTap) + } else if let mention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String { + strongSelf.openActiveTextItem?(.textMention(mention), .longTap) + } else if let command = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand)] as? String { + strongSelf.openActiveTextItem?(.botCommand(command), .longTap) + } else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag { + strongSelf.openActiveTextItem?(.hashtag(hashtag.hashtag), .longTap) + } + } + + let textSize = self.textView.update( + transition: .immediate, + component: AnyComponent(BalancedTextComponent( + text: .plain(attributedText), + balanced: self.balancedTextLayout, + horizontalAlignment: self.textAlignment == .center ? .center : .left, + maximumNumberOfLines: 0, + highlightColor: highlightColor, + highlightAction: highlightAction, + tapAction: tapAction, + longTapAction: longTapAction + )), + environment: {}, + containerSize: CGSize(width: containerWidth - contentInset * 2.0 - animationSize.width - animationSpacing - buttonInset, height: 1000000.0) + ) var backgroundFrame: CGRect @@ -668,7 +683,15 @@ private final class TooltipScreenNode: ViewControllerTracingNode { } let textFrame = CGRect(origin: CGPoint(x: contentInset + animationSize.width + animationSpacing, y: floor((backgroundHeight - textSize.height) / 2.0)), size: textSize) - transition.updateFrame(node: self.textNode, frame: textFrame) + + if let textComponentView = self.textView.view { + if textComponentView.superview == nil { + textComponentView.layer.anchorPoint = CGPoint() + self.containerNode.view.addSubview(textComponentView) + } + transition.updatePosition(layer: textComponentView.layer, position: textFrame.origin) + transition.updateBounds(layer: textComponentView.layer, bounds: CGRect(origin: CGPoint(), size: textFrame.size)) + } if let closeButtonNode = self.closeButtonNode { let closeSize = CGSize(width: 44.0, height: 44.0) @@ -746,7 +769,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode { private var didRequestDismiss = false override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if let event = event { - if let _ = self.openActiveTextItem, let result = self.textNode.hitTest(self.view.convert(point, to: self.textNode.view), with: event) { + if let _ = self.openActiveTextItem, let textComponentView = self.textView.view, let result = textComponentView.hitTest(self.view.convert(point, to: textComponentView), with: event) { return result } @@ -940,6 +963,7 @@ public final class TooltipScreen: ViewController { private let sharedContext: SharedAccountContext public let text: TooltipScreen.Text public let textAlignment: TooltipScreen.Alignment + private let balancedTextLayout: Bool private let style: TooltipScreen.Style private let icon: TooltipScreen.Icon? private let action: TooltipScreen.Action? @@ -976,6 +1000,7 @@ public final class TooltipScreen: ViewController { sharedContext: SharedAccountContext, text: TooltipScreen.Text, textAlignment: TooltipScreen.Alignment = .natural, + balancedTextLayout: Bool = false, style: TooltipScreen.Style = .default, icon: TooltipScreen.Icon? = nil, action: TooltipScreen.Action? = nil, @@ -991,6 +1016,7 @@ public final class TooltipScreen: ViewController { self.sharedContext = sharedContext self.text = text self.textAlignment = textAlignment + self.balancedTextLayout = balancedTextLayout self.style = style self.icon = icon self.action = action @@ -1057,7 +1083,7 @@ public final class TooltipScreen: ViewController { } override public func loadDisplayNode() { - self.displayNode = TooltipScreenNode(context: self.context, account: self.account, sharedContext: self.sharedContext, text: self.text, textAlignment: self.textAlignment, style: self.style, icon: self.icon, action: self.action, location: self.location, displayDuration: self.displayDuration, inset: self.inset, cornerRadius: self.cornerRadius, shouldDismissOnTouch: self.shouldDismissOnTouch, requestDismiss: { [weak self] in + self.displayNode = TooltipScreenNode(context: self.context, account: self.account, sharedContext: self.sharedContext, text: self.text, textAlignment: self.textAlignment, balancedTextLayout: self.balancedTextLayout, style: self.style, icon: self.icon, action: self.action, location: self.location, displayDuration: self.displayDuration, inset: self.inset, cornerRadius: self.cornerRadius, shouldDismissOnTouch: self.shouldDismissOnTouch, requestDismiss: { [weak self] in guard let strongSelf = self else { return }