This commit is contained in:
Ali 2023-07-25 01:45:44 +04:00
parent a196a1684b
commit 1fa20fa4a4
28 changed files with 635 additions and 181 deletions

View File

@ -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.";

View File

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

View File

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

View File

@ -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<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}

View File

@ -223,6 +223,8 @@ open class ViewControllerComponentContainer: ViewController {
private var presentationDataDisposable: Disposable?
public private(set) var validLayout: ContainerViewLayout?
public var wasDismissed: (() -> Void)?
public init<C: Component>(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

View File

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

View File

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

View File

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

View File

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

View File

@ -27,7 +27,7 @@ private final class FramePreviewContext {
private func initializedPreviewContext(queue: Queue, postbox: Postbox, userLocation: MediaResourceUserLocation, userContentType: MediaResourceUserContentType, fileReference: FileMediaReference) -> Signal<QueueLocalObject<FramePreviewContext>, 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: {

View File

@ -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<UniversalSoftwareVideoSourceState>, cancelInitialization: Signal<Bool, NoError>, automaticallyFetchHeader: Bool, hintVP9: Bool = false) {
guard let size = fileReference.media.size else {
return nil
init?(
mediaBox: MediaBox,
source: UniversalSoftwareVideoSource.Source,
state: ValuePromise<UniversalSoftwareVideoSourceState>,
cancelInitialization: Signal<Bool, NoError>,
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<UniversalSoftwareVideoSourceState>
let cancelInitialization: Signal<Bool, NoError>
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<UniversalSoftwareVideoSourceState>,
cancelInitialization: Signal<Bool, NoError>,
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<UniversalSoftwareVideoSourceState> = ValuePromise(.initializing, ignoreRepeated: true)
private let cancelInitialization: ValuePromise<Bool> = 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()
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -407,7 +407,7 @@ final class StoryItemContentComponent: Component {
}
progress = min(1.0, progress)
if actualTimestamp < 0.1 {
if actualTimestamp < 0.3 {
isBuffering = false
}

View File

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

View File

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

View File

@ -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: {},

View File

@ -1,7 +1,7 @@
{
"images" : [
{
"filename" : "smoothGradient 0.6.png",
"filename" : "smoothGradient 0.4.png",
"idiom" : "universal"
}
],

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

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

View File

@ -19,6 +19,7 @@ import GZip
import TelegramUniversalVideoContent
import GradientBackground
import Svg
import UniversalMediaPlayer
public func fetchCachedResourceRepresentation(account: Account, resource: MediaResource, representation: CachedMediaResourceRepresentation) -> Signal<CachedMediaResourceRepresentationResult, NoError> {
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<CachedMediaResourceRepresentationResult, NoError> 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<CachedMediaResourceRepresentationResult, NoError> in

View File

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

View File

@ -24,6 +24,7 @@ swift_library(
"//submodules/ComponentFlow",
"//submodules/Markdown",
"//submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent",
"//submodules/Components/BalancedTextComponent",
],
visibility = [
"//visibility:public",

View File

@ -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<Empty>?
private let textNode: ImmediateTextNode
private let textView = ComponentView<Empty>()
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
}