Merge commit 'b6e2e462641f50d2494f1bfd827f86158863b87f'

This commit is contained in:
Peter Iakovlev 2019-02-08 17:56:35 +04:00
commit 94b5a8fa56
35 changed files with 411 additions and 229 deletions

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "Muted@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "Muted@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -99,8 +99,8 @@ public struct AutomaticMediaDownloadSettings: PreferencesEntry, Equatable {
public static var defaultSettings: AutomaticMediaDownloadSettings { public static var defaultSettings: AutomaticMediaDownloadSettings {
let defaultCategory = AutomaticMediaDownloadCategories( let defaultCategory = AutomaticMediaDownloadCategories(
photo: AutomaticMediaDownloadCategory(cellular: true, wifi: true, sizeLimit: 1 * 1024 * 1024), photo: AutomaticMediaDownloadCategory(cellular: true, wifi: true, sizeLimit: Int32.max),
video: AutomaticMediaDownloadCategory(cellular: false, wifi: false, sizeLimit: 1 * 1024 * 1024), video: AutomaticMediaDownloadCategory(cellular: true, wifi: true, sizeLimit: 10 * 1024 * 1024),
file: AutomaticMediaDownloadCategory(cellular: false, wifi: false, sizeLimit: 1 * 1024 * 1024), file: AutomaticMediaDownloadCategory(cellular: false, wifi: false, sizeLimit: 1 * 1024 * 1024),
voiceMessage: AutomaticMediaDownloadCategory(cellular: true, wifi: true, sizeLimit: 1 * 1024 * 1024), voiceMessage: AutomaticMediaDownloadCategory(cellular: true, wifi: true, sizeLimit: 1 * 1024 * 1024),
videoMessage: AutomaticMediaDownloadCategory(cellular: true, wifi: true, sizeLimit: 4 * 1024 * 1024), videoMessage: AutomaticMediaDownloadCategory(cellular: true, wifi: true, sizeLimit: 4 * 1024 * 1024),

View File

@ -172,7 +172,7 @@ public final class CallController: ViewController {
Queue.mainQueue().after(0.5, { Queue.mainQueue().after(0.5, {
let window = strongSelf.window let window = strongSelf.window
let controller = callRatingController(sharedContext: strongSelf.sharedContext, account: strongSelf.account, callId: callId, present: { [weak self] c, a in let controller = callRatingController(sharedContext: strongSelf.sharedContext, account: strongSelf.account, callId: callId, present: { c, a in
if let window = window { if let window = window {
c.presentationArguments = a c.presentationArguments = a
window.present(c, on: .root, blockInteraction: false, completion: {}) window.present(c, on: .root, blockInteraction: false, completion: {})

View File

@ -147,7 +147,7 @@ private enum CallFeedbackControllerEntry: ItemListNodeEntry {
case let .reasonsHeader(theme, text): case let .reasonsHeader(theme, text):
return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
case let .reason(theme, reason, title, value): case let .reason(theme, reason, title, value):
return ItemListSwitchItem(theme: theme, title: title, value: value, sectionId: self.section, style: .blocks, updated: { value in return ItemListSwitchItem(theme: theme, title: title, value: value, maximumNumberOfLines: 2, sectionId: self.section, style: .blocks, updated: { value in
arguments.toggleReason(reason, value) arguments.toggleReason(reason, value)
}) })
case let .comment(theme, text, placeholder): case let .comment(theme, text, placeholder):

View File

@ -5,6 +5,7 @@ import SwiftSignalKit
final class ChatBubbleVideoDecoration: UniversalVideoDecoration { final class ChatBubbleVideoDecoration: UniversalVideoDecoration {
private let nativeSize: CGSize private let nativeSize: CGSize
private let contentMode: InteractiveMediaNodeContentMode
let backgroundNode: ASDisplayNode? = nil let backgroundNode: ASDisplayNode? = nil
let contentContainerNode: ASDisplayNode let contentContainerNode: ASDisplayNode
@ -14,11 +15,12 @@ final class ChatBubbleVideoDecoration: UniversalVideoDecoration {
private var validLayoutSize: CGSize? private var validLayoutSize: CGSize?
init(cornerRadius: CGFloat, nativeSize: CGSize, backgroudColor: UIColor) { init(cornerRadius: CGFloat, nativeSize: CGSize, contentMode: InteractiveMediaNodeContentMode, backgroundColor: UIColor) {
self.nativeSize = nativeSize self.nativeSize = nativeSize
self.contentMode = contentMode
self.contentContainerNode = ASDisplayNode() self.contentContainerNode = ASDisplayNode()
self.contentContainerNode.backgroundColor = backgroudColor self.contentContainerNode.backgroundColor = backgroundColor
self.contentContainerNode.clipsToBounds = true self.contentContainerNode.clipsToBounds = true
self.contentContainerNode.cornerRadius = cornerRadius self.contentContainerNode.cornerRadius = cornerRadius
} }
@ -38,7 +40,13 @@ final class ChatBubbleVideoDecoration: UniversalVideoDecoration {
if contentNode.supernode !== self.contentContainerNode { if contentNode.supernode !== self.contentContainerNode {
self.contentContainerNode.addSubnode(contentNode) self.contentContainerNode.addSubnode(contentNode)
if let size = self.validLayoutSize { if let size = self.validLayoutSize {
var scaledSize = self.nativeSize.aspectFitted(size) var scaledSize: CGSize
switch self.contentMode {
case .aspectFit:
scaledSize = self.nativeSize.aspectFitted(size)
case .aspectFill:
scaledSize = self.nativeSize.aspectFilled(size)
}
if abs(scaledSize.width - size.width) < 2.0 { if abs(scaledSize.width - size.width) < 2.0 {
scaledSize.width = size.width scaledSize.width = size.width
} }
@ -68,7 +76,13 @@ final class ChatBubbleVideoDecoration: UniversalVideoDecoration {
} }
transition.updateFrame(node: self.contentContainerNode, frame: CGRect(origin: CGPoint(), size: size)) transition.updateFrame(node: self.contentContainerNode, frame: CGRect(origin: CGPoint(), size: size))
if let contentNode = self.contentNode { if let contentNode = self.contentNode {
var scaledSize = self.nativeSize.aspectFitted(size) var scaledSize: CGSize
switch self.contentMode {
case .aspectFit:
scaledSize = self.nativeSize.aspectFitted(size)
case .aspectFill:
scaledSize = self.nativeSize.aspectFilled(size)
}
if abs(scaledSize.width - size.width) < 2.0 { if abs(scaledSize.width - size.width) < 2.0 {
scaledSize.width = size.width scaledSize.width = size.width
} }

View File

@ -386,12 +386,12 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
contentInstantVideoSizeAndApply = (videoLayout, apply) contentInstantVideoSizeAndApply = (videoLayout, apply)
} else if file.isVideo { } else if file.isVideo {
let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, media: file) let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, media: file)
let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData.theme.theme, presentationData.strings, message, file, automaticDownload, associatedData.automaticDownloadPeerType, automaticDownloadSettings.autoplayGifs, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode) let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData.theme.theme, presentationData.strings, message, file, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, automaticDownloadSettings.autoplayGifs, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode)
initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right
refineContentImageLayout = refineLayout refineContentImageLayout = refineLayout
} else if file.isSticker, let _ = file.dimensions { } else if file.isSticker, let _ = file.dimensions {
let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, media: file) let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, media: file)
let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData.theme.theme, presentationData.strings, message, file, automaticDownload, associatedData.automaticDownloadPeerType, automaticDownloadSettings.autoplayGifs, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode) let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData.theme.theme, presentationData.strings, message, file, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, automaticDownloadSettings.autoplayGifs, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode)
initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right
refineContentImageLayout = refineLayout refineContentImageLayout = refineLayout
} else { } else {
@ -416,7 +416,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
} else if let image = media as? TelegramMediaImage { } else if let image = media as? TelegramMediaImage {
if !flags.contains(.preferMediaInline) { if !flags.contains(.preferMediaInline) {
let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, media: image) let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, media: image)
let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData.theme.theme, presentationData.strings, message, image, automaticDownload, associatedData.automaticDownloadPeerType, automaticDownloadSettings.autoplayGifs, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode) let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData.theme.theme, presentationData.strings, message, image, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, automaticDownloadSettings.autoplayGifs, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode)
initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right
refineContentImageLayout = refineLayout refineContentImageLayout = refineLayout
} else if let dimensions = largestImageRepresentation(image.representations)?.dimensions { } else if let dimensions = largestImageRepresentation(image.representations)?.dimensions {
@ -428,11 +428,11 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
} }
} else if let image = media as? TelegramMediaWebFile { } else if let image = media as? TelegramMediaWebFile {
let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, media: image) let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, media: image)
let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData.theme.theme, presentationData.strings, message, image, automaticDownload, associatedData.automaticDownloadPeerType, automaticDownloadSettings.autoplayGifs, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode) let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData.theme.theme, presentationData.strings, message, image, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, automaticDownloadSettings.autoplayGifs, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode)
initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right
refineContentImageLayout = refineLayout refineContentImageLayout = refineLayout
} else if let wallpaper = media as? WallpaperPreviewMedia { } else if let wallpaper = media as? WallpaperPreviewMedia {
let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData.theme.theme, presentationData.strings, message, wallpaper, true, associatedData.automaticDownloadPeerType, automaticDownloadSettings.autoplayGifs, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode) let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData.theme.theme, presentationData.strings, message, wallpaper, .full, associatedData.automaticDownloadPeerType, automaticDownloadSettings.autoplayGifs, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode)
initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right
refineContentImageLayout = refineLayout refineContentImageLayout = refineLayout
} }

View File

@ -373,6 +373,13 @@ class ChatMessageBubbleItemNode: ChatMessageItemView {
} }
} }
var isInlinePlayableVideo = false
for media in item.content.firstMessage.media {
if let file = media as? TelegramMediaFile, file.isVideo, !file.isAnimated, isMediaStreamable(message: item.content.firstMessage, media: file) {
isInlinePlayableVideo = true
}
}
if hasAvatar { if hasAvatar {
avatarInset = layoutConstants.avatarDiameter avatarInset = layoutConstants.avatarDiameter
} else { } else {
@ -436,7 +443,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView {
tmpWidth -= 38.0 tmpWidth -= 38.0
} }
} else { } else {
tmpWidth = layoutConstants.bubble.maximumWidthFill.widthFor(baseWidth) tmpWidth = isInlinePlayableVideo ? baseWidth - 36.0 : layoutConstants.bubble.maximumWidthFill.widthFor(baseWidth)
if needShareButton && tmpWidth + 32.0 > baseWidth { if needShareButton && tmpWidth + 32.0 > baseWidth {
tmpWidth = baseWidth - 32.0 tmpWidth = baseWidth - 32.0
} }

View File

@ -9,14 +9,14 @@ enum ChatMessageInteractiveMediaBadgeShape: Equatable {
enum ChatMessageInteractiveMediaDownloadState: Equatable { enum ChatMessageInteractiveMediaDownloadState: Equatable {
case remote case remote
case fetching(progress: Float) case fetching(progress: Float?)
case compactRemote case compactRemote
case compactFetching(progress: Float) case compactFetching(progress: Float)
} }
enum ChatMessageInteractiveMediaBadgeContent: Equatable { enum ChatMessageInteractiveMediaBadgeContent: Equatable {
case text(inset: CGFloat, backgroundColor: UIColor, foregroundColor: UIColor, shape: ChatMessageInteractiveMediaBadgeShape, text: NSAttributedString) case text(inset: CGFloat, backgroundColor: UIColor, foregroundColor: UIColor, shape: ChatMessageInteractiveMediaBadgeShape, text: NSAttributedString)
case mediaDownload(backgroundColor: UIColor, foregroundColor: UIColor, duration: String, size: String) case mediaDownload(backgroundColor: UIColor, foregroundColor: UIColor, duration: String, size: String?, muted: Bool, active: Bool)
static func ==(lhs: ChatMessageInteractiveMediaBadgeContent, rhs: ChatMessageInteractiveMediaBadgeContent) -> Bool { static func ==(lhs: ChatMessageInteractiveMediaBadgeContent, rhs: ChatMessageInteractiveMediaBadgeContent) -> Bool {
switch lhs { switch lhs {
@ -26,8 +26,8 @@ enum ChatMessageInteractiveMediaBadgeContent: Equatable {
} else { } else {
return false return false
} }
case let .mediaDownload(lhsBackgroundColor, lhsForegroundColor, lhsDuration, lhsSize): case let .mediaDownload(lhsBackgroundColor, lhsForegroundColor, lhsDuration, lhsSize, lhsMuted, lhsActive):
if case let .mediaDownload(rhsBackgroundColor, rhsForegroundColor, rhsDuration, rhsSize) = rhs, lhsBackgroundColor.isEqual(rhsBackgroundColor), lhsForegroundColor.isEqual(rhsForegroundColor), lhsDuration == rhsDuration, lhsSize == rhsSize { if case let .mediaDownload(rhsBackgroundColor, rhsForegroundColor, rhsDuration, rhsSize, rhsMuted, rhsActive) = rhs, lhsBackgroundColor.isEqual(rhsBackgroundColor), lhsForegroundColor.isEqual(rhsForegroundColor), lhsDuration == rhsDuration, lhsSize == rhsSize, lhsMuted == rhsMuted, lhsActive == rhsActive {
return true return true
} else { } else {
return false return false
@ -39,27 +39,28 @@ enum ChatMessageInteractiveMediaBadgeContent: Equatable {
private let font = Font.regular(11.0) private let font = Font.regular(11.0)
private let boldFont = Font.semibold(11.0) private let boldFont = Font.semibold(11.0)
private final class ChatMessageInteractiveMediaBadgeParams: NSObject {
let content: ChatMessageInteractiveMediaBadgeContent?
init(content: ChatMessageInteractiveMediaBadgeContent?) {
self.content = content
}
}
final class ChatMessageInteractiveMediaBadge: ASDisplayNode { final class ChatMessageInteractiveMediaBadge: ASDisplayNode {
private var content: ChatMessageInteractiveMediaBadgeContent?
var pressed: (() -> Void)? var pressed: (() -> Void)?
private var content: ChatMessageInteractiveMediaBadgeContent?
private var mediaDownloadStatusNode: RadialStatusNode?
private var mediaDownloadState: ChatMessageInteractiveMediaDownloadState? private var mediaDownloadState: ChatMessageInteractiveMediaDownloadState?
private var backgroundNodeColor: UIColor?
private var foregroundColor: UIColor?
private let backgroundNode: ASImageNode
private let durationNode: ASTextNode
private var sizeNode: ASTextNode?
private var iconNode: ASImageNode?
private var mediaDownloadStatusNode: RadialStatusNode?
override init() { override init() {
self.backgroundNode = ASImageNode()
self.durationNode = ASTextNode()
super.init() super.init()
self.contentMode = .topLeft self.addSubnode(self.backgroundNode)
self.contentsScale = UIScreenScale self.addSubnode(self.durationNode)
} }
override func didLoad() { override func didLoad() {
@ -74,16 +75,6 @@ final class ChatMessageInteractiveMediaBadge: ASDisplayNode {
} }
} }
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let contents = self.contents, CFGetTypeID(contents as CFTypeRef) == CGImage.typeID {
let image = contents as! CGImage
if CGRect(origin: CGPoint(), size: CGSize(width: CGFloat(image.width) / UIScreenScale, height: CGFloat(image.height) / UIScreenScale)).contains(point) {
return self.view
}
}
return nil
}
func update(theme: PresentationTheme, content: ChatMessageInteractiveMediaBadgeContent?, mediaDownloadState: ChatMessageInteractiveMediaDownloadState?, animated: Bool) { func update(theme: PresentationTheme, content: ChatMessageInteractiveMediaBadgeContent?, mediaDownloadState: ChatMessageInteractiveMediaDownloadState?, animated: Bool) {
if self.content != content { if self.content != content {
self.content = content self.content = content
@ -110,7 +101,11 @@ final class ChatMessageInteractiveMediaBadge: ASDisplayNode {
state = .none state = .none
} }
case let .fetching(progress): case let .fetching(progress):
state = .cloudProgress(color: .white, strokeBackgroundColor: UIColor(white: 1.0, alpha: 0.3), lineWidth: 2.0, value: CGFloat(progress)) var cloudProgress: CGFloat?
if let progress = progress {
cloudProgress = CGFloat(progress)
}
state = .cloudProgress(color: .white, strokeBackgroundColor: UIColor(white: 1.0, alpha: 0.3), lineWidth: 2.0 - UIScreenPixel, value: cloudProgress)
case .compactRemote: case .compactRemote:
state = .download(.white) state = .download(.white)
isCompact = true isCompact = true
@ -131,78 +126,97 @@ final class ChatMessageInteractiveMediaBadge: ASDisplayNode {
mediaDownloadStatusNode.removeFromSupernode() mediaDownloadStatusNode.removeFromSupernode()
} }
} }
}
var contentSize = CGSize()
override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate
return ChatMessageInteractiveMediaBadgeParams(content: self.content)
} if let content = self.content {
@objc override public class func display(withParameters: Any?, isCancelled: () -> Bool) -> UIImage? {
if let content = (withParameters as? ChatMessageInteractiveMediaBadgeParams)?.content {
switch content { switch content {
case let .text(inset, backgroundColor, foregroundColor, shape, text): case let .text(inset, backgroundColor, foregroundColor, shape, text):
if self.backgroundNodeColor != backgroundColor {
self.backgroundNodeColor = backgroundColor
self.backgroundNode.image = generateStretchableFilledCircleImage(radius: 9.0, color: backgroundColor)
}
let convertedText = NSMutableAttributedString(string: text.string, attributes: [.font: font, .foregroundColor: foregroundColor]) let convertedText = NSMutableAttributedString(string: text.string, attributes: [.font: font, .foregroundColor: foregroundColor])
text.enumerateAttributes(in: NSRange(location: 0, length: text.length), options: []) { attributes, range, _ in text.enumerateAttributes(in: NSRange(location: 0, length: text.length), options: []) { attributes, range, _ in
if let _ = attributes[ChatTextInputAttributes.bold] { if let _ = attributes[ChatTextInputAttributes.bold] {
convertedText.addAttribute(.font, value: boldFont, range: range) convertedText.addAttribute(.font, value: boldFont, range: range)
} }
} }
let textRect = convertedText.boundingRect(with: CGSize(width: 200.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil) self.durationNode.attributedText = convertedText
let imageSize = CGSize(width: inset + ceil(textRect.size.width) + 10.0, height: 18.0) let durationSize = self.durationNode.measure(CGSize(width: 160.0, height: 160.0))
return generateImage(imageSize, rotatedContext: { size, context in self.durationNode.frame = CGRect(x: 7.0, y: 2.0, width: durationSize.width, height: durationSize.height)
context.clear(CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size)) contentSize = CGSize(width: durationSize.width + 14.0, height: 18.0)
context.setBlendMode(.copy) case let .mediaDownload(backgroundColor, foregroundColor, duration, size, muted, active):
context.setFillColor(backgroundColor.cgColor) if self.backgroundNodeColor != backgroundColor {
switch shape { self.backgroundNodeColor = backgroundColor
case .round: self.backgroundNode.image = generateStretchableFilledCircleImage(radius: 9.0, color: backgroundColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.height, height: size.height))) }
context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - size.height, y: 0.0), size: CGSize(width: size.height, height: size.height)))
context.fill(CGRect(origin: CGPoint(x: size.height / 2.0, y: 0.0), size: CGSize(width: size.width - size.height, height: size.height)))
case let .corners(radius):
let diameter = radius * 2.0
context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: diameter, height: diameter)))
context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: size.height - diameter), size: CGSize(width: diameter, height: diameter)))
context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - diameter, y: 0.0), size: CGSize(width: diameter, height: diameter)))
context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - diameter, y: size.height - diameter), size: CGSize(width: diameter, height: diameter)))
context.fill(CGRect(origin: CGPoint(x: 0.0, y: radius), size: CGSize(width: diameter, height: size.height - diameter)))
context.fill(CGRect(origin: CGPoint(x: radius, y: 0.0), size: CGSize(width: size.width - diameter, height: size.height)))
context.fill(CGRect(origin: CGPoint(x: size.width - diameter, y: radius), size: CGSize(width: diameter, height: size.height - diameter)))
}
context.setBlendMode(.normal)
UIGraphicsPushContext(context)
convertedText.draw(at: CGPoint(x: inset + floor((size.width - inset - textRect.size.width) / 2.0) + textRect.origin.x, y: 2.0 + textRect.origin.y))
UIGraphicsPopContext()
})
case let .mediaDownload(backgroundColor, foregroundColor, duration, size):
let durationString = NSMutableAttributedString(string: duration, attributes: [.font: font, .foregroundColor: foregroundColor]) let durationString = NSMutableAttributedString(string: duration, attributes: [.font: font, .foregroundColor: foregroundColor])
let sizeString = NSMutableAttributedString(string: size, attributes: [.font: font, .foregroundColor: foregroundColor]) self.durationNode.attributedText = durationString
let durationRect = durationString.boundingRect(with: CGSize(width: 200.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil)
let sizeRect = sizeString.boundingRect(with: CGSize(width: 200.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil) var sizeSize: CGSize = CGSize()
let leftInset: CGFloat = 42.0 if let size = size {
let imageSize = CGSize(width: leftInset + max(ceil(durationRect.width), ceil(sizeRect.width)) + 10.0, height: 40.0) let sizeNode: ASTextNode
return generateImage(imageSize, rotatedContext: { size, context in if let current = self.sizeNode {
context.clear(CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size)) sizeNode = current
context.setBlendMode(.copy) } else {
context.setFillColor(backgroundColor.cgColor) sizeNode = ASTextNode()
self.sizeNode = sizeNode
self.addSubnode(sizeNode)
}
let radius: CGFloat = 12.0 let sizeString = NSMutableAttributedString(string: size, attributes: [.font: font, .foregroundColor: foregroundColor])
let diameter = radius * 2.0 sizeNode.attributedText = sizeString
context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: diameter, height: diameter))) sizeSize = sizeNode.measure(CGSize(width: 160.0, height: 160.0))
context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: size.height - diameter), size: CGSize(width: diameter, height: diameter))) sizeNode.frame = CGRect(x: active ? 42.0 : 7.0, y: active ? 19.0 : 2.0, width: sizeSize.width, height: sizeSize.height)
context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - diameter, y: 0.0), size: CGSize(width: diameter, height: diameter))) transition.updateAlpha(node: sizeNode, alpha: 1.0)
context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - diameter, y: size.height - diameter), size: CGSize(width: diameter, height: diameter))) } else if let sizeNode = self.sizeNode {
context.fill(CGRect(origin: CGPoint(x: 0.0, y: radius), size: CGSize(width: diameter, height: size.height - diameter))) transition.updateAlpha(node: sizeNode, alpha: 0.0)
context.fill(CGRect(origin: CGPoint(x: radius, y: 0.0), size: CGSize(width: size.width - diameter, height: size.height))) }
context.fill(CGRect(origin: CGPoint(x: size.width - diameter, y: radius), size: CGSize(width: diameter, height: size.height - diameter)))
let durationSize = self.durationNode.measure(CGSize(width: 160.0, height: 160.0))
if let statusNode = self.mediaDownloadStatusNode {
transition.updateAlpha(node: statusNode, alpha: active ? 1.0 : 0.0)
}
self.durationNode.frame = CGRect(x: active ? 42.0 : 7.0, y: active ? 7.0 : 2.0, width: durationSize.width, height: durationSize.height)
if muted {
let iconNode: ASImageNode
if let current = self.iconNode {
iconNode = current
} else {
iconNode = ASImageNode()
self.iconNode = iconNode
self.addSubnode(iconNode)
}
context.setBlendMode(.normal) if self.foregroundColor != foregroundColor {
UIGraphicsPushContext(context) self.foregroundColor = foregroundColor
durationString.draw(at: CGPoint(x: leftInset + durationRect.origin.x, y: 7.0 + durationRect.origin.y)) iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/InlineVideoMute"), color: foregroundColor)
sizeString.draw(at: CGPoint(x: leftInset + sizeRect.origin.x, y: 21.0 + sizeRect.origin.y)) }
UIGraphicsPopContext() transition.updateAlpha(node: iconNode, alpha: 1.0)
}) if let icon = iconNode.image {
transition.updateFrame(node: iconNode, frame: CGRect(x: (active ? 42.0 : 7.0) + floor(durationSize.width) + 3.0, y: active ? 9.0 + UIScreenPixel : 4.0 + UIScreenPixel, width: icon.size.width, height: icon.size.height))
}
} else if let iconNode = self.iconNode {
transition.updateAlpha(node: iconNode, alpha: 0.0)
}
var contentWidth: CGFloat = max(sizeSize.width, durationSize.width + (muted ? 17.0 : 0.0)) + 14.0
if active {
contentWidth += 36.0
}
contentSize = CGSize(width: contentWidth, height: active ? 38.0 : 18.0)
} }
} }
return nil transition.updateFrame(node: self.backgroundNode, frame: CGRect(x: 0.0, y: 0.0, width: contentSize.width, height: contentSize.height))
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return self.backgroundNode.frame.contains(point)
} }
} }

