diff --git a/submodules/AccountContext/Sources/GalleryController.swift b/submodules/AccountContext/Sources/GalleryController.swift index 376252b271..fbfab81a96 100644 --- a/submodules/AccountContext/Sources/GalleryController.swift +++ b/submodules/AccountContext/Sources/GalleryController.swift @@ -18,10 +18,10 @@ public final class GalleryControllerActionInteraction { public let openHashtag: (String?, String) -> Void public let openBotCommand: (String) -> Void public let addContact: (String) -> Void - public let storeMediaPlaybackState: (MessageId, Double?) -> Void + public let storeMediaPlaybackState: (MessageId, Double?, Double) -> Void public let editMedia: (MessageId, [UIView], @escaping () -> Void) -> Void - public init(openUrl: @escaping (String, Bool) -> Void, openUrlIn: @escaping (String) -> Void, openPeerMention: @escaping (String) -> Void, openPeer: @escaping (PeerId) -> Void, openHashtag: @escaping (String?, String) -> Void, openBotCommand: @escaping (String) -> Void, addContact: @escaping (String) -> Void, storeMediaPlaybackState: @escaping (MessageId, Double?) -> Void, editMedia: @escaping (MessageId, [UIView], @escaping () -> Void) -> Void) { + public init(openUrl: @escaping (String, Bool) -> Void, openUrlIn: @escaping (String) -> Void, openPeerMention: @escaping (String) -> Void, openPeer: @escaping (PeerId) -> Void, openHashtag: @escaping (String?, String) -> Void, openBotCommand: @escaping (String) -> Void, addContact: @escaping (String) -> Void, storeMediaPlaybackState: @escaping (MessageId, Double?, Double) -> Void, editMedia: @escaping (MessageId, [UIView], @escaping () -> Void) -> Void) { self.openUrl = openUrl self.openUrlIn = openUrlIn self.openPeerMention = openPeerMention diff --git a/submodules/Display/Source/NavigationBar.swift b/submodules/Display/Source/NavigationBar.swift index 8f677e6c73..11f2cf8a17 100644 --- a/submodules/Display/Source/NavigationBar.swift +++ b/submodules/Display/Source/NavigationBar.swift @@ -239,7 +239,7 @@ open class NavigationBar: ASDisplayNode { var presentationData: NavigationBarPresentationData - private var validLayout: (size: CGSize, defaultHeight: CGFloat, additionalTopHeight: CGFloat, additionalContentHeight: CGFloat, additionalBackgroundHeight: CGFloat, leftInset: CGFloat, rightInset: CGFloat, appearsHidden: Bool)? + private var validLayout: (size: CGSize, defaultHeight: CGFloat, additionalTopHeight: CGFloat, additionalContentHeight: CGFloat, additionalBackgroundHeight: CGFloat, leftInset: CGFloat, rightInset: CGFloat, appearsHidden: Bool, isLandscape: Bool)? private var requestedLayout: Bool = false var requestContainerLayout: (ContainedViewLayoutTransition) -> Void = { _ in } @@ -940,16 +940,16 @@ open class NavigationBar: ASDisplayNode { if let validLayout = self.validLayout, self.requestedLayout { self.requestedLayout = false - self.updateLayout(size: validLayout.size, defaultHeight: validLayout.defaultHeight, additionalTopHeight: validLayout.additionalTopHeight, additionalContentHeight: validLayout.additionalContentHeight, additionalBackgroundHeight: validLayout.additionalBackgroundHeight, leftInset: validLayout.leftInset, rightInset: validLayout.rightInset, appearsHidden: validLayout.appearsHidden, transition: .immediate) + self.updateLayout(size: validLayout.size, defaultHeight: validLayout.defaultHeight, additionalTopHeight: validLayout.additionalTopHeight, additionalContentHeight: validLayout.additionalContentHeight, additionalBackgroundHeight: validLayout.additionalBackgroundHeight, leftInset: validLayout.leftInset, rightInset: validLayout.rightInset, appearsHidden: validLayout.appearsHidden, isLandscape: validLayout.isLandscape, transition: .immediate) } } - func updateLayout(size: CGSize, defaultHeight: CGFloat, additionalTopHeight: CGFloat, additionalContentHeight: CGFloat, additionalBackgroundHeight: CGFloat, leftInset: CGFloat, rightInset: CGFloat, appearsHidden: Bool, transition: ContainedViewLayoutTransition) { + func updateLayout(size: CGSize, defaultHeight: CGFloat, additionalTopHeight: CGFloat, additionalContentHeight: CGFloat, additionalBackgroundHeight: CGFloat, leftInset: CGFloat, rightInset: CGFloat, appearsHidden: Bool, isLandscape: Bool, transition: ContainedViewLayoutTransition) { if self.layoutSuspended { return } - self.validLayout = (size, defaultHeight, additionalTopHeight, additionalContentHeight, additionalBackgroundHeight, leftInset, rightInset, appearsHidden) + self.validLayout = (size, defaultHeight, additionalTopHeight, additionalContentHeight, additionalBackgroundHeight, leftInset, rightInset, appearsHidden, isLandscape) let backgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height + additionalBackgroundHeight)) if self.backgroundNode.frame != backgroundFrame { @@ -995,7 +995,7 @@ open class NavigationBar: ASDisplayNode { var leftTitleInset: CGFloat = leftInset + 1.0 var rightTitleInset: CGFloat = rightInset + 1.0 if self.backButtonNode.supernode != nil { - let backButtonSize = self.backButtonNode.updateLayout(constrainedSize: CGSize(width: size.width, height: nominalHeight)) + let backButtonSize = self.backButtonNode.updateLayout(constrainedSize: CGSize(width: size.width, height: nominalHeight), isLandscape: isLandscape) leftTitleInset += backButtonSize.width + backButtonInset + 1.0 let topHitTestSlop = (nominalHeight - backButtonSize.height) * 0.5 @@ -1047,7 +1047,7 @@ open class NavigationBar: ASDisplayNode { self.badgeNode.alpha = 1.0 } } else if self.leftButtonNode.supernode != nil { - let leftButtonSize = self.leftButtonNode.updateLayout(constrainedSize: CGSize(width: size.width, height: nominalHeight)) + let leftButtonSize = self.leftButtonNode.updateLayout(constrainedSize: CGSize(width: size.width, height: nominalHeight), isLandscape: isLandscape) leftTitleInset += leftButtonSize.width + leftButtonInset + 1.0 self.leftButtonNode.alpha = 1.0 @@ -1059,7 +1059,7 @@ open class NavigationBar: ASDisplayNode { transition.updateFrame(node: self.badgeNode, frame: CGRect(origin: backButtonArrowFrame.origin.offsetBy(dx: 7.0, dy: -9.0), size: badgeSize)) if self.rightButtonNode.supernode != nil { - let rightButtonSize = self.rightButtonNode.updateLayout(constrainedSize: (CGSize(width: size.width, height: nominalHeight))) + let rightButtonSize = self.rightButtonNode.updateLayout(constrainedSize: (CGSize(width: size.width, height: nominalHeight)), isLandscape: isLandscape) rightTitleInset += rightButtonSize.width + leftButtonInset + 1.0 self.rightButtonNode.alpha = 1.0 transition.updateFrame(node: self.rightButtonNode, frame: CGRect(origin: CGPoint(x: size.width - leftButtonInset - rightButtonSize.width, y: contentVerticalOrigin + floor((nominalHeight - rightButtonSize.height) / 2.0)), size: rightButtonSize)) @@ -1073,7 +1073,7 @@ open class NavigationBar: ASDisplayNode { break case .bottom: if let transitionBackButtonNode = self.transitionBackButtonNode { - let transitionBackButtonSize = transitionBackButtonNode.updateLayout(constrainedSize: CGSize(width: size.width, height: nominalHeight)) + let transitionBackButtonSize = transitionBackButtonNode.updateLayout(constrainedSize: CGSize(width: size.width, height: nominalHeight), isLandscape: isLandscape) let initialX: CGFloat = backButtonInset + size.width * 0.3 let finalX: CGFloat = floor((size.width - transitionBackButtonSize.width) / 2.0) @@ -1204,7 +1204,7 @@ open class NavigationBar: ASDisplayNode { node.updateManualText(self.backButtonNode.manualText) node.color = accentColor if let validLayout = self.validLayout { - let _ = node.updateLayout(constrainedSize: CGSize(width: validLayout.size.width, height: validLayout.defaultHeight)) + let _ = node.updateLayout(constrainedSize: CGSize(width: validLayout.size.width, height: validLayout.defaultHeight), isLandscape: validLayout.isLandscape) node.frame = self.backButtonNode.frame } return node @@ -1227,7 +1227,7 @@ open class NavigationBar: ASDisplayNode { node.updateItems(items) node.color = accentColor if let validLayout = self.validLayout { - let _ = node.updateLayout(constrainedSize: CGSize(width: validLayout.size.width, height: validLayout.defaultHeight)) + let _ = node.updateLayout(constrainedSize: CGSize(width: validLayout.size.width, height: validLayout.defaultHeight), isLandscape: validLayout.isLandscape) node.frame = self.backButtonNode.frame } return node diff --git a/submodules/Display/Source/NavigationButtonNode.swift b/submodules/Display/Source/NavigationButtonNode.swift index 4ad1cd4318..8cb826e190 100644 --- a/submodules/Display/Source/NavigationButtonNode.swift +++ b/submodules/Display/Source/NavigationButtonNode.swift @@ -255,8 +255,6 @@ private final class NavigationButtonItemNode: ImmediateTextNode { public override func touchesMoved(_ touches: Set, with event: UIEvent?) { super.touchesMoved(touches, with: event) - - //self.updateHighlightedState(self.touchInsideApparentBounds(touches.first!), animated: true) } public override func touchesEnded(_ touches: Set, with event: UIEvent?) { @@ -274,6 +272,15 @@ private final class NavigationButtonItemNode: ImmediateTextNode { self.pressed() } } + + public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if let node = self.node as? HighlightableButtonNode { + let result = node.view.hitTest(self.view.convert(point, to: node.view), with: event) + return result + } else { + return super.hitTest(point, with: event) + } + } public override func touchesCancelled(_ touches: Set?, with event: UIEvent?) { super.touchesCancelled(touches, with: event) @@ -453,7 +460,7 @@ public final class NavigationButtonNode: ASDisplayNode { } } - public func updateLayout(constrainedSize: CGSize) -> CGSize { + public func updateLayout(constrainedSize: CGSize, isLandscape: Bool) -> CGSize { var nodeOrigin = CGPoint() var totalSize = CGSize() for node in self.nodes { @@ -468,6 +475,9 @@ public final class NavigationButtonNode: ASDisplayNode { totalSize.height = max(totalSize.height, nodeSize.height) node.frame = CGRect(origin: CGPoint(x: nodeOrigin.x, y: floor((totalSize.height - nodeSize.height) / 2.0)), size: nodeSize) nodeOrigin.x += node.bounds.width + if isLandscape { + nodeOrigin.x += 16.0 + } } return totalSize } diff --git a/submodules/Display/Source/ViewController.swift b/submodules/Display/Source/ViewController.swift index 924cd07fe6..b5b9e7cac5 100644 --- a/submodules/Display/Source/ViewController.swift +++ b/submodules/Display/Source/ViewController.swift @@ -381,6 +381,8 @@ public enum TabBarItemContextActionType { } self.navigationBarOrigin = navigationBarFrame.origin.y + + let isLandscape = layout.size.width > layout.size.height if let navigationBar = self.navigationBar { if let contentNode = navigationBar.contentNode, case .expansion = contentNode.mode, !self.displayNavigationBar { @@ -392,7 +394,7 @@ public enum TabBarItemContextActionType { navigationBarFrame.size.height += NavigationBar.defaultSecondaryContentHeight //navigationBarFrame.origin.y += NavigationBar.defaultSecondaryContentHeight } - navigationBar.updateLayout(size: navigationBarFrame.size, defaultHeight: navigationLayout.defaultContentHeight, additionalTopHeight: statusBarHeight, additionalContentHeight: self.additionalNavigationBarHeight, additionalBackgroundHeight: additionalBackgroundHeight, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, appearsHidden: !self.displayNavigationBar, transition: transition) + navigationBar.updateLayout(size: navigationBarFrame.size, defaultHeight: navigationLayout.defaultContentHeight, additionalTopHeight: statusBarHeight, additionalContentHeight: self.additionalNavigationBarHeight, additionalBackgroundHeight: additionalBackgroundHeight, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, appearsHidden: !self.displayNavigationBar, isLandscape: isLandscape, transition: transition) if !transition.isAnimated { navigationBar.layer.cancelAnimationsRecursive(key: "bounds") navigationBar.layer.cancelAnimationsRecursive(key: "position") diff --git a/submodules/GalleryData/Sources/GalleryData.swift b/submodules/GalleryData/Sources/GalleryData.swift index b3482de298..ba873e0d3c 100644 --- a/submodules/GalleryData/Sources/GalleryData.swift +++ b/submodules/GalleryData/Sources/GalleryData.swift @@ -230,20 +230,20 @@ public func chatMessageGalleryControllerData(context: AccountContext, chatLocati let gallery = SecretMediaPreviewController(context: context, messageId: message.id) return .secretGallery(gallery) } else { - let startTimecode: Signal + let startState: Signal<(timecode: Double?, rate: Double), NoError> if let timecode = timecode { - startTimecode = .single(timecode) + startState = .single((timecode: timecode, rate: 1.0)) } else { - startTimecode = mediaPlaybackStoredState(postbox: context.account.postbox, messageId: message.id) + startState = mediaPlaybackStoredState(postbox: context.account.postbox, messageId: message.id) |> map { state in - return state?.timestamp + return (state?.timestamp, state?.playbackRate.doubleValue ?? 1.0) } } - return .gallery(startTimecode + return .gallery(startState |> deliverOnMainQueue - |> map { timecode in - let gallery = GalleryController(context: context, source: source ?? (standalone ? .standaloneMessage(message) : .peerMessagesAtId(messageId: message.id, chatLocation: chatLocation ?? .peer(message.id.peerId), chatLocationContextHolder: chatLocationContextHolder ?? Atomic(value: nil))), invertItemOrder: reverseMessageGalleryOrder, streamSingleVideo: stream, fromPlayingVideo: autoplayingVideo, landscape: landscape, timecode: timecode, synchronousLoad: synchronousLoad, replaceRootController: { [weak navigationController] controller, ready in + |> map { startState in + let gallery = GalleryController(context: context, source: source ?? (standalone ? .standaloneMessage(message) : .peerMessagesAtId(messageId: message.id, chatLocation: chatLocation ?? .peer(message.id.peerId), chatLocationContextHolder: chatLocationContextHolder ?? Atomic(value: nil))), invertItemOrder: reverseMessageGalleryOrder, streamSingleVideo: stream, fromPlayingVideo: autoplayingVideo, landscape: landscape, timecode: startState.timecode, playbackRate: startState.rate, synchronousLoad: synchronousLoad, replaceRootController: { [weak navigationController] controller, ready in navigationController?.replaceTopController(controller, animated: false, ready: ready) }, baseNavigationController: navigationController, actionInteraction: actionInteraction) gallery.temporaryDoNotWaitForReady = autoplayingVideo diff --git a/submodules/GalleryUI/BUILD b/submodules/GalleryUI/BUILD index 6a06062b21..59860c2bf8 100644 --- a/submodules/GalleryUI/BUILD +++ b/submodules/GalleryUI/BUILD @@ -28,7 +28,10 @@ swift_library( "//submodules/OverlayStatusController:OverlayStatusController", "//submodules/PresentationDataUtils:PresentationDataUtils", "//submodules/UrlEscaping:UrlEscaping", - "//submodules/ManagedAnimationNode:ManagedAnimationNode" + "//submodules/ManagedAnimationNode:ManagedAnimationNode", + "//submodules/ContextUI:ContextUI", + "//submodules/SaveToCameraRoll:SaveToCameraRoll", + "//submodules/TelegramUIPreferences:TelegramUIPreferences", ], visibility = [ "//visibility:public", diff --git a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift index d099fe3d7b..d7ae82a8aa 100644 --- a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift +++ b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift @@ -31,6 +31,9 @@ private let forwardImage = generateTintedImage(image: UIImage(bundleImageName: " private let cloudFetchIcon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/FileCloudFetch"), color: UIColor.white) +private let fullscreenOnImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Expand"), color: .white) +private let fullscreenOffImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Collapse"), color: .white) + private let captionMaskImage = generateImage(CGSize(width: 1.0, height: 17.0), opaque: false, rotatedContext: { size, context in let bounds = CGRect(origin: CGPoint(), size: size) context.clear(bounds) @@ -119,6 +122,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll private let contentNode: ASDisplayNode private let deleteButton: UIButton + private let fullscreenButton: UIButton private let actionButton: UIButton private let editButton: UIButton private let maskNode: ASDisplayNode @@ -152,6 +156,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll var seekBackward: ((Double) -> Void)? var seekForward: ((Double) -> Void)? var setPlayRate: ((Double) -> Void)? + var toggleFullscreen: (() -> Void)? var fetchControl: (() -> Void)? var interacting: ((Bool) -> Void)? @@ -286,6 +291,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll self.contentNode = ASDisplayNode() self.deleteButton = UIButton() + self.fullscreenButton = UIButton() self.actionButton = UIButton() self.editButton = UIButton() @@ -363,6 +369,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll } self.contentNode.view.addSubview(self.deleteButton) + self.contentNode.view.addSubview(self.fullscreenButton) self.contentNode.view.addSubview(self.actionButton) self.contentNode.view.addSubview(self.editButton) self.contentNode.addSubnode(self.scrollWrapperNode) @@ -381,6 +388,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll self.contentNode.addSubnode(self.statusButtonNode) self.deleteButton.addTarget(self, action: #selector(self.deleteButtonPressed), for: [.touchUpInside]) + self.fullscreenButton.addTarget(self, action: #selector(self.fullscreenButtonPressed), for: [.touchUpInside]) self.actionButton.addTarget(self, action: #selector(self.actionButtonPressed), for: [.touchUpInside]) self.editButton.addTarget(self, action: #selector(self.editButtonPressed), for: [.touchUpInside]) @@ -559,6 +567,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll if origin == nil { self.editButton.isHidden = true self.deleteButton.isHidden = true + self.fullscreenButton.isHidden = true self.editButton.isHidden = true } } @@ -568,12 +577,22 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll let canDelete: Bool var canShare = !message.containsSecretMedia + + var canFullscreen = false var canEdit = false for media in message.media { if media is TelegramMediaImage { canEdit = true - break + } else if let media = media as? TelegramMediaFile, !media.isAnimated { + for attribute in media.attributes { + switch attribute { + case .Video: + canFullscreen = true + default: + break + } + } } } @@ -637,7 +656,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll messageText = galleryCaptionStringWithAppliedEntities(message.text, entities: entities) } - if self.currentMessageText != messageText || canDelete != !self.deleteButton.isHidden || canShare != !self.actionButton.isHidden || canEdit != !self.editButton.isHidden || self.currentAuthorNameText != authorNameText || self.currentDateText != dateText { + if self.currentMessageText != messageText || canDelete != !self.deleteButton.isHidden || canFullscreen != !self.fullscreenButton.isHidden || canShare != !self.actionButton.isHidden || canEdit != !self.editButton.isHidden || self.currentAuthorNameText != authorNameText || self.currentDateText != dateText { self.currentMessageText = messageText if messageText.length == 0 { @@ -654,8 +673,15 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll self.authorNameNode.attributedText = nil } self.dateNode.attributedText = NSAttributedString(string: dateText, font: dateFont, textColor: .white) - - self.deleteButton.isHidden = !canDelete + + if canFullscreen { + self.fullscreenButton.isHidden = false + self.deleteButton.isHidden = true + } else { + self.deleteButton.isHidden = !canDelete + self.fullscreenButton.isHidden = true + } + self.actionButton.isHidden = !canShare self.editButton.isHidden = !canEdit @@ -683,6 +709,9 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll panelHeight += contentInset let isLandscape = size.width > size.height + + self.fullscreenButton.setImage(isLandscape ? fullscreenOffImage : fullscreenOnImage, for: [.normal]) + let displayCaption: Bool if case .compact = metrics.widthClass { displayCaption = !self.textNode.isHidden && !isLandscape @@ -776,10 +805,11 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll let deleteFrame = CGRect(origin: CGPoint(x: width - 44.0 - rightInset, y: panelHeight - bottomInset - 44.0), size: CGSize(width: 44.0, height: 44.0)) var editFrame = CGRect(origin: CGPoint(x: width - 44.0 - 50.0 - rightInset, y: panelHeight - bottomInset - 44.0), size: CGSize(width: 44.0, height: 44.0)) - if self.deleteButton.isHidden { + if self.deleteButton.isHidden && self.fullscreenButton.isHidden { editFrame = deleteFrame } self.deleteButton.frame = deleteFrame + self.fullscreenButton.frame = deleteFrame self.editButton.frame = editFrame if let image = self.backwardButton.backgroundIconNode.image { @@ -789,7 +819,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll self.forwardButton.frame = CGRect(origin: CGPoint(x: floor((width - image.size.width) / 2.0) + 66.0, y: panelHeight - bottomInset - 44.0 + 7.0), size: image.size) } - self.playbackControlButton.frame = CGRect(origin: CGPoint(x: floor((width - 44.0) / 2.0), y: panelHeight - bottomInset - 44.0), size: CGSize(width: 44.0, height: 44.0)) + self.playbackControlButton.frame = CGRect(origin: CGPoint(x: floor((width - 44.0) / 2.0), y: panelHeight - bottomInset - 44.0 - 2.0), size: CGSize(width: 44.0, height: 44.0)) self.playPauseIconNode.frame = self.playbackControlButton.bounds.offsetBy(dx: 2.0, dy: 2.0) let statusSize = CGSize(width: 28.0, height: 28.0) @@ -855,6 +885,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll self.dateNode.alpha = 1.0 self.authorNameNode.alpha = 1.0 self.deleteButton.alpha = 1.0 + self.fullscreenButton.alpha = 1.0 self.actionButton.alpha = 1.0 self.editButton.alpha = 1.0 self.backwardButton.alpha = 1.0 @@ -878,6 +909,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll self.dateNode.alpha = 0.0 self.authorNameNode.alpha = 0.0 self.deleteButton.alpha = 0.0 + self.fullscreenButton.alpha = 0.0 self.actionButton.alpha = 0.0 self.editButton.alpha = 0.0 self.backwardButton.alpha = 0.0 @@ -888,6 +920,10 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll completion() }) } + + @objc func fullscreenButtonPressed() { + self.toggleFullscreen?() + } @objc func deleteButtonPressed() { if let currentMessage = self.currentMessage { diff --git a/submodules/GalleryUI/Sources/GalleryController.swift b/submodules/GalleryUI/Sources/GalleryController.swift index bd46ab1a12..3d32dc868e 100644 --- a/submodules/GalleryUI/Sources/GalleryController.swift +++ b/submodules/GalleryUI/Sources/GalleryController.swift @@ -145,7 +145,7 @@ private func galleryMessageCaptionText(_ message: Message) -> String { return message.text } -public func galleryItemForEntry(context: AccountContext, presentationData: PresentationData, entry: MessageHistoryEntry, isCentral: Bool = false, streamVideos: Bool, loopVideos: Bool = false, hideControls: Bool = false, fromPlayingVideo: Bool = false, isSecret: Bool = false, landscape: Bool = false, timecode: Double? = nil, displayInfoOnTop: Bool = false, configuration: GalleryConfiguration? = nil, tempFilePath: String? = nil, playbackCompleted: @escaping () -> Void = {}, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void = { _ in }, openActionOptions: @escaping (GalleryControllerInteractionTapAction) -> Void = { _ in }, storeMediaPlaybackState: @escaping (MessageId, Double?) -> Void = { _, _ in }, present: @escaping (ViewController, Any?) -> Void) -> GalleryItem? { +public func galleryItemForEntry(context: AccountContext, presentationData: PresentationData, entry: MessageHistoryEntry, isCentral: Bool = false, streamVideos: Bool, loopVideos: Bool = false, hideControls: Bool = false, fromPlayingVideo: Bool = false, isSecret: Bool = false, landscape: Bool = false, timecode: Double? = nil, playbackRate: Double = 1.0, displayInfoOnTop: Bool = false, configuration: GalleryConfiguration? = nil, tempFilePath: String? = nil, playbackCompleted: @escaping () -> Void = {}, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void = { _ in }, openActionOptions: @escaping (GalleryControllerInteractionTapAction) -> Void = { _ in }, storeMediaPlaybackState: @escaping (MessageId, Double?, Double) -> Void = { _, _, _ in }, present: @escaping (ViewController, Any?) -> Void) -> GalleryItem? { let message = entry.message let location = entry.location if let (media, mediaImage) = mediaForMessage(message: message) { @@ -178,7 +178,7 @@ public func galleryItemForEntry(context: AccountContext, presentationData: Prese } let caption = galleryCaptionStringWithAppliedEntities(text, entities: entities) - return UniversalVideoGalleryItem(context: context, presentationData: presentationData, content: content, originData: GalleryItemOriginData(title: message.effectiveAuthor?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), timestamp: message.timestamp), indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) }, contentInfo: .message(message), caption: caption, displayInfoOnTop: displayInfoOnTop, hideControls: hideControls, fromPlayingVideo: fromPlayingVideo, isSecret: isSecret, landscape: landscape, timecode: timecode, configuration: configuration, playbackCompleted: playbackCompleted, performAction: performAction, openActionOptions: openActionOptions, storeMediaPlaybackState: storeMediaPlaybackState, present: present) + return UniversalVideoGalleryItem(context: context, presentationData: presentationData, content: content, originData: GalleryItemOriginData(title: message.effectiveAuthor?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), timestamp: message.timestamp), indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) }, contentInfo: .message(message), caption: caption, displayInfoOnTop: displayInfoOnTop, hideControls: hideControls, fromPlayingVideo: fromPlayingVideo, isSecret: isSecret, landscape: landscape, timecode: timecode, playbackRate: playbackRate, configuration: configuration, playbackCompleted: playbackCompleted, performAction: performAction, openActionOptions: openActionOptions, storeMediaPlaybackState: storeMediaPlaybackState, present: present) } else { if let fileName = file.fileName, (fileName as NSString).pathExtension.lowercased() == "json" { return ChatAnimationGalleryItem(context: context, presentationData: presentationData, message: message, location: location) @@ -188,7 +188,7 @@ public func galleryItemForEntry(context: AccountContext, presentationData: Prese if let dimensions = file.dimensions { pixelsCount = Int(dimensions.width) * Int(dimensions.height) } - if (file.size == nil || file.size! < 4 * 1024 * 1024) && pixelsCount < 4096 * 4096 { + if pixelsCount < 10000 * 10000 { return ChatImageGalleryItem(context: context, presentationData: presentationData, message: message, location: location, displayInfoOnTop: displayInfoOnTop, performAction: performAction, openActionOptions: openActionOptions, present: present) } else { return ChatDocumentGalleryItem(context: context, presentationData: presentationData, message: message, location: location) @@ -219,7 +219,7 @@ public func galleryItemForEntry(context: AccountContext, presentationData: Prese } } if let content = content { - return UniversalVideoGalleryItem(context: context, presentationData: presentationData, content: content, originData: GalleryItemOriginData(title: message.effectiveAuthor?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), timestamp: message.timestamp), indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) }, contentInfo: .message(message), caption: NSAttributedString(string: ""), displayInfoOnTop: displayInfoOnTop, fromPlayingVideo: fromPlayingVideo, isSecret: isSecret, landscape: landscape, timecode: timecode, configuration: configuration, performAction: performAction, openActionOptions: openActionOptions, storeMediaPlaybackState: storeMediaPlaybackState, present: present) + return UniversalVideoGalleryItem(context: context, presentationData: presentationData, content: content, originData: GalleryItemOriginData(title: message.effectiveAuthor?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), timestamp: message.timestamp), indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) }, contentInfo: .message(message), caption: NSAttributedString(string: ""), displayInfoOnTop: displayInfoOnTop, fromPlayingVideo: fromPlayingVideo, isSecret: isSecret, landscape: landscape, timecode: timecode, playbackRate: playbackRate, configuration: configuration, performAction: performAction, openActionOptions: openActionOptions, storeMediaPlaybackState: storeMediaPlaybackState, present: present) } else { return nil } @@ -349,6 +349,7 @@ public class GalleryController: ViewController, StandalonePresentableController private let fromPlayingVideo: Bool private let landscape: Bool private let timecode: Double? + private let playbackRate: Double private let accountInUseDisposable = MetaDisposable() private let disposable = MetaDisposable() @@ -388,7 +389,7 @@ public class GalleryController: ViewController, StandalonePresentableController private var initialOrientation: UIInterfaceOrientation? - public init(context: AccountContext, source: GalleryControllerItemSource, invertItemOrder: Bool = false, streamSingleVideo: Bool = false, fromPlayingVideo: Bool = false, landscape: Bool = false, timecode: Double? = nil, synchronousLoad: Bool = false, replaceRootController: @escaping (ViewController, Promise?) -> Void, baseNavigationController: NavigationController?, actionInteraction: GalleryControllerActionInteraction? = nil) { + public init(context: AccountContext, source: GalleryControllerItemSource, invertItemOrder: Bool = false, streamSingleVideo: Bool = false, fromPlayingVideo: Bool = false, landscape: Bool = false, timecode: Double? = nil, playbackRate: Double = 1.0, synchronousLoad: Bool = false, replaceRootController: @escaping (ViewController, Promise?) -> Void, baseNavigationController: NavigationController?, actionInteraction: GalleryControllerActionInteraction? = nil) { self.context = context self.source = source self.invertItemOrder = invertItemOrder @@ -399,6 +400,7 @@ public class GalleryController: ViewController, StandalonePresentableController self.fromPlayingVideo = fromPlayingVideo self.landscape = landscape self.timecode = timecode + self.playbackRate = playbackRate self.presentationData = context.sharedContext.currentPresentationData.with { $0 } @@ -537,7 +539,7 @@ public class GalleryController: ViewController, StandalonePresentableController if entry.message.stableId == strongSelf.centralEntryStableId { isCentral = true } - if let item = galleryItemForEntry(context: context, presentationData: strongSelf.presentationData, entry: entry, isCentral: isCentral, streamVideos: streamSingleVideo, fromPlayingVideo: isCentral && fromPlayingVideo, landscape: isCentral && landscape, timecode: isCentral ? timecode : nil, displayInfoOnTop: displayInfoOnTop, configuration: configuration, performAction: strongSelf.performAction, openActionOptions: strongSelf.openActionOptions, storeMediaPlaybackState: strongSelf.actionInteraction?.storeMediaPlaybackState ?? { _, _ in }, present: { [weak self] c, a in + if let item = galleryItemForEntry(context: context, presentationData: strongSelf.presentationData, entry: entry, isCentral: isCentral, streamVideos: streamSingleVideo, fromPlayingVideo: isCentral && fromPlayingVideo, landscape: isCentral && landscape, timecode: isCentral ? timecode : nil, playbackRate: isCentral ? playbackRate : 1.0, displayInfoOnTop: displayInfoOnTop, configuration: configuration, performAction: strongSelf.performAction, openActionOptions: strongSelf.openActionOptions, storeMediaPlaybackState: strongSelf.actionInteraction?.storeMediaPlaybackState ?? { _, _, _ in }, present: { [weak self] c, a in if let strongSelf = self { strongSelf.presentInGlobalOverlay(c, with: a) } @@ -1040,6 +1042,9 @@ public class GalleryController: ViewController, StandalonePresentableController self.galleryNode.baseNavigationController = { [weak baseNavigationController] in return baseNavigationController } + self.galleryNode.galleryController = { [weak self] in + return self + } var displayInfoOnTop = false if case .custom = source { @@ -1053,7 +1058,7 @@ public class GalleryController: ViewController, StandalonePresentableController if entry.message.stableId == self.centralEntryStableId { isCentral = true } - if let item = galleryItemForEntry(context: self.context, presentationData: self.presentationData, entry: entry, streamVideos: self.streamVideos, fromPlayingVideo: isCentral && self.fromPlayingVideo, landscape: isCentral && self.landscape, timecode: isCentral ? self.timecode : nil, displayInfoOnTop: displayInfoOnTop, configuration: self.configuration, performAction: self.performAction, openActionOptions: self.openActionOptions, storeMediaPlaybackState: self.actionInteraction?.storeMediaPlaybackState ?? { _, _ in }, present: { [weak self] c, a in + if let item = galleryItemForEntry(context: self.context, presentationData: self.presentationData, entry: entry, streamVideos: self.streamVideos, fromPlayingVideo: isCentral && self.fromPlayingVideo, landscape: isCentral && self.landscape, timecode: isCentral ? self.timecode : nil, playbackRate: isCentral ? self.playbackRate : 1.0, displayInfoOnTop: displayInfoOnTop, configuration: self.configuration, performAction: self.performAction, openActionOptions: self.openActionOptions, storeMediaPlaybackState: self.actionInteraction?.storeMediaPlaybackState ?? { _, _, _ in }, present: { [weak self] c, a in if let strongSelf = self { strongSelf.presentInGlobalOverlay(c, with: a) } @@ -1133,7 +1138,7 @@ public class GalleryController: ViewController, StandalonePresentableController if entry.message.stableId == strongSelf.centralEntryStableId { isCentral = true } - if let item = galleryItemForEntry(context: strongSelf.context, presentationData: strongSelf.presentationData, entry: entry, isCentral: isCentral, streamVideos: false, fromPlayingVideo: isCentral && strongSelf.fromPlayingVideo, landscape: isCentral && strongSelf.landscape, timecode: isCentral ? strongSelf.timecode : nil, displayInfoOnTop: displayInfoOnTop, configuration: strongSelf.configuration, performAction: strongSelf.performAction, openActionOptions: strongSelf.openActionOptions, storeMediaPlaybackState: strongSelf.actionInteraction?.storeMediaPlaybackState ?? { _, _ in }, present: { [weak self] c, a in + if let item = galleryItemForEntry(context: strongSelf.context, presentationData: strongSelf.presentationData, entry: entry, isCentral: isCentral, streamVideos: false, fromPlayingVideo: isCentral && strongSelf.fromPlayingVideo, landscape: isCentral && strongSelf.landscape, timecode: isCentral ? strongSelf.timecode : nil, displayInfoOnTop: displayInfoOnTop, configuration: strongSelf.configuration, performAction: strongSelf.performAction, openActionOptions: strongSelf.openActionOptions, storeMediaPlaybackState: strongSelf.actionInteraction?.storeMediaPlaybackState ?? { _, _, _ in }, present: { [weak self] c, a in if let strongSelf = self { strongSelf.presentInGlobalOverlay(c, with: a) } @@ -1185,7 +1190,7 @@ public class GalleryController: ViewController, StandalonePresentableController if entry.message.stableId == strongSelf.centralEntryStableId { isCentral = true } - if let item = galleryItemForEntry(context: strongSelf.context, presentationData: strongSelf.presentationData, entry: entry, isCentral: isCentral, streamVideos: false, fromPlayingVideo: isCentral && strongSelf.fromPlayingVideo, landscape: isCentral && strongSelf.landscape, timecode: isCentral ? strongSelf.timecode : nil, displayInfoOnTop: displayInfoOnTop, configuration: strongSelf.configuration, performAction: strongSelf.performAction, openActionOptions: strongSelf.openActionOptions, storeMediaPlaybackState: strongSelf.actionInteraction?.storeMediaPlaybackState ?? { _, _ in }, present: { [weak self] c, a in + if let item = galleryItemForEntry(context: strongSelf.context, presentationData: strongSelf.presentationData, entry: entry, isCentral: isCentral, streamVideos: false, fromPlayingVideo: isCentral && strongSelf.fromPlayingVideo, landscape: isCentral && strongSelf.landscape, timecode: isCentral ? strongSelf.timecode : nil, displayInfoOnTop: displayInfoOnTop, configuration: strongSelf.configuration, performAction: strongSelf.performAction, openActionOptions: strongSelf.openActionOptions, storeMediaPlaybackState: strongSelf.actionInteraction?.storeMediaPlaybackState ?? { _, _, _ in }, present: { [weak self] c, a in if let strongSelf = self { strongSelf.presentInGlobalOverlay(c, with: a) } diff --git a/submodules/GalleryUI/Sources/GalleryControllerNode.swift b/submodules/GalleryUI/Sources/GalleryControllerNode.swift index 06780433d2..1ec92c83e3 100644 --- a/submodules/GalleryUI/Sources/GalleryControllerNode.swift +++ b/submodules/GalleryUI/Sources/GalleryControllerNode.swift @@ -22,6 +22,7 @@ open class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate, UIGesture public var beginCustomDismiss: () -> Void = { } public var completeCustomDismiss: () -> Void = { } public var baseNavigationController: () -> NavigationController? = { return nil } + public var galleryController: () -> ViewController? = { return nil } private var presentationState = GalleryControllerPresentationState() @@ -120,6 +121,9 @@ open class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate, UIGesture self.pager.baseNavigationController = { [weak self] in return self?.baseNavigationController() } + self.pager.galleryController = { [weak self] in + return self?.galleryController() + } self.addSubnode(self.backgroundNode) diff --git a/submodules/GalleryUI/Sources/GalleryItemNode.swift b/submodules/GalleryUI/Sources/GalleryItemNode.swift index 6903789863..2dc6cf9672 100644 --- a/submodules/GalleryUI/Sources/GalleryItemNode.swift +++ b/submodules/GalleryUI/Sources/GalleryItemNode.swift @@ -27,6 +27,7 @@ open class GalleryItemNode: ASDisplayNode { public var beginCustomDismiss: () -> Void = { } public var completeCustomDismiss: () -> Void = { } public var baseNavigationController: () -> NavigationController? = { return nil } + public var galleryController: () -> ViewController? = { return nil } public var alternativeDismiss: () -> Bool = { return false } override public init() { diff --git a/submodules/GalleryUI/Sources/GalleryPagerNode.swift b/submodules/GalleryUI/Sources/GalleryPagerNode.swift index b6d9e6b6fa..80217fe39e 100644 --- a/submodules/GalleryUI/Sources/GalleryPagerNode.swift +++ b/submodules/GalleryUI/Sources/GalleryPagerNode.swift @@ -112,6 +112,7 @@ public final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate, UIGest public var beginCustomDismiss: () -> Void = { } public var completeCustomDismiss: () -> Void = { } public var baseNavigationController: () -> NavigationController? = { return nil } + public var galleryController: () -> ViewController? = { return nil } public init(pageGap: CGFloat, disableTapNavigation: Bool) { self.pageGap = pageGap @@ -480,6 +481,7 @@ public final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate, UIGest node.beginCustomDismiss = self.beginCustomDismiss node.completeCustomDismiss = self.completeCustomDismiss node.baseNavigationController = self.baseNavigationController + node.galleryController = self.galleryController node.index = index return node } diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index 26ede5338b..cbb6aa948a 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -15,6 +15,11 @@ import PresentationDataUtils import OverlayStatusController import StickerPackPreviewUI import AppBundle +import AnimationUI +import ContextUI +import SaveToCameraRoll +import UndoUI +import TelegramUIPreferences public enum UniversalVideoGalleryItemContentInfo { case message(Message) @@ -40,14 +45,15 @@ public class UniversalVideoGalleryItem: GalleryItem { let isSecret: Bool let landscape: Bool let timecode: Double? + let playbackRate: Double let configuration: GalleryConfiguration? let playbackCompleted: () -> Void let performAction: (GalleryControllerInteractionTapAction) -> Void let openActionOptions: (GalleryControllerInteractionTapAction) -> Void - let storeMediaPlaybackState: (MessageId, Double?) -> Void + let storeMediaPlaybackState: (MessageId, Double?, Double) -> Void let present: (ViewController, Any?) -> Void - public init(context: AccountContext, presentationData: PresentationData, content: UniversalVideoContent, originData: GalleryItemOriginData?, indexData: GalleryItemIndexData?, contentInfo: UniversalVideoGalleryItemContentInfo?, caption: NSAttributedString, credit: NSAttributedString? = nil, displayInfoOnTop: Bool = false, hideControls: Bool = false, fromPlayingVideo: Bool = false, isSecret: Bool = false, landscape: Bool = false, timecode: Double? = nil, configuration: GalleryConfiguration? = nil, playbackCompleted: @escaping () -> Void = {}, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction) -> Void, storeMediaPlaybackState: @escaping (MessageId, Double?) -> Void, present: @escaping (ViewController, Any?) -> Void) { + public init(context: AccountContext, presentationData: PresentationData, content: UniversalVideoContent, originData: GalleryItemOriginData?, indexData: GalleryItemIndexData?, contentInfo: UniversalVideoGalleryItemContentInfo?, caption: NSAttributedString, credit: NSAttributedString? = nil, displayInfoOnTop: Bool = false, hideControls: Bool = false, fromPlayingVideo: Bool = false, isSecret: Bool = false, landscape: Bool = false, timecode: Double? = nil, playbackRate: Double = 1.0, configuration: GalleryConfiguration? = nil, playbackCompleted: @escaping () -> Void = {}, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction) -> Void, storeMediaPlaybackState: @escaping (MessageId, Double?, Double) -> Void, present: @escaping (ViewController, Any?) -> Void) { self.context = context self.presentationData = presentationData self.content = content @@ -62,6 +68,7 @@ public class UniversalVideoGalleryItem: GalleryItem { self.isSecret = isSecret self.landscape = landscape self.timecode = timecode + self.playbackRate = playbackRate self.configuration = configuration self.playbackCompleted = playbackCompleted self.performAction = performAction @@ -131,6 +138,8 @@ public class UniversalVideoGalleryItem: GalleryItem { private let pictureInPictureImage = UIImage(bundleImageName: "Media Gallery/PictureInPictureIcon")?.precomposed() private let pictureInPictureButtonImage = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/PictureInPictureButton"), color: .white) +private let moreButtonImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/More"), color: .white) + private let placeholderFont = Font.regular(16.0) private final class UniversalVideoGalleryItemPictureInPictureNode: ASDisplayNode { @@ -253,6 +262,196 @@ private struct FetchControls { let cancel: () -> Void } +func optionsBackgroundImage(dark: Bool) -> UIImage? { + return generateImage(CGSize(width: 28.0, height: 28.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.setFillColor(UIColor(rgb: dark ? 0x1c1c1e : 0x2c2c2e).cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + })?.stretchableImage(withLeftCapWidth: 14, topCapHeight: 14) +} + +private func optionsCircleImage(dark: Bool) -> UIImage? { + return generateImage(CGSize(width: 22.0, height: 22.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.setStrokeColor(UIColor.white.cgColor) + let lineWidth: CGFloat = 1.3 + context.setLineWidth(lineWidth) + + context.strokeEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: lineWidth, dy: lineWidth)) + }) +} + +private func optionsRateImage(rate: String, isLarge: Bool, color: UIColor = .white) -> UIImage? { + return generateImage(isLarge ? CGSize(width: 30.0, height: 30.0) : CGSize(width: 24.0, height: 24.0), rotatedContext: { size, context in + UIGraphicsPushContext(context) + + context.clear(CGRect(origin: CGPoint(), size: size)) + + if let image = generateTintedImage(image: UIImage(bundleImageName: isLarge ? "Chat/Context Menu/Playspeed30" : "Chat/Context Menu/Playspeed24"), color: .white) { + image.draw(at: CGPoint(x: 0.0, y: 0.0)) + } + + let string = NSMutableAttributedString(string: rate, font: Font.with(size: isLarge ? 11.0 : 10.0, design: .round, weight: .semibold), textColor: color) + + var offset = CGPoint(x: 1.0, y: 0.0) + if rate.count >= 3 { + if rate == "0.5x" { + string.addAttribute(.kern, value: -0.8 as NSNumber, range: NSRange(string.string.startIndex ..< string.string.endIndex, in: string.string)) + offset.x += -0.5 + } else { + string.addAttribute(.kern, value: -0.5 as NSNumber, range: NSRange(string.string.startIndex ..< string.string.endIndex, in: string.string)) + offset.x += -0.3 + } + } else { + offset.x += -0.3 + } + + if !isLarge { + offset.x *= 0.5 + offset.y *= 0.5 + } + + let boundingRect = string.boundingRect(with: size, options: [], context: nil) + string.draw(at: CGPoint(x: offset.x + floor((size.width - boundingRect.width) / 2.0), y: offset.y + floor((size.height - boundingRect.height) / 2.0))) + + UIGraphicsPopContext() + }) +} + +private final class MoreHeaderButton: HighlightableButtonNode { + enum Content { + case image(UIImage?) + case more(UIImage?) + } + + let referenceNode: ContextReferenceContentNode + let containerNode: ContextControllerSourceNode + private let iconNode: ASImageNode + private var animationNode: AnimationNode? + + var contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? + + private let wide: Bool + + init(wide: Bool = false) { + self.wide = wide + + self.referenceNode = ContextReferenceContentNode() + self.containerNode = ContextControllerSourceNode() + self.containerNode.animateScale = false + self.iconNode = ASImageNode() + self.iconNode.displaysAsynchronously = false + self.iconNode.displayWithoutProcessing = true + self.iconNode.contentMode = .scaleToFill + + super.init() + + self.containerNode.addSubnode(self.referenceNode) + self.referenceNode.addSubnode(self.iconNode) + self.addSubnode(self.containerNode) + + self.containerNode.shouldBegin = { [weak self] location in + guard let strongSelf = self, let _ = strongSelf.contextAction else { + return false + } + return true + } + self.containerNode.activated = { [weak self] gesture, _ in + guard let strongSelf = self else { + return + } + strongSelf.contextAction?(strongSelf.containerNode, gesture) + } + + self.containerNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: wide ? 32.0 : 22.0, height: 22.0)) + self.referenceNode.frame = self.containerNode.bounds + + self.iconNode.image = optionsCircleImage(dark: false) + if let image = self.iconNode.image { + self.iconNode.frame = CGRect(origin: CGPoint(x: floor((self.containerNode.bounds.width - image.size.width) / 2.0), y: floor((self.containerNode.bounds.height - image.size.height) / 2.0)), size: image.size) + } + } + + private var content: Content? + func setContent(_ content: Content, animated: Bool = false) { + if case .more = content, self.animationNode == nil { + let iconColor = UIColor(rgb: 0xffffff) + let animationNode = AnimationNode(animation: "anim_profilemore", colors: ["Point 2.Group 1.Fill 1": iconColor, + "Point 3.Group 1.Fill 1": iconColor, + "Point 1.Group 1.Fill 1": iconColor], scale: 1.0) + animationNode.frame = self.containerNode.bounds + self.addSubnode(animationNode) + self.animationNode = animationNode + } + if animated { + if let snapshotView = self.referenceNode.view.snapshotContentTree() { + snapshotView.frame = self.referenceNode.frame + self.view.addSubview(snapshotView) + + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + snapshotView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.3, removeOnCompletion: false) + + self.iconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.iconNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.3) + + self.animationNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.animationNode?.layer.animateScale(from: 0.1, to: 1.0, duration: 0.3) + } + + switch content { + case let .image(image): + if let image = image { + self.iconNode.frame = CGRect(origin: CGPoint(x: floor((self.containerNode.bounds.width - image.size.width) / 2.0), y: floor((self.containerNode.bounds.height - image.size.height) / 2.0)), size: image.size) + } + + self.iconNode.image = image + self.iconNode.isHidden = false + self.animationNode?.isHidden = true + case let .more(image): + if let image = image { + self.iconNode.frame = CGRect(origin: CGPoint(x: floor((self.containerNode.bounds.width - image.size.width) / 2.0), y: floor((self.containerNode.bounds.height - image.size.height) / 2.0)), size: image.size) + } + + self.iconNode.image = image + self.iconNode.isHidden = false + self.animationNode?.isHidden = false + } + } else { + self.content = content + switch content { + case let .image(image): + self.iconNode.image = image + self.iconNode.isHidden = false + self.animationNode?.isHidden = true + case let .more(image): + self.iconNode.image = image + self.iconNode.isHidden = false + self.animationNode?.isHidden = false + } + } + } + + override func didLoad() { + super.didLoad() + self.view.isOpaque = false + } + + override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { + return CGSize(width: wide ? 32.0 : 22.0, height: 22.0) + } + + func onLayout() { + } + + func play() { + self.animationNode?.playOnce() + } +} + final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { private let context: AccountContext private let presentationData: PresentationData @@ -266,6 +465,10 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { private let scrubberView: ChatVideoGalleryItemScrubberView private let footerContentNode: ChatItemGalleryFooterContentNode private let overlayContentNode: UniversalVideoGalleryItemOverlayNode + + private let moreBarButton: MoreHeaderButton + private var moreBarButtonRate: Double = 1.0 + private var moreBarButtonRateTimestamp: Double? private var videoNode: UniversalVideoNode? private var videoNodeUserInteractionEnabled: Bool = false @@ -294,6 +497,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { private var item: UniversalVideoGalleryItem? private let statusDisposable = MetaDisposable() + private let moreButtonStateDisposable = MetaDisposable() private let mediaPlaybackStateDisposable = MetaDisposable() private let fetchDisposable = MetaDisposable() @@ -307,6 +511,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { private let isPlayingPromise = ValuePromise(false, ignoreRepeated: true) private let isInteractingPromise = ValuePromise(false, ignoreRepeated: true) private let controlsVisiblePromise = ValuePromise(true, ignoreRepeated: true) + private let isShowingContextMenuPromise = ValuePromise(false, ignoreRepeated: true) private var hideControlsDisposable: Disposable? var playbackCompleted: (() -> Void)? @@ -330,8 +535,14 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.statusNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 50.0, height: 50.0)) self._title.set(.single("")) + + self.moreBarButton = MoreHeaderButton() + self.moreBarButton.isUserInteractionEnabled = true + self.moreBarButton.setContent(.more(optionsCircleImage(dark: false))) super.init() + + self.moreBarButton.addTarget(self, action: #selector(self.moreButtonPressed), forControlEvents: .touchUpInside) self.footerContentNode.interacting = { [weak self] value in self?.isInteractingPromise.set(value) @@ -428,6 +639,19 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { break } } + + self.footerContentNode.toggleFullscreen = { [weak self] in + guard let strongSelf = self else { + return + } + var toLandscape = false + let size = strongSelf.bounds.size + if size.width < size.height { + toLandscape = true + } + strongSelf.updateControlsVisibility(!toLandscape) + strongSelf.updateOrientation(toLandscape ? .landscapeRight : .portrait) + } self.scrubbingFrameDisposable = (self.scrubbingFrame.get() |> deliverOnMainQueue).start(next: { [weak self] result in @@ -453,12 +677,19 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { strongSelf.pictureInPictureButtonPressed() return true } + + self.moreBarButton.contextAction = { [weak self] sourceNode, gesture in + self?.openMoreMenu(sourceNode: sourceNode, gesture: gesture) + } self.titleContentView = GalleryTitleView(frame: CGRect()) self._titleView.set(.single(self.titleContentView)) - let shouldHideControlsSignal: Signal = combineLatest(self.isPlayingPromise.get(), self.isInteractingPromise.get(), self.controlsVisiblePromise.get()) - |> mapToSignal { isPlaying, isIntracting, controlsVisible -> Signal in + let shouldHideControlsSignal: Signal = combineLatest(self.isPlayingPromise.get(), self.isInteractingPromise.get(), self.controlsVisiblePromise.get(), self.isShowingContextMenuPromise.get()) + |> mapToSignal { isPlaying, isIntracting, controlsVisible, isShowingContextMenu -> Signal in + if isShowingContextMenu { + return .complete() + } if isPlaying && !isIntracting && controlsVisible { return .single(Void()) |> delay(4.0, queue: Queue.mainQueue()) @@ -477,6 +708,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { deinit { self.statusDisposable.dispose() + self.moreButtonStateDisposable.dispose() self.mediaPlaybackStateDisposable.dispose() self.scrubbingFrameDisposable?.dispose() self.hideControlsDisposable?.dispose() @@ -646,7 +878,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { if status.timestamp > 5.0 && status.timestamp < status.duration - 5.0 { timestamp = status.timestamp } - item.storeMediaPlaybackState(message.id, timestamp) + item.storeMediaPlaybackState(message.id, timestamp, status.baseRate) } })) } @@ -687,6 +919,56 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } } + self.moreButtonStateDisposable.set(combineLatest(queue: .mainQueue(), + videoNode.status, + self.isShowingContextMenuPromise.get() + ).start(next: { [weak self] status, isShowingContextMenu in + guard let strongSelf = self else { + return + } + guard let status = status else { + return + } + + let effectiveBaseRate: Double + if isShowingContextMenu { + effectiveBaseRate = 1.0 + } else { + effectiveBaseRate = status.baseRate + } + + if abs(effectiveBaseRate - strongSelf.moreBarButtonRate) > 0.01 { + strongSelf.moreBarButtonRate = effectiveBaseRate + let animated: Bool + if let moreBarButtonRateTimestamp = strongSelf.moreBarButtonRateTimestamp { + animated = CFAbsoluteTimeGetCurrent() > (moreBarButtonRateTimestamp + 0.2) + } else { + animated = false + } + strongSelf.moreBarButtonRateTimestamp = CFAbsoluteTimeGetCurrent() + + if abs(effectiveBaseRate - 1.0) > 0.01 { + let rateString: String + if abs(effectiveBaseRate - 0.5) < 0.01 { + rateString = "0.5x" + } else if abs(effectiveBaseRate - 1.5) < 0.01 { + rateString = "1.5x" + } else if abs(effectiveBaseRate - 2.0) < 0.01 { + rateString = "2x" + } else { + rateString = "x" + } + strongSelf.moreBarButton.setContent(.image(optionsRateImage(rate: rateString, isLarge: true)), animated: animated) + } else { + strongSelf.moreBarButton.setContent(.more(optionsCircleImage(dark: false)), animated: animated) + } + } else { + if strongSelf.moreBarButtonRateTimestamp == nil { + strongSelf.moreBarButtonRateTimestamp = CFAbsoluteTimeGetCurrent() + } + } + })) + self.statusDisposable.set((combineLatest(queue: .mainQueue(), videoNode.status, mediaFileStatus) |> deliverOnMainQueue).start(next: { [weak self] value, fetchStatus in if let strongSelf = self { @@ -825,6 +1107,27 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } else { self.hasPictureInPicture = false } + + if let contentInfo = item.contentInfo, case let .message(message) = contentInfo { + var file: TelegramMediaFile? + var isWebpage = false + for m in message.media { + if let m = m as? TelegramMediaFile, m.isVideo { + file = m + break + } else if let m = m as? TelegramMediaWebpage, case let .Loaded(content) = m.content, let f = content.file, f.isVideo { + file = f + isWebpage = true + break + } + } + + if !isWebpage, let file = file, !file.isAnimated { + let moreMenuItem = UIBarButtonItem(customDisplayNode: self.moreBarButton)! + barButtonItems.append(moreMenuItem) + } + } + self._rightBarButtonItems.set(.single(barButtonItems)) videoNode.playbackCompleted = { [weak self, weak videoNode] in @@ -1001,18 +1304,21 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { var isAnimated = false var seek = MediaPlayerSeek.start + var playbackRate: Double = 1.0 if let item = self.item { if let content = item.content as? NativeVideoContent { isAnimated = content.fileReference.media.isAnimated if let time = item.timecode { seek = .timecode(time) } + playbackRate = item.playbackRate } else if let _ = item.content as? WebEmbedVideoContent { if let time = item.timecode { seek = .timecode(time) } } } + videoNode.setBaseRate(playbackRate) if isAnimated { videoNode.seek(0.0) videoNode.play() @@ -1615,7 +1921,208 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } } } - + + private func contentInfo() -> (message: Message, file: TelegramMediaFile, isWebpage: Bool)? { + guard let item = self.item else { + return nil + } + if let contentInfo = item.contentInfo, case let .message(message) = contentInfo { + var file: TelegramMediaFile? + var isWebpage = false + for m in message.media { + if let m = m as? TelegramMediaFile, m.isVideo { + file = m + break + } else if let m = m as? TelegramMediaWebpage, case let .Loaded(content) = m.content, let f = content.file, f.isVideo { + file = f + isWebpage = true + break + } + } + + if let file = file { + return (message, file, isWebpage) + } + } + return nil + } + + private func canDelete() -> Bool { + guard let (message, _, _) = self.contentInfo() else { + return false + } + + var canDelete = false + if let peer = message.peers[message.id.peerId] { + if peer is TelegramUser || peer is TelegramSecretChat { + canDelete = true + } else if let _ = peer as? TelegramGroup { + canDelete = true + } else if let channel = peer as? TelegramChannel { + if message.flags.contains(.Incoming) { + canDelete = channel.hasPermission(.deleteAllMessages) + } else { + canDelete = true + } + } else { + canDelete = false + } + } else { + canDelete = false + } + return canDelete + } + + @objc private func moreButtonPressed() { + self.moreBarButton.play() + self.moreBarButton.contextAction?(self.moreBarButton.containerNode, nil) + } + + private func openMoreMenu(sourceNode: ASDisplayNode, gesture: ContextGesture?) { + let items: Signal<[ContextMenuItem], NoError> = self.contextMenuMainItems() + guard let controller = self.baseNavigationController()?.topViewController as? ViewController else { + return + } + + let contextController = ContextController(account: self.context.account, presentationData: self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme), source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: self.moreBarButton.referenceNode)), items: items, reactionItems: [], gesture: gesture) + self.isShowingContextMenuPromise.set(true) + controller.presentInGlobalOverlay(contextController) + + contextController.dismissed = { [weak self] in + Queue.mainQueue().after(0.1, { + self?.isShowingContextMenuPromise.set(false) + }) + } + } + + private func speedList() -> [(String, String, Double)] { + let speedList: [(String, String, Double)] = [ + ("0.5x", "0.5x", 0.5), + ("Normal", "1x", 1.0), + ("1.5x", "1.5x", 1.5), + ("2x", "2x", 2.0) + ] + + return speedList + } + + private func contextMenuMainItems() -> Signal<[ContextMenuItem], NoError> { + guard let videoNode = self.videoNode else { + return .single([]) + } + + return videoNode.status + |> take(1) + |> deliverOnMainQueue + |> map { [weak self] status -> [ContextMenuItem] in + guard let status = status, let strongSelf = self else { + return [] + } + + var items: [ContextMenuItem] = [] + + var speedValue: String = "Normal" + var speedIconText: String = "1x" + for (text, iconText, speed) in strongSelf.speedList() { + if abs(speed - status.baseRate) < 0.01 { + speedValue = text + speedIconText = iconText + break + } + } + + items.append(.action(ContextMenuActionItem(text: "Playback Speed", textLayout: .secondLineWithValue(speedValue), icon: { theme in + return optionsRateImage(rate: speedIconText, isLarge: false, color: theme.contextMenu.primaryColor) + }, action: { c, _ in + guard let strongSelf = self else { + c.dismiss(completion: nil) + return + } + + c.setItems(strongSelf.contextMenuSpeedItems()) + }))) + if let (message, file, isWebpage) = strongSelf.contentInfo(), !isWebpage { + items.append(.action(ContextMenuActionItem(text: "Save to Gallery", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Download"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in + f(.default) + + if let strongSelf = self { + let _ = (SaveToCameraRoll.saveToCameraRoll(context: strongSelf.context, postbox: strongSelf.context.account.postbox, mediaReference: .message(message: MessageReference(message), media: file)) + |> deliverOnMainQueue).start(completed: { + guard let strongSelf = self else { + return + } + guard let controller = strongSelf.galleryController() else { + return + } + //TODO:localize + controller.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .mediaSaved(text: "Video Saved"), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) + }) + } + }))) + } + if strongSelf.canDelete() { + items.append(.action(ContextMenuActionItem(text: "Delete", textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { _, f in + f(.default) + + if let strongSelf = self { + strongSelf.footerContentNode.deleteButtonPressed() + } + }))) + } + + return items + } + } + + private func contextMenuSpeedItems() -> Signal<[ContextMenuItem], NoError> { + guard let videoNode = self.videoNode else { + return .single([]) + } + + return videoNode.status + |> take(1) + |> deliverOnMainQueue + |> map { [weak self] status -> [ContextMenuItem] in + guard let status = status, let strongSelf = self else { + return [] + } + + var items: [ContextMenuItem] = [] + + for (text, _, rate) in strongSelf.speedList() { + let isSelected = abs(status.baseRate - rate) < 0.01 + items.append(.action(ContextMenuActionItem(text: text, icon: { theme in + if isSelected { + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) + } else { + return nil + } + }, action: { _, f in + f(.default) + + guard let strongSelf = self, let videoNode = strongSelf.videoNode else { + return + } + + videoNode.setBaseRate(rate) + }))) + } + + items.append(.separator) + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Common_Back, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor) + }, action: { c, _ in + guard let strongSelf = self else { + c.dismiss(completion: nil) + return + } + c.setItems(strongSelf.contextMenuMainItems()) + }))) + + return items + } + } + @objc func openStickersButtonPressed() { if let content = self.item?.content as? NativeVideoContent { let media = content.fileReference.abstract @@ -1667,6 +2174,20 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } override func footerContent() -> Signal<(GalleryFooterContentNode?, GalleryOverlayContentNode?), NoError> { - return .single((self.footerContentNode, self.overlayContentNode)) + return .single((self.footerContentNode, nil)) + } +} + +private final class HeaderContextReferenceContentSource: ContextReferenceContentSource { + private let controller: ViewController + private let sourceNode: ContextReferenceContentNode + + init(controller: ViewController, sourceNode: ContextReferenceContentNode) { + self.controller = controller + self.sourceNode = sourceNode + } + + func transitionInfo() -> ContextControllerReferenceViewInfo? { + return ContextControllerReferenceViewInfo(referenceNode: self.sourceNode, contentAreaInScreenSpace: UIScreen.main.bounds) } } diff --git a/submodules/InstantPageUI/Sources/InstantPageGalleryController.swift b/submodules/InstantPageUI/Sources/InstantPageGalleryController.swift index fa8909089b..a0732db93e 100644 --- a/submodules/InstantPageUI/Sources/InstantPageGalleryController.swift +++ b/submodules/InstantPageUI/Sources/InstantPageGalleryController.swift @@ -113,7 +113,7 @@ public struct InstantPageGalleryEntry: Equatable { nativeId = .instantPage(self.pageId, file.fileId) } - return UniversalVideoGalleryItem(context: context, presentationData: presentationData, content: NativeVideoContent(id: nativeId, fileReference: .webPage(webPage: WebpageReference(webPage), media: file), streamVideo: isMediaStreamable(media: file) ? .conservative : .none), originData: nil, indexData: indexData, contentInfo: .webPage(webPage, file, nil), caption: caption, credit: credit, fromPlayingVideo: fromPlayingVideo, landscape: landscape, performAction: { _ in }, openActionOptions: { _ in }, storeMediaPlaybackState: { _, _ in }, present: { _, _ in }) + return UniversalVideoGalleryItem(context: context, presentationData: presentationData, content: NativeVideoContent(id: nativeId, fileReference: .webPage(webPage: WebpageReference(webPage), media: file), streamVideo: isMediaStreamable(media: file) ? .conservative : .none), originData: nil, indexData: indexData, contentInfo: .webPage(webPage, file, nil), caption: caption, credit: credit, fromPlayingVideo: fromPlayingVideo, landscape: landscape, performAction: { _ in }, openActionOptions: { _ in }, storeMediaPlaybackState: { _, _, _ in }, present: { _, _ in }) } else { var representations: [TelegramMediaImageRepresentation] = [] representations.append(contentsOf: file.previewRepresentations) @@ -135,12 +135,12 @@ public struct InstantPageGalleryEntry: Equatable { present(gallery, InstantPageGalleryControllerPresentationArguments(transitionArguments: { entry -> GalleryTransitionArguments? in return makeArguments() })) - }), caption: NSAttributedString(string: ""), fromPlayingVideo: fromPlayingVideo, landscape: landscape, performAction: { _ in }, openActionOptions: { _ in }, storeMediaPlaybackState: { _, _ in }, present: { _, _ in }) + }), caption: NSAttributedString(string: ""), fromPlayingVideo: fromPlayingVideo, landscape: landscape, performAction: { _ in }, openActionOptions: { _ in }, storeMediaPlaybackState: { _, _, _ in }, present: { _, _ in }) } else { if let content = WebEmbedVideoContent(webPage: embedWebpage, webpageContent: webpageContent, openUrl: { url in }) { - return UniversalVideoGalleryItem(context: context, presentationData: presentationData, content: content, originData: nil, indexData: nil, contentInfo: .webPage(webPage, embedWebpage, nil), caption: NSAttributedString(string: ""), fromPlayingVideo: fromPlayingVideo, landscape: landscape, performAction: { _ in }, openActionOptions: { _ in }, storeMediaPlaybackState: { _, _ in }, present: { _, _ in }) + return UniversalVideoGalleryItem(context: context, presentationData: presentationData, content: content, originData: nil, indexData: nil, contentInfo: .webPage(webPage, embedWebpage, nil), caption: NSAttributedString(string: ""), fromPlayingVideo: fromPlayingVideo, landscape: landscape, performAction: { _ in }, openActionOptions: { _ in }, storeMediaPlaybackState: { _, _, _ in }, present: { _, _ in }) } else { preconditionFailure() } diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Collapse.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Collapse.imageset/Contents.json new file mode 100644 index 0000000000..829a0300d8 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Collapse.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "smallscreen_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Collapse.imageset/smallscreen_30.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Collapse.imageset/smallscreen_30.pdf new file mode 100644 index 0000000000..7c2c50ebb8 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Collapse.imageset/smallscreen_30.pdf @@ -0,0 +1,161 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 5.334991 5.334999 cm +0.000000 0.000000 0.000000 scn +6.665000 19.330002 m +7.032269 19.330002 7.330000 19.032270 7.330000 18.665001 c +7.330000 15.865002 l +7.330000 15.837518 l +7.330000 15.837514 l +7.330000 15.837511 l +7.330009 15.300820 7.330016 14.857977 7.300543 14.497252 c +7.269935 14.122624 7.204253 13.778399 7.039532 13.455116 c +6.784030 12.953665 6.376337 12.545971 5.874885 12.290468 c +5.551602 12.125748 5.207376 12.060066 4.832748 12.029457 c +4.472024 11.999985 4.029181 11.999992 3.492490 12.000001 c +3.492487 12.000001 l +3.492483 12.000001 l +3.465000 12.000001 l +0.665000 12.000001 l +0.297731 12.000001 0.000000 12.297731 0.000000 12.665001 c +0.000000 13.032270 0.297731 13.330001 0.665000 13.330001 c +3.465000 13.330001 l +4.036026 13.330001 4.424301 13.330519 4.724444 13.355041 c +5.016824 13.378929 5.166537 13.422241 5.271077 13.475508 c +5.522274 13.603498 5.726503 13.807728 5.854494 14.058924 c +5.907760 14.163464 5.951072 14.313177 5.974960 14.605556 c +5.999483 14.905701 6.000000 15.293976 6.000000 15.865002 c +6.000000 18.665001 l +6.000000 19.032270 6.297730 19.330002 6.665000 19.330002 c +h +13.330000 18.665001 m +13.330000 19.032270 13.032269 19.330002 12.665000 19.330002 c +12.297730 19.330002 12.000000 19.032270 12.000000 18.665001 c +12.000000 15.865002 l +12.000000 15.837526 l +12.000000 15.837497 l +11.999991 15.300814 11.999985 14.857974 12.029457 14.497252 c +12.060065 14.122624 12.125747 13.778399 12.290467 13.455116 c +12.545970 12.953665 12.953663 12.545971 13.455115 12.290468 c +13.778399 12.125748 14.122623 12.060066 14.497252 12.029457 c +14.857978 11.999985 15.300823 11.999992 15.837517 12.000001 c +15.865000 12.000001 l +18.665001 12.000001 l +19.032270 12.000001 19.330002 12.297731 19.330002 12.665001 c +19.330002 13.032270 19.032270 13.330001 18.665001 13.330001 c +15.865000 13.330001 l +15.293975 13.330001 14.905700 13.330519 14.605556 13.355041 c +14.313176 13.378929 14.163464 13.422241 14.058923 13.475508 c +13.807726 13.603498 13.603498 13.807728 13.475507 14.058924 c +13.422240 14.163464 13.378928 14.313177 13.355040 14.605556 c +13.330517 14.905701 13.330000 15.293976 13.330000 15.865002 c +13.330000 18.665001 l +h +15.865000 7.330001 m +15.837525 7.330001 l +15.837497 7.330001 l +15.300811 7.330009 14.857973 7.330016 14.497252 7.300544 c +14.122623 7.269936 13.778399 7.204254 13.455115 7.039534 c +12.953663 6.784031 12.545970 6.376338 12.290467 5.874886 c +12.125747 5.551602 12.060065 5.207377 12.029457 4.832749 c +11.999985 4.472028 11.999991 4.029190 12.000000 3.492504 c +12.000000 3.492476 l +12.000000 3.465000 l +12.000000 0.665001 l +12.000000 0.297731 12.297730 0.000000 12.665000 0.000000 c +13.032269 0.000000 13.330000 0.297731 13.330000 0.665001 c +13.330000 3.465000 l +13.330000 4.036026 13.330517 4.424301 13.355040 4.724444 c +13.378928 5.016825 13.422240 5.166537 13.475507 5.271078 c +13.603498 5.522275 13.807726 5.726503 14.058923 5.854494 c +14.163464 5.907761 14.313176 5.951073 14.605556 5.974961 c +14.905700 5.999484 15.293975 6.000001 15.865000 6.000001 c +18.665001 6.000001 l +19.032270 6.000001 19.330002 6.297731 19.330002 6.665001 c +19.330002 7.032270 19.032270 7.330001 18.665001 7.330001 c +15.865000 7.330001 l +h +3.465000 6.000001 m +4.036026 6.000001 4.424301 5.999484 4.724444 5.974961 c +5.016824 5.951073 5.166537 5.907761 5.271077 5.854494 c +5.522274 5.726503 5.726503 5.522275 5.854494 5.271078 c +5.907760 5.166537 5.951072 5.016825 5.974960 4.724444 c +5.999483 4.424301 6.000000 4.036026 6.000000 3.465000 c +6.000000 0.665001 l +6.000000 0.297731 6.297730 0.000000 6.665000 0.000000 c +7.032269 0.000000 7.330000 0.297731 7.330000 0.665001 c +7.330000 3.465000 l +7.330000 3.492484 l +7.330009 4.029178 7.330016 4.472023 7.300543 4.832749 c +7.269935 5.207377 7.204253 5.551602 7.039532 5.874886 c +6.784030 6.376338 6.376337 6.784031 5.874885 7.039534 c +5.551602 7.204254 5.207376 7.269936 4.832748 7.300544 c +4.472027 7.330016 4.029188 7.330009 3.492504 7.330001 c +3.492474 7.330001 l +3.465000 7.330001 l +0.665000 7.330001 l +0.297731 7.330001 0.000000 7.032270 0.000000 6.665001 c +0.000000 6.297731 0.297731 6.000001 0.665000 6.000001 c +3.465000 6.000001 l +h +f* +n +Q + +endstream +endobj + +3 0 obj + 4194 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Type /Catalog + /Pages 5 0 R + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000004284 00000 n +0000004307 00000 n +0000004480 00000 n +0000004554 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +4613 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Download.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Download.imageset/Contents.json new file mode 100644 index 0000000000..c5c261d24f --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Download.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "down_24.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Download.imageset/down_24.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Download.imageset/down_24.pdf new file mode 100644 index 0000000000..074b2bc7ae --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Download.imageset/down_24.pdf @@ -0,0 +1,92 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 3.334999 3.334999 cm +0.000000 0.000000 0.000000 scn +8.665000 16.000002 m +4.613991 16.000002 1.330000 12.716011 1.330000 8.665002 c +1.330000 4.613994 4.613991 1.330002 8.665000 1.330002 c +12.716008 1.330002 16.000000 4.613994 16.000000 8.665002 c +16.000000 12.716011 12.716008 16.000002 8.665000 16.000002 c +h +0.000000 8.665002 m +0.000000 13.450549 3.879453 17.330002 8.665000 17.330002 c +13.450547 17.330002 17.330002 13.450549 17.330002 8.665002 c +17.330002 3.879455 13.450547 0.000000 8.665000 0.000000 c +3.879453 0.000000 0.000000 3.879455 0.000000 8.665002 c +h +8.665000 12.830002 m +9.032269 12.830002 9.330000 12.532271 9.330000 12.165002 c +9.330000 6.770453 l +11.194775 8.635228 l +11.454473 8.894926 11.875527 8.894926 12.135225 8.635228 c +12.394924 8.375529 12.394924 7.954474 12.135225 7.694776 c +9.135226 4.694776 l +8.875527 4.435078 8.454473 4.435078 8.194774 4.694776 c +5.194774 7.694776 l +4.935075 7.954474 4.935075 8.375529 5.194774 8.635228 c +5.454473 8.894926 5.875527 8.894926 6.135226 8.635228 c +8.000000 6.770453 l +8.000000 12.165002 l +8.000000 12.532271 8.297730 12.830002 8.665000 12.830002 c +h +f* +n +Q + +endstream +endobj + +3 0 obj + 1188 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Type /Catalog + /Pages 5 0 R + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000001278 00000 n +0000001301 00000 n +0000001474 00000 n +0000001548 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1607 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Expand.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Expand.imageset/Contents.json new file mode 100644 index 0000000000..fa440a32b6 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Expand.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "fullscreen_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Expand.imageset/fullscreen_30.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Expand.imageset/fullscreen_30.pdf new file mode 100644 index 0000000000..7df6de5287 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Expand.imageset/fullscreen_30.pdf @@ -0,0 +1,161 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 5.334991 5.334970 cm +0.000000 0.000000 0.000000 scn +15.465001 18.000031 m +16.036026 18.000031 16.424301 17.999512 16.724445 17.974989 c +17.016825 17.951101 17.166538 17.907789 17.271078 17.854523 c +17.522274 17.726532 17.726503 17.522303 17.854494 17.271107 c +17.907761 17.166567 17.951073 17.016853 17.974960 16.724474 c +17.999483 16.424330 18.000000 16.036055 18.000000 15.465029 c +18.000000 12.665030 l +18.000000 12.297760 18.297731 12.000029 18.665001 12.000029 c +19.032270 12.000029 19.330002 12.297760 19.330002 12.665030 c +19.330002 15.465029 l +19.330002 15.492513 l +19.330002 15.492586 l +19.330009 16.029245 19.330015 16.472069 19.300545 16.832777 c +19.269936 17.207407 19.204254 17.551632 19.039534 17.874914 c +18.784031 18.376366 18.376337 18.784060 17.874886 19.039562 c +17.551603 19.204283 17.207378 19.269964 16.832748 19.300573 c +16.472025 19.330046 16.029184 19.330038 15.492496 19.330030 c +15.492476 19.330030 l +15.465001 19.330030 l +12.665001 19.330030 l +12.297731 19.330030 12.000001 19.032299 12.000001 18.665030 c +12.000001 18.297760 12.297731 18.000031 12.665001 18.000031 c +15.465001 18.000031 l +h +3.865001 19.330030 m +3.837527 19.330030 l +3.837508 19.330030 l +3.300819 19.330038 2.857976 19.330046 2.497253 19.300573 c +2.122624 19.269964 1.778399 19.204283 1.455116 19.039562 c +0.953664 18.784060 0.545971 18.376366 0.290469 17.874914 c +0.125748 17.551632 0.060066 17.207407 0.029458 16.832777 c +-0.000015 16.472054 -0.000008 16.029211 0.000000 15.492522 c +0.000000 15.492504 l +0.000000 15.465029 l +0.000000 12.665030 l +0.000000 12.297760 0.297732 12.000029 0.665001 12.000029 c +1.032270 12.000029 1.330001 12.297760 1.330001 12.665030 c +1.330001 15.465029 l +1.330001 16.036055 1.330518 16.424330 1.355041 16.724474 c +1.378929 17.016853 1.422241 17.166567 1.475507 17.271107 c +1.603498 17.522303 1.807727 17.726532 2.058923 17.854523 c +2.163464 17.907789 2.313177 17.951101 2.605557 17.974989 c +2.905700 17.999512 3.293975 18.000031 3.865001 18.000031 c +6.665001 18.000031 l +7.032271 18.000031 7.330001 18.297760 7.330001 18.665030 c +7.330001 19.032299 7.032271 19.330030 6.665001 19.330030 c +3.865001 19.330030 l +h +1.330001 6.665030 m +1.330001 7.032299 1.032270 7.330029 0.665001 7.330029 c +0.297732 7.330029 0.000000 7.032299 0.000000 6.665030 c +0.000000 3.865030 l +0.000000 3.837555 l +0.000000 3.837535 l +-0.000008 3.300846 -0.000015 2.858006 0.029458 2.497282 c +0.060066 2.122652 0.125748 1.778427 0.290469 1.455145 c +0.545971 0.953693 0.953664 0.546000 1.455116 0.290497 c +1.778399 0.125776 2.122624 0.060095 2.497253 0.029486 c +2.857963 0.000015 3.300784 0.000021 3.837445 0.000029 c +3.837518 0.000029 l +3.865001 0.000029 l +6.665001 0.000029 l +7.032271 0.000029 7.330001 0.297760 7.330001 0.665030 c +7.330001 1.032299 7.032271 1.330030 6.665001 1.330030 c +3.865001 1.330030 l +3.293975 1.330030 2.905700 1.330547 2.605557 1.355070 c +2.313177 1.378958 2.163464 1.422270 2.058923 1.475536 c +1.807727 1.603527 1.603498 1.807756 1.475507 2.058952 c +1.422241 2.163492 1.378929 2.313206 1.355041 2.605585 c +1.330518 2.905729 1.330001 3.294004 1.330001 3.865030 c +1.330001 6.665030 l +h +18.665001 7.330029 m +19.032270 7.330029 19.330002 7.032299 19.330002 6.665030 c +19.330002 3.865030 l +19.330002 3.837546 l +19.330002 3.837475 l +19.330009 3.300812 19.330015 2.857990 19.300545 2.497282 c +19.269936 2.122652 19.204254 1.778427 19.039534 1.455145 c +18.784031 0.953693 18.376337 0.546000 17.874886 0.290497 c +17.551603 0.125776 17.207378 0.060095 16.832748 0.029486 c +16.472040 0.000015 16.029217 0.000021 15.492556 0.000029 c +15.492484 0.000029 l +15.465001 0.000029 l +12.665001 0.000029 l +12.297731 0.000029 12.000001 0.297760 12.000001 0.665030 c +12.000001 1.032299 12.297731 1.330030 12.665001 1.330030 c +15.465001 1.330030 l +16.036026 1.330030 16.424301 1.330547 16.724445 1.355070 c +17.016825 1.378958 17.166538 1.422270 17.271078 1.475536 c +17.522274 1.603527 17.726503 1.807756 17.854494 2.058952 c +17.907761 2.163492 17.951073 2.313206 17.974960 2.605585 c +17.999483 2.905729 18.000000 3.294004 18.000000 3.865030 c +18.000000 6.665030 l +18.000000 7.032299 18.297731 7.330029 18.665001 7.330029 c +h +f* +n +Q + +endstream +endobj + +3 0 obj + 4198 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Type /Catalog + /Pages 5 0 R + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000004288 00000 n +0000004311 00000 n +0000004484 00000 n +0000004558 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +4617 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Playspeed24.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Playspeed24.imageset/Contents.json new file mode 100644 index 0000000000..3bf115d3ba --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Playspeed24.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "playspeed_24.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Playspeed24.imageset/playspeed_24.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Playspeed24.imageset/playspeed_24.pdf new file mode 100644 index 0000000000..c8af2af3b2 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Playspeed24.imageset/playspeed_24.pdf @@ -0,0 +1,113 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 3.335007 3.334999 cm +0.000000 0.000000 0.000000 scn +0.000007 16.665001 m +0.000007 17.032270 0.297737 17.330002 0.665007 17.330002 c +1.165007 17.330002 l +1.532276 17.330002 1.830007 17.032270 1.830007 16.665001 c +1.830007 16.297733 1.532276 16.000002 1.165007 16.000002 c +0.665007 16.000002 l +0.297737 16.000002 0.000007 16.297733 0.000007 16.665001 c +h +9.665000 17.330002 m +9.297730 17.330002 9.000000 17.032270 9.000000 16.665001 c +9.000000 16.297731 9.297730 16.000000 9.665000 16.000000 c +16.665001 16.000000 l +17.032270 16.000000 17.330002 16.297731 17.330002 16.665001 c +17.330002 17.032270 17.032270 17.330002 16.665001 17.330002 c +9.665000 17.330002 l +h +9.665000 1.330002 m +9.297730 1.330002 9.000000 1.032270 9.000000 0.665001 c +9.000000 0.297731 9.297730 0.000000 9.665000 0.000000 c +16.665001 0.000000 l +17.032270 0.000000 17.330002 0.297731 17.330002 0.665001 c +17.330002 1.032270 17.032270 1.330002 16.665001 1.330002 c +9.665000 1.330002 l +h +0.665000 1.330002 m +0.297731 1.330002 0.000000 1.032270 0.000000 0.665001 c +0.000000 0.297731 0.297731 0.000000 0.665000 0.000000 c +1.165000 0.000000 l +1.532269 0.000000 1.830000 0.297731 1.830000 0.665001 c +1.830000 1.032270 1.532269 1.330002 1.165000 1.330002 c +0.665000 1.330002 l +h +3.000000 16.665001 m +3.000000 17.032270 3.297731 17.330002 3.665000 17.330002 c +7.165000 17.330002 l +7.532269 17.330002 7.830000 17.032270 7.830000 16.665001 c +7.830000 16.297731 7.532269 16.000000 7.165000 16.000000 c +3.665000 16.000000 l +3.297731 16.000000 3.000000 16.297731 3.000000 16.665001 c +h +3.665000 1.330002 m +3.297731 1.330002 3.000000 1.032270 3.000000 0.665001 c +3.000000 0.297731 3.297731 0.000000 3.665000 0.000000 c +7.165000 0.000000 l +7.532269 0.000000 7.830000 0.297731 7.830000 0.665001 c +7.830000 1.032270 7.532269 1.330002 7.165000 1.330002 c +3.665000 1.330002 l +h +f* +n +Q + +endstream +endobj + +3 0 obj + 1901 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Type /Catalog + /Pages 5 0 R + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000001991 00000 n +0000002014 00000 n +0000002187 00000 n +0000002261 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +2320 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Playspeed30.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Playspeed30.imageset/Contents.json new file mode 100644 index 0000000000..e75becabad --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Playspeed30.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "playspeed_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Playspeed30.imageset/playspeed_30.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Playspeed30.imageset/playspeed_30.pdf new file mode 100644 index 0000000000..4d99d69bdb --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Playspeed30.imageset/playspeed_30.pdf @@ -0,0 +1,113 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 5.335007 5.334999 cm +0.000000 0.000000 0.000000 scn +0.000000 18.665001 m +0.000000 19.032270 0.297731 19.330002 0.665000 19.330002 c +1.665000 19.330002 l +2.032269 19.330002 2.330000 19.032270 2.330000 18.665001 c +2.330000 18.297733 2.032269 18.000002 1.665000 18.000002 c +0.665000 18.000002 l +0.297731 18.000002 0.000000 18.297733 0.000000 18.665001 c +h +11.665000 19.330002 m +11.297730 19.330002 11.000000 19.032270 11.000000 18.665001 c +11.000000 18.297733 11.297730 18.000002 11.665000 18.000002 c +18.665001 18.000002 l +19.032269 18.000002 19.330000 18.297733 19.330000 18.665001 c +19.330000 19.032270 19.032269 19.330002 18.665001 19.330002 c +11.665000 19.330002 l +h +11.665000 1.330002 m +11.297730 1.330002 11.000000 1.032270 11.000000 0.665001 c +11.000000 0.297731 11.297730 0.000000 11.665000 0.000000 c +18.665001 0.000000 l +19.032269 0.000000 19.330000 0.297731 19.330000 0.665001 c +19.330000 1.032270 19.032269 1.330002 18.665001 1.330002 c +11.665000 1.330002 l +h +0.665000 1.330002 m +0.297731 1.330002 0.000000 1.032270 0.000000 0.665001 c +0.000000 0.297731 0.297731 0.000000 0.665000 0.000000 c +1.665000 0.000000 l +2.032269 0.000000 2.330000 0.297731 2.330000 0.665001 c +2.330000 1.032270 2.032269 1.330002 1.665000 1.330002 c +0.665000 1.330002 l +h +4.000000 18.665001 m +4.000000 19.032270 4.297730 19.330002 4.665000 19.330002 c +8.665000 19.330002 l +9.032269 19.330002 9.330000 19.032270 9.330000 18.665001 c +9.330000 18.297733 9.032269 18.000002 8.665000 18.000002 c +4.665000 18.000002 l +4.297730 18.000002 4.000000 18.297733 4.000000 18.665001 c +h +4.665000 1.330002 m +4.297730 1.330002 4.000000 1.032270 4.000000 0.665001 c +4.000000 0.297731 4.297730 0.000000 4.665000 0.000000 c +8.665000 0.000000 l +9.032269 0.000000 9.330000 0.297731 9.330000 0.665001 c +9.330000 1.032270 9.032269 1.330002 8.665000 1.330002 c +4.665000 1.330002 l +h +f* +n +Q + +endstream +endobj + +3 0 obj + 1917 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Type /Catalog + /Pages 5 0 R + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000002007 00000 n +0000002030 00000 n +0000002203 00000 n +0000002277 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +2336 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 85de671d29..fbf469106b 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -804,13 +804,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let strongSelf = self { strongSelf.controllerInteraction?.addContact(phoneNumber) } - }, storeMediaPlaybackState: { [weak self] messageId, timestamp in + }, storeMediaPlaybackState: { [weak self] messageId, timestamp, playbackRate in guard let strongSelf = self else { return } var storedState: MediaPlaybackStoredState? if let timestamp = timestamp { - storedState = MediaPlaybackStoredState(timestamp: timestamp, playbackRate: .x1) + storedState = MediaPlaybackStoredState(timestamp: timestamp, playbackRate: AudioPlaybackRate(playbackRate)) } let _ = updateMediaPlaybackStoredStateInteractively(postbox: strongSelf.context.account.postbox, messageId: messageId, state: storedState).start() }, editMedia: { [weak self] messageId, snapshots, transitionCompletion in diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 2ab5172acb..7a34df2f69 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -3072,13 +3072,13 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD } }, completed: {}) } - }, storeMediaPlaybackState: { [weak self] messageId, timestamp in + }, storeMediaPlaybackState: { [weak self] messageId, timestamp, playbackRate in guard let strongSelf = self else { return } var storedState: MediaPlaybackStoredState? if let timestamp = timestamp { - storedState = MediaPlaybackStoredState(timestamp: timestamp, playbackRate: .x1) + storedState = MediaPlaybackStoredState(timestamp: timestamp, playbackRate: AudioPlaybackRate(playbackRate) ?? .x1) } let _ = updateMediaPlaybackStoredStateInteractively(postbox: strongSelf.context.account.postbox, messageId: messageId, state: storedState).start() }, editMedia: { [weak self] messageId, snapshots, transitionCompletion in diff --git a/submodules/TelegramUI/Sources/SharedMediaPlayer.swift b/submodules/TelegramUI/Sources/SharedMediaPlayer.swift index c9eba8d4c0..0277922cae 100644 --- a/submodules/TelegramUI/Sources/SharedMediaPlayer.swift +++ b/submodules/TelegramUI/Sources/SharedMediaPlayer.swift @@ -442,19 +442,7 @@ final class SharedMediaPlayer { case let .setBaseRate(baseRate): self.playbackRate = baseRate if let playbackItem = self.playbackItem { - let rateValue: Double - switch baseRate { - case .x1: - rateValue = 1.0 - case .x2: - rateValue = 1.8 - case .x4: - rateValue = 4.0 - case .x8: - rateValue = 8.0 - case .x16: - rateValue = 16.0 - } + let rateValue: Double = baseRate.doubleValue switch playbackItem { case let .audio(player): player.setBaseRate(rateValue) diff --git a/submodules/TelegramUIPreferences/Sources/MusicPlaybackSettings.swift b/submodules/TelegramUIPreferences/Sources/MusicPlaybackSettings.swift index 14cfe6a36c..d2707fa138 100644 --- a/submodules/TelegramUIPreferences/Sources/MusicPlaybackSettings.swift +++ b/submodules/TelegramUIPreferences/Sources/MusicPlaybackSettings.swift @@ -15,15 +15,25 @@ public enum MusicPlaybackSettingsLooping: Int32 { } public enum AudioPlaybackRate: Int32 { + case x0_5 = 500 case x1 = 1000 + case x1_5 = 1500 case x2 = 2000 case x4 = 4000 case x8 = 8000 case x16 = 16000 - var doubleValue: Double { + public var doubleValue: Double { return Double(self.rawValue) / 1000.0 } + + public init(_ value: Double) { + if let resolved = AudioPlaybackRate(rawValue: Int32(value * 1000.0)) { + self = resolved + } else { + self = .x1 + } + } } public struct MusicPlaybackSettings: PreferencesEntry, Equatable {