Progress indicators

This commit is contained in:
Ali 2023-10-20 14:32:12 +04:00
parent 89fbca6fdb
commit dd46ccd6ed
14 changed files with 558 additions and 163 deletions

View File

@ -195,7 +195,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
addAppLogEvent(postbox: context.account.postbox, type: "search_global_open_message", peerId: peer.id, data: .dictionary(["msg_id": .number(Double(messageId.id))])) addAppLogEvent(postbox: context.account.postbox, type: "search_global_open_message", peerId: peer.id, data: .dictionary(["msg_id": .number(Double(messageId.id))]))
} }
}, openUrl: { [weak self] url in }, openUrl: { [weak self] url in
openUserGeneratedUrl(context: context, peerId: nil, url: url, concealed: false, present: { c in let _ = openUserGeneratedUrl(context: context, peerId: nil, url: url, concealed: false, present: { c in
present(c, nil) present(c, nil)
}, openResolved: { [weak self] resolved in }, openResolved: { [weak self] resolved in
context.sharedContext.openResolvedUrl(resolved, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, openPeer: { peerId, navigation in context.sharedContext.openResolvedUrl(resolved, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, openPeer: { peerId, navigation in

View File

@ -789,7 +789,7 @@ public extension ContainedViewLayoutTransition {
} }
} }
func updateAlpha(layer: CALayer, alpha: CGFloat, completion: ((Bool) -> Void)? = nil) { func updateAlpha(layer: CALayer, alpha: CGFloat, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) {
if layer.opacity.isEqual(to: Float(alpha)) { if layer.opacity.isEqual(to: Float(alpha)) {
if let completion = completion { if let completion = completion {
completion(true) completion(true)
@ -804,7 +804,12 @@ public extension ContainedViewLayoutTransition {
completion(true) completion(true)
} }
case let .animated(duration, curve): case let .animated(duration, curve):
let previousAlpha = layer.opacity let previousAlpha: Float
if beginWithCurrentState, let presentation = layer.presentation() {
previousAlpha = presentation.opacity
} else {
previousAlpha = layer.opacity
}
layer.opacity = Float(alpha) layer.opacity = Float(alpha)
layer.animateAlpha(from: CGFloat(previousAlpha), to: alpha, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in layer.animateAlpha(from: CGFloat(previousAlpha), to: alpha, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in
if let completion = completion { if let completion = completion {

View File

@ -6,33 +6,53 @@ import AccountContext
import OverlayStatusController import OverlayStatusController
import UrlWhitelist import UrlWhitelist
public func openUserGeneratedUrl(context: AccountContext, peerId: PeerId?, url: String, concealed: Bool, skipUrlAuth: Bool = false, skipConcealedAlert: Bool = false, present: @escaping (ViewController) -> Void, openResolved: @escaping (ResolvedUrl) -> Void) { public func openUserGeneratedUrl(context: AccountContext, peerId: PeerId?, url: String, concealed: Bool, skipUrlAuth: Bool = false, skipConcealedAlert: Bool = false, present: @escaping (ViewController) -> Void, openResolved: @escaping (ResolvedUrl) -> Void, progress: Promise<Bool>? = nil) -> Disposable {
var concealed = concealed var concealed = concealed
let presentationData = context.sharedContext.currentPresentationData.with { $0 } let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let openImpl: () -> Void = { let openImpl: () -> Disposable = {
let disposable = MetaDisposable() let disposable = MetaDisposable()
var cancelImpl: (() -> Void)? var cancelImpl: (() -> Void)?
let progressSignal = Signal<Never, NoError> { subscriber in let progressSignal: Signal<Never, NoError>
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
cancelImpl?() if let progress {
})) progressSignal = Signal<Never, NoError> { subscriber in
present(controller) progress.set(.single(true))
return ActionDisposable { [weak controller] in return ActionDisposable {
Queue.mainQueue().async() { progress.set(.single(false))
controller?.dismiss()
} }
} }
|> runOn(Queue.mainQueue())
|> delay(0.05, queue: Queue.mainQueue())
} else {
progressSignal = Signal<Never, NoError> { subscriber in
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
cancelImpl?()
}))
present(controller)
return ActionDisposable { [weak controller] in
Queue.mainQueue().async() {
controller?.dismiss()
}
}
}
|> runOn(Queue.mainQueue())
|> delay(0.1, queue: Queue.mainQueue())
} }
|> runOn(Queue.mainQueue())
|> delay(0.15, queue: Queue.mainQueue())
let progressDisposable = progressSignal.start() let progressDisposable = progressSignal.start()
cancelImpl = { cancelImpl = {
disposable.dispose() disposable.dispose()
} }
disposable.set((context.sharedContext.resolveUrl(context: context, peerId: peerId, url: url, skipUrlAuth: skipUrlAuth)
var resolveSignal: Signal<ResolvedUrl, NoError>
resolveSignal = context.sharedContext.resolveUrl(context: context, peerId: peerId, url: url, skipUrlAuth: skipUrlAuth)
#if DEBUG
resolveSignal = resolveSignal |> delay(2.0, queue: .mainQueue())
#endif
disposable.set((resolveSignal
|> afterDisposed { |> afterDisposed {
Queue.mainQueue().async { Queue.mainQueue().async {
progressDisposable.dispose() progressDisposable.dispose()
@ -42,6 +62,10 @@ public func openUserGeneratedUrl(context: AccountContext, peerId: PeerId?, url:
progressDisposable.dispose() progressDisposable.dispose()
openResolved(result) openResolved(result)
})) }))
return ActionDisposable {
cancelImpl?()
}
} }
let (parsedString, parsedConcealed) = parseUrl(url: url, wasConcealed: concealed) let (parsedString, parsedConcealed) = parseUrl(url: url, wasConcealed: concealed)
@ -55,10 +79,12 @@ public func openUserGeneratedUrl(context: AccountContext, peerId: PeerId?, url:
} }
var displayUrl = rawDisplayUrl var displayUrl = rawDisplayUrl
displayUrl = displayUrl.replacingOccurrences(of: "\u{202e}", with: "") displayUrl = displayUrl.replacingOccurrences(of: "\u{202e}", with: "")
let disposable = MetaDisposable()
present(textAlertController(context: context, title: nil, text: presentationData.strings.Generic_OpenHiddenLinkAlert(displayUrl).string, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_No, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Yes, action: { present(textAlertController(context: context, title: nil, text: presentationData.strings.Generic_OpenHiddenLinkAlert(displayUrl).string, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_No, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Yes, action: {
openImpl() disposable.set(openImpl())
})])) })]))
return disposable
} else { } else {
openImpl() return openImpl()
} }
} }

View File

@ -39,6 +39,7 @@ swift_library(
"//submodules/TelegramUI/Components/WallpaperPreviewMedia", "//submodules/TelegramUI/Components/WallpaperPreviewMedia",
"//submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentButtonNode", "//submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentButtonNode",
"//submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode", "//submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/MessageInlineBlockBackgroundView",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -28,6 +28,7 @@ import ChatMessageInteractiveFileNode
import ChatMessageInteractiveMediaNode import ChatMessageInteractiveMediaNode
import WallpaperPreviewMedia import WallpaperPreviewMedia
import ChatMessageAttachedContentButtonNode import ChatMessageAttachedContentButtonNode
import MessageInlineBlockBackgroundView
public enum ChatMessageAttachedContentActionIcon { public enum ChatMessageAttachedContentActionIcon {
case instant case instant
@ -52,8 +53,7 @@ public struct ChatMessageAttachedContentNodeMediaFlags: OptionSet {
} }
public final class ChatMessageAttachedContentNode: ASDisplayNode { public final class ChatMessageAttachedContentNode: ASDisplayNode {
private var backgroundView: UIImageView? private var backgroundView: MessageInlineBlockBackgroundView?
private var lineDashView: UIImageView?
private let transformContainer: ASDisplayNode private let transformContainer: ASDisplayNode
private var title: TextNodeWithEntities? private var title: TextNodeWithEntities?
@ -84,6 +84,8 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
public var activateAction: (() -> Void)? public var activateAction: (() -> Void)?
public var requestUpdateLayout: (() -> Void)? public var requestUpdateLayout: (() -> Void)?
private var currentProgressDisposable: Disposable?
public var defaultContentAction: () -> ChatMessageBubbleContentTapAction = { return ChatMessageBubbleContentTapAction(content: .none) } public var defaultContentAction: () -> ChatMessageBubbleContentTapAction = { return ChatMessageBubbleContentTapAction(content: .none) }
public var visibility: ListViewItemNodeVisibility = .none { public var visibility: ListViewItemNodeVisibility = .none {
@ -125,6 +127,20 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
public typealias AsyncLayout = (_ presentationData: ChatPresentationData, _ automaticDownloadSettings: MediaAutoDownloadSettings, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ context: AccountContext, _ controllerInteraction: ChatControllerInteraction, _ message: Message, _ messageRead: Bool, _ chatLocation: ChatLocation, _ title: String?, _ subtitle: NSAttributedString?, _ text: String?, _ entities: [MessageTextEntity]?, _ media: (Media, ChatMessageAttachedContentNodeMediaFlags)?, _ mediaBadge: String?, _ actionIcon: ChatMessageAttachedContentActionIcon?, _ actionTitle: String?, _ displayLine: Bool, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ constrainedSize: CGSize, _ animationCache: AnimationCache, _ animationRenderer: MultiAnimationRenderer) -> (CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) public typealias AsyncLayout = (_ presentationData: ChatPresentationData, _ automaticDownloadSettings: MediaAutoDownloadSettings, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ context: AccountContext, _ controllerInteraction: ChatControllerInteraction, _ message: Message, _ messageRead: Bool, _ chatLocation: ChatLocation, _ title: String?, _ subtitle: NSAttributedString?, _ text: String?, _ entities: [MessageTextEntity]?, _ media: (Media, ChatMessageAttachedContentNodeMediaFlags)?, _ mediaBadge: String?, _ actionIcon: ChatMessageAttachedContentActionIcon?, _ actionTitle: String?, _ displayLine: Bool, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ constrainedSize: CGSize, _ animationCache: AnimationCache, _ animationRenderer: MultiAnimationRenderer) -> (CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void)))
public func makeProgress() -> Promise<Bool> {
let progress = Promise<Bool>()
self.currentProgressDisposable?.dispose()
self.currentProgressDisposable = (progress.get()
|> distinctUntilChanged
|> deliverOnMainQueue).start(next: { [weak self] hasProgress in
guard let self else {
return
}
self.backgroundView?.displayProgress = hasProgress
})
return progress
}
public func asyncLayout() -> AsyncLayout { public func asyncLayout() -> AsyncLayout {
let makeTitleLayout = TextNodeWithEntities.asyncLayout(self.title) let makeTitleLayout = TextNodeWithEntities.asyncLayout(self.title)
let makeSubtitleLayout = TextNodeWithEntities.asyncLayout(self.subtitle) let makeSubtitleLayout = TextNodeWithEntities.asyncLayout(self.subtitle)
@ -167,31 +183,12 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
if !incoming { if !incoming {
mainColor = messageTheme.accentTextColor mainColor = messageTheme.accentTextColor
if let _ = author?.nameColor?.dashColors.1 { if let _ = author?.nameColor?.dashColors.1 {
secondaryColor = messageTheme.accentTextColor secondaryColor = .clear
} }
} else { } else {
var authorNameColor: UIColor? var authorNameColor: UIColor?
// if [Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel].contains(message.id.peerId.namespace), author?.id.namespace == Namespaces.Peer.CloudUser { authorNameColor = author?.nameColor?.color
authorNameColor = author?.nameColor?.color secondaryColor = author?.nameColor?.dashColors.1
secondaryColor = author?.nameColor?.dashColors.1
// if let rawAuthorNameColor = authorNameColor {
// var dimColors = false
// switch presentationData.theme.theme.name {
// case .builtin(.nightAccent), .builtin(.night):
// dimColors = true
// default:
// break
// }
// if dimColors {
// var hue: CGFloat = 0.0
// var saturation: CGFloat = 0.0
// var brightness: CGFloat = 0.0
// rawAuthorNameColor.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: nil)
// authorNameColor = UIColor(hue: hue, saturation: saturation * 0.7, brightness: min(1.0, brightness * 1.2), alpha: 1.0)
// }
// }
// }
if let authorNameColor { if let authorNameColor {
mainColor = authorNameColor mainColor = authorNameColor
@ -768,39 +765,18 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
if displayLine { if displayLine {
let backgroundFrame = CGRect(origin: CGPoint(x: backgroundInsets.left, y: backgroundInsets.top), size: CGSize(width: actualSize.width - backgroundInsets.left - backgroundInsets.right, height: actualSize.height - backgroundInsets.top - backgroundInsets.bottom)) let backgroundFrame = CGRect(origin: CGPoint(x: backgroundInsets.left, y: backgroundInsets.top), size: CGSize(width: actualSize.width - backgroundInsets.left - backgroundInsets.right, height: actualSize.height - backgroundInsets.top - backgroundInsets.bottom))
let backgroundView: UIImageView let backgroundView: MessageInlineBlockBackgroundView
if let current = self.backgroundView { if let current = self.backgroundView {
backgroundView = current backgroundView = current
animation.animator.updateFrame(layer: backgroundView.layer, frame: backgroundFrame, completion: nil) animation.animator.updateFrame(layer: backgroundView.layer, frame: backgroundFrame, completion: nil)
} else { } else {
backgroundView = UIImageView() backgroundView = MessageInlineBlockBackgroundView()
backgroundView.image = PresentationResourcesChat.chatReplyBackgroundTemplateImage(presentationData.theme.theme, dashedOutgoing: !incoming && secondaryColor != nil)
self.backgroundView = backgroundView self.backgroundView = backgroundView
backgroundView.frame = backgroundFrame backgroundView.frame = backgroundFrame
self.transformContainer.view.insertSubview(backgroundView, at: 0) self.transformContainer.view.insertSubview(backgroundView, at: 0)
} }
backgroundView.tintColor = mainColor backgroundView.update(size: backgroundFrame.size, primaryColor: mainColor, secondaryColor: secondaryColor, pattern: nil, animation: animation)
if let secondaryColor {
let lineDashView: UIImageView
if let current = self.lineDashView {
lineDashView = current
} else {
lineDashView = UIImageView(image: PresentationResourcesChat.chatReplyLineDashTemplateImage(presentationData.theme.theme, incoming: incoming))
lineDashView.clipsToBounds = true
self.lineDashView = lineDashView
self.transformContainer.view.insertSubview(lineDashView, aboveSubview: backgroundView)
}
lineDashView.tintColor = secondaryColor
lineDashView.frame = CGRect(origin: backgroundFrame.origin, size: CGSize(width: 12.0, height: backgroundFrame.height))
lineDashView.layer.cornerRadius = 6.0
} else {
if let lineDashView = self.lineDashView {
self.lineDashView = nil
lineDashView.removeFromSuperview()
}
}
} else { } else {
if let backgroundView = self.backgroundView { if let backgroundView = self.backgroundView {
self.backgroundView = nil self.backgroundView = nil

View File

@ -3858,8 +3858,15 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
if let item = self.item { if let item = self.item {
for attribute in item.message.attributes { for attribute in item.message.attributes {
if let attribute = attribute as? ReplyMessageAttribute { if let attribute = attribute as? ReplyMessageAttribute {
return .action({ return .action({ [weak self] in
item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.quote?.text)) guard let self else {
return
}
var progress: Promise<Bool>?
if let replyInfoNode = self.replyInfoNode {
progress = replyInfoNode.makeProgress()
}
item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.quote?.text, progress: progress))
}) })
} else if let attribute = attribute as? ReplyStoryAttribute { } else if let attribute = attribute as? ReplyStoryAttribute {
return .action({ return .action({

View File

@ -130,6 +130,8 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
private var previousMediaReference: AnyMediaReference? private var previousMediaReference: AnyMediaReference?
private var expiredStoryIconView: UIImageView? private var expiredStoryIconView: UIImageView?
private var currentProgressDisposable: Disposable?
override public init() { override public init() {
self.backgroundView = MessageInlineBlockBackgroundView(frame: CGRect()) self.backgroundView = MessageInlineBlockBackgroundView(frame: CGRect())
@ -144,6 +146,24 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
self.addSubnode(self.contentNode) self.addSubnode(self.contentNode)
} }
deinit {
self.currentProgressDisposable?.dispose()
}
public func makeProgress() -> Promise<Bool> {
let progress = Promise<Bool>()
self.currentProgressDisposable?.dispose()
self.currentProgressDisposable = (progress.get()
|> distinctUntilChanged
|> deliverOnMainQueue).start(next: { [weak self] hasProgress in
guard let self else {
return
}
self.backgroundView.displayProgress = hasProgress
})
return progress
}
public static func asyncLayout(_ maybeNode: ChatMessageReplyInfoNode?) -> (_ arguments: Arguments) -> (CGSize, (CGSize, Bool, ListViewItemUpdateAnimation) -> ChatMessageReplyInfoNode) { public static func asyncLayout(_ maybeNode: ChatMessageReplyInfoNode?) -> (_ arguments: Arguments) -> (CGSize, (CGSize, Bool, ListViewItemUpdateAnimation) -> ChatMessageReplyInfoNode) {
let titleNodeLayout = TextNode.asyncLayout(maybeNode?.titleNode) let titleNodeLayout = TextNode.asyncLayout(maybeNode?.titleNode)
let textNodeLayout = TextNodeWithEntities.asyncLayout(maybeNode?.textNode) let textNodeLayout = TextNodeWithEntities.asyncLayout(maybeNode?.textNode)

View File

@ -100,7 +100,7 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent
if item.message.text.contains(webpage.url) { if item.message.text.contains(webpage.url) {
isConcealed = false isConcealed = false
} }
item.controllerInteraction.openUrl(ChatControllerInteraction.OpenUrl(url: webpage.url, concealed: isConcealed)) item.controllerInteraction.openUrl(ChatControllerInteraction.OpenUrl(url: webpage.url, concealed: isConcealed, progress: strongSelf.contentNode.makeProgress()))
} }
} }
} }
@ -119,7 +119,12 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent
if item.message.text.contains(content.url) { if item.message.text.contains(content.url) {
isConcealed = false isConcealed = false
} }
return ChatMessageBubbleContentTapAction(content: .url(ChatMessageBubbleContentTapAction.Url(url: content.url, concealed: isConcealed, allowInlineWebpageResolution: true))) return ChatMessageBubbleContentTapAction(content: .url(ChatMessageBubbleContentTapAction.Url(url: content.url, concealed: isConcealed, allowInlineWebpageResolution: true)), activate: { [weak self] in
guard let self else {
return nil
}
return self.contentNode.makeProgress()
})
} }
} }

View File

@ -28,7 +28,7 @@ private func addRoundedRectPath(context: CGContext, rect: CGRect, radius: CGFloa
context.restoreGState() context.restoreGState()
} }
private func generateTemplateImage(isMonochrome: Bool) -> UIImage { private func generateBackgroundTemplateImage(addStripe: Bool) -> UIImage {
return generateImage(CGSize(width: radius * 2.0 + 4.0, height: radius * 2.0 + 8.0), rotatedContext: { size, context in return generateImage(CGSize(width: radius * 2.0 + 4.0, height: radius * 2.0 + 8.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size)) context.clear(CGRect(origin: CGPoint(), size: size))
@ -38,34 +38,56 @@ private func generateTemplateImage(isMonochrome: Bool) -> UIImage {
context.setFillColor(UIColor.white.withMultipliedAlpha(0.1).cgColor) context.setFillColor(UIColor.white.withMultipliedAlpha(0.1).cgColor)
context.fill(CGRect(origin: CGPoint(), size: size)) context.fill(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor.white.withAlphaComponent(isMonochrome ? 0.2 : 1.0).cgColor) if addStripe {
context.fill(CGRect(origin: CGPoint(), size: CGSize(width: lineWidth, height: size.height))) context.setFillColor(UIColor.white.withMultipliedAlpha(0.2).cgColor)
context.fill(CGRect(origin: CGPoint(), size: CGSize(width: lineWidth, height: size.height)))
}
})!.stretchableImage(withLeftCapWidth: Int(radius) + 2, topCapHeight: Int(radius) + 3).withRenderingMode(.alwaysTemplate) })!.stretchableImage(withLeftCapWidth: Int(radius) + 2, topCapHeight: Int(radius) + 3).withRenderingMode(.alwaysTemplate)
} }
private let plainTemplateImage: UIImage = { private func generateProgressTemplateImage() -> UIImage {
return generateTemplateImage(isMonochrome: false)
}()
private let monochromePatternTemplateImage: UIImage = {
return generateTemplateImage(isMonochrome: true)
}()
private func generateDashBackgroundTemplateImage() -> UIImage {
return generateImage(CGSize(width: radius * 2.0 + 4.0, height: radius * 2.0 + 8.0), rotatedContext: { size, context in return generateImage(CGSize(width: radius * 2.0 + 4.0, height: radius * 2.0 + 8.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size)) context.clear(CGRect(origin: CGPoint(), size: size))
context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: radius).cgPath) addRoundedRectPath(context: context, rect: CGRect(origin: CGPoint(), size: size), radius: radius)
context.clip() context.clip()
context.setFillColor(UIColor.white.withMultipliedAlpha(0.1).cgColor) context.setFillColor(UIColor.white.withMultipliedAlpha(0.4).cgColor)
context.fill(CGRect(origin: CGPoint(), size: size)) context.fill(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor.white.withAlphaComponent(0.2).cgColor) context.setFillColor(UIColor.white.withMultipliedAlpha(0.7).cgColor)
context.fill(CGRect(origin: CGPoint(), size: CGSize(width: lineWidth, height: size.height))) context.fill(CGRect(origin: CGPoint(), size: CGSize(width: lineWidth, height: size.height)))
context.resetClip()
let borderWidth: CGFloat = 1.5
addRoundedRectPath(context: context, rect: CGRect(origin: CGPoint(), size: size).insetBy(dx: borderWidth * 0.5, dy: borderWidth * 0.5), radius: radius)
context.setStrokeColor(UIColor.white.withAlphaComponent(0.7).cgColor)
context.strokePath()
})!.stretchableImage(withLeftCapWidth: Int(radius) + 2, topCapHeight: Int(radius) + 3).withRenderingMode(.alwaysTemplate) })!.stretchableImage(withLeftCapWidth: Int(radius) + 2, topCapHeight: Int(radius) + 3).withRenderingMode(.alwaysTemplate)
} }
private let backgroundSolidTemplateImage: UIImage = {
return generateBackgroundTemplateImage(addStripe: true)
}()
private let backgroundDashTemplateImage: UIImage = {
return generateBackgroundTemplateImage(addStripe: false)
}()
private func generateDashBackgroundTemplateImage() -> UIImage {
return generateImage(CGSize(width: lineWidth, height: radius * 2.0 + 8.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: CGSize(width: radius * 2.0, height: size.height)), cornerRadius: radius).cgPath)
context.clip()
context.setFillColor(UIColor.white.withAlphaComponent(1.0).cgColor)
context.fill(CGRect(origin: CGPoint(), size: CGSize(width: lineWidth, height: size.height)))
})!.stretchableImage(withLeftCapWidth: 0, topCapHeight: Int(radius) + 3).withRenderingMode(.alwaysTemplate)
}
private let dashBackgroundTemplateImage: UIImage = { private let dashBackgroundTemplateImage: UIImage = {
return generateDashBackgroundTemplateImage() return generateDashBackgroundTemplateImage()
}() }()
@ -102,6 +124,35 @@ private let dashMonochromeTemplateImage: UIImage = {
return generateDashTemplateImage(isMonochrome: true) return generateDashTemplateImage(isMonochrome: true)
}() }()
private func generateGradient(gradientWidth: CGFloat, baseAlpha: CGFloat) -> UIImage {
return generateImage(CGSize(width: gradientWidth, height: 16.0), opaque: false, scale: 1.0, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
let foregroundColor = UIColor(white: 1.0, alpha: min(1.0, baseAlpha * 4.0))
if let shadowImage = UIImage(named: "Stories/PanelGradient") {
UIGraphicsPushContext(context)
for i in 0 ..< 2 {
let shadowFrame = CGRect(origin: CGPoint(x: CGFloat(i) * (size.width * 0.5), y: 0.0), size: CGSize(width: size.width * 0.5, height: size.height))
context.saveGState()
context.translateBy(x: shadowFrame.midX, y: shadowFrame.midY)
context.rotate(by: CGFloat(i == 0 ? 1.0 : -1.0) * CGFloat.pi * 0.5)
let adjustedRect = CGRect(origin: CGPoint(x: -shadowFrame.height * 0.5, y: -shadowFrame.width * 0.5), size: CGSize(width: shadowFrame.height, height: shadowFrame.width))
context.clip(to: adjustedRect, mask: shadowImage.cgImage!)
context.setFillColor(foregroundColor.cgColor)
context.fill(adjustedRect)
context.restoreGState()
}
UIGraphicsPopContext()
}
})!.withRenderingMode(.alwaysTemplate)
}
private final class PatternContentsTarget: MultiAnimationRenderTarget { private final class PatternContentsTarget: MultiAnimationRenderTarget {
private let imageUpdated: () -> Void private let imageUpdated: () -> Void
@ -121,6 +172,165 @@ private final class PatternContentsTarget: MultiAnimationRenderTarget {
} }
} }
private final class LineView: UIView {
private let backgroundView: UIImageView
private var dashBackgroundView: UIImageView?
private var params: Params?
private var isAnimating: Bool = false
private struct Params: Equatable {
var size: CGSize
var primaryColor: UIColor
var secondaryColor: UIColor?
var displayProgress: Bool
init(size: CGSize, primaryColor: UIColor, secondaryColor: UIColor?, displayProgress: Bool) {
self.size = size
self.primaryColor = primaryColor
self.secondaryColor = secondaryColor
self.displayProgress = displayProgress
}
}
override init(frame: CGRect) {
self.backgroundView = UIImageView()
self.backgroundView.image = dashBackgroundTemplateImage
super.init(frame: frame)
self.layer.cornerRadius = radius
if #available(iOS 13.0, *) {
self.layer.cornerCurve = .circular
}
self.addSubview(self.backgroundView)
}
required init(coder: NSCoder) {
preconditionFailure()
}
func updateAnimations() {
guard let params = self.params else {
return
}
if params.displayProgress {
if let dashBackgroundView = self.dashBackgroundView {
if dashBackgroundView.layer.animation(forKey: "progress") == nil {
let animation = dashBackgroundView.layer.makeAnimation(from: 18.0 as NSNumber, to: 0.0 as NSNumber, keyPath: "position.y", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.2, delay: 0.0, mediaTimingFunction: nil, removeOnCompletion: true, additive: true)
animation.repeatCount = 1.0
self.isAnimating = true
animation.completion = { [weak self] _ in
guard let self else {
return
}
self.isAnimating = false
self.updateAnimations()
}
dashBackgroundView.layer.add(animation, forKey: "progress")
}
} else {
let phaseDuration: Double = 0.8
if self.backgroundView.layer.animation(forKey: "progress") == nil {
let animation = self.backgroundView.layer.makeAnimation(from: 0.0 as NSNumber, to: -params.size.height as NSNumber, keyPath: "position.y", timingFunction: kCAMediaTimingFunctionSpring, duration: phaseDuration * 0.5, delay: 0.0, mediaTimingFunction: nil, removeOnCompletion: false, additive: true)
animation.repeatCount = 1.0
self.isAnimating = true
animation.completion = { [weak self] _ in
guard let self else {
return
}
let animation = self.backgroundView.layer.makeAnimation(from: params.size.height as NSNumber, to: 0.0 as NSNumber, keyPath: "position.y", timingFunction: kCAMediaTimingFunctionSpring, duration: phaseDuration * 0.5, delay: self.params?.displayProgress == true ? 0.1 : 0.0, mediaTimingFunction: nil, removeOnCompletion: true, additive: true)
animation.repeatCount = 1.0
self.isAnimating = true
animation.completion = { [weak self] _ in
guard let self else {
return
}
self.isAnimating = false
self.updateAnimations()
}
self.backgroundView.layer.add(animation, forKey: "progress")
}
self.backgroundView.layer.add(animation, forKey: "progress")
}
}
}
if self.isAnimating && self.dashBackgroundView == nil {
self.backgroundView.backgroundColor = params.primaryColor
self.backgroundView.layer.masksToBounds = true
self.backgroundView.layer.cornerRadius = radius * 0.5
} else {
self.backgroundView.backgroundColor = nil
self.backgroundView.layer.masksToBounds = false
}
self.layer.masksToBounds = params.secondaryColor != nil || self.isAnimating
}
func update(size: CGSize, primaryColor: UIColor, secondaryColor: UIColor?, displayProgress: Bool, animation: ListViewItemUpdateAnimation) {
let params = Params(
size: size,
primaryColor: primaryColor,
secondaryColor: secondaryColor,
displayProgress: displayProgress
)
if self.params == params {
return
}
let previousParams = self.params
self.params = params
let _ = previousParams
self.backgroundView.tintColor = primaryColor
if let secondaryColor {
let dashBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: -18.0), size: CGSize(width: radius * 2.0, height: size.height + 18.0))
let dashBackgroundView: UIImageView
if let current = self.dashBackgroundView {
dashBackgroundView = current
animation.animator.updateFrame(layer: dashBackgroundView.layer, frame: dashBackgroundFrame, completion: nil)
} else {
dashBackgroundView = UIImageView()
self.dashBackgroundView = dashBackgroundView
self.addSubview(dashBackgroundView)
dashBackgroundView.frame = dashBackgroundFrame
}
if secondaryColor.alpha == 0.0 {
self.backgroundView.alpha = 0.2
dashBackgroundView.image = dashMonochromeTemplateImage
dashBackgroundView.tintColor = primaryColor
} else {
self.backgroundView.alpha = 1.0
dashBackgroundView.image = dashOpaqueTemplateImage
dashBackgroundView.tintColor = secondaryColor
}
} else {
if let dashBackgroundView = self.dashBackgroundView {
self.dashBackgroundView = nil
dashBackgroundView.removeFromSuperview()
}
self.backgroundView.alpha = 1.0
}
self.layer.masksToBounds = params.secondaryColor != nil || self.isAnimating
animation.animator.updateFrame(layer: self.backgroundView.layer, frame: CGRect(origin: CGPoint(), size: CGSize(width: lineWidth, height: size.height)), completion: nil)
self.updateAnimations()
}
}
public final class MessageInlineBlockBackgroundView: UIView { public final class MessageInlineBlockBackgroundView: UIView {
public final class Pattern: Equatable { public final class Pattern: Equatable {
public let context: AccountContext public let context: AccountContext
@ -157,6 +367,20 @@ public final class MessageInlineBlockBackgroundView: UIView {
var secondaryColor: UIColor? var secondaryColor: UIColor?
var pattern: Pattern? var pattern: Pattern?
var displayProgress: Bool var displayProgress: Bool
init(
size: CGSize,
primaryColor: UIColor,
secondaryColor: UIColor?,
pattern: Pattern?,
displayProgress: Bool
) {
self.size = size
self.primaryColor = primaryColor
self.secondaryColor = secondaryColor
self.pattern = pattern
self.displayProgress = displayProgress
}
} }
private var params: Params? private var params: Params?
@ -170,11 +394,7 @@ public final class MessageInlineBlockBackgroundView: UIView {
primaryColor: params.primaryColor, primaryColor: params.primaryColor,
secondaryColor: params.secondaryColor, secondaryColor: params.secondaryColor,
pattern: params.pattern, pattern: params.pattern,
animation: .System(duration: 0.2, transition: ControlledTransition( animation: .None
duration: 0.2,
curve: .easeInOut,
interactive: false
))
) )
} }
} }
@ -182,7 +402,7 @@ public final class MessageInlineBlockBackgroundView: UIView {
} }
private let backgroundView: UIImageView private let backgroundView: UIImageView
private var dashView: UIImageView? private var lineView: LineView
private var hierarchyTrackingLayer: HierarchyTrackingLayer? private var hierarchyTrackingLayer: HierarchyTrackingLayer?
private var patternContentsTarget: PatternContentsTarget? private var patternContentsTarget: PatternContentsTarget?
@ -191,13 +411,19 @@ public final class MessageInlineBlockBackgroundView: UIView {
private var patternFileDisposable: Disposable? private var patternFileDisposable: Disposable?
private var patternImage: UIImage? private var patternImage: UIImage?
private var patternImageDisposable: Disposable? private var patternImageDisposable: Disposable?
private var progressBackgroundContentsView: UIImageView?
private var progressBackgroundMaskContainer: UIView?
private var progressBackgroundGradientView: UIImageView?
override public init(frame: CGRect) { override public init(frame: CGRect) {
self.backgroundView = UIImageView() self.backgroundView = UIImageView()
self.lineView = LineView()
super.init(frame: frame) super.init(frame: frame)
self.addSubview(self.backgroundView) self.addSubview(self.backgroundView)
self.addSubview(self.lineView)
} }
required public init?(coder: NSCoder) { required public init?(coder: NSCoder) {
@ -210,6 +436,27 @@ public final class MessageInlineBlockBackgroundView: UIView {
} }
private func updateAnimations() { private func updateAnimations() {
guard let hierarchyTrackingLayer = self.hierarchyTrackingLayer, hierarchyTrackingLayer.isInHierarchy else {
return
}
guard let params = self.params else {
return
}
guard let progressBackgroundGradientView = self.progressBackgroundGradientView else {
return
}
let gradientWidth = progressBackgroundGradientView.bounds.width
if progressBackgroundGradientView.layer.animation(forKey: "shimmer") != nil {
return
}
let duration: Double = 1.0
let animation = progressBackgroundGradientView.layer.makeAnimation(from: 0.0 as NSNumber, to: (params.size.width + gradientWidth + params.size.width * 0.1) as NSNumber, keyPath: "position.x", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: duration, delay: 0.0, mediaTimingFunction: nil, removeOnCompletion: true, additive: true)
animation.repeatCount = Float.infinity
progressBackgroundGradientView.layer.add(animation, forKey: "shimmer")
self.lineView.updateAnimations()
} }
private func loadPatternFromFile() { private func loadPatternFromFile() {
@ -276,37 +523,12 @@ public final class MessageInlineBlockBackgroundView: UIView {
patternContentLayer.layerTintColor = primaryColor.cgColor patternContentLayer.layerTintColor = primaryColor.cgColor
} }
if let secondaryColor = params.secondaryColor { if params.secondaryColor != nil {
self.backgroundView.tintColor = params.primaryColor self.backgroundView.image = backgroundDashTemplateImage
if self.dashView == nil {
let dashView = UIImageView()
dashView.layer.cornerRadius = radius
if #available(iOS 13.0, *) {
dashView.layer.cornerCurve = .circular
}
self.dashView = dashView
self.addSubview(dashView)
}
if secondaryColor.alpha == 0.0 {
self.backgroundView.image = monochromePatternTemplateImage
self.dashView?.image = dashMonochromeTemplateImage
self.dashView?.tintColor = primaryColor
} else {
self.backgroundView.image = plainTemplateImage
self.dashView?.image = dashOpaqueTemplateImage
self.dashView?.tintColor = secondaryColor
}
} else { } else {
self.backgroundView.image = plainTemplateImage self.backgroundView.image = backgroundSolidTemplateImage
self.backgroundView.tintColor = params.primaryColor
if let dashView = self.dashView {
self.dashView = dashView
dashView.removeFromSuperview()
}
} }
self.backgroundView.tintColor = params.primaryColor
} }
if previousParams?.pattern != params.pattern { if previousParams?.pattern != params.pattern {
@ -359,15 +581,19 @@ public final class MessageInlineBlockBackgroundView: UIView {
} }
} }
self.dashView?.layer.masksToBounds = params.pattern == nil && params.secondaryColor != nil
animation.animator.updateFrame(layer: self.backgroundView.layer, frame: CGRect(origin: CGPoint(), size: size), completion: nil) animation.animator.updateFrame(layer: self.backgroundView.layer, frame: CGRect(origin: CGPoint(), size: size), completion: nil)
if let dashView = self.dashView {
animation.animator.updateFrame(layer: dashView.layer, frame: CGRect(origin: CGPoint(), size: CGSize(width: radius * 2.0, height: size.height)), completion: nil) let lineFrame = CGRect(origin: CGPoint(), size: CGSize(width: radius * 2.0, height: size.height))
} self.lineView.update(
size: lineFrame.size,
primaryColor: params.primaryColor,
secondaryColor: params.secondaryColor,
displayProgress: params.displayProgress,
animation: animation
)
animation.animator.updateFrame(layer: lineView.layer, frame: lineFrame, completion: nil)
if params.pattern != nil { if params.pattern != nil {
var maxIndex = 0 var maxIndex = 0
struct Placement { struct Placement {
@ -425,6 +651,79 @@ public final class MessageInlineBlockBackgroundView: UIView {
self.patternContentLayers.removeAll() self.patternContentLayers.removeAll()
} }
let gradientWidth: CGFloat = min(300.0, max(200.0, size.width * 0.9))
if previousParams?.displayProgress != params.displayProgress {
if params.displayProgress {
let progressBackgroundContentsView: UIImageView
if let current = self.progressBackgroundContentsView {
progressBackgroundContentsView = current
} else {
progressBackgroundContentsView = UIImageView()
progressBackgroundContentsView.image = generateProgressTemplateImage()
self.insertSubview(progressBackgroundContentsView, aboveSubview: self.backgroundView)
progressBackgroundContentsView.tintColor = primaryColor
}
let progressBackgroundMaskContainer: UIView
if let current = self.progressBackgroundMaskContainer {
progressBackgroundMaskContainer = current
} else {
progressBackgroundMaskContainer = UIView()
self.progressBackgroundMaskContainer = progressBackgroundMaskContainer
progressBackgroundContentsView.mask = progressBackgroundMaskContainer
}
let progressBackgroundGradientView: UIImageView
if let current = self.progressBackgroundGradientView {
progressBackgroundGradientView = current
} else {
progressBackgroundGradientView = UIImageView()
self.progressBackgroundGradientView = progressBackgroundGradientView
progressBackgroundMaskContainer.addSubview(progressBackgroundGradientView)
progressBackgroundGradientView.image = generateGradient(gradientWidth: 100.0, baseAlpha: 0.5)
}
progressBackgroundContentsView.frame = CGRect(origin: CGPoint(), size: size)
progressBackgroundMaskContainer.frame = CGRect(origin: CGPoint(), size: size)
progressBackgroundGradientView.frame = CGRect(origin: CGPoint(x: -gradientWidth, y: 0.0), size: CGSize(width: gradientWidth, height: size.height))
if self.hierarchyTrackingLayer == nil {
let hierarchyTrackingLayer = HierarchyTrackingLayer()
self.hierarchyTrackingLayer = hierarchyTrackingLayer
self.layer.addSublayer(hierarchyTrackingLayer)
hierarchyTrackingLayer.isInHierarchyUpdated = { [weak self] _ in
self?.updateAnimations()
}
}
} else {
if let progressBackgroundContentsView = self.progressBackgroundContentsView {
self.progressBackgroundContentsView = nil
let transition = ContainedViewLayoutTransition.animated(duration: 0.15, curve: .easeInOut)
transition.updateAlpha(layer: progressBackgroundContentsView.layer, alpha: 0.0)
}
self.progressBackgroundMaskContainer = nil
self.progressBackgroundGradientView = nil
if let hierarchyTrackingLayer = self.hierarchyTrackingLayer {
self.hierarchyTrackingLayer = nil
hierarchyTrackingLayer.isInHierarchyUpdated = nil
hierarchyTrackingLayer.removeFromSuperlayer()
}
}
} else {
if let progressBackgroundContentsView = self.progressBackgroundContentsView {
animation.animator.updateFrame(layer: progressBackgroundContentsView.layer, frame: CGRect(origin: CGPoint(), size: size), completion: nil)
progressBackgroundContentsView.tintColor = primaryColor
}
if let progressBackgroundMaskContainer = self.progressBackgroundMaskContainer {
animation.animator.updateFrame(layer: progressBackgroundMaskContainer.layer, frame: CGRect(origin: CGPoint(), size: size), completion: nil)
}
if let progressBackgroundGradientView = self.progressBackgroundGradientView {
animation.animator.updateFrame(layer: progressBackgroundGradientView.layer, frame: CGRect(origin: CGPoint(x: -gradientWidth, y: 0.0), size: CGSize(width: gradientWidth, height: size.height)), completion: nil)
}
}
self.updateAnimations() self.updateAnimations()
} }
} }