View File

@ -25,6 +25,12 @@ enum InteractiveMediaNodeActivateContent {
case stream case stream
} }
enum InteractiveMediaNodeAutodownloadMode {
case none
case prefetch
case full
}
final class ChatMessageInteractiveMediaNode: ASDisplayNode { final class ChatMessageInteractiveMediaNode: ASDisplayNode {
private let imageNode: TransformImageNode private let imageNode: TransformImageNode
private var currentImageArguments: TransformImageArguments? private var currentImageArguments: TransformImageArguments?
@ -38,7 +44,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode {
private var media: Media? private var media: Media?
private var themeAndStrings: (PresentationTheme, PresentationStrings)? private var themeAndStrings: (PresentationTheme, PresentationStrings)?
private var sizeCalculation: InteractiveMediaNodeSizeCalculation? private var sizeCalculation: InteractiveMediaNodeSizeCalculation?
private var automaticDownload: Bool? private var automaticDownload: InteractiveMediaNodeAutodownloadMode?
private var automaticPlayback: Bool? private var automaticPlayback: Bool?
private let statusDisposable = MetaDisposable() private let statusDisposable = MetaDisposable()
@ -46,8 +52,12 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode {
private var fetchStatus: MediaResourceStatus? private var fetchStatus: MediaResourceStatus?
private let fetchDisposable = MetaDisposable() private let fetchDisposable = MetaDisposable()
private let playerStatusDisposable = MetaDisposable()
private var playerStatus: MediaPlayerStatus?
private var secretTimer: SwiftSignalKit.Timer? private var secretTimer: SwiftSignalKit.Timer?
var visibilityPromise = ValuePromise<ListViewItemNodeVisibility>(.none)
var visibility: ListViewItemNodeVisibility = .none { var visibility: ListViewItemNodeVisibility = .none {
didSet { didSet {
if let videoNode = self.videoNode { if let videoNode = self.videoNode {
@ -61,6 +71,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode {
videoNode.canAttachContent = false videoNode.canAttachContent = false
} }
} }
self.visibilityPromise.set(self.visibility)
} }
} }
@ -78,6 +89,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode {
deinit { deinit {
self.statusDisposable.dispose() self.statusDisposable.dispose()
self.playerStatusDisposable.dispose()
self.fetchDisposable.dispose() self.fetchDisposable.dispose()
self.secretTimer?.invalidate() self.secretTimer?.invalidate()
} }
@ -92,7 +104,13 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode {
private func progressPressed(canActivate: Bool) { private func progressPressed(canActivate: Bool) {
if let fetchStatus = self.fetchStatus { if let fetchStatus = self.fetchStatus {
if canActivate, let state = self.statusNode?.state, case .play = state { var activateContent = false
if let state = self.statusNode?.state, case .play = state {
activateContent = true
} else if (self.automaticPlayback ?? false) {
activateContent = true
}
if canActivate, activateContent {
switch fetchStatus { switch fetchStatus {
case .Remote, .Fetching: case .Remote, .Fetching:
self.activateLocalContent(.stream) self.activateLocalContent(.stream)
@ -145,7 +163,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode {
} }
} }
func asyncLayout() -> (_ context: AccountContext, _ theme: PresentationTheme, _ strings: PresentationStrings, _ message: Message, _ media: Media, _ automaticDownload: Bool, _ peerType: AutomaticMediaDownloadPeerType, _ automaticPlayback: Bool, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode) -> (CGSize, CGFloat, (CGSize, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ContainedViewLayoutTransition, Bool) -> Void))) { func asyncLayout() -> (_ context: AccountContext, _ theme: PresentationTheme, _ strings: PresentationStrings, _ message: Message, _ media: Media, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: AutomaticMediaDownloadPeerType, _ automaticPlayback: Bool, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode) -> (CGSize, CGFloat, (CGSize, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ContainedViewLayoutTransition, Bool) -> Void))) {
let currentMessage = self.message let currentMessage = self.message
let currentMedia = self.media let currentMedia = self.media
let imageLayout = self.imageNode.asyncLayout() let imageLayout = self.imageNode.asyncLayout()
@ -179,6 +197,8 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode {
var isInlinePlayableVideo = false var isInlinePlayableVideo = false
var maxDimensions = layoutConstants.image.maxDimensions
var unboundSize: CGSize var unboundSize: CGSize
if let image = media as? TelegramMediaImage, let dimensions = largestImageRepresentation(image.representations)?.dimensions { if let image = media as? TelegramMediaImage, let dimensions = largestImageRepresentation(image.representations)?.dimensions {
unboundSize = CGSize(width: floor(dimensions.width * 0.5), height: floor(dimensions.height * 0.5)) unboundSize = CGSize(width: floor(dimensions.width * 0.5), height: floor(dimensions.height * 0.5))
@ -193,10 +213,12 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode {
unboundSize = CGSize(width: floor(dimensions.width * 0.5), height: floor(dimensions.height * 0.5)) unboundSize = CGSize(width: floor(dimensions.width * 0.5), height: floor(dimensions.height * 0.5))
if file.isAnimated { if file.isAnimated {
unboundSize = unboundSize.aspectFilled(CGSize(width: 480.0, height: 480.0)) unboundSize = unboundSize.aspectFilled(CGSize(width: 480.0, height: 480.0))
} else if file.isVideo && automaticPlayback && !file.isAnimated, case let .constrained(constrainedSize) = sizeCalculation {
maxDimensions = constrainedSize
} else if file.isSticker { } else if file.isSticker {
unboundSize = unboundSize.aspectFilled(CGSize(width: 162.0, height: 162.0)) unboundSize = unboundSize.aspectFilled(CGSize(width: 162.0, height: 162.0))
} }
isInlinePlayableVideo = file.isVideo && file.isAnimated && !isSecretMedia && automaticPlayback isInlinePlayableVideo = file.isVideo && !isSecretMedia && automaticPlayback
} else if let image = media as? TelegramMediaWebFile, let dimensions = image.dimensions { } else if let image = media as? TelegramMediaWebFile, let dimensions = image.dimensions {
unboundSize = CGSize(width: floor(dimensions.width * 0.5), height: floor(dimensions.height * 0.5)) unboundSize = CGSize(width: floor(dimensions.width * 0.5), height: floor(dimensions.height * 0.5))
} else if let wallpaper = media as? WallpaperPreviewMedia { } else if let wallpaper = media as? WallpaperPreviewMedia {
@ -230,7 +252,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode {
if isSecretMedia { if isSecretMedia {
maxWidth = 180.0 maxWidth = 180.0
} else { } else {
maxWidth = layoutConstants.image.maxDimensions.width maxWidth = maxDimensions.width
} }
if isSecretMedia { if isSecretMedia {
let _ = PresentationResourcesChat.chatBubbleSecretMediaIcon(theme) let _ = PresentationResourcesChat.chatBubbleSecretMediaIcon(theme)
@ -244,8 +266,8 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode {
if isSecretMedia { if isSecretMedia {
resultWidth = maxWidth resultWidth = maxWidth
} else { } else {
let maxFittedSize = nativeSize.aspectFitted (layoutConstants.image.maxDimensions) let maxFittedSize = nativeSize.aspectFitted(maxDimensions)
resultWidth = min(nativeSize.width, min(maxFittedSize.width, min(constrainedSize.width, layoutConstants.image.maxDimensions.width))) resultWidth = min(nativeSize.width, min(maxFittedSize.width, min(constrainedSize.width, maxDimensions.width)))
resultWidth = max(resultWidth, layoutConstants.image.minDimensions.width) resultWidth = max(resultWidth, layoutConstants.image.minDimensions.width)
} }
@ -313,14 +335,8 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode {
return chatSecretPhoto(account: context.account, photoReference: .message(message: MessageReference(message), media: image)) return chatSecretPhoto(account: context.account, photoReference: .message(message: MessageReference(message), media: image))
} }
} else { } else {
let tinyThumbnailData: TinyThumbnailData?
if GlobalExperimentalSettings.enableTinyThumbnails {
tinyThumbnailData = parseTinyThumbnail(message.text)
} else {
tinyThumbnailData = nil
}
updateImageSignal = { synchronousLoad in updateImageSignal = { synchronousLoad in
return chatMessagePhoto(postbox: context.account.postbox, photoReference: .message(message: MessageReference(message), media: image), synchronousLoad: synchronousLoad, tinyThumbnailData: tinyThumbnailData) return chatMessagePhoto(postbox: context.account.postbox, photoReference: .message(message: MessageReference(message), media: image), synchronousLoad: synchronousLoad)
} }
} }
@ -370,7 +386,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode {
} }
} }
if file.isVideo && file.isAnimated && !isSecretMedia && automaticPlayback { if file.isVideo && !isSecretMedia && automaticPlayback {
updateVideoFile = file updateVideoFile = file
if hasCurrentVideoNode { if hasCurrentVideoNode {
if let currentFile = currentMedia as? TelegramMediaFile, currentFile.resource is EmptyMediaResource { if let currentFile = currentMedia as? TelegramMediaFile, currentFile.resource is EmptyMediaResource {
@ -507,6 +523,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode {
statusNode.frame = statusFrame statusNode.frame = statusFrame
} }
var updatedPlayerStatusSignal: Signal<MediaPlayerStatus?, NoError>?
if let replaceVideoNode = replaceVideoNode { if let replaceVideoNode = replaceVideoNode {
if let videoNode = strongSelf.videoNode { if let videoNode = strongSelf.videoNode {
videoNode.canAttachContent = false videoNode.canAttachContent = false
@ -517,11 +534,13 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode {
if replaceVideoNode, let updatedVideoFile = updateVideoFile { if replaceVideoNode, let updatedVideoFile = updateVideoFile {
let mediaManager = context.sharedContext.mediaManager let mediaManager = context.sharedContext.mediaManager
let cornerRadius: CGFloat = arguments.corners.topLeft.radius let cornerRadius: CGFloat = arguments.corners.topLeft.radius
let videoNode = UniversalVideoNode(postbox: context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: ChatBubbleVideoDecoration(cornerRadius: cornerRadius, nativeSize: nativeSize, backgroudColor: arguments.emptyColor ?? .black), content: NativeVideoContent(id: .message(message.id, message.stableId, updatedVideoFile.fileId), fileReference: .message(message: MessageReference(message), media: updatedVideoFile), enableSound: false, fetchAutomatically: false), priority: .embedded) let videoNode = UniversalVideoNode(postbox: context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: ChatBubbleVideoDecoration(cornerRadius: cornerRadius, nativeSize: nativeSize, contentMode: contentMode, backgroundColor: arguments.emptyColor ?? .black), content: NativeVideoContent(id: .message(message.id, message.stableId, updatedVideoFile.fileId), fileReference: .message(message: MessageReference(message), media: updatedVideoFile), streamVideo: true, enableSound: false, fetchAutomatically: false), priority: .embedded)
videoNode.isUserInteractionEnabled = false videoNode.isUserInteractionEnabled = false
strongSelf.videoNode = videoNode strongSelf.videoNode = videoNode
strongSelf.insertSubnode(videoNode, aboveSubnode: strongSelf.imageNode) strongSelf.insertSubnode(videoNode, aboveSubnode: strongSelf.imageNode)
updatedPlayerStatusSignal = videoNode.status
} }
} }
@ -563,20 +582,79 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode {
})) }))
} }
if let updatedPlayerStatusSignal = updatedPlayerStatusSignal {
strongSelf.playerStatusDisposable.set((updatedPlayerStatusSignal |> deliverOnMainQueue).start(next: { [weak strongSelf] status in
displayLinkDispatcher.dispatch {
if let strongSelf = strongSelf {
strongSelf.playerStatus = status
strongSelf.updateFetchStatus()
}
}
}))
}
if let updatedFetchControls = updatedFetchControls { if let updatedFetchControls = updatedFetchControls {
let _ = strongSelf.fetchControls.swap(updatedFetchControls) let _ = strongSelf.fetchControls.swap(updatedFetchControls)
if automaticDownload { if case .full = automaticDownload {
if let _ = media as? TelegramMediaImage { if let _ = media as? TelegramMediaImage {
updatedFetchControls.fetch(false) updatedFetchControls.fetch(false)
} else if let image = media as? TelegramMediaWebFile { } else if let image = media as? TelegramMediaWebFile {
strongSelf.fetchDisposable.set(chatMessageWebFileInteractiveFetched(account: context.account, image: image).start()) strongSelf.fetchDisposable.set(chatMessageWebFileInteractiveFetched(account: context.account, image: image).start())
} else if let file = media as? TelegramMediaFile { } else if let file = media as? TelegramMediaFile {
if automaticPlayback || !file.isAnimated { if automaticPlayback || !file.isAnimated {
strongSelf.fetchDisposable.set(messageMediaFileInteractiveFetched(context: context, message: message, file: file, userInitiated: false).start()) let fetchSignal = messageMediaFileInteractiveFetched(context: context, message: message, file: file, userInitiated: false)
if !file.isAnimated {
let visibilityAwareFetchSignal = strongSelf.visibilityPromise.get()
|> mapToSignal { visibility -> Signal<Void, NoError> in
switch visibility {
case .visible, .nearlyVisible:
return fetchSignal
|> mapToSignal { _ -> Signal<Void, NoError> in
return .complete()
}
case .none:
return .complete()
}
}
strongSelf.fetchDisposable.set(visibilityAwareFetchSignal.start())
} else {
strongSelf.fetchDisposable.set(fetchSignal.start())
}
} }
} }
} else if case .prefetch = automaticDownload {
if let file = media as? TelegramMediaFile, let fileSize = file.size {
let fetchHeadRange: Range<Int> = 0 ..< 2 * 1024 * 1024
let fetchTailRange: Range<Int> = fileSize - 64 * 1024 ..< Int(Int32.max)
let fetchHeadSignal = fetchedMediaResource(postbox: context.account.postbox, reference: AnyMediaReference.message(message: MessageReference(message), media: file).resourceReference(file.resource), range: (fetchHeadRange, .default), statsCategory: statsCategoryForFileWithAttributes(file.attributes))
|> ignoreValues
|> `catch` { _ -> Signal<Never, NoError> in
return .complete()
}
let fetchTailSignal = fetchedMediaResource(postbox: context.account.postbox, reference: AnyMediaReference.message(message: MessageReference(message), media: file).resourceReference(file.resource), range: (fetchTailRange, .default), statsCategory: statsCategoryForFileWithAttributes(file.attributes))
|> ignoreValues
|> `catch` { _ -> Signal<Never, NoError> in
return .complete()
}
let visibilityAwareFetchSignal = strongSelf.visibilityPromise.get()
|> mapToSignal { visibility -> Signal<Void, NoError> in
switch visibility {
case .visible, .nearlyVisible:
return combineLatest(fetchHeadSignal, fetchTailSignal)
|> mapToSignal { _ -> Signal<Void, NoError> in
return .complete()
}
case .none:
return .complete()
}
}
strongSelf.fetchDisposable.set(visibilityAwareFetchSignal.start())
}
} }
} else if previousAutomaticDownload != automaticDownload, automaticDownload { } else if previousAutomaticDownload != automaticDownload, case .full = automaticDownload {
strongSelf.fetchControls.with({ $0 })?.fetch(false) strongSelf.fetchControls.with({ $0 })?.fetch(false)
} }
@ -636,7 +714,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode {
} }
} }
case .Remote, .Fetching: case .Remote, .Fetching:
if let webpage = webpage, let automaticDownload = self.automaticDownload, automaticDownload, case let .Loaded(content) = webpage.content { if let webpage = webpage, let automaticDownload = self.automaticDownload, case .full = automaticDownload, case let .Loaded(content) = webpage.content {
if content.type == "telegram_background" { if content.type == "telegram_background" {
progressRequired = true progressRequired = true
} else if content.embedUrl != nil { } else if content.embedUrl != nil {
@ -698,6 +776,15 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode {
badgeContent = .text(inset: 0.0, backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, shape: .round, text: string) badgeContent = .text(inset: 0.0, backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, shape: .round, text: string)
} }
if let fetchStatus = self.fetchStatus { if let fetchStatus = self.fetchStatus {
var position: Int32?
if let timestamp = self.playerStatus?.timestamp {
position = Int32(timestamp)
}
var active = false
if let status = self.playerStatus?.status, case .buffering = status {
active = true
}
switch fetchStatus { switch fetchStatus {
case let .Fetching(_, progress): case let .Fetching(_, progress):
let adjustedProgress = max(progress, 0.027) let adjustedProgress = max(progress, 0.027)
@ -716,11 +803,14 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode {
if let size = file.size { if let size = file.size {
if let duration = file.duration, !message.flags.contains(.Unsent) { if let duration = file.duration, !message.flags.contains(.Unsent) {
if isMediaStreamable(message: message, media: file) { if isMediaStreamable(message: message, media: file) {
let durationString = stringForDuration(duration) let durationString = stringForDuration(duration, position: position)
let sizeString = "\(dataSizeString(Int(Float(size) * progress), forceDecimal: true)) / \(dataSizeString(size, forceDecimal: true))" let sizeString = "\(dataSizeString(Int(Float(size) * progress), forceDecimal: true)) / \(dataSizeString(size, forceDecimal: true))"
badgeContent = .mediaDownload(backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, duration: durationString, size: sizeString) badgeContent = .mediaDownload(backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, duration: durationString, size: active ? sizeString : nil, muted: automaticPlayback, active: active)
mediaDownloadState = .fetching(progress: progress) mediaDownloadState = .fetching(progress: automaticPlayback ? nil : progress)
state = .play(bubbleTheme.mediaOverlayControlForegroundColor) if self.playerStatus?.status == .playing {
mediaDownloadState = nil
}
state = automaticPlayback ? .none : .play(bubbleTheme.mediaOverlayControlForegroundColor)
} else { } else {
badgeContent = .text(inset: 0.0, backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, shape: .round, text: NSAttributedString(string: "\(dataSizeString(Int(Float(size) * progress), forceDecimal: true)) / \(dataSizeString(size, forceDecimal: true))")) badgeContent = .text(inset: 0.0, backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, shape: .round, text: NSAttributedString(string: "\(dataSizeString(Int(Float(size) * progress), forceDecimal: true)) / \(dataSizeString(size, forceDecimal: true))"))
} }
@ -739,12 +829,12 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode {
} }
} else { } else {
if let duration = file.duration, !file.isAnimated { if let duration = file.duration, !file.isAnimated {
let durationString = stringForDuration(duration) let durationString = stringForDuration(duration, position: position)
badgeContent = .text(inset: 0.0, backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, shape: .round, text: NSAttributedString(string: durationString)) badgeContent = .text(inset: 0.0, backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, shape: .round, text: NSAttributedString(string: durationString))
} }
} }
} }
} else if let webpage = webpage, let automaticDownload = self.automaticDownload, automaticDownload, case let .Loaded(content) = webpage.content, content.type != "telegram_background" { } else if let webpage = webpage, let automaticDownload = self.automaticDownload, case .full = automaticDownload, case let .Loaded(content) = webpage.content, content.type != "telegram_background" {
state = .play(bubbleTheme.mediaOverlayControlForegroundColor) state = .play(bubbleTheme.mediaOverlayControlForegroundColor)
} }
case .Local: case .Local:
@ -760,8 +850,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode {
} else if isSecretMedia, let secretProgressIcon = secretProgressIcon { } else if isSecretMedia, let secretProgressIcon = secretProgressIcon {
state = .customIcon(secretProgressIcon) state = .customIcon(secretProgressIcon)
} else if let file = media as? TelegramMediaFile { } else if let file = media as? TelegramMediaFile {
let isInlinePlayableVideo = file.isVideo && file.isAnimated && !isSecretMedia && automaticPlayback let isInlinePlayableVideo = file.isVideo && !isSecretMedia && automaticPlayback
if !isInlinePlayableVideo && file.isVideo { if !isInlinePlayableVideo && file.isVideo {
state = .play(bubbleTheme.mediaOverlayControlForegroundColor) state = .play(bubbleTheme.mediaOverlayControlForegroundColor)
} else { } else {
@ -775,17 +864,17 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode {
} }
} }
if let file = media as? TelegramMediaFile, let duration = file.duration, !file.isAnimated { if let file = media as? TelegramMediaFile, let duration = file.duration, !file.isAnimated {
let durationString = stringForDuration(duration) let durationString = stringForDuration(duration, position: position)
badgeContent = .text(inset: 0.0, backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, shape: .round, text: NSAttributedString(string: durationString)) badgeContent = .mediaDownload(backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, duration: durationString, size: nil, muted: true, active: false)
} }
case .Remote: case .Remote:
state = .download(bubbleTheme.mediaOverlayControlForegroundColor) state = .download(bubbleTheme.mediaOverlayControlForegroundColor)
if let file = self.media as? TelegramMediaFile, !file.isAnimated { if let file = self.media as? TelegramMediaFile, !file.isAnimated {
let durationString = stringForDuration(file.duration ?? 0) let durationString = stringForDuration(file.duration ?? 0, position: position)
if case .constrained = sizeCalculation { if case .constrained = sizeCalculation {
if isMediaStreamable(message: message, media: file) { if isMediaStreamable(message: message, media: file) {
state = .play(bubbleTheme.mediaOverlayControlForegroundColor) state = automaticPlayback ? .none : .play(bubbleTheme.mediaOverlayControlForegroundColor)
badgeContent = .mediaDownload(backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, duration: durationString, size: dataSizeString(file.size ?? 0)) badgeContent = .mediaDownload(backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, duration: durationString, size: dataSizeString(file.size ?? 0), muted: automaticPlayback, active: true)
mediaDownloadState = .remote mediaDownloadState = .remote
} else { } else {
badgeContent = .text(inset: 0.0, backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, shape: .round, text: NSAttributedString(string: durationString)) badgeContent = .text(inset: 0.0, backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, shape: .round, text: NSAttributedString(string: durationString))
@ -799,7 +888,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode {
badgeContent = .text(inset: 0.0, backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, shape: .round, text: NSAttributedString(string: durationString)) badgeContent = .text(inset: 0.0, backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, shape: .round, text: NSAttributedString(string: durationString))
} }
} }
} else if let webpage = webpage, let automaticDownload = self.automaticDownload, automaticDownload, case let .Loaded(content) = webpage.content, content.type != "telegram_background" { } else if let webpage = webpage, let automaticDownload = self.automaticDownload, case .full = automaticDownload, case let .Loaded(content) = webpage.content, content.type != "telegram_background" {
state = .play(bubbleTheme.mediaOverlayControlForegroundColor) state = .play(bubbleTheme.mediaOverlayControlForegroundColor)
} }
} }
@ -845,7 +934,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode {
self.badgeNode = badgeNode self.badgeNode = badgeNode
self.addSubnode(badgeNode) self.addSubnode(badgeNode)
} }
self.badgeNode?.update(theme: theme, content: badgeContent, mediaDownloadState: mediaDownloadState, animated: false) self.badgeNode?.update(theme: theme, content: badgeContent, mediaDownloadState: mediaDownloadState, animated: true)
} else if let badgeNode = self.badgeNode { } else if let badgeNode = self.badgeNode {
self.badgeNode = nil self.badgeNode = nil
badgeNode.removeFromSupernode() badgeNode.removeFromSupernode()
@ -866,12 +955,12 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode {
} }
} }
static func asyncLayout(_ node: ChatMessageInteractiveMediaNode?) -> (_ context: AccountContext, _ theme: PresentationTheme, _ strings: PresentationStrings, _ message: Message, _ media: Media, _ automaticDownload: Bool, _ peerType: AutomaticMediaDownloadPeerType, _ automaticPlayback: Bool, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode) -> (CGSize, CGFloat, (CGSize, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ContainedViewLayoutTransition, Bool) -> ChatMessageInteractiveMediaNode))) { static func asyncLayout(_ node: ChatMessageInteractiveMediaNode?) -> (_ context: AccountContext, _ theme: PresentationTheme, _ strings: PresentationStrings, _ message: Message, _ media: Media, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: AutomaticMediaDownloadPeerType, _ automaticPlayback: Bool, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode) -> (CGSize, CGFloat, (CGSize, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ContainedViewLayoutTransition, Bool) -> ChatMessageInteractiveMediaNode))) {
let currentAsyncLayout = node?.asyncLayout() let currentAsyncLayout = node?.asyncLayout()
return { context, theme, strings, message, media, automaticDownload, peerType, automaticPlayback, sizeCalculation, layoutConstants, contentMode in return { context, theme, strings, message, media, automaticDownload, peerType, automaticPlayback, sizeCalculation, layoutConstants, contentMode in
var imageNode: ChatMessageInteractiveMediaNode var imageNode: ChatMessageInteractiveMediaNode
var imageLayout: (_ context: AccountContext, _ theme: PresentationTheme, _ strings: PresentationStrings, _ message: Message, _ media: Media, _ automaticDownload: Bool, _ peerType: AutomaticMediaDownloadPeerType, _ automaticPlayback: Bool, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode) -> (CGSize, CGFloat, (CGSize, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ContainedViewLayoutTransition, Bool) -> Void))) var imageLayout: (_ context: AccountContext, _ theme: PresentationTheme, _ strings: PresentationStrings, _ message: Message, _ media: Media, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: AutomaticMediaDownloadPeerType, _ automaticPlayback: Bool, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode) -> (CGSize, CGFloat, (CGSize, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ContainedViewLayoutTransition, Bool) -> Void)))
if let node = node, let currentAsyncLayout = currentAsyncLayout { if let node = node, let currentAsyncLayout = currentAsyncLayout {
imageNode = node imageNode = node

View File

@ -50,14 +50,32 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
return { item, layoutConstants, preparePosition, selection, constrainedSize in return { item, layoutConstants, preparePosition, selection, constrainedSize in
var selectedMedia: Media? var selectedMedia: Media?
var automaticDownload: Bool = false var automaticDownload: InteractiveMediaNodeAutodownloadMode = .none
var automaticPlayback: Bool = false
var contentMode: InteractiveMediaNodeContentMode = .aspectFit
for media in item.message.media { for media in item.message.media {
if let telegramImage = media as? TelegramMediaImage { if let telegramImage = media as? TelegramMediaImage {
selectedMedia = telegramImage selectedMedia = telegramImage
automaticDownload = shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, media: telegramImage) if shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, media: telegramImage) {
automaticDownload = .full
}
} else if let telegramFile = media as? TelegramMediaFile { } else if let telegramFile = media as? TelegramMediaFile {
selectedMedia = telegramFile selectedMedia = telegramFile
automaticDownload = shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, media: telegramFile) if shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, media: telegramFile) {
automaticDownload = .full
}
if telegramFile.isAnimated {
automaticPlayback = item.controllerInteraction.automaticMediaDownloadSettings.autoplayGifs
} else {
if case .full = automaticDownload {
automaticPlayback = true
contentMode = .aspectFill
} else {
automaticDownload = .prefetch
}
}
} }
} }
@ -86,10 +104,10 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
sizeCalculation = .unconstrained sizeCalculation = .unconstrained
} }
let (unboundSize, initialWidth, refineLayout) = interactiveImageLayout(item.context, item.presentationData.theme.theme, item.presentationData.strings, item.message, selectedMedia!, automaticDownload, item.associatedData.automaticDownloadPeerType, item.controllerInteraction.automaticMediaDownloadSettings.autoplayGifs, sizeCalculation, layoutConstants, .aspectFit) let (unboundSize, initialWidth, refineLayout) = interactiveImageLayout(item.context, item.presentationData.theme.theme, item.presentationData.strings, item.message, selectedMedia!, automaticDownload, item.associatedData.automaticDownloadPeerType, automaticPlayback, sizeCalculation, layoutConstants, contentMode)
var forceFullCorners = false var forceFullCorners = false
if let media = selectedMedia as? TelegramMediaFile, media.isAnimated { if let media = selectedMedia as? TelegramMediaFile, media.isVideo || media.isAnimated {
forceFullCorners = true forceFullCorners = true
} }

View File

@ -19,7 +19,7 @@ private func readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: Unsa
} }
} }
return -1 return -1
//return Int32(bufferPointer) return Int32(bufferPointer)
} }
private func seekCallback(userData: UnsafeMutableRawPointer?, offset: Int64, whence: Int32) -> Int64 { private func seekCallback(userData: UnsafeMutableRawPointer?, offset: Int64, whence: Int32) -> Int64 {
@ -64,14 +64,14 @@ private final class FetchVideoThumbnailSource {
private var videoStream: SoftwareVideoStream? private var videoStream: SoftwareVideoStream?
private var avIoContext: UnsafeMutablePointer<AVIOContext>? private var avIoContext: UnsafeMutablePointer<AVIOContext>?
private var avFormatContext: UnsafeMutablePointer<AVFormatContext>? private var avFormatContext: UnsafeMutablePointer<AVFormatContext>?
init(mediaBox: MediaBox, resourceReference: MediaResourceReference, size: Int32) { init(mediaBox: MediaBox, resourceReference: MediaResourceReference, size: Int32) {
let _ = FFMpegMediaFrameSourceContextHelpers.registerFFMpegGlobals let _ = FFMpegMediaFrameSourceContextHelpers.registerFFMpegGlobals
self.mediaBox = mediaBox self.mediaBox = mediaBox
self.resourceReference = resourceReference self.resourceReference = resourceReference
self.size = size self.size = size
var avFormatContextRef = avformat_alloc_context() var avFormatContextRef = avformat_alloc_context()
guard let avFormatContext = avFormatContextRef else { guard let avFormatContext = avFormatContextRef else {
self.readingError = true self.readingError = true
@ -82,7 +82,7 @@ private final class FetchVideoThumbnailSource {
let avIoBuffer = av_malloc(ioBufferSize)! let avIoBuffer = av_malloc(ioBufferSize)!
let avIoContextRef = avio_alloc_context(avIoBuffer.assumingMemoryBound(to: UInt8.self), Int32(ioBufferSize), 0, Unmanaged.passUnretained(self).toOpaque(), readPacketCallback, nil, seekCallback) let avIoContextRef = avio_alloc_context(avIoBuffer.assumingMemoryBound(to: UInt8.self), Int32(ioBufferSize), 0, Unmanaged.passUnretained(self).toOpaque(), readPacketCallback, nil, seekCallback)
self.avIoContext = avIoContextRef self.avIoContext = avIoContextRef
avFormatContext.pointee.pb = self.avIoContext avFormatContext.pointee.pb = self.avIoContext
guard avformat_open_input(&avFormatContextRef, nil, nil, nil) >= 0 else { guard avformat_open_input(&avFormatContextRef, nil, nil, nil) >= 0 else {
@ -94,7 +94,7 @@ private final class FetchVideoThumbnailSource {
self.readingError = true self.readingError = true
return return
} }
self.avFormatContext = avFormatContext self.avFormatContext = avFormatContext
var videoStream: SoftwareVideoStream? var videoStream: SoftwareVideoStream?
@ -111,7 +111,7 @@ private final class FetchVideoThumbnailSource {
let (fps, timebase) = FFMpegMediaFrameSourceContextHelpers.streamFpsAndTimeBase(stream: avFormatContext.pointee.streams.advanced(by: streamIndex).pointee!, defaultTimeBase: CMTimeMake(1, 24)) let (fps, timebase) = FFMpegMediaFrameSourceContextHelpers.streamFpsAndTimeBase(stream: avFormatContext.pointee.streams.advanced(by: streamIndex).pointee!, defaultTimeBase: CMTimeMake(1, 24))
let duration = CMTimeMake(avFormatContext.pointee.streams.advanced(by: streamIndex).pointee!.pointee.duration, timebase.timescale) let duration = CMTimeMake(avFormatContext.pointee.streams.advanced(by: streamIndex).pointee!.pointee.duration, timebase.timescale)
var rotationAngle: Double = 0.0 var rotationAngle: Double = 0.0
if let rotationInfo = av_dict_get(avFormatContext.pointee.streams.advanced(by: streamIndex).pointee!.pointee.metadata, "rotate", nil, 0), let value = rotationInfo.pointee.value { if let rotationInfo = av_dict_get(avFormatContext.pointee.streams.advanced(by: streamIndex).pointee!.pointee.metadata, "rotate", nil, 0), let value = rotationInfo.pointee.value {
if strcmp(value, "0") != 0 { if strcmp(value, "0") != 0 {
@ -178,10 +178,10 @@ private final class FetchVideoThumbnailSource {
let avNoPtsRawValue: UInt64 = 0x8000000000000000 let avNoPtsRawValue: UInt64 = 0x8000000000000000
let avNoPtsValue = Int64(bitPattern: avNoPtsRawValue) let avNoPtsValue = Int64(bitPattern: avNoPtsRawValue)
let packetPts = packet.packet.pts == avNoPtsValue ? packet.packet.dts : packet.packet.pts let packetPts = packet.packet.pts == avNoPtsValue ? packet.packet.dts : packet.packet.pts
let pts = CMTimeMake(packetPts, videoStream.timebase.timescale) let pts = CMTimeMake(packetPts, videoStream.timebase.timescale)
let dts = CMTimeMake(packet.packet.dts, videoStream.timebase.timescale) let dts = CMTimeMake(packet.packet.dts, videoStream.timebase.timescale)
let duration: CMTime let duration: CMTime
let frameDuration = packet.packet.duration let frameDuration = packet.packet.duration
@ -198,7 +198,7 @@ private final class FetchVideoThumbnailSource {
self.readingError = true self.readingError = true
} }
} }
return frames.first return frames.first
} }
@ -264,7 +264,7 @@ private final class FetchVideoThumbnailSourceThreadImpl: NSObject {
} }
} }
} }
@objc func fetch(_ parameters: FetchVideoThumbnailSourceParameters) { @objc func fetch(_ parameters: FetchVideoThumbnailSourceParameters) {
let source = FetchVideoThumbnailSource(mediaBox: parameters.mediaBox, resourceReference: parameters.resourceReference, size: parameters.size) let source = FetchVideoThumbnailSource(mediaBox: parameters.mediaBox, resourceReference: parameters.resourceReference, size: parameters.size)
let _ = source.readFrame() let _ = source.readFrame()
@ -396,7 +396,7 @@ func streamingVideoThumbnail(postbox: Postbox, fileReference: FileMediaReference
let impl = FetchVideoThumbnailSourceThreadImpl() let impl = FetchVideoThumbnailSourceThreadImpl()
let thread = Thread(target: impl, selector: #selector(impl.entryPoint), object: nil) let thread = Thread(target: impl, selector: #selector(impl.entryPoint), object: nil)
thread.name = "streamingVideoThumbnail" thread.name = "streamingVideoThumbnail"
//impl.perform(#selector(impl.fetch(_:)), on: thread, with: FetchVideoThumbnailSourceParameters(), waitUntilDone: false) impl.perform(#selector(impl.fetch(_:)), on: thread, with: FetchVideoThumbnailSourceParameters(), waitUntilDone: false)
thread.start() thread.start()
return ActionDisposable { return ActionDisposable {

View File

@ -3,7 +3,5 @@ import Foundation
public struct GlobalExperimentalSettings { public struct GlobalExperimentalSettings {
public static var isAppStoreBuild: Bool = false public static var isAppStoreBuild: Bool = false
public static var enableFeed: Bool = false public static var enableFeed: Bool = false
public static var enableTinyThumbnails: Bool = false
public static var forceTinyThumbnailsPreview: Bool = false
public static var animatedStickers: Bool = false public static var animatedStickers: Bool = false
} }

View File

@ -57,7 +57,7 @@ func imageHasTransparency(_ cgImage: CGImage) -> Bool {
transparentCount += histogramBins[alphaBinIndex][i] transparentCount += histogramBins[alphaBinIndex][i]
} }
let totalCount = opaqueCount + transparentCount let totalCount = opaqueCount + transparentCount
return Double(transparentCount) / Double(totalCount) > 0.02 return Double(transparentCount) / Double(totalCount) > 0.05
} }
return false return false
} }

View File

@ -15,17 +15,19 @@ class ItemListSwitchItem: ListViewItem, ItemListItem {
let type: ItemListSwitchItemNodeType let type: ItemListSwitchItemNodeType
let enableInteractiveChanges: Bool let enableInteractiveChanges: Bool
let enabled: Bool let enabled: Bool
let maximumNumberOfLines: Int
let sectionId: ItemListSectionId let sectionId: ItemListSectionId
let style: ItemListStyle let style: ItemListStyle
let updated: (Bool) -> Void let updated: (Bool) -> Void
init(theme: PresentationTheme, title: String, value: Bool, type: ItemListSwitchItemNodeType = .regular, enableInteractiveChanges: Bool = true, enabled: Bool = true, sectionId: ItemListSectionId, style: ItemListStyle, updated: @escaping (Bool) -> Void) { init(theme: PresentationTheme, title: String, value: Bool, type: ItemListSwitchItemNodeType = .regular, enableInteractiveChanges: Bool = true, enabled: Bool = true, maximumNumberOfLines: Int = 1, sectionId: ItemListSectionId, style: ItemListStyle, updated: @escaping (Bool) -> Void) {
self.theme = theme self.theme = theme
self.title = title self.title = title
self.value = value self.value = value
self.type = type self.type = type
self.enableInteractiveChanges = enableInteractiveChanges self.enableInteractiveChanges = enableInteractiveChanges
self.enabled = enabled self.enabled = enabled
self.maximumNumberOfLines = maximumNumberOfLines
self.sectionId = sectionId self.sectionId = sectionId
self.style = style self.style = style
self.updated = updated self.updated = updated
@ -154,7 +156,7 @@ class ItemListSwitchItemNode: ListViewItemNode {
var currentDisabledOverlayNode = self.disabledOverlayNode var currentDisabledOverlayNode = self.disabledOverlayNode
return { item, params, neighbors in return { item, params, neighbors in
let contentSize: CGSize var contentSize: CGSize
let insets: UIEdgeInsets let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel let separatorHeight = UIScreenPixel
let itemBackgroundColor: UIColor let itemBackgroundColor: UIColor
@ -179,7 +181,9 @@ class ItemListSwitchItemNode: ListViewItemNode {
insets = itemListNeighborsGroupedInsets(neighbors) insets = itemListNeighborsGroupedInsets(neighbors)
} }
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 80.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: item.maximumNumberOfLines, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 80.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
contentSize.height = max(contentSize.height, titleLayout.size.height + 22.0)
if !item.enabled { if !item.enabled {
if currentDisabledOverlayNode == nil { if currentDisabledOverlayNode == nil {

View File

@ -303,13 +303,7 @@ func legacyAssetPickerEnqueueMessages(account: Account, signals: [Any]) -> Signa
attributes.append(AutoremoveTimeoutMessageAttribute(timeout: Int32(timer), countdownBeginTime: nil)) attributes.append(AutoremoveTimeoutMessageAttribute(timeout: Int32(timer), countdownBeginTime: nil))
} }
var text = caption ?? "" var text = caption ?? ""
if text.isEmpty && GlobalExperimentalSettings.enableTinyThumbnails {
if let tinyThumbnail = compressTinyThumbnail(scaledImage) {
text = serializeTinyThumbnail(tinyThumbnail)
}
}
messages.append(.message(text: text, attributes: attributes, mediaReference: .standalone(media: media), replyToMessageId: nil, localGroupingKey: item.groupedId)) messages.append(.message(text: text, attributes: attributes, mediaReference: .standalone(media: media), replyToMessageId: nil, localGroupingKey: item.groupedId))
} }
} }

View File

@ -48,6 +48,11 @@ enum MediaPlayerActionAtEnd {
case stop case stop
} }
enum MediaPlayerPlayOnceWithSoundActionAtEnd {
case loopDisablingSound
case stop
}
private final class MediaPlayerAudioRendererContext { private final class MediaPlayerAudioRendererContext {
let renderer: MediaPlayerAudioRenderer let renderer: MediaPlayerAudioRenderer
var requestedFrames = false var requestedFrames = false

View File

@ -257,13 +257,19 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent
self.player.seek(timestamp: timestamp) self.player.seek(timestamp: timestamp)
} }
func playOnceWithSound(playAndRecord: Bool) { func playOnceWithSound(playAndRecord: Bool, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) {
assert(Queue.mainQueue().isCurrent()) assert(Queue.mainQueue().isCurrent())
self.player.actionAtEnd = .loopDisablingSound({ [weak self] in let action = { [weak self] in
Queue.mainQueue().async { Queue.mainQueue().async {
self?.performActionAtEnd() self?.performActionAtEnd()
} }
}) }
switch actionAtEnd {
case .loopDisablingSound:
self.player.actionAtEnd = .loopDisablingSound(action)
case .stop:
self.player.actionAtEnd = .action(action)
}
self.player.playOnceWithSound(playAndRecord: playAndRecord) self.player.playOnceWithSound(playAndRecord: playAndRecord)
} }
@ -276,8 +282,19 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent
self.player.setBaseRate(baseRate) self.player.setBaseRate(baseRate)
} }
func continuePlayingWithoutSound() { func continuePlayingWithoutSound(actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) {
assert(Queue.mainQueue().isCurrent()) assert(Queue.mainQueue().isCurrent())
let action = { [weak self] in
Queue.mainQueue().async {
self?.performActionAtEnd()
}
}
switch actionAtEnd {
case .loopDisablingSound:
self.player.actionAtEnd = .loopDisablingSound(action)
case .stop:
self.player.actionAtEnd = .action(action)
}
self.player.continuePlayingWithoutSound() self.player.continuePlayingWithoutSound()
} }

View File

@ -634,14 +634,14 @@ func rawMessagePhoto(postbox: Postbox, photoReference: ImageMediaReference) -> S
} }
} }
public func chatMessagePhoto(postbox: Postbox, photoReference: ImageMediaReference, synchronousLoad: Bool = false, tinyThumbnailData: TinyThumbnailData? = nil) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { public func chatMessagePhoto(postbox: Postbox, photoReference: ImageMediaReference, synchronousLoad: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> {
return chatMessagePhotoInternal(photoData: chatMessagePhotoDatas(postbox: postbox, photoReference: photoReference, tryAdditionalRepresentations: true, synchronousLoad: synchronousLoad), synchronousLoad: synchronousLoad, tinyThumbnailData: tinyThumbnailData) return chatMessagePhotoInternal(photoData: chatMessagePhotoDatas(postbox: postbox, photoReference: photoReference, tryAdditionalRepresentations: true, synchronousLoad: synchronousLoad), synchronousLoad: synchronousLoad)
|> map { _, generate in |> map { _, generate in
return generate return generate
} }
} }
public func chatMessagePhotoInternal(photoData: Signal<(Data?, Data?, Bool), NoError>, synchronousLoad: Bool = false, tinyThumbnailData: TinyThumbnailData? = nil) -> Signal<(() -> CGSize?, (TransformImageArguments) -> DrawingContext?), NoError> { public func chatMessagePhotoInternal(photoData: Signal<(Data?, Data?, Bool), NoError>, synchronousLoad: Bool = false) -> Signal<(() -> CGSize?, (TransformImageArguments) -> DrawingContext?), NoError> {
return photoData return photoData
|> map { (thumbnailData, fullSizeData, fullSizeComplete) in |> map { (thumbnailData, fullSizeData, fullSizeComplete) in
return ({ return ({
@ -685,22 +685,7 @@ public func chatMessagePhotoInternal(photoData: Signal<(Data?, Data?, Bool), NoE
if let thumbnailData = thumbnailData, let imageSource = CGImageSourceCreateWithData(thumbnailData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) { if let thumbnailData = thumbnailData, let imageSource = CGImageSourceCreateWithData(thumbnailData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) {
thumbnailImage = image thumbnailImage = image
} }
if GlobalExperimentalSettings.forceTinyThumbnailsPreview {
if let fullSizeImageValue = fullSizeImage {
if let data = compressTinyThumbnail(UIImage(cgImage: fullSizeImageValue)) {
if let result = decompressTinyThumbnail(data: data) {
fullSizeImage = nil
thumbnailImage = result.cgImage
}
}
}
} else if let tinyThumbnailData = tinyThumbnailData {
if let result = decompressTinyThumbnail(data: tinyThumbnailData) {
thumbnailImage = result.cgImage
}
}
var blurredThumbnailImage: UIImage? var blurredThumbnailImage: UIImage?
if let thumbnailImage = thumbnailImage { if let thumbnailImage = thumbnailImage {
let thumbnailSize = CGSize(width: thumbnailImage.width, height: thumbnailImage.height) let thumbnailSize = CGSize(width: thumbnailImage.width, height: thumbnailImage.height)

View File

@ -305,13 +305,13 @@ private final class PlatformVideoContentNode: ASDisplayNode, UniversalVideoConte
self.player.seek(to: CMTime(seconds: timestamp, preferredTimescale: 30)) self.player.seek(to: CMTime(seconds: timestamp, preferredTimescale: 30))
} }
func playOnceWithSound(playAndRecord: Bool) { func playOnceWithSound(playAndRecord: Bool, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) {
} }
func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) { func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) {
} }
func continuePlayingWithoutSound() { func continuePlayingWithoutSound(actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) {
} }
func setBaseRate(_ baseRate: Double) { func setBaseRate(_ baseRate: Double) {

View File

@ -446,5 +446,5 @@ public func defaultPresentationData() -> PresentationData {
let nameSortOrder = currentPersonNameSortOrder() let nameSortOrder = currentPersonNameSortOrder()
let themeSettings = PresentationThemeSettings.defaultSettings let themeSettings = PresentationThemeSettings.defaultSettings
return PresentationData(strings: defaultPresentationStrings, theme: defaultPresentationTheme, chatWallpaper: .builtin, volumeControlStatusBarIcons: volumeControlStatusBarIcons(), fontSize: themeSettings.fontSize, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, nameSortOrder: nameSortOrder, disableAnimations: themeSettings.disableAnimations) return PresentationData(strings: defaultPresentationStrings, theme: defaultPresentationTheme, chatWallpaper: .builtin(WallpaperSettings()), volumeControlStatusBarIcons: volumeControlStatusBarIcons(), fontSize: themeSettings.fontSize, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, nameSortOrder: nameSortOrder, disableAnimations: themeSettings.disableAnimations)
} }

View File

@ -452,10 +452,11 @@ public func bubbleColorComponents(theme: PresentationTheme, incoming: Bool, wall
} }
public func bubbleVariableColor(variableColor: PresentationThemeVariableColor, wallpaper: TelegramWallpaper) -> UIColor { public func bubbleVariableColor(variableColor: PresentationThemeVariableColor, wallpaper: TelegramWallpaper) -> UIColor {
if wallpaper != .builtin && wallpaper != .color(0xffffff) { switch wallpaper {
return variableColor.withWallpaper case .builtin, .color(0xffffff):
} else { return variableColor.withoutWallpaper
return variableColor.withoutWallpaper default:
return variableColor.withWallpaper
} }
} }
@ -649,10 +650,11 @@ public func serviceMessageColorComponents(theme: PresentationTheme, wallpaper: T
} }
public func serviceMessageColorComponents(chatTheme: PresentationThemeChat, wallpaper: TelegramWallpaper) -> PresentationThemeServiceMessageColorComponents { public func serviceMessageColorComponents(chatTheme: PresentationThemeChat, wallpaper: TelegramWallpaper) -> PresentationThemeServiceMessageColorComponents {
if wallpaper != .builtin && wallpaper != .color(0xffffff) { switch wallpaper {
return chatTheme.serviceMessage.components.withCustomWallpaper case .builtin, .color(0xffffff):
} else { return chatTheme.serviceMessage.components.withDefaultWallpaper
return chatTheme.serviceMessage.components.withDefaultWallpaper default:
return chatTheme.serviceMessage.components.withCustomWallpaper
} }
} }

View File

@ -199,7 +199,7 @@ public struct PresentationThemeSettings: PreferencesEntry {
} }
public static var defaultSettings: PresentationThemeSettings { public static var defaultSettings: PresentationThemeSettings {
return PresentationThemeSettings(chatWallpaper: .builtin, theme: .builtin(.dayClassic), themeAccentColor: nil, themeSpecificChatWallpapers: [:], fontSize: .regular, automaticThemeSwitchSetting: AutomaticThemeSwitchSetting(trigger: .none, theme: .nightAccent), disableAnimations: true) return PresentationThemeSettings(chatWallpaper: .builtin(WallpaperSettings()), theme: .builtin(.dayClassic), themeAccentColor: nil, themeSpecificChatWallpapers: [:], fontSize: .regular, automaticThemeSwitchSetting: AutomaticThemeSwitchSetting(trigger: .none, theme: .nightAccent), disableAnimations: true)
} }
public init(chatWallpaper: TelegramWallpaper, theme: PresentationThemeReference, themeAccentColor: Int32?, themeSpecificChatWallpapers: Dictionary<Int64, TelegramWallpaper>, fontSize: PresentationFontSize, automaticThemeSwitchSetting: AutomaticThemeSwitchSetting, disableAnimations: Bool) { public init(chatWallpaper: TelegramWallpaper, theme: PresentationThemeReference, themeAccentColor: Int32?, themeSpecificChatWallpapers: Dictionary<Int64, TelegramWallpaper>, fontSize: PresentationFontSize, automaticThemeSwitchSetting: AutomaticThemeSwitchSetting, disableAnimations: Bool) {
@ -213,7 +213,7 @@ public struct PresentationThemeSettings: PreferencesEntry {
} }
public init(decoder: PostboxDecoder) { public init(decoder: PostboxDecoder) {
self.chatWallpaper = (decoder.decodeObjectForKey("w", decoder: { TelegramWallpaper(decoder: $0) }) as? TelegramWallpaper) ?? .builtin self.chatWallpaper = (decoder.decodeObjectForKey("w", decoder: { TelegramWallpaper(decoder: $0) }) as? TelegramWallpaper) ?? .builtin(WallpaperSettings())
self.theme = decoder.decodeObjectForKey("t", decoder: { PresentationThemeReference(decoder: $0) }) as! PresentationThemeReference self.theme = decoder.decodeObjectForKey("t", decoder: { PresentationThemeReference(decoder: $0) }) as! PresentationThemeReference
self.themeAccentColor = decoder.decodeOptionalInt32ForKey("themeAccentColor") self.themeAccentColor = decoder.decodeOptionalInt32ForKey("themeAccentColor")
self.themeSpecificChatWallpapers = decoder.decodeObjectDictionaryForKey("themeSpecificChatWallpapers", keyDecoder: { decoder in self.themeSpecificChatWallpapers = decoder.decodeObjectDictionaryForKey("themeSpecificChatWallpapers", keyDecoder: { decoder in

View File

@ -214,7 +214,7 @@ private final class RadialCloudProgressContentCancelNode: ASDisplayNode {
if let parameters = parameters as? RadialCloudProgressContentCancelNodeParameters { if let parameters = parameters as? RadialCloudProgressContentCancelNodeParameters {
let size: CGFloat = 8.0 let size: CGFloat = 8.0
context.setFillColor(parameters.color.cgColor) context.setFillColor(parameters.color.cgColor)
let path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: floor((bounds.size.width - size) / 2.0), y: floor((bounds.size.height - size) / 2.0)), size: CGSize(width: size, height: size)), cornerRadius: 1.0) let path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: floor((bounds.size.width - size) / 2.0), y: floor((bounds.size.height - size) / 2.0)), size: CGSize(width: size, height: size)), cornerRadius: 2.0)
path.fill() path.fill()
} }
} }

View File

@ -1,6 +1,10 @@
import Foundation import Foundation
func stringForDuration(_ duration: Int32) -> String { func stringForDuration(_ duration: Int32, position: Int32? = nil) -> String {
var duration = duration
if let position = position {
duration = duration - position
}
let hours = duration / 3600 let hours = duration / 3600
let minutes = duration / 60 % 60 let minutes = duration / 60 % 60
let seconds = duration % 60 let seconds = duration % 60
@ -14,3 +18,4 @@ func stringForDuration(_ duration: Int32) -> String {
} }

View File

@ -227,13 +227,13 @@ private final class SystemVideoContentNode: ASDisplayNode, UniversalVideoContent
//self.playerView.seek(toPosition: timestamp) //self.playerView.seek(toPosition: timestamp)
} }
func playOnceWithSound(playAndRecord: Bool) { func playOnceWithSound(playAndRecord: Bool, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) {
} }
func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) { func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) {
} }
func continuePlayingWithoutSound() { func continuePlayingWithoutSound(actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) {
} }
func setBaseRate(_ baseRate: Double) { func setBaseRate(_ baseRate: Double) {

View File

@ -195,7 +195,7 @@ final class ThemeGridController: ViewController {
for wallpaper in wallpapers { for wallpaper in wallpapers {
if wallpaper == strongSelf.presentationData.chatWallpaper { if wallpaper == strongSelf.presentationData.chatWallpaper {
let _ = (updatePresentationThemeSettingsInteractively(accountManager: strongSelf.context.sharedContext.accountManager, { current in let _ = (updatePresentationThemeSettingsInteractively(accountManager: strongSelf.context.sharedContext.accountManager, { current in
var fallbackWallpaper: TelegramWallpaper = .builtin var fallbackWallpaper: TelegramWallpaper = .builtin(WallpaperSettings())
if case let .builtin(theme) = current.theme { if case let .builtin(theme) = current.theme {
switch theme { switch theme {
case .day: case .day:
@ -205,7 +205,7 @@ final class ThemeGridController: ViewController {
case .nightAccent: case .nightAccent:
fallbackWallpaper = .color(0x18222d) fallbackWallpaper = .color(0x18222d)
default: default:
fallbackWallpaper = .builtin fallbackWallpaper = .builtin(WallpaperSettings())
} }
} }
@ -278,10 +278,10 @@ final class ThemeGridController: ViewController {
case .nightAccent: case .nightAccent:
wallpaper = .color(0x18222d) wallpaper = .color(0x18222d)
default: default:
wallpaper = .builtin wallpaper = .builtin(WallpaperSettings())
} }
} else { } else {
wallpaper = .builtin wallpaper = .builtin(WallpaperSettings())
} }
return PresentationThemeSettings(chatWallpaper: wallpaper, theme: current.theme, themeAccentColor: current.themeAccentColor, themeSpecificChatWallpapers: [:], fontSize: current.fontSize, automaticThemeSwitchSetting: current.automaticThemeSwitchSetting, disableAnimations: current.disableAnimations) return PresentationThemeSettings(chatWallpaper: wallpaper, theme: current.theme, themeAccentColor: current.themeAccentColor, themeSpecificChatWallpapers: [:], fontSize: current.fontSize, automaticThemeSwitchSetting: current.automaticThemeSwitchSetting, disableAnimations: current.disableAnimations)
}) })
@ -445,10 +445,6 @@ final class ThemeGridController: ViewController {
themeSpecificChatWallpapers[current.theme.index] = wallpaper themeSpecificChatWallpapers[current.theme.index] = wallpaper
return PresentationThemeSettings(chatWallpaper: wallpaper, theme: current.theme, themeAccentColor: current.themeAccentColor, themeSpecificChatWallpapers: themeSpecificChatWallpapers, fontSize: current.fontSize, automaticThemeSwitchSetting: current.automaticThemeSwitchSetting, disableAnimations: current.disableAnimations) return PresentationThemeSettings(chatWallpaper: wallpaper, theme: current.theme, themeAccentColor: current.themeAccentColor, themeSpecificChatWallpapers: themeSpecificChatWallpapers, fontSize: current.fontSize, automaticThemeSwitchSetting: current.automaticThemeSwitchSetting, disableAnimations: current.disableAnimations)
})).start() })).start()
if let strongSelf = self, case .file = wallpaper {
strongSelf.controllerNode.updateWallpapers()
}
} }
let apply: () -> Void = { let apply: () -> Void = {

View File

@ -156,7 +156,6 @@ private func selectedWallpapers(entries: [ThemeGridControllerEntry]?, state: The
guard let entries = entries, state.editing else { guard let entries = entries, state.editing else {
return [] return []
} }
var wallpapers: [TelegramWallpaper] = [] var wallpapers: [TelegramWallpaper] = []
for entry in entries { for entry in entries {
if case let .file(file) = entry.wallpaper { if case let .file(file) = entry.wallpaper {

View File

@ -288,7 +288,7 @@ public func themeSettingsController(context: AccountContext) -> ViewController {
case 3: case 3:
wallpaper = .color(0x18222d) wallpaper = .color(0x18222d)
default: default:
wallpaper = .builtin wallpaper = .builtin(WallpaperSettings())
} }
} }

View File

@ -459,6 +459,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
} }
} }
videoNode.playOnceWithSound(playAndRecord: false, actionAtEnd: .stop)
self._ready.set(videoNode.ready) self._ready.set(videoNode.ready)
} }
@ -698,6 +699,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
}) })
pictureInPictureNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) pictureInPictureNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
} }
videoNode.continuePlayingWithoutSound()
} }
func animateOut(toOverlay node: ASDisplayNode, completion: @escaping () -> Void) { func animateOut(toOverlay node: ASDisplayNode, completion: @escaping () -> Void) {

View File

@ -17,9 +17,9 @@ protocol UniversalVideoContentNode: class {
func togglePlayPause() func togglePlayPause()
func setSoundEnabled(_ value: Bool) func setSoundEnabled(_ value: Bool)
func seek(_ timestamp: Double) func seek(_ timestamp: Double)
func playOnceWithSound(playAndRecord: Bool) func playOnceWithSound(playAndRecord: Bool, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd)
func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool)
func continuePlayingWithoutSound() func continuePlayingWithoutSound(actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd)
func setBaseRate(_ baseRate: Double) func setBaseRate(_ baseRate: Double)
func addPlaybackCompleted(_ f: @escaping () -> Void) -> Int func addPlaybackCompleted(_ f: @escaping () -> Void) -> Int
func removePlaybackCompleted(_ index: Int) func removePlaybackCompleted(_ index: Int)
@ -85,7 +85,6 @@ final class UniversalVideoNode: ASDisplayNode {
private let autoplay: Bool private let autoplay: Bool
private let snapshotContentWhenGone: Bool private let snapshotContentWhenGone: Bool
private var contentNode: (UniversalVideoContentNode & ASDisplayNode)? private var contentNode: (UniversalVideoContentNode & ASDisplayNode)?
private var contentNodeId: Int32? private var contentNodeId: Int32?
@ -270,10 +269,10 @@ final class UniversalVideoNode: ASDisplayNode {
}) })
} }
func playOnceWithSound(playAndRecord: Bool) { func playOnceWithSound(playAndRecord: Bool, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd = .loopDisablingSound) {
self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in
if let contentNode = contentNode { if let contentNode = contentNode {
contentNode.playOnceWithSound(playAndRecord: playAndRecord) contentNode.playOnceWithSound(playAndRecord: playAndRecord, actionAtEnd: actionAtEnd)
} }
}) })
} }
@ -294,10 +293,10 @@ final class UniversalVideoNode: ASDisplayNode {
}) })
} }
func continuePlayingWithoutSound() { func continuePlayingWithoutSound(actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd = .loopDisablingSound) {
self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in
if let contentNode = contentNode { if let contentNode = contentNode {
contentNode.continuePlayingWithoutSound() contentNode.continuePlayingWithoutSound(actionAtEnd: actionAtEnd)
} }
}) })
} }

