diff --git a/Images.xcassets/Chat/Message/InlineVideoMute.imageset/Contents.json b/Images.xcassets/Chat/Message/InlineVideoMute.imageset/Contents.json new file mode 100644 index 0000000000..f7db2122bf --- /dev/null +++ b/Images.xcassets/Chat/Message/InlineVideoMute.imageset/Contents.json @@ -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" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat/Message/InlineVideoMute.imageset/Muted@2x.png b/Images.xcassets/Chat/Message/InlineVideoMute.imageset/Muted@2x.png new file mode 100644 index 0000000000..86ddc8f794 Binary files /dev/null and b/Images.xcassets/Chat/Message/InlineVideoMute.imageset/Muted@2x.png differ diff --git a/Images.xcassets/Chat/Message/InlineVideoMute.imageset/Muted@3x.png b/Images.xcassets/Chat/Message/InlineVideoMute.imageset/Muted@3x.png new file mode 100644 index 0000000000..e9912719d1 Binary files /dev/null and b/Images.xcassets/Chat/Message/InlineVideoMute.imageset/Muted@3x.png differ diff --git a/TelegramUI/AutomaticMediaDownloadSettings.swift b/TelegramUI/AutomaticMediaDownloadSettings.swift index 9dd98b5260..c2ca6e9ba0 100644 --- a/TelegramUI/AutomaticMediaDownloadSettings.swift +++ b/TelegramUI/AutomaticMediaDownloadSettings.swift @@ -99,8 +99,8 @@ public struct AutomaticMediaDownloadSettings: PreferencesEntry, Equatable { public static var defaultSettings: AutomaticMediaDownloadSettings { let defaultCategory = AutomaticMediaDownloadCategories( - photo: AutomaticMediaDownloadCategory(cellular: true, wifi: true, sizeLimit: 1 * 1024 * 1024), - video: AutomaticMediaDownloadCategory(cellular: false, wifi: false, sizeLimit: 1 * 1024 * 1024), + photo: AutomaticMediaDownloadCategory(cellular: true, wifi: true, sizeLimit: Int32.max), + video: AutomaticMediaDownloadCategory(cellular: true, wifi: true, sizeLimit: 10 * 1024 * 1024), file: AutomaticMediaDownloadCategory(cellular: false, wifi: false, sizeLimit: 1 * 1024 * 1024), voiceMessage: AutomaticMediaDownloadCategory(cellular: true, wifi: true, sizeLimit: 1 * 1024 * 1024), videoMessage: AutomaticMediaDownloadCategory(cellular: true, wifi: true, sizeLimit: 4 * 1024 * 1024), diff --git a/TelegramUI/CallController.swift b/TelegramUI/CallController.swift index 2bbaea599c..a1a3818169 100644 --- a/TelegramUI/CallController.swift +++ b/TelegramUI/CallController.swift @@ -172,7 +172,7 @@ public final class CallController: ViewController { Queue.mainQueue().after(0.5, { 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 { c.presentationArguments = a window.present(c, on: .root, blockInteraction: false, completion: {}) diff --git a/TelegramUI/CallFeedbackController.swift b/TelegramUI/CallFeedbackController.swift index ffc24fd9de..39656f649b 100644 --- a/TelegramUI/CallFeedbackController.swift +++ b/TelegramUI/CallFeedbackController.swift @@ -147,7 +147,7 @@ private enum CallFeedbackControllerEntry: ItemListNodeEntry { case let .reasonsHeader(theme, text): return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) 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) }) case let .comment(theme, text, placeholder): diff --git a/TelegramUI/ChatBubbleVideoDecoration.swift b/TelegramUI/ChatBubbleVideoDecoration.swift index 6dd3d711fb..8153812f67 100644 --- a/TelegramUI/ChatBubbleVideoDecoration.swift +++ b/TelegramUI/ChatBubbleVideoDecoration.swift @@ -5,6 +5,7 @@ import SwiftSignalKit final class ChatBubbleVideoDecoration: UniversalVideoDecoration { private let nativeSize: CGSize + private let contentMode: InteractiveMediaNodeContentMode let backgroundNode: ASDisplayNode? = nil let contentContainerNode: ASDisplayNode @@ -14,11 +15,12 @@ final class ChatBubbleVideoDecoration: UniversalVideoDecoration { private var validLayoutSize: CGSize? - init(cornerRadius: CGFloat, nativeSize: CGSize, backgroudColor: UIColor) { + init(cornerRadius: CGFloat, nativeSize: CGSize, contentMode: InteractiveMediaNodeContentMode, backgroundColor: UIColor) { self.nativeSize = nativeSize + self.contentMode = contentMode self.contentContainerNode = ASDisplayNode() - self.contentContainerNode.backgroundColor = backgroudColor + self.contentContainerNode.backgroundColor = backgroundColor self.contentContainerNode.clipsToBounds = true self.contentContainerNode.cornerRadius = cornerRadius } @@ -38,7 +40,13 @@ final class ChatBubbleVideoDecoration: UniversalVideoDecoration { if contentNode.supernode !== self.contentContainerNode { self.contentContainerNode.addSubnode(contentNode) 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 { scaledSize.width = size.width } @@ -68,7 +76,13 @@ final class ChatBubbleVideoDecoration: UniversalVideoDecoration { } transition.updateFrame(node: self.contentContainerNode, frame: CGRect(origin: CGPoint(), size: size)) 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 { scaledSize.width = size.width } diff --git a/TelegramUI/ChatMessageAttachedContentNode.swift b/TelegramUI/ChatMessageAttachedContentNode.swift index 8d357d9963..fd840a5af1 100644 --- a/TelegramUI/ChatMessageAttachedContentNode.swift +++ b/TelegramUI/ChatMessageAttachedContentNode.swift @@ -386,12 +386,12 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { contentInstantVideoSizeAndApply = (videoLayout, apply) } else if file.isVideo { 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 refineContentImageLayout = refineLayout } else if file.isSticker, let _ = file.dimensions { 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 refineContentImageLayout = refineLayout } else { @@ -416,7 +416,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { } else if let image = media as? TelegramMediaImage { if !flags.contains(.preferMediaInline) { 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 refineContentImageLayout = refineLayout } else if let dimensions = largestImageRepresentation(image.representations)?.dimensions { @@ -428,11 +428,11 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { } } else if let image = media as? TelegramMediaWebFile { 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 refineContentImageLayout = refineLayout } 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 refineContentImageLayout = refineLayout } diff --git a/TelegramUI/ChatMessageBubbleItemNode.swift b/TelegramUI/ChatMessageBubbleItemNode.swift index 17d86234c9..30439db61e 100644 --- a/TelegramUI/ChatMessageBubbleItemNode.swift +++ b/TelegramUI/ChatMessageBubbleItemNode.swift @@ -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 { avatarInset = layoutConstants.avatarDiameter } else { @@ -436,7 +443,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { tmpWidth -= 38.0 } } else { - tmpWidth = layoutConstants.bubble.maximumWidthFill.widthFor(baseWidth) + tmpWidth = isInlinePlayableVideo ? baseWidth - 36.0 : layoutConstants.bubble.maximumWidthFill.widthFor(baseWidth) if needShareButton && tmpWidth + 32.0 > baseWidth { tmpWidth = baseWidth - 32.0 } diff --git a/TelegramUI/ChatMessageInteractiveMediaBadge.swift b/TelegramUI/ChatMessageInteractiveMediaBadge.swift index 8c91b3fabf..1129b2e555 100644 --- a/TelegramUI/ChatMessageInteractiveMediaBadge.swift +++ b/TelegramUI/ChatMessageInteractiveMediaBadge.swift @@ -9,14 +9,14 @@ enum ChatMessageInteractiveMediaBadgeShape: Equatable { enum ChatMessageInteractiveMediaDownloadState: Equatable { case remote - case fetching(progress: Float) + case fetching(progress: Float?) case compactRemote case compactFetching(progress: Float) } enum ChatMessageInteractiveMediaBadgeContent: Equatable { 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 { switch lhs { @@ -26,8 +26,8 @@ enum ChatMessageInteractiveMediaBadgeContent: Equatable { } else { return false } - case let .mediaDownload(lhsBackgroundColor, lhsForegroundColor, lhsDuration, lhsSize): - if case let .mediaDownload(rhsBackgroundColor, rhsForegroundColor, rhsDuration, rhsSize) = rhs, lhsBackgroundColor.isEqual(rhsBackgroundColor), lhsForegroundColor.isEqual(rhsForegroundColor), lhsDuration == rhsDuration, lhsSize == rhsSize { + case let .mediaDownload(lhsBackgroundColor, lhsForegroundColor, lhsDuration, lhsSize, lhsMuted, lhsActive): + 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 } else { return false @@ -39,27 +39,28 @@ enum ChatMessageInteractiveMediaBadgeContent: Equatable { private let font = Font.regular(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 { + private var content: ChatMessageInteractiveMediaBadgeContent? var pressed: (() -> Void)? - private var content: ChatMessageInteractiveMediaBadgeContent? - - private var mediaDownloadStatusNode: RadialStatusNode? 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() { + self.backgroundNode = ASImageNode() + self.durationNode = ASTextNode() + super.init() - self.contentMode = .topLeft - self.contentsScale = UIScreenScale + self.addSubnode(self.backgroundNode) + self.addSubnode(self.durationNode) } 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) { if self.content != content { self.content = content @@ -110,7 +101,11 @@ final class ChatMessageInteractiveMediaBadge: ASDisplayNode { state = .none } 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: state = .download(.white) isCompact = true @@ -131,78 +126,97 @@ final class ChatMessageInteractiveMediaBadge: ASDisplayNode { mediaDownloadStatusNode.removeFromSupernode() } } - } - - override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { - return ChatMessageInteractiveMediaBadgeParams(content: self.content) - } - - @objc override public class func display(withParameters: Any?, isCancelled: () -> Bool) -> UIImage? { - if let content = (withParameters as? ChatMessageInteractiveMediaBadgeParams)?.content { + + var contentSize = CGSize() + let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate + + if let content = self.content { switch content { 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]) text.enumerateAttributes(in: NSRange(location: 0, length: text.length), options: []) { attributes, range, _ in if let _ = attributes[ChatTextInputAttributes.bold] { convertedText.addAttribute(.font, value: boldFont, range: range) } } - let textRect = convertedText.boundingRect(with: CGSize(width: 200.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil) - let imageSize = CGSize(width: inset + ceil(textRect.size.width) + 10.0, height: 18.0) - return generateImage(imageSize, rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size)) - context.setBlendMode(.copy) - context.setFillColor(backgroundColor.cgColor) - switch shape { - case .round: - 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): + self.durationNode.attributedText = convertedText + let durationSize = self.durationNode.measure(CGSize(width: 160.0, height: 160.0)) + self.durationNode.frame = CGRect(x: 7.0, y: 2.0, width: durationSize.width, height: durationSize.height) + contentSize = CGSize(width: durationSize.width + 14.0, height: 18.0) + case let .mediaDownload(backgroundColor, foregroundColor, duration, size, muted, active): + if self.backgroundNodeColor != backgroundColor { + self.backgroundNodeColor = backgroundColor + self.backgroundNode.image = generateStretchableFilledCircleImage(radius: 9.0, color: backgroundColor) + } + let durationString = NSMutableAttributedString(string: duration, attributes: [.font: font, .foregroundColor: foregroundColor]) - let sizeString = NSMutableAttributedString(string: size, attributes: [.font: font, .foregroundColor: foregroundColor]) - 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) - let leftInset: CGFloat = 42.0 - let imageSize = CGSize(width: leftInset + max(ceil(durationRect.width), ceil(sizeRect.width)) + 10.0, height: 40.0) - return generateImage(imageSize, rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size)) - context.setBlendMode(.copy) - context.setFillColor(backgroundColor.cgColor) + self.durationNode.attributedText = durationString + + var sizeSize: CGSize = CGSize() + if let size = size { + let sizeNode: ASTextNode + if let current = self.sizeNode { + sizeNode = current + } else { + sizeNode = ASTextNode() + self.sizeNode = sizeNode + self.addSubnode(sizeNode) + } - let radius: CGFloat = 12.0 - 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))) + let sizeString = NSMutableAttributedString(string: size, attributes: [.font: font, .foregroundColor: foregroundColor]) + sizeNode.attributedText = sizeString + sizeSize = sizeNode.measure(CGSize(width: 160.0, height: 160.0)) + sizeNode.frame = CGRect(x: active ? 42.0 : 7.0, y: active ? 19.0 : 2.0, width: sizeSize.width, height: sizeSize.height) + transition.updateAlpha(node: sizeNode, alpha: 1.0) + } else if let sizeNode = self.sizeNode { + transition.updateAlpha(node: sizeNode, alpha: 0.0) + } + + 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) - UIGraphicsPushContext(context) - durationString.draw(at: CGPoint(x: leftInset + durationRect.origin.x, y: 7.0 + durationRect.origin.y)) - sizeString.draw(at: CGPoint(x: leftInset + sizeRect.origin.x, y: 21.0 + sizeRect.origin.y)) - UIGraphicsPopContext() - }) + if self.foregroundColor != foregroundColor { + self.foregroundColor = foregroundColor + iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/InlineVideoMute"), color: foregroundColor) + } + 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) } } + diff --git a/TelegramUI/ChatMessageInteractiveMediaNode.swift b/TelegramUI/ChatMessageInteractiveMediaNode.swift index 0c68f00895..79f730e7e9 100644 --- a/TelegramUI/ChatMessageInteractiveMediaNode.swift +++ b/TelegramUI/ChatMessageInteractiveMediaNode.swift @@ -25,6 +25,12 @@ enum InteractiveMediaNodeActivateContent { case stream } +enum InteractiveMediaNodeAutodownloadMode { + case none + case prefetch + case full +} + final class ChatMessageInteractiveMediaNode: ASDisplayNode { private let imageNode: TransformImageNode private var currentImageArguments: TransformImageArguments? @@ -38,7 +44,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode { private var media: Media? private var themeAndStrings: (PresentationTheme, PresentationStrings)? private var sizeCalculation: InteractiveMediaNodeSizeCalculation? - private var automaticDownload: Bool? + private var automaticDownload: InteractiveMediaNodeAutodownloadMode? private var automaticPlayback: Bool? private let statusDisposable = MetaDisposable() @@ -46,8 +52,12 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode { private var fetchStatus: MediaResourceStatus? private let fetchDisposable = MetaDisposable() + private let playerStatusDisposable = MetaDisposable() + private var playerStatus: MediaPlayerStatus? + private var secretTimer: SwiftSignalKit.Timer? + var visibilityPromise = ValuePromise(.none) var visibility: ListViewItemNodeVisibility = .none { didSet { if let videoNode = self.videoNode { @@ -61,6 +71,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode { videoNode.canAttachContent = false } } + self.visibilityPromise.set(self.visibility) } } @@ -78,6 +89,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode { deinit { self.statusDisposable.dispose() + self.playerStatusDisposable.dispose() self.fetchDisposable.dispose() self.secretTimer?.invalidate() } @@ -92,7 +104,13 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode { private func progressPressed(canActivate: Bool) { 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 { case .Remote, .Fetching: 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 currentMedia = self.media let imageLayout = self.imageNode.asyncLayout() @@ -179,6 +197,8 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode { var isInlinePlayableVideo = false + var maxDimensions = layoutConstants.image.maxDimensions + var unboundSize: CGSize 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)) @@ -193,10 +213,12 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode { unboundSize = CGSize(width: floor(dimensions.width * 0.5), height: floor(dimensions.height * 0.5)) if file.isAnimated { 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 { 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 { unboundSize = CGSize(width: floor(dimensions.width * 0.5), height: floor(dimensions.height * 0.5)) } else if let wallpaper = media as? WallpaperPreviewMedia { @@ -230,7 +252,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode { if isSecretMedia { maxWidth = 180.0 } else { - maxWidth = layoutConstants.image.maxDimensions.width + maxWidth = maxDimensions.width } if isSecretMedia { let _ = PresentationResourcesChat.chatBubbleSecretMediaIcon(theme) @@ -244,8 +266,8 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode { if isSecretMedia { resultWidth = maxWidth } else { - let maxFittedSize = nativeSize.aspectFitted (layoutConstants.image.maxDimensions) - resultWidth = min(nativeSize.width, min(maxFittedSize.width, min(constrainedSize.width, layoutConstants.image.maxDimensions.width))) + let maxFittedSize = nativeSize.aspectFitted(maxDimensions) + resultWidth = min(nativeSize.width, min(maxFittedSize.width, min(constrainedSize.width, maxDimensions.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)) } } else { - let tinyThumbnailData: TinyThumbnailData? - if GlobalExperimentalSettings.enableTinyThumbnails { - tinyThumbnailData = parseTinyThumbnail(message.text) - } else { - tinyThumbnailData = nil - } 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 if hasCurrentVideoNode { if let currentFile = currentMedia as? TelegramMediaFile, currentFile.resource is EmptyMediaResource { @@ -507,6 +523,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode { statusNode.frame = statusFrame } + var updatedPlayerStatusSignal: Signal? if let replaceVideoNode = replaceVideoNode { if let videoNode = strongSelf.videoNode { videoNode.canAttachContent = false @@ -517,11 +534,13 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode { if replaceVideoNode, let updatedVideoFile = updateVideoFile { let mediaManager = context.sharedContext.mediaManager 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 strongSelf.videoNode = videoNode 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 { let _ = strongSelf.fetchControls.swap(updatedFetchControls) - if automaticDownload { + if case .full = automaticDownload { if let _ = media as? TelegramMediaImage { updatedFetchControls.fetch(false) } else if let image = media as? TelegramMediaWebFile { strongSelf.fetchDisposable.set(chatMessageWebFileInteractiveFetched(account: context.account, image: image).start()) } else if let file = media as? TelegramMediaFile { 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 in + switch visibility { + case .visible, .nearlyVisible: + return fetchSignal + |> mapToSignal { _ -> Signal 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 = 0 ..< 2 * 1024 * 1024 + let fetchTailRange: Range = 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 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 in + return .complete() + } + + let visibilityAwareFetchSignal = strongSelf.visibilityPromise.get() + |> mapToSignal { visibility -> Signal in + switch visibility { + case .visible, .nearlyVisible: + return combineLatest(fetchHeadSignal, fetchTailSignal) + |> mapToSignal { _ -> Signal 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) } @@ -636,7 +714,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode { } } 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" { progressRequired = true } 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) } 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 { case let .Fetching(_, progress): let adjustedProgress = max(progress, 0.027) @@ -716,11 +803,14 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode { if let size = file.size { if let duration = file.duration, !message.flags.contains(.Unsent) { 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))" - badgeContent = .mediaDownload(backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, duration: durationString, size: sizeString) - mediaDownloadState = .fetching(progress: progress) - state = .play(bubbleTheme.mediaOverlayControlForegroundColor) + badgeContent = .mediaDownload(backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, duration: durationString, size: active ? sizeString : nil, muted: automaticPlayback, active: active) + mediaDownloadState = .fetching(progress: automaticPlayback ? nil : progress) + if self.playerStatus?.status == .playing { + mediaDownloadState = nil + } + state = automaticPlayback ? .none : .play(bubbleTheme.mediaOverlayControlForegroundColor) } 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))")) } @@ -739,12 +829,12 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode { } } else { 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)) } } } - } 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) } case .Local: @@ -760,8 +850,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode { } else if isSecretMedia, let secretProgressIcon = secretProgressIcon { state = .customIcon(secretProgressIcon) } 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 { state = .play(bubbleTheme.mediaOverlayControlForegroundColor) } else { @@ -775,17 +864,17 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode { } } if let file = media as? TelegramMediaFile, let duration = file.duration, !file.isAnimated { - let durationString = stringForDuration(duration) - badgeContent = .text(inset: 0.0, backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, shape: .round, text: NSAttributedString(string: durationString)) + let durationString = stringForDuration(duration, position: position) + badgeContent = .mediaDownload(backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, duration: durationString, size: nil, muted: true, active: false) } case .Remote: state = .download(bubbleTheme.mediaOverlayControlForegroundColor) 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 isMediaStreamable(message: message, media: file) { - state = .play(bubbleTheme.mediaOverlayControlForegroundColor) - badgeContent = .mediaDownload(backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, duration: durationString, size: dataSizeString(file.size ?? 0)) + state = automaticPlayback ? .none : .play(bubbleTheme.mediaOverlayControlForegroundColor) + badgeContent = .mediaDownload(backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, duration: durationString, size: dataSizeString(file.size ?? 0), muted: automaticPlayback, active: true) mediaDownloadState = .remote } else { 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)) } } - } 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) } } @@ -845,7 +934,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode { self.badgeNode = 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 { self.badgeNode = nil 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() return { context, theme, strings, message, media, automaticDownload, peerType, automaticPlayback, sizeCalculation, layoutConstants, contentMode in 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 { imageNode = node diff --git a/TelegramUI/ChatMessageMediaBubbleContentNode.swift b/TelegramUI/ChatMessageMediaBubbleContentNode.swift index 2f4241508a..92592ba79c 100644 --- a/TelegramUI/ChatMessageMediaBubbleContentNode.swift +++ b/TelegramUI/ChatMessageMediaBubbleContentNode.swift @@ -50,14 +50,32 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { return { item, layoutConstants, preparePosition, selection, constrainedSize in 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 { if let telegramImage = media as? TelegramMediaImage { 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 { 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 } - 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 - if let media = selectedMedia as? TelegramMediaFile, media.isAnimated { + if let media = selectedMedia as? TelegramMediaFile, media.isVideo || media.isAnimated { forceFullCorners = true } diff --git a/TelegramUI/FetchVideoThumbnail.swift b/TelegramUI/FetchVideoThumbnail.swift index b206c9be41..abfa807e18 100644 --- a/TelegramUI/FetchVideoThumbnail.swift +++ b/TelegramUI/FetchVideoThumbnail.swift @@ -19,7 +19,7 @@ private func readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: Unsa } } return -1 - //return Int32(bufferPointer) + return Int32(bufferPointer) } private func seekCallback(userData: UnsafeMutableRawPointer?, offset: Int64, whence: Int32) -> Int64 { @@ -64,14 +64,14 @@ private final class FetchVideoThumbnailSource { private var videoStream: SoftwareVideoStream? private var avIoContext: UnsafeMutablePointer? private var avFormatContext: UnsafeMutablePointer? - + init(mediaBox: MediaBox, resourceReference: MediaResourceReference, size: Int32) { let _ = FFMpegMediaFrameSourceContextHelpers.registerFFMpegGlobals self.mediaBox = mediaBox self.resourceReference = resourceReference self.size = size - + var avFormatContextRef = avformat_alloc_context() guard let avFormatContext = avFormatContextRef else { self.readingError = true @@ -82,7 +82,7 @@ private final class FetchVideoThumbnailSource { 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) self.avIoContext = avIoContextRef - + avFormatContext.pointee.pb = self.avIoContext guard avformat_open_input(&avFormatContextRef, nil, nil, nil) >= 0 else { @@ -94,7 +94,7 @@ private final class FetchVideoThumbnailSource { self.readingError = true return } - + self.avFormatContext = avFormatContext 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 duration = CMTimeMake(avFormatContext.pointee.streams.advanced(by: streamIndex).pointee!.pointee.duration, timebase.timescale) - + 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 strcmp(value, "0") != 0 { @@ -178,10 +178,10 @@ private final class FetchVideoThumbnailSource { let avNoPtsRawValue: UInt64 = 0x8000000000000000 let avNoPtsValue = Int64(bitPattern: avNoPtsRawValue) let packetPts = packet.packet.pts == avNoPtsValue ? packet.packet.dts : packet.packet.pts - + let pts = CMTimeMake(packetPts, videoStream.timebase.timescale) let dts = CMTimeMake(packet.packet.dts, videoStream.timebase.timescale) - + let duration: CMTime let frameDuration = packet.packet.duration @@ -198,7 +198,7 @@ private final class FetchVideoThumbnailSource { self.readingError = true } } - + return frames.first } @@ -264,7 +264,7 @@ private final class FetchVideoThumbnailSourceThreadImpl: NSObject { } } } - + @objc func fetch(_ parameters: FetchVideoThumbnailSourceParameters) { let source = FetchVideoThumbnailSource(mediaBox: parameters.mediaBox, resourceReference: parameters.resourceReference, size: parameters.size) let _ = source.readFrame() @@ -396,7 +396,7 @@ func streamingVideoThumbnail(postbox: Postbox, fileReference: FileMediaReference let impl = FetchVideoThumbnailSourceThreadImpl() let thread = Thread(target: impl, selector: #selector(impl.entryPoint), object: nil) 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() return ActionDisposable { diff --git a/TelegramUI/GlobalExperimentalSettings.swift b/TelegramUI/GlobalExperimentalSettings.swift index 4df6dd8ba0..54dfa899e6 100644 --- a/TelegramUI/GlobalExperimentalSettings.swift +++ b/TelegramUI/GlobalExperimentalSettings.swift @@ -3,7 +3,5 @@ import Foundation public struct GlobalExperimentalSettings { public static var isAppStoreBuild: 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 } diff --git a/TelegramUI/ImageTransparency.swift b/TelegramUI/ImageTransparency.swift index a484761fd8..252896e3fb 100644 --- a/TelegramUI/ImageTransparency.swift +++ b/TelegramUI/ImageTransparency.swift @@ -57,7 +57,7 @@ func imageHasTransparency(_ cgImage: CGImage) -> Bool { transparentCount += histogramBins[alphaBinIndex][i] } let totalCount = opaqueCount + transparentCount - return Double(transparentCount) / Double(totalCount) > 0.02 + return Double(transparentCount) / Double(totalCount) > 0.05 } return false } diff --git a/TelegramUI/ItemListSwitchItem.swift b/TelegramUI/ItemListSwitchItem.swift index 8ad810cbaf..f3f8ecbc77 100644 --- a/TelegramUI/ItemListSwitchItem.swift +++ b/TelegramUI/ItemListSwitchItem.swift @@ -15,17 +15,19 @@ class ItemListSwitchItem: ListViewItem, ItemListItem { let type: ItemListSwitchItemNodeType let enableInteractiveChanges: Bool let enabled: Bool + let maximumNumberOfLines: Int let sectionId: ItemListSectionId let style: ItemListStyle 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.title = title self.value = value self.type = type self.enableInteractiveChanges = enableInteractiveChanges self.enabled = enabled + self.maximumNumberOfLines = maximumNumberOfLines self.sectionId = sectionId self.style = style self.updated = updated @@ -154,7 +156,7 @@ class ItemListSwitchItemNode: ListViewItemNode { var currentDisabledOverlayNode = self.disabledOverlayNode return { item, params, neighbors in - let contentSize: CGSize + var contentSize: CGSize let insets: UIEdgeInsets let separatorHeight = UIScreenPixel let itemBackgroundColor: UIColor @@ -179,7 +181,9 @@ class ItemListSwitchItemNode: ListViewItemNode { 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 currentDisabledOverlayNode == nil { diff --git a/TelegramUI/LegacyMediaPickers.swift b/TelegramUI/LegacyMediaPickers.swift index f375bd281c..37b94f4dca 100644 --- a/TelegramUI/LegacyMediaPickers.swift +++ b/TelegramUI/LegacyMediaPickers.swift @@ -303,13 +303,7 @@ func legacyAssetPickerEnqueueMessages(account: Account, signals: [Any]) -> Signa attributes.append(AutoremoveTimeoutMessageAttribute(timeout: Int32(timer), countdownBeginTime: nil)) } - var text = caption ?? "" - if text.isEmpty && GlobalExperimentalSettings.enableTinyThumbnails { - if let tinyThumbnail = compressTinyThumbnail(scaledImage) { - text = serializeTinyThumbnail(tinyThumbnail) - } - } - + var text = caption ?? "" messages.append(.message(text: text, attributes: attributes, mediaReference: .standalone(media: media), replyToMessageId: nil, localGroupingKey: item.groupedId)) } } diff --git a/TelegramUI/MediaPlayer.swift b/TelegramUI/MediaPlayer.swift index 6bbfe7cc6e..068b57a717 100644 --- a/TelegramUI/MediaPlayer.swift +++ b/TelegramUI/MediaPlayer.swift @@ -48,6 +48,11 @@ enum MediaPlayerActionAtEnd { case stop } +enum MediaPlayerPlayOnceWithSoundActionAtEnd { + case loopDisablingSound + case stop +} + private final class MediaPlayerAudioRendererContext { let renderer: MediaPlayerAudioRenderer var requestedFrames = false diff --git a/TelegramUI/NativeVideoContent.swift b/TelegramUI/NativeVideoContent.swift index 9c201d1ac2..39eba2d97f 100644 --- a/TelegramUI/NativeVideoContent.swift +++ b/TelegramUI/NativeVideoContent.swift @@ -257,13 +257,19 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent self.player.seek(timestamp: timestamp) } - func playOnceWithSound(playAndRecord: Bool) { + func playOnceWithSound(playAndRecord: Bool, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) { assert(Queue.mainQueue().isCurrent()) - self.player.actionAtEnd = .loopDisablingSound({ [weak self] in + 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.playOnceWithSound(playAndRecord: playAndRecord) } @@ -276,8 +282,19 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent self.player.setBaseRate(baseRate) } - func continuePlayingWithoutSound() { + func continuePlayingWithoutSound(actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) { 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() } diff --git a/TelegramUI/PhotoResources.swift b/TelegramUI/PhotoResources.swift index 816395cfa1..55c034869e 100644 --- a/TelegramUI/PhotoResources.swift +++ b/TelegramUI/PhotoResources.swift @@ -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> { - return chatMessagePhotoInternal(photoData: chatMessagePhotoDatas(postbox: postbox, photoReference: photoReference, tryAdditionalRepresentations: true, synchronousLoad: synchronousLoad), synchronousLoad: synchronousLoad, tinyThumbnailData: tinyThumbnailData) +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) |> map { _, generate in 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 |> map { (thumbnailData, fullSizeData, fullSizeComplete) in 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) { 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? if let thumbnailImage = thumbnailImage { let thumbnailSize = CGSize(width: thumbnailImage.width, height: thumbnailImage.height) diff --git a/TelegramUI/PlatformVideoContent.swift b/TelegramUI/PlatformVideoContent.swift index 26fe6f548a..3c2b5ef7ba 100644 --- a/TelegramUI/PlatformVideoContent.swift +++ b/TelegramUI/PlatformVideoContent.swift @@ -305,13 +305,13 @@ private final class PlatformVideoContentNode: ASDisplayNode, UniversalVideoConte self.player.seek(to: CMTime(seconds: timestamp, preferredTimescale: 30)) } - func playOnceWithSound(playAndRecord: Bool) { + func playOnceWithSound(playAndRecord: Bool, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) { } func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) { } - func continuePlayingWithoutSound() { + func continuePlayingWithoutSound(actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) { } func setBaseRate(_ baseRate: Double) { diff --git a/TelegramUI/PresentationData.swift b/TelegramUI/PresentationData.swift index 7fc05964ce..2914fbd0b8 100644 --- a/TelegramUI/PresentationData.swift +++ b/TelegramUI/PresentationData.swift @@ -446,5 +446,5 @@ public func defaultPresentationData() -> PresentationData { let nameSortOrder = currentPersonNameSortOrder() 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) } diff --git a/TelegramUI/PresentationTheme.swift b/TelegramUI/PresentationTheme.swift index 9484ab30fc..ae43928110 100644 --- a/TelegramUI/PresentationTheme.swift +++ b/TelegramUI/PresentationTheme.swift @@ -452,10 +452,11 @@ public func bubbleColorComponents(theme: PresentationTheme, incoming: Bool, wall } public func bubbleVariableColor(variableColor: PresentationThemeVariableColor, wallpaper: TelegramWallpaper) -> UIColor { - if wallpaper != .builtin && wallpaper != .color(0xffffff) { - return variableColor.withWallpaper - } else { - return variableColor.withoutWallpaper + switch wallpaper { + case .builtin, .color(0xffffff): + 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 { - if wallpaper != .builtin && wallpaper != .color(0xffffff) { - return chatTheme.serviceMessage.components.withCustomWallpaper - } else { - return chatTheme.serviceMessage.components.withDefaultWallpaper + switch wallpaper { + case .builtin, .color(0xffffff): + return chatTheme.serviceMessage.components.withDefaultWallpaper + default: + return chatTheme.serviceMessage.components.withCustomWallpaper } } diff --git a/TelegramUI/PresentationThemeSettings.swift b/TelegramUI/PresentationThemeSettings.swift index 6d3714fd01..6e13cf2cce 100644 --- a/TelegramUI/PresentationThemeSettings.swift +++ b/TelegramUI/PresentationThemeSettings.swift @@ -199,7 +199,7 @@ public struct PresentationThemeSettings: PreferencesEntry { } 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, fontSize: PresentationFontSize, automaticThemeSwitchSetting: AutomaticThemeSwitchSetting, disableAnimations: Bool) { @@ -213,7 +213,7 @@ public struct PresentationThemeSettings: PreferencesEntry { } 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.themeAccentColor = decoder.decodeOptionalInt32ForKey("themeAccentColor") self.themeSpecificChatWallpapers = decoder.decodeObjectDictionaryForKey("themeSpecificChatWallpapers", keyDecoder: { decoder in diff --git a/TelegramUI/RadialCloudProgressContentNode.swift b/TelegramUI/RadialCloudProgressContentNode.swift index 3644030ce2..605b24ee62 100644 --- a/TelegramUI/RadialCloudProgressContentNode.swift +++ b/TelegramUI/RadialCloudProgressContentNode.swift @@ -214,7 +214,7 @@ private final class RadialCloudProgressContentCancelNode: ASDisplayNode { if let parameters = parameters as? RadialCloudProgressContentCancelNodeParameters { let size: CGFloat = 8.0 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() } } diff --git a/TelegramUI/StringForDuration.swift b/TelegramUI/StringForDuration.swift index 1a93b32a2e..ae5d58d73d 100644 --- a/TelegramUI/StringForDuration.swift +++ b/TelegramUI/StringForDuration.swift @@ -1,6 +1,10 @@ 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 minutes = duration / 60 % 60 let seconds = duration % 60 @@ -14,3 +18,4 @@ func stringForDuration(_ duration: Int32) -> String { } + diff --git a/TelegramUI/SystemVideoContent.swift b/TelegramUI/SystemVideoContent.swift index 44227cb529..dfc305b6aa 100644 --- a/TelegramUI/SystemVideoContent.swift +++ b/TelegramUI/SystemVideoContent.swift @@ -227,13 +227,13 @@ private final class SystemVideoContentNode: ASDisplayNode, UniversalVideoContent //self.playerView.seek(toPosition: timestamp) } - func playOnceWithSound(playAndRecord: Bool) { + func playOnceWithSound(playAndRecord: Bool, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) { } func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) { } - func continuePlayingWithoutSound() { + func continuePlayingWithoutSound(actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) { } func setBaseRate(_ baseRate: Double) { diff --git a/TelegramUI/ThemeGridController.swift b/TelegramUI/ThemeGridController.swift index 665e2b6c1d..1c92801a23 100644 --- a/TelegramUI/ThemeGridController.swift +++ b/TelegramUI/ThemeGridController.swift @@ -195,7 +195,7 @@ final class ThemeGridController: ViewController { for wallpaper in wallpapers { if wallpaper == strongSelf.presentationData.chatWallpaper { 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 { switch theme { case .day: @@ -205,7 +205,7 @@ final class ThemeGridController: ViewController { case .nightAccent: fallbackWallpaper = .color(0x18222d) default: - fallbackWallpaper = .builtin + fallbackWallpaper = .builtin(WallpaperSettings()) } } @@ -278,10 +278,10 @@ final class ThemeGridController: ViewController { case .nightAccent: wallpaper = .color(0x18222d) default: - wallpaper = .builtin + wallpaper = .builtin(WallpaperSettings()) } } 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) }) @@ -445,10 +445,6 @@ final class ThemeGridController: ViewController { 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) })).start() - - if let strongSelf = self, case .file = wallpaper { - strongSelf.controllerNode.updateWallpapers() - } } let apply: () -> Void = { diff --git a/TelegramUI/ThemeGridControllerNode.swift b/TelegramUI/ThemeGridControllerNode.swift index 6849e77c92..9319a0764d 100644 --- a/TelegramUI/ThemeGridControllerNode.swift +++ b/TelegramUI/ThemeGridControllerNode.swift @@ -156,7 +156,6 @@ private func selectedWallpapers(entries: [ThemeGridControllerEntry]?, state: The guard let entries = entries, state.editing else { return [] } - var wallpapers: [TelegramWallpaper] = [] for entry in entries { if case let .file(file) = entry.wallpaper { diff --git a/TelegramUI/ThemeSettingsController.swift b/TelegramUI/ThemeSettingsController.swift index b63fbd5a91..5dd587404d 100644 --- a/TelegramUI/ThemeSettingsController.swift +++ b/TelegramUI/ThemeSettingsController.swift @@ -288,7 +288,7 @@ public func themeSettingsController(context: AccountContext) -> ViewController { case 3: wallpaper = .color(0x18222d) default: - wallpaper = .builtin + wallpaper = .builtin(WallpaperSettings()) } } diff --git a/TelegramUI/UniversalVideoGalleryItem.swift b/TelegramUI/UniversalVideoGalleryItem.swift index 2ae05d5411..326e608182 100644 --- a/TelegramUI/UniversalVideoGalleryItem.swift +++ b/TelegramUI/UniversalVideoGalleryItem.swift @@ -459,6 +459,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } } + videoNode.playOnceWithSound(playAndRecord: false, actionAtEnd: .stop) 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) } + + videoNode.continuePlayingWithoutSound() } func animateOut(toOverlay node: ASDisplayNode, completion: @escaping () -> Void) { diff --git a/TelegramUI/UniversalVideoNode.swift b/TelegramUI/UniversalVideoNode.swift index dac73d68cf..4605ad5561 100644 --- a/TelegramUI/UniversalVideoNode.swift +++ b/TelegramUI/UniversalVideoNode.swift @@ -17,9 +17,9 @@ protocol UniversalVideoContentNode: class { func togglePlayPause() func setSoundEnabled(_ value: Bool) func seek(_ timestamp: Double) - func playOnceWithSound(playAndRecord: Bool) + func playOnceWithSound(playAndRecord: Bool, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) - func continuePlayingWithoutSound() + func continuePlayingWithoutSound(actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) func setBaseRate(_ baseRate: Double) func addPlaybackCompleted(_ f: @escaping () -> Void) -> Int func removePlaybackCompleted(_ index: Int) @@ -85,7 +85,6 @@ final class UniversalVideoNode: ASDisplayNode { private let autoplay: Bool private let snapshotContentWhenGone: Bool - private var contentNode: (UniversalVideoContentNode & ASDisplayNode)? 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 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 if let contentNode = contentNode { - contentNode.continuePlayingWithoutSound() + contentNode.continuePlayingWithoutSound(actionAtEnd: actionAtEnd) } }) } diff --git a/TelegramUI/UpgradedAccounts.swift b/TelegramUI/UpgradedAccounts.swift index 34dfd453f7..871ee2d7d6 100644 --- a/TelegramUI/UpgradedAccounts.swift +++ b/TelegramUI/UpgradedAccounts.swift @@ -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) { 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: 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, _): for representation in representations { diff --git a/TelegramUI/WallpaperUploadManager.swift b/TelegramUI/WallpaperUploadManager.swift index 5f3a9e02d5..c82dcf122c 100644 --- a/TelegramUI/WallpaperUploadManager.swift +++ b/TelegramUI/WallpaperUploadManager.swift @@ -96,6 +96,9 @@ final class WallpaperUploadManager { let sharedContext = self.sharedContext let account = self.account 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 { let updateWallpaper: (TelegramWallpaper) -> Void = { wallpaper in 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: {}) } - if self?.currentPresentationData?.theme.name == presentationData.theme.name { + if strongSelf.currentPresentationData?.theme.name == presentationData.theme.name { let _ = (updatePresentationThemeSettingsInteractively(accountManager: sharedContext.accountManager, { current in let updatedWallpaper: TelegramWallpaper if let currentSettings = current.chatWallpaper.settings { diff --git a/TelegramUI/WebEmbedVideoContent.swift b/TelegramUI/WebEmbedVideoContent.swift index 62148ec24b..b61b9a811c 100644 --- a/TelegramUI/WebEmbedVideoContent.swift +++ b/TelegramUI/WebEmbedVideoContent.swift @@ -141,13 +141,13 @@ private final class WebEmbedVideoContentNode: ASDisplayNode, UniversalVideoConte self.playerNode.seek(timestamp: timestamp) } - func playOnceWithSound(playAndRecord: Bool) { + func playOnceWithSound(playAndRecord: Bool, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) { } func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) { } - func continuePlayingWithoutSound() { + func continuePlayingWithoutSound(actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) { } func setBaseRate(_ baseRate: Double) {