mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2026-01-04 04:05:00 +00:00
Fixed Apple Pay
Added ability to download music without streaming Added progress indicators for various blocking tasks Fixed image gallery swipe to dismiss after zooming Added online member count indication in supergroups Fixed contact statuses in contact search
This commit is contained in:
@@ -25,6 +25,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
|
||||
private var iconNode: TransformImageNode?
|
||||
private var statusNode: RadialStatusNode?
|
||||
private var streamingStatusNode: RadialStatusNode?
|
||||
private var tapRecognizer: UITapGestureRecognizer?
|
||||
|
||||
private let statusDisposable = MetaDisposable()
|
||||
@@ -35,11 +36,16 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
private let fetchDisposable = MetaDisposable()
|
||||
|
||||
var activateLocalContent: () -> Void = { }
|
||||
var requestUpdateLayout: (Bool) -> Void = { _ in }
|
||||
|
||||
private var account: Account?
|
||||
private var message: Message?
|
||||
private var themeAndStrings: (ChatPresentationThemeData, PresentationStrings)?
|
||||
private var file: TelegramMediaFile?
|
||||
private var progressFrame: CGRect?
|
||||
private var streamingCacheStatusFrame: CGRect?
|
||||
private var fileIconImage: UIImage?
|
||||
private var cloudFetchIconImage: UIImage?
|
||||
|
||||
override init() {
|
||||
self.titleNode = TextNode()
|
||||
@@ -77,9 +83,27 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
self.tapRecognizer = tapRecognizer
|
||||
}
|
||||
|
||||
@objc func cacheProgressPressed() {
|
||||
guard let resourceStatus = self.resourceStatus else {
|
||||
return
|
||||
}
|
||||
switch resourceStatus.fetchStatus {
|
||||
case .Fetching:
|
||||
if let cancel = self.fetchControls.with({ return $0?.cancel }) {
|
||||
cancel()
|
||||
}
|
||||
case .Remote:
|
||||
if let fetch = self.fetchControls.with({ return $0?.fetch }) {
|
||||
fetch()
|
||||
}
|
||||
case .Local:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@objc func progressPressed() {
|
||||
if let resourceStatus = self.resourceStatus {
|
||||
switch resourceStatus {
|
||||
switch resourceStatus.mediaStatus {
|
||||
case let .fetchStatus(fetchStatus):
|
||||
if let account = self.account, let message = self.message, message.flags.isSending {
|
||||
let _ = account.postbox.transaction({ transaction -> Void in
|
||||
@@ -109,7 +133,11 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
|
||||
@objc func fileTap(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
self.progressPressed()
|
||||
if let streamingCacheStatusFrame = self.streamingCacheStatusFrame, streamingCacheStatusFrame.contains(recognizer.location(in: self.view)) {
|
||||
self.cacheProgressPressed()
|
||||
} else {
|
||||
self.progressPressed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,6 +150,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
|
||||
let currentMessage = self.message
|
||||
let currentTheme = self.themeAndStrings?.0
|
||||
let currentResourceStatus = self.resourceStatus
|
||||
|
||||
return { account, presentationData, message, file, automaticDownload, incoming, isRecentActions, dateAndStatusType, constrainedSize in
|
||||
var updatedTheme: ChatPresentationThemeData?
|
||||
@@ -224,13 +253,14 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
if case let .Audio(voice, duration, title, performer, waveform) = attribute {
|
||||
isAudio = true
|
||||
if let currentUpdatedStatusSignal = updatedStatusSignal {
|
||||
updatedStatusSignal = currentUpdatedStatusSignal |> map { status in
|
||||
switch status {
|
||||
updatedStatusSignal = currentUpdatedStatusSignal
|
||||
|> map { status in
|
||||
switch status.mediaStatus {
|
||||
case let .fetchStatus(fetchStatus):
|
||||
if !voice {
|
||||
return .fetchStatus(.Local)
|
||||
return FileMediaResourceStatus(mediaStatus: .fetchStatus(.Local), fetchStatus: status.fetchStatus)
|
||||
} else {
|
||||
return .fetchStatus(fetchStatus)
|
||||
return FileMediaResourceStatus(mediaStatus: .fetchStatus(fetchStatus), fetchStatus: status.fetchStatus)
|
||||
}
|
||||
case .playbackStatus:
|
||||
return status
|
||||
@@ -289,6 +319,23 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
textConstrainedSize.width -= 80.0
|
||||
}
|
||||
|
||||
let streamingProgressDiameter: CGFloat = 28.0
|
||||
var hasStreamingProgress = false
|
||||
if isAudio && !isVoice {
|
||||
if let resourceStatus = currentResourceStatus {
|
||||
switch resourceStatus.fetchStatus {
|
||||
case .Fetching, .Remote:
|
||||
hasStreamingProgress = true
|
||||
case .Local:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if hasStreamingProgress {
|
||||
textConstrainedSize.width -= streamingProgressDiameter + 4.0
|
||||
}
|
||||
}
|
||||
|
||||
let (titleLayout, titleApply) = titleAsyncLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
let (descriptionLayout, descriptionApply) = descriptionAsyncLayout(TextNodeLayoutArguments(attributedString: descriptionString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
@@ -311,6 +358,12 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
minLayoutWidth = max(minLayoutWidth, statusSize.width)
|
||||
}
|
||||
|
||||
var cloudFetchIconImage: UIImage?
|
||||
if hasStreamingProgress {
|
||||
minLayoutWidth += streamingProgressDiameter + 4.0
|
||||
cloudFetchIconImage = incoming ? PresentationResourcesChat.chatBubbleFileCloudFetchIncomingIcon(presentationData.theme.theme) : PresentationResourcesChat.chatBubbleFileCloudFetchOutgoingIcon(presentationData.theme.theme)
|
||||
}
|
||||
|
||||
let fileIconImage: UIImage?
|
||||
if hasThumbnail {
|
||||
fileIconImage = nil
|
||||
@@ -325,6 +378,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
|
||||
var iconFrame: CGRect?
|
||||
let progressFrame: CGRect
|
||||
let streamingCacheStatusFrame: CGRect
|
||||
let controlAreaWidth: CGFloat
|
||||
|
||||
if hasThumbnail {
|
||||
@@ -362,7 +416,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
fittedLayoutSize = CGSize(width: minLayoutWidth, height: 27.0)
|
||||
} else {
|
||||
let unionSize = titleFrame.union(descriptionFrame).union(progressFrame).size
|
||||
fittedLayoutSize = CGSize(width: unionSize.width, height: unionSize.height + 4.0)
|
||||
fittedLayoutSize = CGSize(width: unionSize.width, height: unionSize.height + 6.0)
|
||||
}
|
||||
|
||||
var statusFrame: CGRect?
|
||||
@@ -376,6 +430,15 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
statusFrame = statusFrameValue.offsetBy(dx: 0.0, dy: 10.0)
|
||||
}
|
||||
|
||||
if isAudio && !isVoice {
|
||||
streamingCacheStatusFrame = CGRect(origin: CGPoint(x: fittedLayoutSize.width + 6.0, y: 4.0), size: CGSize(width: streamingProgressDiameter, height: streamingProgressDiameter))
|
||||
if hasStreamingProgress {
|
||||
fittedLayoutSize.width += streamingProgressDiameter + 6.0
|
||||
}
|
||||
} else {
|
||||
streamingCacheStatusFrame = CGRect()
|
||||
}
|
||||
|
||||
return (fittedLayoutSize, { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.account = account
|
||||
@@ -468,78 +531,27 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
strongSelf.statusDisposable.set((updatedStatusSignal |> deliverOnMainQueue).start(next: { [weak strongSelf] status in
|
||||
displayLinkDispatcher.dispatch {
|
||||
if let strongSelf = strongSelf {
|
||||
var previousHadCacheStatus = false
|
||||
if let resourceStatus = strongSelf.resourceStatus {
|
||||
switch resourceStatus.fetchStatus {
|
||||
case .Fetching, .Remote:
|
||||
previousHadCacheStatus = true
|
||||
case .Local:
|
||||
previousHadCacheStatus = false
|
||||
}
|
||||
}
|
||||
var hasCacheStatus = false
|
||||
switch status.fetchStatus {
|
||||
case .Fetching, .Remote:
|
||||
hasCacheStatus = true
|
||||
case .Local:
|
||||
hasCacheStatus = false
|
||||
}
|
||||
strongSelf.resourceStatus = status
|
||||
|
||||
if strongSelf.statusNode == nil {
|
||||
let backgroundNodeColor: UIColor
|
||||
if strongSelf.iconNode != nil {
|
||||
backgroundNodeColor = bubbleTheme.mediaOverlayControlBackgroundColor
|
||||
} else if incoming {
|
||||
backgroundNodeColor = bubbleTheme.incomingMediaActiveControlColor
|
||||
} else {
|
||||
backgroundNodeColor = bubbleTheme.outgoingMediaActiveControlColor
|
||||
}
|
||||
let statusNode = RadialStatusNode(backgroundNodeColor: backgroundNodeColor)
|
||||
strongSelf.statusNode = statusNode
|
||||
statusNode.frame = progressFrame
|
||||
strongSelf.addSubnode(statusNode)
|
||||
} else if let _ = updatedTheme {
|
||||
//strongSelf.progressNode?.updateTheme(RadialProgressTheme(backgroundColor: incoming ? bubbleTheme.incomingAccentColor : bubbleTheme.outgoingAccentColor, foregroundColor: incoming ? bubbleTheme.incomingFillColor : bubbleTheme.outgoingFillColor, icon: fileIconImage))
|
||||
}
|
||||
|
||||
let state: RadialStatusNodeState
|
||||
let statusForegroundColor: UIColor
|
||||
if strongSelf.iconNode != nil {
|
||||
statusForegroundColor = bubbleTheme.mediaOverlayControlForegroundColor
|
||||
} else if incoming {
|
||||
statusForegroundColor = presentationData.theme.wallpaper.isEmpty ? bubbleTheme.incoming.withoutWallpaper.fill : bubbleTheme.incoming.withWallpaper.fill
|
||||
if isAudio && !isVoice && previousHadCacheStatus != hasCacheStatus {
|
||||
strongSelf.requestUpdateLayout(false)
|
||||
} else {
|
||||
statusForegroundColor = presentationData.theme.wallpaper.isEmpty ? bubbleTheme.outgoing.withoutWallpaper.fill : bubbleTheme.outgoing.withWallpaper.fill
|
||||
}
|
||||
switch status {
|
||||
case let .fetchStatus(fetchStatus):
|
||||
strongSelf.waveformScrubbingNode?.enableScrubbing = false
|
||||
switch fetchStatus {
|
||||
case let .Fetching(isActive, progress):
|
||||
var adjustedProgress = progress
|
||||
if isActive {
|
||||
adjustedProgress = max(adjustedProgress, 0.027)
|
||||
}
|
||||
state = .progress(color: statusForegroundColor, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true)
|
||||
case .Local:
|
||||
if isAudio {
|
||||
state = .play(statusForegroundColor)
|
||||
} else if let fileIconImage = fileIconImage {
|
||||
state = .customIcon(fileIconImage)
|
||||
} else {
|
||||
state = .none
|
||||
}
|
||||
case .Remote:
|
||||
if isAudio && !isVoice {
|
||||
state = .play(statusForegroundColor)
|
||||
} else {
|
||||
state = .download(statusForegroundColor)
|
||||
}
|
||||
}
|
||||
case let .playbackStatus(playbackStatus):
|
||||
strongSelf.waveformScrubbingNode?.enableScrubbing = true
|
||||
switch playbackStatus {
|
||||
case .playing:
|
||||
state = .pause(statusForegroundColor)
|
||||
case .paused:
|
||||
state = .play(statusForegroundColor)
|
||||
}
|
||||
}
|
||||
|
||||
if let statusNode = strongSelf.statusNode {
|
||||
if state == .none {
|
||||
strongSelf.statusNode = nil
|
||||
}
|
||||
statusNode.transitionToState(state, completion: { [weak statusNode] in
|
||||
if state == .none {
|
||||
statusNode?.removeFromSupernode()
|
||||
}
|
||||
})
|
||||
strongSelf.updateStatus()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -551,6 +563,10 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
strongSelf.statusNode?.frame = progressFrame
|
||||
strongSelf.progressFrame = progressFrame
|
||||
strongSelf.streamingCacheStatusFrame = streamingCacheStatusFrame
|
||||
strongSelf.fileIconImage = fileIconImage
|
||||
strongSelf.cloudFetchIconImage = cloudFetchIconImage
|
||||
|
||||
if let updatedFetchControls = updatedFetchControls {
|
||||
let _ = strongSelf.fetchControls.swap(updatedFetchControls)
|
||||
@@ -558,6 +574,8 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
updatedFetchControls.fetch()
|
||||
}
|
||||
}
|
||||
|
||||
strongSelf.updateStatus()
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -565,6 +583,158 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
private func updateStatus() {
|
||||
guard let resourceStatus = self.resourceStatus else {
|
||||
return
|
||||
}
|
||||
guard let message = self.message else {
|
||||
return
|
||||
}
|
||||
guard let account = self.account else {
|
||||
return
|
||||
}
|
||||
guard let presentationData = self.themeAndStrings?.0 else {
|
||||
return
|
||||
}
|
||||
guard let progressFrame = self.progressFrame, let streamingCacheStatusFrame = self.streamingCacheStatusFrame else {
|
||||
return
|
||||
}
|
||||
guard let file = self.file else {
|
||||
return
|
||||
}
|
||||
let incoming = message.effectivelyIncoming(account.peerId)
|
||||
let bubbleTheme = presentationData.theme.chat.bubble
|
||||
|
||||
var isAudio = false
|
||||
var isVoice = false
|
||||
for attribute in file.attributes {
|
||||
if case let .Audio(voice, _, _, _, _) = attribute {
|
||||
isAudio = true
|
||||
|
||||
if voice {
|
||||
isVoice = true
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
let state: RadialStatusNodeState
|
||||
var streamingState: RadialStatusNodeState = .none
|
||||
|
||||
if isAudio && !isVoice {
|
||||
let streamingStatusForegroundColor: UIColor = incoming ? bubbleTheme.incomingAccentControlColor : bubbleTheme.outgoingAccentControlColor
|
||||
let streamingStatusBackgroundColor: UIColor = incoming ? bubbleTheme.incomingMediaInactiveControlColor : bubbleTheme.outgoingMediaInactiveControlColor
|
||||
switch resourceStatus.fetchStatus {
|
||||
case let .Fetching(isActive, progress):
|
||||
var adjustedProgress = progress
|
||||
if isActive {
|
||||
adjustedProgress = max(adjustedProgress, 0.027)
|
||||
}
|
||||
streamingState = .cloudProgress(color: streamingStatusForegroundColor, strokeBackgroundColor: streamingStatusBackgroundColor, lineWidth: 2.0, value: CGFloat(adjustedProgress))
|
||||
case .Local:
|
||||
streamingState = .none
|
||||
case .Remote:
|
||||
if let cloudFetchIconImage = self.cloudFetchIconImage {
|
||||
streamingState = .customIcon(cloudFetchIconImage)
|
||||
} else {
|
||||
streamingState = .none
|
||||
}
|
||||
}
|
||||
} else {
|
||||
streamingState = .none
|
||||
}
|
||||
|
||||
let statusForegroundColor: UIColor
|
||||
if self.iconNode != nil {
|
||||
statusForegroundColor = bubbleTheme.mediaOverlayControlForegroundColor
|
||||
} else if incoming {
|
||||
statusForegroundColor = presentationData.wallpaper.isEmpty ? bubbleTheme.incoming.withoutWallpaper.fill : bubbleTheme.incoming.withWallpaper.fill
|
||||
} else {
|
||||
statusForegroundColor = presentationData.wallpaper.isEmpty ? bubbleTheme.outgoing.withoutWallpaper.fill : bubbleTheme.outgoing.withWallpaper.fill
|
||||
}
|
||||
switch resourceStatus.mediaStatus {
|
||||
case let .fetchStatus(fetchStatus):
|
||||
self.waveformScrubbingNode?.enableScrubbing = false
|
||||
switch fetchStatus {
|
||||
case let .Fetching(isActive, progress):
|
||||
var adjustedProgress = progress
|
||||
if isActive {
|
||||
adjustedProgress = max(adjustedProgress, 0.027)
|
||||
}
|
||||
state = .progress(color: statusForegroundColor, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true)
|
||||
case .Local:
|
||||
if isAudio {
|
||||
state = .play(statusForegroundColor)
|
||||
} else if let fileIconImage = self.fileIconImage {
|
||||
state = .customIcon(fileIconImage)
|
||||
} else {
|
||||
state = .none
|
||||
}
|
||||
case .Remote:
|
||||
if isAudio && !isVoice {
|
||||
state = .play(statusForegroundColor)
|
||||
} else {
|
||||
state = .download(statusForegroundColor)
|
||||
}
|
||||
}
|
||||
case let .playbackStatus(playbackStatus):
|
||||
self.waveformScrubbingNode?.enableScrubbing = true
|
||||
switch playbackStatus {
|
||||
case .playing:
|
||||
state = .pause(statusForegroundColor)
|
||||
case .paused:
|
||||
state = .play(statusForegroundColor)
|
||||
}
|
||||
}
|
||||
|
||||
if state != .none && self.statusNode == nil {
|
||||
let backgroundNodeColor: UIColor
|
||||
if self.iconNode != nil {
|
||||
backgroundNodeColor = bubbleTheme.mediaOverlayControlBackgroundColor
|
||||
} else if incoming {
|
||||
backgroundNodeColor = bubbleTheme.incomingMediaActiveControlColor
|
||||
} else {
|
||||
backgroundNodeColor = bubbleTheme.outgoingMediaActiveControlColor
|
||||
}
|
||||
let statusNode = RadialStatusNode(backgroundNodeColor: backgroundNodeColor)
|
||||
self.statusNode = statusNode
|
||||
statusNode.frame = progressFrame
|
||||
self.addSubnode(statusNode)
|
||||
}
|
||||
|
||||
if streamingState != .none && self.streamingStatusNode == nil {
|
||||
let streamingStatusNode = RadialStatusNode(backgroundNodeColor: .clear)
|
||||
self.streamingStatusNode = streamingStatusNode
|
||||
streamingStatusNode.frame = streamingCacheStatusFrame
|
||||
self.addSubnode(streamingStatusNode)
|
||||
}
|
||||
|
||||
if let statusNode = self.statusNode {
|
||||
if state == .none {
|
||||
self.statusNode = nil
|
||||
}
|
||||
statusNode.transitionToState(state, completion: { [weak statusNode] in
|
||||
if state == .none {
|
||||
statusNode?.removeFromSupernode()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if let streamingStatusNode = self.streamingStatusNode {
|
||||
if streamingState == .none {
|
||||
self.streamingStatusNode = nil
|
||||
streamingStatusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak streamingStatusNode] _ in
|
||||
if streamingState == .none {
|
||||
streamingStatusNode?.removeFromSupernode()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
streamingStatusNode.transitionToState(streamingState, completion: {
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func asyncLayout(_ node: ChatMessageInteractiveFileNode?) -> (_ account: Account, _ presentationData: ChatPresentationData, _ message: Message, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ isRecentActions: Bool, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> ChatMessageInteractiveFileNode))) {
|
||||
let currentAsyncLayout = node?.asyncLayout()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user