View File

@ -139,7 +139,15 @@ public func upgradedAccounts(accountManager: AccountManager, rootPath: String) -
if let path = mediaBox.completedResourcePath(file.file.resource), let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { if let path = mediaBox.completedResourcePath(file.file.resource), let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) {
accountManager.mediaBox.storeResourceData(file.file.resource.id, data: data) accountManager.mediaBox.storeResourceData(file.file.resource.id, data: data)
let _ = accountManager.mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 720.0, height: 720.0), mode: .aspectFit), complete: true, fetch: true).start() let _ = accountManager.mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 720.0, height: 720.0), mode: .aspectFit), complete: true, fetch: true).start()
let _ = accountManager.mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedBlurredWallpaperRepresentation(), complete: true, fetch: true).start() if file.isPattern {
if let color = file.settings.color, let intensity = file.settings.intensity {
let _ = accountManager.mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedPatternWallpaperRepresentation(color: color, intensity: intensity), complete: true, fetch: true).start()
}
} else {
if file.settings.blur {
let _ = accountManager.mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedBlurredWallpaperRepresentation(), complete: true, fetch: true).start()
}
}
} }
case let .image(representations, _): case let .image(representations, _):
for representation in representations { for representation in representations {

View File

@ -96,6 +96,9 @@ final class WallpaperUploadManager {
let sharedContext = self.sharedContext let sharedContext = self.sharedContext
let account = self.account let account = self.account
disposable.set(uploadWallpaper(account: account, resource: currentResource, settings: currentWallpaper.settings ?? WallpaperSettings()).start(next: { [weak self] status in disposable.set(uploadWallpaper(account: account, resource: currentResource, settings: currentWallpaper.settings ?? WallpaperSettings()).start(next: { [weak self] status in
guard let strongSelf = self else {
return
}
if case let .complete(wallpaper) = status { if case let .complete(wallpaper) = status {
let updateWallpaper: (TelegramWallpaper) -> Void = { wallpaper in let updateWallpaper: (TelegramWallpaper) -> Void = { wallpaper in
if let resource = wallpaper.mainResource { if let resource = wallpaper.mainResource {
@ -103,7 +106,7 @@ final class WallpaperUploadManager {
let _ = sharedContext.accountManager.mediaBox.cachedResourceRepresentation(resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 720.0, height: 720.0), mode: .aspectFit), complete: true, fetch: true).start(completed: {}) let _ = sharedContext.accountManager.mediaBox.cachedResourceRepresentation(resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 720.0, height: 720.0), mode: .aspectFit), complete: true, fetch: true).start(completed: {})
} }
if self?.currentPresentationData?.theme.name == presentationData.theme.name { if strongSelf.currentPresentationData?.theme.name == presentationData.theme.name {
let _ = (updatePresentationThemeSettingsInteractively(accountManager: sharedContext.accountManager, { current in let _ = (updatePresentationThemeSettingsInteractively(accountManager: sharedContext.accountManager, { current in
let updatedWallpaper: TelegramWallpaper let updatedWallpaper: TelegramWallpaper
if let currentSettings = current.chatWallpaper.settings { if let currentSettings = current.chatWallpaper.settings {

View File

@ -141,13 +141,13 @@ private final class WebEmbedVideoContentNode: ASDisplayNode, UniversalVideoConte
self.playerNode.seek(timestamp: timestamp) self.playerNode.seek(timestamp: timestamp)
} }
func playOnceWithSound(playAndRecord: Bool) { func playOnceWithSound(playAndRecord: Bool, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) {
} }
func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) { func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) {
} }
func continuePlayingWithoutSound() { func continuePlayingWithoutSound(actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) {
} }
func setBaseRate(_ baseRate: Double) { func setBaseRate(_ baseRate: Double) {