View File

@ -76,10 +76,12 @@ public protocol ChatMessageTransitionProtocol: ASDisplayNode {
public struct NavigateToMessageParams { public struct NavigateToMessageParams {
public var timestamp: Double? public var timestamp: Double?
public var quote: String? public var quote: String?
public var progress: Promise<Bool>?
public init(timestamp: Double?, quote: String?) { public init(timestamp: Double?, quote: String?, progress: Promise<Bool>? = nil) {
self.timestamp = timestamp self.timestamp = timestamp
self.quote = quote self.quote = quote
self.progress = progress
} }
} }

View File

@ -4092,7 +4092,7 @@ public final class StoryItemSetContainerComponent: Component {
} }
switch action { switch action {
case let .url(url, concealed): case let .url(url, concealed):
openUserGeneratedUrl(context: component.context, peerId: component.slice.peer.id, url: url, concealed: concealed, skipUrlAuth: false, skipConcealedAlert: false, present: { [weak self] c in let _ = openUserGeneratedUrl(context: component.context, peerId: component.slice.peer.id, url: url, concealed: concealed, skipUrlAuth: false, skipConcealedAlert: false, present: { [weak self] c in
guard let self, let component = self.component, let controller = component.controller() else { guard let self, let component = self.component, let controller = component.controller() else {
return return
} }
@ -4120,7 +4120,7 @@ public final class StoryItemSetContainerComponent: Component {
return return
} }
self.sendMessageContext.presentTextEntityActions(view: self, action: action, openUrl: { [weak self] url, concealed in self.sendMessageContext.presentTextEntityActions(view: self, action: action, openUrl: { [weak self] url, concealed in
openUserGeneratedUrl(context: component.context, peerId: component.slice.peer.id, url: url, concealed: concealed, skipUrlAuth: false, skipConcealedAlert: false, present: { [weak self] c in let _ = openUserGeneratedUrl(context: component.context, peerId: component.slice.peer.id, url: url, concealed: concealed, skipUrlAuth: false, skipConcealedAlert: false, present: { [weak self] c in
guard let self, let component = self.component, let controller = component.controller() else { guard let self, let component = self.component, let controller = component.controller() else {
return return
} }

View File

@ -823,25 +823,19 @@ private func chatLinkOptions(selfController: ChatControllerImpl, sourceNode: ASD
return ContextController.Items(id: AnyHashable(linkOptions.url), content: .list(items)) return ContextController.Items(id: AnyHashable(linkOptions.url), content: .list(items))
} }
var webpageCache: [String: TelegramMediaWebpage] = [:]
chatController.performOpenURL = { [weak selfController] message, url, progress in chatController.performOpenURL = { [weak selfController] message, url, progress in
guard let selfController else { guard let selfController else {
return return
} }
if let (updatedUrlPreviewUrl, signal) = urlPreviewStateForInputText(NSAttributedString(string: url), context: selfController.context, currentQuery: nil), let updatedUrlPreviewUrl { if let (updatedUrlPreviewUrl, signal) = urlPreviewStateForInputText(NSAttributedString(string: url), context: selfController.context, currentQuery: nil), let updatedUrlPreviewUrl {
progress?.set(.single(true)) if let webpage = webpageCache[updatedUrlPreviewUrl] {
let _ = (signal
|> afterDisposed {
progress?.set(.single(false)) progress?.set(.single(false))
}
|> deliverOnMainQueue).start(next: { [weak selfController] result in
guard let selfController else {
return
}
selfController.updateChatPresentationInterfaceState(animated: true, interactive: false, { state in selfController.updateChatPresentationInterfaceState(animated: true, interactive: false, { state in
if state.interfaceState.editMessage != nil { if state.interfaceState.editMessage != nil {
if let webpage = result(nil), var urlPreview = state.editingUrlPreview { if var urlPreview = state.editingUrlPreview {
urlPreview.url = updatedUrlPreviewUrl urlPreview.url = updatedUrlPreviewUrl
urlPreview.webPage = webpage urlPreview.webPage = webpage
@ -850,7 +844,7 @@ private func chatLinkOptions(selfController: ChatControllerImpl, sourceNode: ASD
return state return state
} }
} else { } else {
if let webpage = result(nil), var urlPreview = state.urlPreview { if var urlPreview = state.urlPreview {
urlPreview.url = updatedUrlPreviewUrl urlPreview.url = updatedUrlPreviewUrl
urlPreview.webPage = webpage urlPreview.webPage = webpage
@ -860,7 +854,42 @@ private func chatLinkOptions(selfController: ChatControllerImpl, sourceNode: ASD
} }
} }
}) })
}) } else {
progress?.set(.single(true))
let _ = (signal
|> afterDisposed {
progress?.set(.single(false))
}
|> deliverOnMainQueue).start(next: { [weak selfController] result in
guard let selfController else {
return
}
selfController.updateChatPresentationInterfaceState(animated: true, interactive: false, { state in
if state.interfaceState.editMessage != nil {
if let webpage = result(nil), var urlPreview = state.editingUrlPreview {
urlPreview.url = updatedUrlPreviewUrl
urlPreview.webPage = webpage
webpageCache[updatedUrlPreviewUrl] = webpage
return state.updatedEditingUrlPreview(urlPreview)
} else {
return state
}
} else {
if let webpage = result(nil), var urlPreview = state.urlPreview {
urlPreview.url = updatedUrlPreviewUrl
urlPreview.webPage = webpage
webpageCache[updatedUrlPreviewUrl] = webpage
return state.updatedUrlPreview(urlPreview)
} else {
return state
}
}
})
})
}
} }
} }

View File

@ -2762,8 +2762,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if let performOpenURL = strongSelf.performOpenURL { if let performOpenURL = strongSelf.performOpenURL {
performOpenURL(message, url, progress) performOpenURL(message, url, progress)
} else { } else {
progress?.set(.single(false)) strongSelf.openUrl(url, concealed: concealed, skipConcealedAlert: skipConcealedAlert, message: message, allowInlineWebpageResolution: urlData.allowInlineWebpageResolution, progress: progress)
strongSelf.openUrl(url, concealed: concealed, skipConcealedAlert: skipConcealedAlert, message: message, allowInlineWebpageResolution: urlData.allowInlineWebpageResolution)
} }
} }
}, shareCurrentLocation: { [weak self] in }, shareCurrentLocation: { [weak self] in
@ -16895,7 +16894,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if case let .id(messageId, params) = messageLocation, params.timestamp != nil { if case let .id(messageId, params) = messageLocation, params.timestamp != nil {
self.scheduledScrollToMessageId = (messageId, params) self.scheduledScrollToMessageId = (messageId, params)
} }
var progress: Promise<Bool>?
if case let .id(_, params) = messageLocation {
progress = params.progress
}
self.loadingMessage.set(.single(statusSubject) |> delay(0.1, queue: .mainQueue())) self.loadingMessage.set(.single(statusSubject) |> delay(0.1, queue: .mainQueue()))
let searchLocation: ChatHistoryInitialSearchLocation let searchLocation: ChatHistoryInitialSearchLocation
switch messageLocation { switch messageLocation {
case let .id(id, _): case let .id(id, _):
@ -16909,9 +16913,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
searchLocation = .index(.absoluteUpperBound()) searchLocation = .index(.absoluteUpperBound())
} }
} }
let historyView = preloadedChatHistoryViewForLocation(ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: searchLocation, quote: nil), count: 50, highlight: true), id: 0), context: self.context, chatLocation: self.chatLocation, subject: self.subject, chatLocationContextHolder: self.chatLocationContextHolder, fixedCombinedReadStates: nil, tagMask: nil, additionalData: []) var historyView: Signal<ChatHistoryViewUpdate, NoError>
historyView = preloadedChatHistoryViewForLocation(ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: searchLocation, quote: nil), count: 50, highlight: true), id: 0), context: self.context, chatLocation: self.chatLocation, subject: self.subject, chatLocationContextHolder: self.chatLocationContextHolder, fixedCombinedReadStates: nil, tagMask: nil, additionalData: [])
let signal = historyView var signal: Signal<(MessageIndex?, Bool), NoError>
signal = historyView
|> mapToSignal { historyView -> Signal<(MessageIndex?, Bool), NoError> in |> mapToSignal { historyView -> Signal<(MessageIndex?, Bool), NoError> in
switch historyView { switch historyView {
case .Loading: case .Loading:
@ -16932,11 +16938,22 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return SignalTakeAction(passthrough: true, complete: !index.1) return SignalTakeAction(passthrough: true, complete: !index.1)
}) })
#if DEBUG
signal = .single((nil, true)) |> then(signal |> delay(2.0, queue: .mainQueue()))
#endif
var cancelImpl: (() -> Void)? var cancelImpl: (() -> Void)?
let presentationData = self.presentationData let presentationData = self.presentationData
let displayTime = CACurrentMediaTime() let displayTime = CACurrentMediaTime()
let progressSignal = Signal<Never, NoError> { [weak self] subscriber in let progressSignal = Signal<Never, NoError> { [weak self] subscriber in
if case .generic = statusSubject { if let progress {
progress.set(.single(true))
return ActionDisposable {
Queue.mainQueue().async() {
progress.set(.single(false))
}
}
} else if case .generic = statusSubject {
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
if CACurrentMediaTime() - displayTime > 1.5 { if CACurrentMediaTime() - displayTime > 1.5 {
cancelImpl?() cancelImpl?()
@ -17013,21 +17030,23 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} }
let historyView = preloadedChatHistoryViewForLocation(ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: searchLocation, quote: quote), count: 50, highlight: true), id: 0), context: self.context, chatLocation: self.chatLocation, subject: self.subject, chatLocationContextHolder: self.chatLocationContextHolder, fixedCombinedReadStates: nil, tagMask: nil, additionalData: []) let historyView = preloadedChatHistoryViewForLocation(ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: searchLocation, quote: quote), count: 50, highlight: true), id: 0), context: self.context, chatLocation: self.chatLocation, subject: self.subject, chatLocationContextHolder: self.chatLocationContextHolder, fixedCombinedReadStates: nil, tagMask: nil, additionalData: [])
let signal = historyView var signal: Signal<MessageIndex?, NoError>
|> mapToSignal { historyView -> Signal<MessageIndex?, NoError> in signal = historyView
switch historyView { |> mapToSignal { historyView -> Signal<MessageIndex?, NoError> in
case .Loading: switch historyView {
return .complete() case .Loading:
case let .HistoryView(view, _, _, _, _, _, _): return .complete()
for entry in view.entries { case let .HistoryView(view, _, _, _, _, _, _):
if entry.message.id == messageLocation.messageId { for entry in view.entries {
return .single(entry.message.index) if entry.message.id == messageLocation.messageId {
} return .single(entry.message.index)
} }
return .single(nil) }
} return .single(nil)
} }
|> take(1) }
|> take(1)
self.messageIndexDisposable.set((signal |> deliverOnMainQueue).startStrict(next: { [weak self] index in self.messageIndexDisposable.set((signal |> deliverOnMainQueue).startStrict(next: { [weak self] index in
if let strongSelf = self { if let strongSelf = self {
if let index = index { if let index = index {
@ -17995,7 +18014,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}, contentContext: nil) }, contentContext: nil)
} }
func openUrl(_ url: String, concealed: Bool, forceExternal: Bool = false, skipUrlAuth: Bool = false, skipConcealedAlert: Bool = false, message: Message? = nil, allowInlineWebpageResolution: Bool = false, commit: @escaping () -> Void = {}) { func openUrl(_ url: String, concealed: Bool, forceExternal: Bool = false, skipUrlAuth: Bool = false, skipConcealedAlert: Bool = false, message: Message? = nil, allowInlineWebpageResolution: Bool = false, progress: Promise<Bool>? = nil, commit: @escaping () -> Void = {}) {
self.commitPurposefulAction() self.commitPurposefulAction()
if allowInlineWebpageResolution, let message, let webpage = message.media.first(where: { $0 is TelegramMediaWebpage }) as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content, content.url == url { if allowInlineWebpageResolution, let message, let webpage = message.media.first(where: { $0 is TelegramMediaWebpage }) as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content, content.url == url {
@ -18005,22 +18024,28 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
case .album: case .album:
break break
default: default:
progress?.set(.single(false))
self.context.sharedContext.openChatInstantPage(context: self.context, message: message, sourcePeerType: nil, navigationController: navigationController) self.context.sharedContext.openChatInstantPage(context: self.context, message: message, sourcePeerType: nil, navigationController: navigationController)
return return
} }
} }
} else if let embedUrl = content.embedUrl, !embedUrl.isEmpty { } else if let embedUrl = content.embedUrl, !embedUrl.isEmpty {
progress?.set(.single(false))
let _ = self.controllerInteraction?.openMessage(message, .default) let _ = self.controllerInteraction?.openMessage(message, .default)
return return
} }
} }
let _ = self.presentVoiceMessageDiscardAlert(action: { let _ = self.presentVoiceMessageDiscardAlert(action: { [weak self] in
openUserGeneratedUrl(context: self.context, peerId: self.peerView?.peerId, url: url, concealed: concealed, skipUrlAuth: skipUrlAuth, skipConcealedAlert: skipConcealedAlert, present: { [weak self] c in guard let self else {
return
}
let disposable = openUserGeneratedUrl(context: self.context, peerId: self.peerView?.peerId, url: url, concealed: concealed, skipUrlAuth: skipUrlAuth, skipConcealedAlert: skipConcealedAlert, present: { [weak self] c in
self?.present(c, in: .window(.root)) self?.present(c, in: .window(.root))
}, openResolved: { [weak self] resolved in }, openResolved: { [weak self] resolved in
self?.openResolved(result: resolved, sourceMessageId: message?.id, forceExternal: forceExternal, concealed: concealed, commit: commit) self?.openResolved(result: resolved, sourceMessageId: message?.id, forceExternal: forceExternal, concealed: concealed, commit: commit)
}) }, progress: progress)
self.navigationActionDisposable.set(disposable)
}, performAction: true) }, performAction: true)
} }

View File

@ -4516,7 +4516,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
} }
private func openUrl(url: String, concealed: Bool, external: Bool, forceExternal: Bool = false, commit: @escaping () -> Void = {}) { private func openUrl(url: String, concealed: Bool, external: Bool, forceExternal: Bool = false, commit: @escaping () -> Void = {}) {
openUserGeneratedUrl(context: self.context, peerId: self.peerId, url: url, concealed: concealed, present: { [weak self] c in let _ = openUserGeneratedUrl(context: self.context, peerId: self.peerId, url: url, concealed: concealed, present: { [weak self] c in
self?.controller?.present(c, in: .window(.root)) self?.controller?.present(c, in: .window(.root))
}, openResolved: { [weak self] tempResolved in }, openResolved: { [weak self] tempResolved in
guard let strongSelf = self else { guard let strongSelf = self else {