Peer media redesign

This commit is contained in:
Ali 2020-05-01 19:24:03 +04:00
parent 890db9606c
commit 84848c6e2e
24 changed files with 3061 additions and 2449 deletions

View File

@ -5338,7 +5338,7 @@ Any member of this group will be able to see messages in the channel.";
"PeerInfo.PaneMedia" = "Media"; "PeerInfo.PaneMedia" = "Media";
"PeerInfo.PaneFiles" = "Files"; "PeerInfo.PaneFiles" = "Files";
"PeerInfo.PaneLinks" = "Links"; "PeerInfo.PaneLinks" = "Links";
"PeerInfo.PaneVoice" = "Voice Messages"; "PeerInfo.PaneVoiceAndVideo" = "Voice";
"PeerInfo.PaneAudio" = "Audio"; "PeerInfo.PaneAudio" = "Audio";
"PeerInfo.PaneGroups" = "Groups"; "PeerInfo.PaneGroups" = "Groups";
"PeerInfo.PaneMembers" = "Members"; "PeerInfo.PaneMembers" = "Members";

View File

@ -228,6 +228,8 @@ public final class ContextGesture: UIGestureRecognizer, UIGestureRecognizerDeleg
self.delayTimer?.invalidate() self.delayTimer?.invalidate()
self.animator?.invalidate() self.animator?.invalidate()
self.state = .failed self.state = .failed
} else {
self.state = .failed
} }
} }

View File

@ -131,6 +131,19 @@ public enum GeneralScrollDirection {
case down case down
} }
private func cancelContextGestures(view: UIView) {
if let gestureRecognizers = view.gestureRecognizers {
for gesture in gestureRecognizers {
if let gesture = gesture as? ContextGesture {
gesture.cancel()
}
}
}
for subview in view.subviews {
cancelContextGestures(view: subview)
}
}
open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGestureRecognizerDelegate { open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGestureRecognizerDelegate {
public final let scroller: ListViewScroller public final let scroller: ListViewScroller
private final var visibleSize: CGSize = CGSize() private final var visibleSize: CGSize = CGSize()
@ -666,6 +679,10 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
self.scrolledToItem = nil self.scrolledToItem = nil
self.beganInteractiveDragging() self.beganInteractiveDragging()
for itemNode in self.itemNodes {
cancelContextGestures(view: itemNode.view)
}
} }
public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
@ -739,8 +756,10 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
self.decelerationAnimator?.isPaused = false self.decelerationAnimator?.isPaused = false
} }
public var defaultToSynchronousTransactionWhileScrolling: Bool = false
public func scrollViewDidScroll(_ scrollView: UIScrollView) { public func scrollViewDidScroll(_ scrollView: UIScrollView) {
self.updateScrollViewDidScroll(scrollView, synchronous: false) self.updateScrollViewDidScroll(scrollView, synchronous: self.defaultToSynchronousTransactionWhileScrolling)
} }
private var generalAccumulatedDeltaY: CGFloat = 0.0 private var generalAccumulatedDeltaY: CGFloat = 0.0
@ -3606,11 +3625,15 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
var updatedState = state var updatedState = state
var updatedOperations = operations var updatedOperations = operations
updatedState.removeInvisibleNodes(&updatedOperations) updatedState.removeInvisibleNodes(&updatedOperations)
if synchronous {
self.replayOperations(animated: false, animateAlpha: false, animateCrossfade: false, synchronous: false, animateTopItemVerticalOrigin: false, operations: updatedOperations, requestItemInsertionAnimationsIndices: Set(), scrollToItem: nil, additionalScrollDistance: 0.0, updateSizeAndInsets: nil, stationaryItemIndex: nil, updateOpaqueState: nil, completion: completion)
} else {
self.dispatchOnVSync { self.dispatchOnVSync {
self.replayOperations(animated: false, animateAlpha: false, animateCrossfade: false, synchronous: false, animateTopItemVerticalOrigin: false, operations: updatedOperations, requestItemInsertionAnimationsIndices: Set(), scrollToItem: nil, additionalScrollDistance: 0.0, updateSizeAndInsets: nil, stationaryItemIndex: nil, updateOpaqueState: nil, completion: completion) self.replayOperations(animated: false, animateAlpha: false, animateCrossfade: false, synchronous: false, animateTopItemVerticalOrigin: false, operations: updatedOperations, requestItemInsertionAnimationsIndices: Set(), scrollToItem: nil, additionalScrollDistance: 0.0, updateSizeAndInsets: nil, stationaryItemIndex: nil, updateOpaqueState: nil, completion: completion)
} }
} }
} }
}
if synchronous { if synchronous {
begin() begin()
} else { } else {

View File

@ -264,7 +264,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode {
let editableControlLayout = ItemListEditableControlNode.asyncLayout(self.editableControlNode) let editableControlLayout = ItemListEditableControlNode.asyncLayout(self.editableControlNode)
let reorderControlLayout = ItemListEditableReorderControlNode.asyncLayout(self.reorderControlNode) let reorderControlLayout = ItemListEditableReorderControlNode.asyncLayout(self.reorderControlNode)
var previousThumbnailItem = self.currentThumbnailItem let previousThumbnailItem = self.currentThumbnailItem
var currentDisabledOverlayNode = self.disabledOverlayNode var currentDisabledOverlayNode = self.disabledOverlayNode
let currentItem = self.layoutParams?.0 let currentItem = self.layoutParams?.0

View File

@ -1279,14 +1279,14 @@ public func gifPaneVideoThumbnail(account: Account, videoReference: FileMediaRef
} }
} }
public func mediaGridMessageVideo(postbox: Postbox, videoReference: FileMediaReference, onlyFullSize: Bool = false, synchronousLoad: Bool = false, autoFetchFullSizeThumbnail: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { public func mediaGridMessageVideo(postbox: Postbox, videoReference: FileMediaReference, onlyFullSize: Bool = false, synchronousLoad: Bool = false, autoFetchFullSizeThumbnail: Bool = false, overlayColor: UIColor? = nil) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> {
return internalMediaGridMessageVideo(postbox: postbox, videoReference: videoReference, onlyFullSize: onlyFullSize, synchronousLoad: synchronousLoad, autoFetchFullSizeThumbnail: autoFetchFullSizeThumbnail) return internalMediaGridMessageVideo(postbox: postbox, videoReference: videoReference, onlyFullSize: onlyFullSize, synchronousLoad: synchronousLoad, autoFetchFullSizeThumbnail: autoFetchFullSizeThumbnail, overlayColor: overlayColor)
|> map { |> map {
return $0.1 return $0.1
} }
} }
public func internalMediaGridMessageVideo(postbox: Postbox, videoReference: FileMediaReference, imageReference: ImageMediaReference? = nil, onlyFullSize: Bool = false, synchronousLoad: Bool = false, autoFetchFullSizeThumbnail: Bool = false) -> Signal<(() -> CGSize?, (TransformImageArguments) -> DrawingContext?), NoError> { public func internalMediaGridMessageVideo(postbox: Postbox, videoReference: FileMediaReference, imageReference: ImageMediaReference? = nil, onlyFullSize: Bool = false, synchronousLoad: Bool = false, autoFetchFullSizeThumbnail: Bool = false, overlayColor: UIColor? = nil) -> Signal<(() -> CGSize?, (TransformImageArguments) -> DrawingContext?), NoError> {
let signal: Signal<Tuple3<Data?, Tuple2<Data, String>?, Bool>, NoError> let signal: Signal<Tuple3<Data?, Tuple2<Data, String>?, Bool>, NoError>
if let imageReference = imageReference { if let imageReference = imageReference {
signal = chatMessagePhotoDatas(postbox: postbox, photoReference: imageReference, tryAdditionalRepresentations: true, synchronousLoad: synchronousLoad) signal = chatMessagePhotoDatas(postbox: postbox, photoReference: imageReference, tryAdditionalRepresentations: true, synchronousLoad: synchronousLoad)
@ -1480,6 +1480,14 @@ public func internalMediaGridMessageVideo(postbox: Postbox, videoReference: File
} }
} }
if let overlayColor = overlayColor {
context.withFlippedContext { c in
c.setBlendMode(.normal)
c.setFillColor(overlayColor.cgColor)
c.fill(arguments.drawingRect)
}
}
addCorners(context, arguments: arguments) addCorners(context, arguments: arguments)
return context return context
@ -2400,7 +2408,7 @@ private func drawAlbumArtPlaceholder(into c: CGContext, arguments: TransformImag
} }
} }
public func playerAlbumArt(postbox: Postbox, fileReference: FileMediaReference?, albumArt: SharedMediaPlaybackAlbumArt?, thumbnail: Bool) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { public func playerAlbumArt(postbox: Postbox, fileReference: FileMediaReference?, albumArt: SharedMediaPlaybackAlbumArt?, thumbnail: Bool, overlayColor: UIColor? = nil, emptyColor: UIColor? = nil) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> {
var fileArtworkData: Signal<Data?, NoError> = .single(nil) var fileArtworkData: Signal<Data?, NoError> = .single(nil)
if let fileReference = fileReference { if let fileReference = fileReference {
let size = thumbnail ? CGSize(width: 48.0, height: 48.0) : CGSize(width: 320.0, height: 320.0) let size = thumbnail ? CGSize(width: 48.0, height: 48.0) : CGSize(width: 320.0, height: 320.0)
@ -2471,12 +2479,24 @@ public func playerAlbumArt(postbox: Postbox, fileReference: FileMediaReference?,
let imageSize = sourceImage.size.aspectFilled(arguments.drawingRect.size) let imageSize = sourceImage.size.aspectFilled(arguments.drawingRect.size)
context.withFlippedContext { c in context.withFlippedContext { c in
c.draw(cgImage, in: CGRect(origin: CGPoint(x: floor((arguments.drawingRect.size.width - imageSize.width) / 2.0), y: floor((arguments.drawingRect.size.height - imageSize.height) / 2.0)), size: imageSize)) c.draw(cgImage, in: CGRect(origin: CGPoint(x: floor((arguments.drawingRect.size.width - imageSize.width) / 2.0), y: floor((arguments.drawingRect.size.height - imageSize.height) / 2.0)), size: imageSize))
if let overlayColor = overlayColor {
c.setFillColor(overlayColor.cgColor)
c.fill(arguments.drawingRect)
}
}
} else {
if let emptyColor = emptyColor {
context.withFlippedContext { c in
let rect = arguments.drawingRect
c.setFillColor(emptyColor.cgColor)
c.fill(rect)
} }
} else { } else {
context.withFlippedContext { c in context.withFlippedContext { c in
drawAlbumArtPlaceholder(into: c, arguments: arguments, thumbnail: thumbnail) drawAlbumArtPlaceholder(into: c, arguments: arguments, thumbnail: thumbnail)
} }
} }
}
addCorners(context, arguments: arguments) addCorners(context, arguments: arguments)

View File

@ -440,18 +440,23 @@ private final class SemanticStatusNodeTransitionContext {
public final class SemanticStatusNode: ASControlNode { public final class SemanticStatusNode: ASControlNode {
public var backgroundNodeColor: UIColor { public var backgroundNodeColor: UIColor {
didSet { didSet {
if !self.backgroundNodeColor.isEqual(oldValue) {
self.setNeedsDisplay() self.setNeedsDisplay()
} }
} }
}
public var foregroundNodeColor: UIColor { public var foregroundNodeColor: UIColor {
didSet { didSet {
if !self.foregroundNodeColor.isEqual(oldValue) {
self.setNeedsDisplay() self.setNeedsDisplay()
} }
} }
}
private var animator: ConstantDisplayLinkAnimator? private var animator: ConstantDisplayLinkAnimator?
private var hasState: Bool = false
public private(set) var state: SemanticStatusNodeState public private(set) var state: SemanticStatusNodeState
private var transtionContext: SemanticStatusNodeTransitionContext? private var transtionContext: SemanticStatusNodeTransitionContext?
private var stateContext: SemanticStatusNodeStateContext private var stateContext: SemanticStatusNodeStateContext
@ -505,8 +510,11 @@ public final class SemanticStatusNode: ASControlNode {
public func transitionToState(_ state: SemanticStatusNodeState, animated: Bool = true, synchronous: Bool = false, completion: @escaping () -> Void = {}) { public func transitionToState(_ state: SemanticStatusNodeState, animated: Bool = true, synchronous: Bool = false, completion: @escaping () -> Void = {}) {
var animated = animated var animated = animated
if !self.hasState {
self.hasState = true
animated = false
}
if self.state != state { if self.state != state {
let fromState = self.state
self.state = state self.state = state
let previousStateContext = self.stateContext let previousStateContext = self.stateContext
self.stateContext = self.state.context(current: self.stateContext) self.stateContext = self.state.context(current: self.stateContext)

View File

@ -7,9 +7,9 @@ import SyncCore
import TelegramPresentationData import TelegramPresentationData
import AccountContext import AccountContext
final class MediaNavigationAccessoryContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { public final class MediaNavigationAccessoryContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
let backgroundNode: ASDisplayNode public let backgroundNode: ASDisplayNode
let headerNode: MediaNavigationAccessoryHeaderNode public let headerNode: MediaNavigationAccessoryHeaderNode
private let currentHeaderHeight: CGFloat = MediaNavigationAccessoryHeaderNode.minimizedHeight private let currentHeaderHeight: CGFloat = MediaNavigationAccessoryHeaderNode.minimizedHeight
@ -44,7 +44,7 @@ final class MediaNavigationAccessoryContainerNode: ASDisplayNode, UIGestureRecog
self.headerNode.updateLayout(size: CGSize(width: size.width, height: headerHeight), leftInset: leftInset, rightInset: rightInset, transition: transition) self.headerNode.updateLayout(size: CGSize(width: size.width, height: headerHeight), leftInset: leftInset, rightInset: rightInset, transition: transition)
} }
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if !self.headerNode.frame.contains(point) { if !self.headerNode.frame.contains(point) {
return nil return nil
} }

View File

@ -127,8 +127,8 @@ private func generateMaskImage(color: UIColor) -> UIImage? {
}) })
} }
final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollViewDelegate { public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollViewDelegate {
static let minimizedHeight: CGFloat = 37.0 public static let minimizedHeight: CGFloat = 37.0
private var theme: PresentationTheme private var theme: PresentationTheme
private var strings: PresentationStrings private var strings: PresentationStrings
@ -156,7 +156,7 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollViewDeleg
private var validLayout: (CGSize, CGFloat, CGFloat)? private var validLayout: (CGSize, CGFloat, CGFloat)?
var displayScrubber: Bool = true { public var displayScrubber: Bool = true {
didSet { didSet {
self.scrubbingNode.isHidden = !self.displayScrubber self.scrubbingNode.isHidden = !self.displayScrubber
} }
@ -166,14 +166,14 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollViewDeleg
private var tapRecognizer: UITapGestureRecognizer? private var tapRecognizer: UITapGestureRecognizer?
var tapAction: (() -> Void)? public var tapAction: (() -> Void)?
var close: (() -> Void)? public var close: (() -> Void)?
var toggleRate: (() -> Void)? public var toggleRate: (() -> Void)?
var togglePlayPause: (() -> Void)? public var togglePlayPause: (() -> Void)?
var playPrevious: (() -> Void)? public var playPrevious: (() -> Void)?
var playNext: (() -> Void)? public var playNext: (() -> Void)?
var playbackBaseRate: AudioPlaybackRate? = nil { public var playbackBaseRate: AudioPlaybackRate? = nil {
didSet { didSet {
guard self.playbackBaseRate != oldValue, let playbackBaseRate = self.playbackBaseRate else { guard self.playbackBaseRate != oldValue, let playbackBaseRate = self.playbackBaseRate else {
return return
@ -193,13 +193,13 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollViewDeleg
} }
} }
var playbackStatus: Signal<MediaPlayerStatus, NoError>? { public var playbackStatus: Signal<MediaPlayerStatus, NoError>? {
didSet { didSet {
self.scrubbingNode.status = self.playbackStatus self.scrubbingNode.status = self.playbackStatus
} }
} }
var playbackItems: (SharedMediaPlaylistItem?, SharedMediaPlaylistItem?, SharedMediaPlaylistItem?)? { public var playbackItems: (SharedMediaPlaylistItem?, SharedMediaPlaylistItem?, SharedMediaPlaylistItem?)? {
didSet { didSet {
if !arePlaylistItemsEqual(self.playbackItems?.0, oldValue?.0) || !arePlaylistItemsEqual(self.playbackItems?.1, oldValue?.1) || !arePlaylistItemsEqual(self.playbackItems?.2, oldValue?.2), let layout = validLayout { if !arePlaylistItemsEqual(self.playbackItems?.0, oldValue?.0) || !arePlaylistItemsEqual(self.playbackItems?.1, oldValue?.1) || !arePlaylistItemsEqual(self.playbackItems?.2, oldValue?.2), let layout = validLayout {
self.updateLayout(size: layout.0, leftInset: layout.1, rightInset: layout.2, transition: .immediate) self.updateLayout(size: layout.0, leftInset: layout.1, rightInset: layout.2, transition: .immediate)
@ -207,7 +207,7 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollViewDeleg
} }
} }
init(presentationData: PresentationData) { public init(presentationData: PresentationData) {
self.theme = presentationData.theme self.theme = presentationData.theme
self.strings = presentationData.strings self.strings = presentationData.strings
self.dateTimeFormat = presentationData.dateTimeFormat self.dateTimeFormat = presentationData.dateTimeFormat
@ -346,7 +346,7 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollViewDeleg
} }
} }
override func didLoad() { override public func didLoad() {
super.didLoad() super.didLoad()
self.view.disablesInteractiveTransitionGestureRecognizer = true self.view.disablesInteractiveTransitionGestureRecognizer = true
@ -361,7 +361,7 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollViewDeleg
self.view.addGestureRecognizer(tapRecognizer) self.view.addGestureRecognizer(tapRecognizer)
} }
func updatePresentationData(_ presentationData: PresentationData) { public func updatePresentationData(_ presentationData: PresentationData) {
self.theme = presentationData.theme self.theme = presentationData.theme
self.strings = presentationData.strings self.strings = presentationData.strings
self.nameDisplayOrder = presentationData.nameDisplayOrder self.nameDisplayOrder = presentationData.nameDisplayOrder
@ -390,17 +390,17 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollViewDeleg
} }
} }
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
if scrollView.isDecelerating { if scrollView.isDecelerating {
self.changeTrack() self.changeTrack()
} }
} }
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
self.changeTrack() self.changeTrack()
} }
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
guard !decelerate else { guard !decelerate else {
return return
} }
@ -418,7 +418,7 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollViewDeleg
} }
} }
func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) { public func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) {
self.validLayout = (size, leftInset, rightInset) self.validLayout = (size, leftInset, rightInset)
let minHeight = MediaNavigationAccessoryHeaderNode.minimizedHeight let minHeight = MediaNavigationAccessoryHeaderNode.minimizedHeight
@ -472,19 +472,19 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollViewDeleg
self.accessibilityAreaNode.frame = CGRect(origin: CGPoint(x: self.actionButton.frame.maxX, y: 0.0), size: CGSize(width: self.rateButton.frame.minX - self.actionButton.frame.maxX, height: minHeight)) self.accessibilityAreaNode.frame = CGRect(origin: CGPoint(x: self.actionButton.frame.maxX, y: 0.0), size: CGSize(width: self.rateButton.frame.minX - self.actionButton.frame.maxX, height: minHeight))
} }
@objc func closeButtonPressed() { @objc public func closeButtonPressed() {
self.close?() self.close?()
} }
@objc func rateButtonPressed() { @objc public func rateButtonPressed() {
self.toggleRate?() self.toggleRate?()
} }
@objc func actionButtonPressed() { @objc public func actionButtonPressed() {
self.togglePlayPause?() self.togglePlayPause?()
} }
@objc func tapGesture(_ recognizer: UITapGestureRecognizer) { @objc public func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state { if case .ended = recognizer.state {
self.tapAction?() self.tapAction?()
} }

View File

@ -6,17 +6,17 @@ import TelegramCore
import SyncCore import SyncCore
import AccountContext import AccountContext
final class MediaNavigationAccessoryPanel: ASDisplayNode { public final class MediaNavigationAccessoryPanel: ASDisplayNode {
let containerNode: MediaNavigationAccessoryContainerNode public let containerNode: MediaNavigationAccessoryContainerNode
var close: (() -> Void)? public var close: (() -> Void)?
var toggleRate: (() -> Void)? public var toggleRate: (() -> Void)?
var togglePlayPause: (() -> Void)? public var togglePlayPause: (() -> Void)?
var tapAction: (() -> Void)? public var tapAction: (() -> Void)?
var playPrevious: (() -> Void)? public var playPrevious: (() -> Void)?
var playNext: (() -> Void)? public var playNext: (() -> Void)?
init(context: AccountContext) { public init(context: AccountContext) {
self.containerNode = MediaNavigationAccessoryContainerNode(context: context) self.containerNode = MediaNavigationAccessoryContainerNode(context: context)
super.init() super.init()
@ -53,12 +53,12 @@ final class MediaNavigationAccessoryPanel: ASDisplayNode {
} }
} }
func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) { public func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) {
transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(), size: size)) transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(), size: size))
self.containerNode.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, transition: transition) self.containerNode.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, transition: transition)
} }
func animateIn(transition: ContainedViewLayoutTransition) { public func animateIn(transition: ContainedViewLayoutTransition) {
self.clipsToBounds = true self.clipsToBounds = true
let contentPosition = self.containerNode.layer.position let contentPosition = self.containerNode.layer.position
transition.animatePosition(node: self.containerNode, from: CGPoint(x: contentPosition.x, y: contentPosition.y - 37.0), completion: { [weak self] _ in transition.animatePosition(node: self.containerNode, from: CGPoint(x: contentPosition.x, y: contentPosition.y - 37.0), completion: { [weak self] _ in
@ -66,7 +66,7 @@ final class MediaNavigationAccessoryPanel: ASDisplayNode {
}) })
} }
func animateOut(transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { public func animateOut(transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
self.clipsToBounds = true self.clipsToBounds = true
let contentPosition = self.containerNode.layer.position let contentPosition = self.containerNode.layer.position
transition.animatePosition(node: self.containerNode, to: CGPoint(x: contentPosition.x, y: contentPosition.y - 37.0), removeOnCompletion: false, completion: { [weak self] _ in transition.animatePosition(node: self.containerNode, to: CGPoint(x: contentPosition.x, y: contentPosition.y - 37.0), removeOnCompletion: false, completion: { [weak self] _ in
@ -75,7 +75,7 @@ final class MediaNavigationAccessoryPanel: ASDisplayNode {
}) })
} }
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return self.containerNode.hitTest(point, with: event) return self.containerNode.hitTest(point, with: event)
} }
} }

View File

@ -72,7 +72,7 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder {
public var tempVoicePlaylistEnded: (() -> Void)? public var tempVoicePlaylistEnded: (() -> Void)?
public var tempVoicePlaylistItemChanged: ((SharedMediaPlaylistItem?, SharedMediaPlaylistItem?) -> Void)? public var tempVoicePlaylistItemChanged: ((SharedMediaPlaylistItem?, SharedMediaPlaylistItem?) -> Void)?
private var mediaAccessoryPanel: (MediaNavigationAccessoryPanel, MediaManagerPlayerType)? public var mediaAccessoryPanel: (MediaNavigationAccessoryPanel, MediaManagerPlayerType)?
private var locationBroadcastMode: LocationBroadcastNavigationAccessoryPanelMode? private var locationBroadcastMode: LocationBroadcastNavigationAccessoryPanelMode?
private var locationBroadcastPeers: [Peer]? private var locationBroadcastPeers: [Peer]?

View File

@ -577,19 +577,47 @@ public struct PresentationResourcesChat {
public static func sharedMediaFileDownloadStartIcon(_ theme: PresentationTheme) -> UIImage? { public static func sharedMediaFileDownloadStartIcon(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.sharedMediaFileDownloadStartIcon.rawValue, { theme in return theme.image(PresentationResourceKey.sharedMediaFileDownloadStartIcon.rawValue, { theme in
return generateTintedImage(image: UIImage(bundleImageName: "List Menu/ListDownloadStartIcon"), color: theme.list.itemAccentColor) return generateImage(CGSize(width: 12.0, height: 12.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setStrokeColor(theme.list.itemAccentColor.cgColor)
context.setLineWidth(1.67)
context.setLineCap(.round)
context.setLineJoin(.round)
context.translateBy(x: 2.0, y: 1.0)
context.move(to: CGPoint(x: 4.0, y: 0.0))
context.addLine(to: CGPoint(x: 4.0, y: 10.0))
context.strokePath()
context.move(to: CGPoint(x: 0.0, y: 6.0))
context.addLine(to: CGPoint(x: 4.0, y: 10.0))
context.addLine(to: CGPoint(x: 8.0, y: 6.0))
context.strokePath()
})
}) })
} }
public static func sharedMediaFileDownloadPauseIcon(_ theme: PresentationTheme) -> UIImage? { public static func sharedMediaFileDownloadPauseIcon(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.sharedMediaFileDownloadPauseIcon.rawValue, { theme in return theme.image(PresentationResourceKey.sharedMediaFileDownloadPauseIcon.rawValue, { theme in
return generateImage(CGSize(width: 11.0, height: 11.0), contextGenerator: { size, context in return generateImage(CGSize(width: 12.0, height: 12.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size)) context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(theme.list.itemAccentColor.cgColor) context.setStrokeColor(theme.list.itemAccentColor.cgColor)
context.setLineWidth(1.67)
context.setLineCap(.round)
context.setLineJoin(.round)
context.fill(CGRect(x: 2.0, y: 0.0, width: 2.0, height: 11.0 - 1.0)) context.translateBy(x: 2.0, y: 2.0)
context.fill(CGRect(x: 2.0 + 2.0 + 2.0, y: 0.0, width: 2.0, height: 11.0 - 1.0))
context.move(to: CGPoint(x: 0.0, y: 0.0))
context.addLine(to: CGPoint(x: 8.0, y: 8.0))
context.strokePath()
context.move(to: CGPoint(x: 8.0, y: 0.0))
context.addLine(to: CGPoint(x: 0.0, y: 8.0))
context.strokePath()
}) })
}) })
} }

View File

@ -19,6 +19,10 @@ private func generatePlayIcon(_ theme: PresentationTheme) -> UIImage? {
return generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/MinimizedPlay"), color: theme.chat.inputPanel.actionControlForegroundColor) return generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/MinimizedPlay"), color: theme.chat.inputPanel.actionControlForegroundColor)
} }
extension AudioWaveformNode: CustomMediaPlayerScrubbingForegroundNode {
}
final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode {
private let deleteButton: HighlightableButtonNode private let deleteButton: HighlightableButtonNode
let sendButton: HighlightTrackingButtonNode let sendButton: HighlightTrackingButtonNode

View File

@ -6,6 +6,7 @@ import AsyncDisplayKit
import TelegramPresentationData import TelegramPresentationData
import TelegramUIPreferences import TelegramUIPreferences
import TelegramStringFormatting import TelegramStringFormatting
import ListSectionHeaderNode
private let timezoneOffset: Int32 = { private let timezoneOffset: Int32 = {
let nowTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) let nowTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
@ -16,7 +17,7 @@ private let timezoneOffset: Int32 = {
}() }()
func listMessageDateHeaderId(timestamp: Int32) -> Int64 { func listMessageDateHeaderId(timestamp: Int32) -> Int64 {
var unclippedValue: Int64 = min(Int64(Int32.max), Int64(timestamp) + Int64(timezoneOffset)) let unclippedValue: Int64 = min(Int64(Int32.max), Int64(timestamp) + Int64(timezoneOffset))
var time: time_t = time_t(Int32(clamping: unclippedValue)) var time: time_t = time_t(Int32(clamping: unclippedValue))
var timeinfo: tm = tm() var timeinfo: tm = tm()
@ -65,7 +66,7 @@ final class ListMessageDateHeader: ListViewItemHeader {
let stickDirection: ListViewItemHeaderStickDirection = .top let stickDirection: ListViewItemHeaderStickDirection = .top
let height: CGFloat = 36.0 let height: CGFloat = 28.0
func node() -> ListViewItemHeaderNode { func node() -> ListViewItemHeaderNode {
return ListMessageDateHeaderNode(theme: self.theme, strings: self.strings, fontSize: self.fontSize, roundedTimestamp: self.roundedTimestamp, month: self.month, year: self.year) return ListMessageDateHeaderNode(theme: self.theme, strings: self.strings, fontSize: self.fontSize, roundedTimestamp: self.roundedTimestamp, month: self.month, year: self.year)
@ -78,51 +79,39 @@ final class ListMessageDateHeader: ListViewItemHeader {
final class ListMessageDateHeaderNode: ListViewItemHeaderNode { final class ListMessageDateHeaderNode: ListViewItemHeaderNode {
var theme: PresentationTheme var theme: PresentationTheme
var strings: PresentationStrings var strings: PresentationStrings
var fontSize: PresentationFontSize let headerNode: ListSectionHeaderNode
let titleNode: ASTextNode
let backgroundNode: ASDisplayNode let month: Int32
let year: Int32
init(theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize, roundedTimestamp: Int32, month: Int32, year: Int32) { init(theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize, roundedTimestamp: Int32, month: Int32, year: Int32) {
self.theme = theme self.theme = theme
self.strings = strings self.strings = strings
self.fontSize = fontSize self.month = month
self.year = year
self.backgroundNode = ASDisplayNode() self.headerNode = ListSectionHeaderNode(theme: theme)
self.backgroundNode.isLayerBacked = true
self.backgroundNode.backgroundColor = theme.list.plainBackgroundColor.withAlphaComponent(0.9)
self.titleNode = ASTextNode()
self.titleNode.isUserInteractionEnabled = false
super.init() super.init()
let dateText = stringForMonth(strings: strings, month: month, ofYear: year) self.addSubnode(self.headerNode)
let sectionTitleFont = Font.regular(floor(fontSize.baseDisplaySize * 14.0 / 17.0)) self.headerNode.title = stringForMonth(strings: strings, month: month, ofYear: year).uppercased()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.titleNode)
self.titleNode.attributedText = NSAttributedString(string: dateText, font: sectionTitleFont, textColor: theme.list.itemPrimaryTextColor)
self.titleNode.maximumNumberOfLines = 1
self.titleNode.truncationMode = .byTruncatingTail
} }
func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
self.theme = theme self.theme = theme
if let attributedString = self.titleNode.attributedText?.mutableCopy() as? NSMutableAttributedString { self.headerNode.updateTheme(theme: theme)
attributedString.addAttribute(NSAttributedString.Key.foregroundColor, value: theme.list.itemPrimaryTextColor, range: NSMakeRange(0, attributedString.length))
self.titleNode.attributedText = attributedString
}
self.strings = strings self.strings = strings
self.headerNode.title = stringForMonth(strings: strings, month: self.month, ofYear: self.year).uppercased()
self.backgroundNode.backgroundColor = theme.list.plainBackgroundColor.withAlphaComponent(0.9)
self.setNeedsLayout() self.setNeedsLayout()
} }
override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) { override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) {
let titleSize = self.titleNode.measure(CGSize(width: size.width - leftInset - rightInset - 24.0, height: CGFloat.greatestFiniteMagnitude)) let headerFrame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: size.width, height: size.height + UIScreenPixel))
self.titleNode.frame = CGRect(origin: CGPoint(x: leftInset + 12.0, y: floor((size.height - titleSize.height) / 2.0)), size: titleSize) self.headerNode.frame = headerFrame
self.backgroundNode.frame = CGRect(origin: CGPoint(), size: size) self.headerNode.updateLayout(size: headerFrame.size, leftInset: leftInset, rightInset: rightInset)
} }
} }

View File

@ -13,9 +13,11 @@ import AccountContext
import TelegramStringFormatting import TelegramStringFormatting
import AccountContext import AccountContext
import RadialStatusNode import RadialStatusNode
import SemanticStatusNode
import PhotoResources import PhotoResources
import MusicAlbumArtResources import MusicAlbumArtResources
import UniversalMediaPlayer import UniversalMediaPlayer
import ContextUI
private let extensionImageCache = Atomic<[UInt32: UIImage]>(value: [:]) private let extensionImageCache = Atomic<[UInt32: UIImage]>(value: [:])
@ -41,50 +43,19 @@ private let extensionColorsMap: [String: (UInt32, UInt32)] = [
] ]
private func generateExtensionImage(colors: (UInt32, UInt32)) -> UIImage? { private func generateExtensionImage(colors: (UInt32, UInt32)) -> UIImage? {
return generateImage(CGSize(width: 42.0, height: 42.0), contextGenerator: { size, context in return generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size)) context.clear(CGRect(origin: CGPoint(), size: size))
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -size.width / 2.0 + 1.0, y: -size.height / 2.0 + 1.0)
let radius: CGFloat = 2.0
let cornerSize: CGFloat = 10.0
let size = CGSize(width: 42.0, height: 42.0)
context.setFillColor(UIColor(rgb: colors.0).cgColor) context.setFillColor(UIColor(rgb: colors.0).cgColor)
let _ = try? drawSvgPath(context, path: "M6,0 L26.7573593,0 C27.5530088,-8.52837125e-16 28.3160705,0.316070521 28.8786797,0.878679656 L39.1213203,11.1213203 C39.6839295,11.6839295 40,12.4469912 40,13.2426407 L40,34 C40,37.3137085 37.3137085,40 34,40 L6,40 C2.6862915,40 4.05812251e-16,37.3137085 0,34 L0,6 C-4.05812251e-16,2.6862915 2.6862915,6.08718376e-16 6,0 Z ")
context.beginPath() context.beginPath()
context.move(to: CGPoint(x: 0.0, y: radius)) let _ = try? drawSvgPath(context, path: "M6,0 L26.7573593,0 C27.5530088,-8.52837125e-16 28.3160705,0.316070521 28.8786797,0.878679656 L39.1213203,11.1213203 C39.6839295,11.6839295 40,12.4469912 40,13.2426407 L40,34 C40,37.3137085 37.3137085,40 34,40 L6,40 C2.6862915,40 4.05812251e-16,37.3137085 0,34 L0,6 C-4.05812251e-16,2.6862915 2.6862915,6.08718376e-16 6,0 ")
if !radius.isZero { context.clip()
context.addArc(tangent1End: CGPoint(x: 0.0, y: 0.0), tangent2End: CGPoint(x: radius, y: 0.0), radius: radius)
}
context.addLine(to: CGPoint(x: size.width - cornerSize, y: 0.0))
context.addLine(to: CGPoint(x: size.width - cornerSize + cornerSize / 4.0, y: cornerSize - cornerSize / 4.0))
context.addLine(to: CGPoint(x: size.width, y: cornerSize))
context.addLine(to: CGPoint(x: size.width, y: size.height - radius))
if !radius.isZero {
context.addArc(tangent1End: CGPoint(x: size.width, y: size.height), tangent2End: CGPoint(x: size.width - radius, y: size.height), radius: radius)
}
context.addLine(to: CGPoint(x: radius, y: size.height))
if !radius.isZero { context.setFillColor(UIColor(rgb: colors.0).withMultipliedBrightnessBy(0.85).cgColor)
context.addArc(tangent1End: CGPoint(x: 0.0, y: size.height), tangent2End: CGPoint(x: 0.0, y: size.height - radius), radius: radius) context.translateBy(x: 40.0 - 14.0, y: 0.0)
} let _ = try? drawSvgPath(context, path: "M-1,0 L14,0 L14,15 L14,14 C14,12.8954305 13.1045695,12 12,12 L4,12 C2.8954305,12 2,11.1045695 2,10 L2,2 C2,0.8954305 1.1045695,-2.02906125e-16 0,0 L-1,0 L-1,0 Z ")
context.closePath()
context.fillPath()
context.setFillColor(UIColor(rgb: colors.1).cgColor)
context.beginPath()
context.move(to: CGPoint(x: size.width - cornerSize, y: 0.0))
context.addLine(to: CGPoint(x: size.width, y: cornerSize))
context.addLine(to: CGPoint(x: size.width - cornerSize + radius, y: cornerSize))
if !radius.isZero {
context.addArc(tangent1End: CGPoint(x: size.width - cornerSize, y: cornerSize), tangent2End: CGPoint(x: size.width - cornerSize, y: cornerSize - radius), radius: radius)
}
context.closePath()
context.fillPath()
}) })
} }
@ -115,7 +86,7 @@ private func extensionImage(fileExtension: String?) -> UIImage? {
return nil return nil
} }
} }
private let extensionFont = Font.medium(13.0) private let extensionFont = Font.with(size: 15.0, design: .round, traits: [.bold])
private struct FetchControls { private struct FetchControls {
let fetch: () -> Void let fetch: () -> Void
@ -151,11 +122,16 @@ private enum FileIconImage: Equatable {
} }
} }
extension AudioWaveformNode: CustomMediaPlayerScrubbingForegroundNode {
}
final class ListMessageFileItemNode: ListMessageNode { final class ListMessageFileItemNode: ListMessageNode {
private let contextSourceNode: ContextExtractedContentContainingNode
private let containerNode: ContextControllerSourceNode
private let extractedBackgroundImageNode: ASImageNode
private var extractedRect: CGRect?
private var nonExtractedRect: CGRect?
private let offsetContainerNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode private let highlightedBackgroundNode: ASDisplayNode
private let separatorNode: ASDisplayNode private let separatorNode: ASDisplayNode
@ -168,12 +144,7 @@ final class ListMessageFileItemNode: ListMessageNode {
private let extensionIconNode: ASImageNode private let extensionIconNode: ASImageNode
private let extensionIconText: TextNode private let extensionIconText: TextNode
private let iconImageNode: TransformImageNode private let iconImageNode: TransformImageNode
private let statusButtonNode: HighlightTrackingButtonNode private let iconStatusNode: SemanticStatusNode
private let statusNode: RadialStatusNode
private var waveformNode: AudioWaveformNode?
private var waveformForegroundNode: AudioWaveformNode?
private var waveformScrubbingNode: MediaPlayerScrubbingNode?
private var currentIconImage: FileIconImage? private var currentIconImage: FileIconImage?
private var currentMedia: Media? private var currentMedia: Media?
@ -187,10 +158,7 @@ final class ListMessageFileItemNode: ListMessageNode {
private let playbackStatus = Promise<MediaPlayerStatus>() private let playbackStatus = Promise<MediaPlayerStatus>()
private var downloadStatusIconNode: ASImageNode private var downloadStatusIconNode: ASImageNode
private var linearProgressNode: ASDisplayNode private var linearProgressNode: LinearProgressNode?
private let progressNode: RadialProgressNode
private var playbackOverlayNode: ListMessagePlaybackOverlayNode?
private var context: AccountContext? private var context: AccountContext?
private (set) var message: Message? private (set) var message: Message?
@ -200,15 +168,20 @@ final class ListMessageFileItemNode: ListMessageNode {
private var contentSizeValue: CGSize? private var contentSizeValue: CGSize?
private var currentLeftOffset: CGFloat = 0.0 private var currentLeftOffset: CGFloat = 0.0
override var canBeLongTapped: Bool {
return true
}
public required init() { public required init() {
self.contextSourceNode = ContextExtractedContentContainingNode()
self.containerNode = ContextControllerSourceNode()
self.separatorNode = ASDisplayNode() self.separatorNode = ASDisplayNode()
self.separatorNode.displaysAsynchronously = false self.separatorNode.displaysAsynchronously = false
self.separatorNode.isLayerBacked = true self.separatorNode.isLayerBacked = true
self.extractedBackgroundImageNode = ASImageNode()
self.extractedBackgroundImageNode.displaysAsynchronously = false
self.extractedBackgroundImageNode.alpha = 0.0
self.offsetContainerNode = ASDisplayNode()
self.highlightedBackgroundNode = ASDisplayNode() self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true self.highlightedBackgroundNode.isLayerBacked = true
@ -234,45 +207,60 @@ final class ListMessageFileItemNode: ListMessageNode {
self.iconImageNode.displaysAsynchronously = false self.iconImageNode.displaysAsynchronously = false
self.iconImageNode.contentAnimations = .subsequentUpdates self.iconImageNode.contentAnimations = .subsequentUpdates
self.statusButtonNode = HighlightTrackingButtonNode() self.iconStatusNode = SemanticStatusNode(backgroundNodeColor: .clear, foregroundNodeColor: .white)
self.statusNode = RadialStatusNode(backgroundNodeColor: .clear) self.iconStatusNode.isUserInteractionEnabled = false
self.statusNode.isUserInteractionEnabled = false
self.downloadStatusIconNode = ASImageNode() self.downloadStatusIconNode = ASImageNode()
self.downloadStatusIconNode.isLayerBacked = true self.downloadStatusIconNode.isLayerBacked = true
self.downloadStatusIconNode.displaysAsynchronously = false self.downloadStatusIconNode.displaysAsynchronously = false
self.downloadStatusIconNode.displayWithoutProcessing = true self.downloadStatusIconNode.displayWithoutProcessing = true
self.progressNode = RadialProgressNode(theme: RadialProgressTheme(backgroundColor: .black, foregroundColor: .white, icon: nil))
//self.progressNode.isLayerBacked = true
self.linearProgressNode = ASDisplayNode()
self.linearProgressNode.isLayerBacked = true
super.init() super.init()
self.addSubnode(self.separatorNode) self.addSubnode(self.separatorNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.progressNode)
self.addSubnode(self.descriptionNode)
self.addSubnode(self.descriptionProgressNode)
self.addSubnode(self.extensionIconNode)
self.addSubnode(self.extensionIconText)
self.addSubnode(self.statusNode)
self.addSubnode(self.statusButtonNode)
self.statusButtonNode.highligthedChanged = { [weak self] highlighted in self.containerNode.addSubnode(self.contextSourceNode)
if let strongSelf = self { self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode
if highlighted { self.addSubnode(self.containerNode)
strongSelf.statusNode.layer.removeAnimation(forKey: "opacity")
strongSelf.statusNode.alpha = 0.4 self.contextSourceNode.contentNode.addSubnode(self.extractedBackgroundImageNode)
} else { self.contextSourceNode.contentNode.addSubnode(self.offsetContainerNode)
strongSelf.statusNode.alpha = 1.0 self.offsetContainerNode.addSubnode(self.titleNode)
strongSelf.statusNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) self.offsetContainerNode.addSubnode(self.descriptionNode)
self.offsetContainerNode.addSubnode(self.descriptionProgressNode)
self.offsetContainerNode.addSubnode(self.extensionIconNode)
self.offsetContainerNode.addSubnode(self.extensionIconText)
self.offsetContainerNode.addSubnode(self.iconStatusNode)
self.containerNode.activated = { [weak self] gesture, _ in
guard let strongSelf = self, let item = strongSelf.item else {
return
} }
item.controllerInteraction.openMessageContextMenu(item.message, false, strongSelf.contextSourceNode, strongSelf.contextSourceNode.bounds, gesture)
} }
self.contextSourceNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, transition in
guard let strongSelf = self, let item = strongSelf.item else {
return
}
if isExtracted {
strongSelf.extractedBackgroundImageNode.image = generateStretchableFilledCircleImage(diameter: 28.0, color: item.theme.list.plainBackgroundColor)
}
if let extractedRect = strongSelf.extractedRect, let nonExtractedRect = strongSelf.nonExtractedRect {
let rect = isExtracted ? extractedRect : nonExtractedRect
transition.updateFrame(node: strongSelf.extractedBackgroundImageNode, frame: rect)
}
transition.updateSublayerTransformOffset(layer: strongSelf.offsetContainerNode.layer, offset: CGPoint(x: isExtracted ? 12.0 : 0.0, y: 0.0))
transition.updateAlpha(node: strongSelf.extractedBackgroundImageNode, alpha: isExtracted ? 1.0 : 0.0, completion: { _ in
if !isExtracted {
self?.extractedBackgroundImageNode.image = nil
}
})
} }
self.statusButtonNode.addTarget(self, action: #selector(self.statusPressed), forControlEvents: .touchUpInside)
} }
deinit { deinit {
@ -331,9 +319,9 @@ final class ListMessageFileItemNode: ListMessageNode {
updatedTheme = item.theme updatedTheme = item.theme
} }
let titleFont = Font.medium(floor(item.fontSize.baseDisplaySize * 16.0 / 17.0)) let titleFont = Font.semibold(floor(item.fontSize.baseDisplaySize * 16.0 / 17.0))
let audioTitleFont = Font.regular(floor(item.fontSize.baseDisplaySize * 16.0 / 17.0)) let audioTitleFont = Font.semibold(floor(item.fontSize.baseDisplaySize * 16.0 / 17.0))
let descriptionFont = Font.regular(floor(item.fontSize.baseDisplaySize * 13.0 / 17.0)) let descriptionFont = Font.regular(floor(item.fontSize.baseDisplaySize * 14.0 / 17.0))
var leftInset: CGFloat = 65.0 + params.leftInset var leftInset: CGFloat = 65.0 + params.leftInset
let rightInset: CGFloat = 8.0 + params.rightInset let rightInset: CGFloat = 8.0 + params.rightInset
@ -356,7 +344,6 @@ final class ListMessageFileItemNode: ListMessageNode {
var updatedStatusSignal: Signal<FileMediaResourceStatus, NoError>? var updatedStatusSignal: Signal<FileMediaResourceStatus, NoError>?
var updatedPlaybackStatusSignal: Signal<MediaPlayerStatus, NoError>? var updatedPlaybackStatusSignal: Signal<MediaPlayerStatus, NoError>?
var updatedFetchControls: FetchControls? var updatedFetchControls: FetchControls?
var waveform: AudioWaveform?
var isAudio = false var isAudio = false
var isVoice = false var isVoice = false
@ -372,7 +359,7 @@ final class ListMessageFileItemNode: ListMessageNode {
isInstantVideo = file.isInstantVideo isInstantVideo = file.isInstantVideo
for attribute in file.attributes { for attribute in file.attributes {
if case let .Audio(voice, _, title, performer, waveformValue) = attribute { if case let .Audio(voice, duration, title, performer, _) = attribute {
isAudio = true isAudio = true
isVoice = voice isVoice = voice
@ -380,7 +367,7 @@ final class ListMessageFileItemNode: ListMessageNode {
let descriptionString: String let descriptionString: String
if let performer = performer { if let performer = performer {
descriptionString = performer descriptionString = "\(stringForDuration(Int32(duration)))\(performer)"
} else if let size = file.size { } else if let size = file.size {
descriptionString = dataSizeString(size, decimalSeparator: item.dateTimeFormat.decimalSeparator) descriptionString = dataSizeString(size, decimalSeparator: item.dateTimeFormat.decimalSeparator)
} else { } else {
@ -394,16 +381,39 @@ final class ListMessageFileItemNode: ListMessageNode {
} else { } else {
titleText = NSAttributedString(string: " ", font: audioTitleFont, textColor: item.theme.list.itemPrimaryTextColor) titleText = NSAttributedString(string: " ", font: audioTitleFont, textColor: item.theme.list.itemPrimaryTextColor)
descriptionText = NSAttributedString(string: item.message.author?.displayTitle(strings: item.strings, displayOrder: .firstLast) ?? " ", font: descriptionFont, textColor: item.theme.list.itemSecondaryTextColor) descriptionText = NSAttributedString(string: item.message.author?.displayTitle(strings: item.strings, displayOrder: .firstLast) ?? " ", font: descriptionFont, textColor: item.theme.list.itemSecondaryTextColor)
waveformValue?.withDataNoCopy { data in
waveform = AudioWaveform(bitstream: data, bitsPerSample: 5)
}
} }
} }
} }
if isInstantVideo { if isInstantVideo || isVoice {
titleText = NSAttributedString(string: item.strings.Message_VideoMessage, font: audioTitleFont, textColor: item.theme.list.itemPrimaryTextColor) let authorName: String
descriptionText = NSAttributedString(string: item.message.author?.displayTitle(strings: item.strings, displayOrder: .firstLast) ?? " ", font: descriptionFont, textColor: item.theme.list.itemSecondaryTextColor) if let author = message.forwardInfo?.author {
if author.id == item.context.account.peerId {
authorName = item.strings.DialogList_You
} else {
authorName = author.displayTitle(strings: item.strings, displayOrder: .firstLast)
}
} else if let signature = message.forwardInfo?.authorSignature {
authorName = signature
} else if let author = message.author {
if author.id == item.context.account.peerId {
authorName = item.strings.DialogList_You
} else {
authorName = author.displayTitle(strings: item.strings, displayOrder: .firstLast)
}
} else {
authorName = " "
}
titleText = NSAttributedString(string: authorName, font: audioTitleFont, textColor: item.theme.list.itemPrimaryTextColor)
let dateString = stringForFullDate(timestamp: item.message.timestamp, strings: item.strings, dateTimeFormat: item.dateTimeFormat)
let descriptionString: String
if let duration = file.duration {
descriptionString = "\(stringForDuration(Int32(duration)))\(dateString)"
} else {
descriptionString = dateString
}
descriptionText = NSAttributedString(string: descriptionString, font: descriptionFont, textColor: item.theme.list.itemSecondaryTextColor)
iconImage = .roundVideo(file) iconImage = .roundVideo(file)
} else if !isAudio { } else if !isAudio {
let fileName: String = file.fileName ?? "" let fileName: String = file.fileName ?? ""
@ -438,9 +448,6 @@ final class ListMessageFileItemNode: ListMessageNode {
} }
} }
if isAudio && !isVoice {
leftInset += 14.0
}
var mediaUpdated = false var mediaUpdated = false
if let currentMedia = currentMedia { if let currentMedia = currentMedia {
@ -492,7 +499,7 @@ final class ListMessageFileItemNode: ListMessageNode {
} }
} }
let (titleNodeLayout, titleNodeApply) = titleNodeMakeLayout(TextNodeLayoutArguments(attributedString: titleText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 40.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let (titleNodeLayout, titleNodeApply) = titleNodeMakeLayout(TextNodeLayoutArguments(attributedString: titleText, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .middle, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 40.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (descriptionNodeLayout, descriptionNodeApply) = descriptionNodeMakeLayout(TextNodeLayoutArguments(attributedString: descriptionText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 12.0 - 40.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let (descriptionNodeLayout, descriptionNodeApply) = descriptionNodeMakeLayout(TextNodeLayoutArguments(attributedString: descriptionText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 12.0 - 40.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
@ -502,18 +509,18 @@ final class ListMessageFileItemNode: ListMessageNode {
if let iconImage = iconImage { if let iconImage = iconImage {
switch iconImage { switch iconImage {
case let .imageRepresentation(_, representation): case let .imageRepresentation(_, representation):
let iconSize = CGSize(width: 42.0, height: 42.0) let iconSize = CGSize(width: 40.0, height: 40.0)
let imageCorners = ImageCorners(topLeft: .Corner(4.0), topRight: .Corner(4.0), bottomLeft: .Corner(4.0), bottomRight: .Corner(4.0)) let imageCorners = ImageCorners(radius: 6.0)
let arguments = TransformImageArguments(corners: imageCorners, imageSize: representation.dimensions.cgSize.aspectFilled(iconSize), boundingSize: iconSize, intrinsicInsets: UIEdgeInsets(), emptyColor: item.theme.list.mediaPlaceholderColor) let arguments = TransformImageArguments(corners: imageCorners, imageSize: representation.dimensions.cgSize.aspectFilled(iconSize), boundingSize: iconSize, intrinsicInsets: UIEdgeInsets(), emptyColor: item.theme.list.mediaPlaceholderColor)
iconImageApply = iconImageLayout(arguments) iconImageApply = iconImageLayout(arguments)
case .albumArt: case .albumArt:
let iconSize = CGSize(width: 46.0, height: 46.0) let iconSize = CGSize(width: 40.0, height: 40.0)
let imageCorners = ImageCorners(topLeft: .Corner(4.0), topRight: .Corner(4.0), bottomLeft: .Corner(4.0), bottomRight: .Corner(4.0)) let imageCorners = ImageCorners(radius: iconSize.width / 2.0)
let arguments = TransformImageArguments(corners: imageCorners, imageSize: iconSize, boundingSize: iconSize, intrinsicInsets: UIEdgeInsets(), emptyColor: item.theme.list.mediaPlaceholderColor) let arguments = TransformImageArguments(corners: imageCorners, imageSize: iconSize, boundingSize: iconSize, intrinsicInsets: UIEdgeInsets(), emptyColor: item.theme.list.mediaPlaceholderColor)
iconImageApply = iconImageLayout(arguments) iconImageApply = iconImageLayout(arguments)
case let .roundVideo(file): case let .roundVideo(file):
let iconSize = CGSize(width: 42.0, height: 42.0) let iconSize = CGSize(width: 40.0, height: 40.0)
let imageCorners = ImageCorners(topLeft: .Corner(iconSize.width / 2.0), topRight: .Corner(iconSize.width / 2.0), bottomLeft: .Corner(iconSize.width / 2.0), bottomRight: .Corner(iconSize.width / 2.0)) let imageCorners = ImageCorners(radius: iconSize.width / 2.0)
let arguments = TransformImageArguments(corners: imageCorners, imageSize: (file.dimensions ?? PixelDimensions(width: 320, height: 320)).cgSize.aspectFilled(iconSize), boundingSize: iconSize, intrinsicInsets: UIEdgeInsets(), emptyColor: item.theme.list.mediaPlaceholderColor) let arguments = TransformImageArguments(corners: imageCorners, imageSize: (file.dimensions ?? PixelDimensions(width: 320, height: 320)).cgSize.aspectFilled(iconSize), boundingSize: iconSize, intrinsicInsets: UIEdgeInsets(), emptyColor: item.theme.list.mediaPlaceholderColor)
iconImageApply = iconImageLayout(arguments) iconImageApply = iconImageLayout(arguments)
} }
@ -525,9 +532,9 @@ final class ListMessageFileItemNode: ListMessageNode {
case let .imageRepresentation(file, representation): case let .imageRepresentation(file, representation):
updateIconImageSignal = chatWebpageSnippetFile(account: item.context.account, fileReference: .message(message: MessageReference(message), media: file), representation: representation) updateIconImageSignal = chatWebpageSnippetFile(account: item.context.account, fileReference: .message(message: MessageReference(message), media: file), representation: representation)
case let .albumArt(file, albumArt): case let .albumArt(file, albumArt):
updateIconImageSignal = playerAlbumArt(postbox: item.context.account.postbox, fileReference: .message(message: MessageReference(message), media: file), albumArt: albumArt, thumbnail: true) updateIconImageSignal = playerAlbumArt(postbox: item.context.account.postbox, fileReference: .message(message: MessageReference(message), media: file), albumArt: albumArt, thumbnail: true, overlayColor: UIColor(white: 0.0, alpha: 0.3), emptyColor: item.theme.list.itemAccentColor)
case let .roundVideo(file): case let .roundVideo(file):
updateIconImageSignal = mediaGridMessageVideo(postbox: item.context.account.postbox, videoReference: FileMediaReference.message(message: MessageReference(message), media: file), autoFetchFullSizeThumbnail: true) updateIconImageSignal = mediaGridMessageVideo(postbox: item.context.account.postbox, videoReference: FileMediaReference.message(message: MessageReference(message), media: file), autoFetchFullSizeThumbnail: true, overlayColor: UIColor(white: 0.0, alpha: 0.3))
} }
} else { } else {
updateIconImageSignal = .complete() updateIconImageSignal = .complete()
@ -550,6 +557,23 @@ final class ListMessageFileItemNode: ListMessageNode {
transition = .immediate transition = .immediate
} }
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: nodeLayout.contentSize)
strongSelf.contextSourceNode.frame = CGRect(origin: CGPoint(), size: nodeLayout.contentSize)
strongSelf.offsetContainerNode.frame = CGRect(origin: CGPoint(), size: nodeLayout.contentSize)
strongSelf.contextSourceNode.contentNode.frame = CGRect(origin: CGPoint(), size: nodeLayout.contentSize)
let nonExtractedRect = CGRect(origin: CGPoint(), size: CGSize(width: nodeLayout.contentSize.width - 16.0, height: nodeLayout.contentSize.height))
let extractedRect = CGRect(origin: CGPoint(), size: nodeLayout.contentSize).insetBy(dx: 16.0, dy: 0.0)
strongSelf.extractedRect = extractedRect
strongSelf.nonExtractedRect = nonExtractedRect
if strongSelf.contextSourceNode.isExtractedToContextPreview {
strongSelf.extractedBackgroundImageNode.frame = extractedRect
} else {
strongSelf.extractedBackgroundImageNode.frame = nonExtractedRect
}
strongSelf.contextSourceNode.contentRect = extractedRect
strongSelf.currentMedia = selectedMedia strongSelf.currentMedia = selectedMedia
strongSelf.message = message strongSelf.message = message
strongSelf.context = item.context strongSelf.context = item.context
@ -561,9 +585,7 @@ final class ListMessageFileItemNode: ListMessageNode {
if let _ = updatedTheme { if let _ = updatedTheme {
strongSelf.separatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor strongSelf.separatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor
strongSelf.linearProgressNode?.updateTheme(theme: item.theme)
strongSelf.progressNode.updateTheme(RadialProgressTheme(backgroundColor: item.theme.list.itemAccentColor, foregroundColor: item.theme.list.plainBackgroundColor, icon: nil))
strongSelf.linearProgressNode.backgroundColor = item.theme.list.itemAccentColor
} }
if let (selectionWidth, selectionApply) = selectionNodeWidthAndApply { if let (selectionWidth, selectionApply) = selectionNodeWidthAndApply {
@ -572,7 +594,7 @@ final class ListMessageFileItemNode: ListMessageNode {
if selectionNode !== strongSelf.selectionNode { if selectionNode !== strongSelf.selectionNode {
strongSelf.selectionNode?.removeFromSupernode() strongSelf.selectionNode?.removeFromSupernode()
strongSelf.selectionNode = selectionNode strongSelf.selectionNode = selectionNode
strongSelf.addSubnode(selectionNode) strongSelf.contextSourceNode.contentNode.addSubnode(selectionNode)
selectionNode.frame = selectionFrame selectionNode.frame = selectionFrame
transition.animatePosition(node: selectionNode, from: CGPoint(x: -selectionFrame.size.width / 2.0, y: selectionFrame.midY)) transition.animatePosition(node: selectionNode, from: CGPoint(x: -selectionFrame.size.width / 2.0, y: selectionFrame.midY))
} else { } else {
@ -589,7 +611,7 @@ final class ListMessageFileItemNode: ListMessageNode {
transition.updateFrame(node: strongSelf.separatorNode, frame: CGRect(origin: CGPoint(x: leftInset + leftOffset, y: nodeLayout.contentSize.height - UIScreenPixel), size: CGSize(width: params.width - leftInset - leftOffset, height: UIScreenPixel))) transition.updateFrame(node: strongSelf.separatorNode, frame: CGRect(origin: CGPoint(x: leftInset + leftOffset, y: nodeLayout.contentSize.height - UIScreenPixel), size: CGSize(width: params.width - leftInset - leftOffset, height: UIScreenPixel)))
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel - nodeLayout.insets.top), size: CGSize(width: params.width, height: nodeLayout.size.height + UIScreenPixel)) strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel - nodeLayout.insets.top), size: CGSize(width: params.width, height: nodeLayout.size.height + UIScreenPixel))
transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset, y: 8.0), size: titleNodeLayout.size)) transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset, y: 9.0), size: titleNodeLayout.size))
let _ = titleNodeApply() let _ = titleNodeApply()
var descriptionOffset: CGFloat = 0.0 var descriptionOffset: CGFloat = 0.0
@ -607,67 +629,27 @@ final class ListMessageFileItemNode: ListMessageNode {
} }
} }
transition.updateFrame(node: strongSelf.descriptionNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset + descriptionOffset, y: strongSelf.titleNode.frame.maxY + 3.0), size: descriptionNodeLayout.size)) transition.updateFrame(node: strongSelf.descriptionNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset + descriptionOffset, y: strongSelf.titleNode.frame.maxY + 1.0), size: descriptionNodeLayout.size))
let _ = descriptionNodeApply() let _ = descriptionNodeApply()
let iconFrame: CGRect let iconFrame: CGRect
if isAudio { if isAudio {
let iconSize = CGSize(width: 48.0, height: 48.0) let iconSize = CGSize(width: 40.0, height: 40.0)
iconFrame = CGRect(origin: CGPoint(x: params.leftInset + leftOffset + 12.0, y: 5.0), size: iconSize) iconFrame = CGRect(origin: CGPoint(x: params.leftInset + leftOffset + 12.0, y: 8.0), size: iconSize)
} else { } else {
let iconSize = CGSize(width: 42.0, height: 42.0) let iconSize = CGSize(width: 40.0, height: 40.0)
iconFrame = CGRect(origin: CGPoint(x: params.leftInset + leftOffset + 12.0, y: 8.0), size: iconSize) iconFrame = CGRect(origin: CGPoint(x: params.leftInset + leftOffset + 12.0, y: 8.0), size: iconSize)
} }
transition.updateFrame(node: strongSelf.extensionIconNode, frame: iconFrame) transition.updateFrame(node: strongSelf.extensionIconNode, frame: iconFrame)
strongSelf.extensionIconNode.image = extensionIconImage strongSelf.extensionIconNode.image = extensionIconImage
transition.updateFrame(node: strongSelf.extensionIconText, frame: CGRect(origin: CGPoint(x: leftOffset + 12.0 + floor((42.0 - extensionTextLayout.size.width) / 2.0), y: 8.0 + floor((42.0 - extensionTextLayout.size.height) / 2.0)), size: extensionTextLayout.size)) transition.updateFrame(node: strongSelf.extensionIconText, frame: CGRect(origin: CGPoint(x: iconFrame.minX + floor((iconFrame.width - extensionTextLayout.size.width) / 2.0), y: iconFrame.minY + 2.0 + floor((iconFrame.height - extensionTextLayout.size.height) / 2.0)), size: extensionTextLayout.size))
transition.updateFrame(node: strongSelf.iconStatusNode, frame: iconFrame)
let _ = extensionTextApply() let _ = extensionTextApply()
strongSelf.currentIconImage = iconImage strongSelf.currentIconImage = iconImage
if isVoice {
let waveformNode: AudioWaveformNode
let waveformForegroundNode: AudioWaveformNode
let waveformScrubbingNode: MediaPlayerScrubbingNode
if let current = strongSelf.waveformNode {
waveformNode = current
} else {
waveformNode = AudioWaveformNode()
waveformNode.isLayerBacked = true
strongSelf.waveformNode = waveformNode
strongSelf.addSubnode(waveformNode)
}
if let current = strongSelf.waveformForegroundNode {
waveformForegroundNode = current
} else {
waveformForegroundNode = AudioWaveformNode()
waveformForegroundNode.isLayerBacked = true
strongSelf.waveformForegroundNode = waveformForegroundNode
strongSelf.addSubnode(waveformForegroundNode)
}
if let current = strongSelf.waveformScrubbingNode {
waveformScrubbingNode = current
} else {
waveformScrubbingNode = MediaPlayerScrubbingNode(content: .custom(backgroundNode: waveformNode, foregroundContentNode: waveformForegroundNode))
waveformScrubbingNode.hitTestSlop = UIEdgeInsets(top: -10.0, left: 0.0, bottom: -10.0, right: 0.0)
waveformScrubbingNode.seek = { timestamp in
if let strongSelf = self, let context = strongSelf.context, let message = strongSelf.message, let type = peerMessageMediaPlayerType(message) {
context.sharedContext.mediaManager.playlistControl(.seek(timestamp), type: type)
}
}
waveformScrubbingNode.enableScrubbing = false
waveformScrubbingNode.status = strongSelf.playbackStatus.get()
strongSelf.waveformScrubbingNode = waveformScrubbingNode
strongSelf.addSubnode(waveformScrubbingNode)
}
transition.updateFrame(node: waveformScrubbingNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset, y: 10.0), size: CGSize(width: params.width - leftInset - 16.0, height: 12.0)))
waveformNode.setup(color: item.theme.list.controlSecondaryColor, waveform: waveform)
waveformForegroundNode.setup(color: item.theme.list.itemAccentColor, waveform: waveform)
}
if let iconImageApply = iconImageApply { if let iconImageApply = iconImageApply {
if let updateImageSignal = updateIconImageSignal { if let updateImageSignal = updateIconImageSignal {
strongSelf.iconImageNode.setSignal(updateImageSignal) strongSelf.iconImageNode.setSignal(updateImageSignal)
@ -675,7 +657,7 @@ final class ListMessageFileItemNode: ListMessageNode {
transition.updateFrame(node: strongSelf.iconImageNode, frame: iconFrame) transition.updateFrame(node: strongSelf.iconImageNode, frame: iconFrame)
if strongSelf.iconImageNode.supernode == nil { if strongSelf.iconImageNode.supernode == nil {
strongSelf.addSubnode(strongSelf.iconImageNode) strongSelf.offsetContainerNode.insertSubnode(strongSelf.iconImageNode, belowSubnode: strongSelf.iconStatusNode)
} }
iconImageApply() iconImageApply()
@ -690,22 +672,13 @@ final class ListMessageFileItemNode: ListMessageNode {
strongSelf.iconImageNode.removeFromSupernode() strongSelf.iconImageNode.removeFromSupernode()
if strongSelf.extensionIconNode.supernode == nil { if strongSelf.extensionIconNode.supernode == nil {
strongSelf.addSubnode(strongSelf.extensionIconNode) strongSelf.offsetContainerNode.insertSubnode(strongSelf.extensionIconNode, belowSubnode: strongSelf.iconStatusNode)
} }
if strongSelf.extensionIconText.supernode == nil { if strongSelf.extensionIconText.supernode == nil {
strongSelf.addSubnode(strongSelf.extensionIconText) strongSelf.offsetContainerNode.insertSubnode(strongSelf.extensionIconText, belowSubnode: strongSelf.iconStatusNode)
} }
} }
if let playbackOverlayNode = strongSelf.playbackOverlayNode {
transition.updateFrame(node: playbackOverlayNode, frame: iconFrame)
}
let statusSize = CGSize(width: 28.0, height: 28.0)
transition.updateFrame(node: strongSelf.statusNode, frame: CGRect(origin: CGPoint(x: params.width - params.rightInset - rightInset - statusSize.width + leftOffset, y: floor((nodeLayout.contentSize.height - statusSize.height) / 2.0)), size: statusSize))
strongSelf.statusButtonNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - rightInset - 40.0 + leftOffset, y: 0.0), size: CGSize(width: 40.0, height: nodeLayout.contentSize.height))
if let updatedStatusSignal = updatedStatusSignal { if let updatedStatusSignal = updatedStatusSignal {
strongSelf.statusDisposable.set((updatedStatusSignal strongSelf.statusDisposable.set((updatedStatusSignal
|> deliverOnMainQueue).start(next: { [weak strongSelf] fileStatus in |> deliverOnMainQueue).start(next: { [weak strongSelf] fileStatus in
@ -717,10 +690,7 @@ final class ListMessageFileItemNode: ListMessageNode {
})) }))
} }
transition.updateFrame(node: strongSelf.downloadStatusIconNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset, y: strongSelf.descriptionNode.frame.minY + floor((strongSelf.descriptionNode.frame.height - 11.0) / 2.0)), size: CGSize(width: 11.0, height: 11.0))) transition.updateFrame(node: strongSelf.downloadStatusIconNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset, y: strongSelf.descriptionNode.frame.minY + floor((strongSelf.descriptionNode.frame.height - 12.0) / 2.0)), size: CGSize(width: 12.0, height: 12.0)))
let progressSize: CGFloat = 40.0
transition.updateFrame(node: strongSelf.progressNode, frame: CGRect(origin: CGPoint(x: leftOffset + params.leftInset + floor((leftInset - params.leftInset - progressSize) / 2.0), y: floor((nodeLayout.contentSize.height - progressSize) / 2.0)), size: CGSize(width: progressSize, height: progressSize)))
if let updatedFetchControls = updatedFetchControls { if let updatedFetchControls = updatedFetchControls {
let _ = strongSelf.fetchControls.swap(updatedFetchControls) let _ = strongSelf.fetchControls.swap(updatedFetchControls)
@ -757,74 +727,44 @@ final class ListMessageFileItemNode: ListMessageNode {
isInstantVideo = file.isInstantVideo isInstantVideo = file.isInstantVideo
} }
self.progressNode.isHidden = !isVoice var iconStatusState: SemanticStatusNodeState = .none
var iconStatusBackgroundColor: UIColor = .clear
var iconStatusForegroundColor: UIColor = .white
if isVoice {
iconStatusBackgroundColor = item.theme.list.itemAccentColor
iconStatusForegroundColor = item.theme.list.itemCheckColors.foregroundColor
}
var enableScrubbing = false
var musicIsPlaying: Bool?
var statusState: RadialStatusNodeState = .none
if !isAudio && !isInstantVideo { if !isAudio && !isInstantVideo {
self.updateProgressFrame(size: contentSize, leftInset: layoutParams.leftInset, rightInset: layoutParams.rightInset, transition: .immediate) self.updateProgressFrame(size: contentSize, leftInset: layoutParams.leftInset, rightInset: layoutParams.rightInset, transition: .immediate)
} else { } else {
if !isVoice && !isInstantVideo {
switch fetchStatus {
case let .Fetching(_, progress):
let adjustedProgress = max(progress, 0.027)
statusState = .cloudProgress(color: item.theme.list.itemAccentColor, strokeBackgroundColor: item.theme.list.itemAccentColor.withAlphaComponent(0.5), lineWidth: 2.0, value: CGFloat(adjustedProgress))
case .Local:
break
case .Remote:
if let image = PresentationResourcesItemList.cloudFetchIcon(item.theme) {
statusState = .customIcon(image)
}
}
}
self.statusNode.transitionToState(statusState, completion: {})
self.statusButtonNode.isUserInteractionEnabled = statusState != .none
switch status { switch status {
case let .fetchStatus(fetchStatus): case let .fetchStatus(fetchStatus):
switch fetchStatus { switch fetchStatus {
case let .Fetching(_, progress): case .Fetching:
let adjustedProgress = max(progress, 0.027) break
self.progressNode.state = .Fetching(progress: adjustedProgress)
case .Local: case .Local:
if isAudio { if isAudio || isInstantVideo {
self.progressNode.state = .Play iconStatusState = .play
} else {
self.progressNode.state = .Icon
} }
case .Remote: case .Remote:
if isAudio { if isAudio || isInstantVideo {
self.progressNode.state = .Play iconStatusState = .play
} else {
self.progressNode.state = .Remote
} }
} }
case let .playbackStatus(playbackStatus): case let .playbackStatus(playbackStatus):
enableScrubbing = true
switch playbackStatus { switch playbackStatus {
case .playing: case .playing:
musicIsPlaying = true iconStatusState = .pause
self.progressNode.state = .Pause
case .paused: case .paused:
musicIsPlaying = false iconStatusState = .play
self.progressNode.state = .Play
} }
} }
} }
self.waveformScrubbingNode?.enableScrubbing = enableScrubbing self.iconStatusNode.backgroundNodeColor = iconStatusBackgroundColor
if let musicIsPlaying = musicIsPlaying, !isVoice, !isInstantVideo { self.iconStatusNode.foregroundNodeColor = iconStatusForegroundColor
if self.playbackOverlayNode == nil { self.iconStatusNode.transitionToState(iconStatusState)
let playbackOverlayNode = ListMessagePlaybackOverlayNode()
playbackOverlayNode.frame = self.iconImageNode.frame
self.playbackOverlayNode = playbackOverlayNode
self.addSubnode(playbackOverlayNode)
}
self.playbackOverlayNode?.isPlaying = musicIsPlaying
} else if let playbackOverlayNode = self.playbackOverlayNode {
self.playbackOverlayNode = nil
playbackOverlayNode.removeFromSupernode()
}
} }
override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
@ -903,35 +843,54 @@ final class ListMessageFileItemNode: ListMessageNode {
switch maybeFetchStatus { switch maybeFetchStatus {
case let .Fetching(_, progress): case let .Fetching(_, progress):
let progressFrame = CGRect(x: self.currentLeftOffset + leftInset + 65.0, y: size.height - 2.0, width: floor((size.width - 65.0 - leftInset - rightInset) * CGFloat(progress)), height: 2.0) let progressFrame = CGRect(x: self.currentLeftOffset + leftInset + 65.0, y: size.height - 2.0, width: floor((size.width - 65.0 - leftInset - rightInset)), height: 3.0)
if self.linearProgressNode.supernode == nil { let linearProgressNode: LinearProgressNode
self.addSubnode(self.linearProgressNode) if let current = self.linearProgressNode {
linearProgressNode = current
} else {
linearProgressNode = LinearProgressNode()
linearProgressNode.updateTheme(theme: item.theme)
self.linearProgressNode = linearProgressNode
self.addSubnode(linearProgressNode)
} }
transition.updateFrame(node: self.linearProgressNode, frame: progressFrame) transition.updateFrame(node: linearProgressNode, frame: progressFrame)
linearProgressNode.updateProgress(value: CGFloat(progress), completion: {})
if self.downloadStatusIconNode.supernode == nil { if self.downloadStatusIconNode.supernode == nil {
self.addSubnode(self.downloadStatusIconNode) self.offsetContainerNode.addSubnode(self.downloadStatusIconNode)
} }
self.downloadStatusIconNode.image = PresentationResourcesChat.sharedMediaFileDownloadPauseIcon(item.theme) self.downloadStatusIconNode.image = PresentationResourcesChat.sharedMediaFileDownloadPauseIcon(item.theme)
case .Local: case .Local:
if self.linearProgressNode.supernode != nil { if let linearProgressNode = self.linearProgressNode {
self.linearProgressNode.removeFromSupernode() self.linearProgressNode = nil
linearProgressNode.updateProgress(value: 1.0, completion: { [weak linearProgressNode] in
linearProgressNode?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { _ in
linearProgressNode?.removeFromSupernode()
})
})
} }
if self.downloadStatusIconNode.supernode != nil { if self.downloadStatusIconNode.supernode != nil {
self.downloadStatusIconNode.removeFromSupernode() self.downloadStatusIconNode.removeFromSupernode()
} }
self.downloadStatusIconNode.image = nil self.downloadStatusIconNode.image = nil
case .Remote: case .Remote:
if self.linearProgressNode.supernode != nil { if let linearProgressNode = self.linearProgressNode {
self.linearProgressNode.removeFromSupernode() self.linearProgressNode = nil
linearProgressNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak linearProgressNode] _ in
linearProgressNode?.removeFromSupernode()
})
} }
if self.downloadStatusIconNode.supernode == nil { if self.downloadStatusIconNode.supernode == nil {
self.addSubnode(self.downloadStatusIconNode) self.offsetContainerNode.addSubnode(self.downloadStatusIconNode)
} }
self.downloadStatusIconNode.image = PresentationResourcesChat.sharedMediaFileDownloadStartIcon(item.theme) self.downloadStatusIconNode.image = PresentationResourcesChat.sharedMediaFileDownloadStartIcon(item.theme)
} }
} else { } else {
if self.linearProgressNode.supernode != nil { if let linearProgressNode = self.linearProgressNode {
self.linearProgressNode.removeFromSupernode() self.linearProgressNode = nil
linearProgressNode.layer.animateAlpha(from: 1.0, to: 1.0, duration: 0.2, removeOnCompletion: false, completion: { [weak linearProgressNode] _ in
linearProgressNode?.removeFromSupernode()
})
} }
if self.downloadStatusIconNode.supernode != nil { if self.downloadStatusIconNode.supernode != nil {
self.downloadStatusIconNode.removeFromSupernode() self.downloadStatusIconNode.removeFromSupernode()
@ -1002,12 +961,6 @@ final class ListMessageFileItemNode: ListMessageNode {
return super.hitTest(point, with: event) return super.hitTest(point, with: event)
} }
override func longTapped() {
if let item = self.item {
item.controllerInteraction.openMessageContextMenu(item.message, false, self, self.bounds, nil)
}
}
@objc private func statusPressed() { @objc private func statusPressed() {
guard let _ = self.item, let fetchStatus = self.fetchStatus else { guard let _ = self.item, let fetchStatus = self.fetchStatus else {
return return
@ -1027,3 +980,129 @@ final class ListMessageFileItemNode: ListMessageNode {
} }
} }
} }
private final class LinearProgressNode: ASDisplayNode {
private let trackingNode: HierarchyTrackingNode
private let barNode: ASImageNode
private let shimmerNode: ASImageNode
private let shimmerClippingNode: ASDisplayNode
private var currentProgress: CGFloat = 0.0
private var currentProgressAnimation: (from: CGFloat, to: CGFloat, startTime: Double, completion: () -> Void)?
private var shimmerPhase: CGFloat = 0.0
private var inHierarchyValue: Bool = false
private var shouldAnimate: Bool = false
private let animator: ConstantDisplayLinkAnimator
override init() {
var updateInHierarchy: ((Bool) -> Void)?
self.trackingNode = HierarchyTrackingNode { value in
updateInHierarchy?(value)
}
var animationStep: (() -> Void)?
self.animator = ConstantDisplayLinkAnimator {
animationStep?()
}
self.barNode = ASImageNode()
self.barNode.isLayerBacked = true
self.shimmerNode = ASImageNode()
self.shimmerNode.contentMode = .scaleToFill
self.shimmerClippingNode = ASDisplayNode()
self.shimmerClippingNode.clipsToBounds = true
super.init()
self.addSubnode(trackingNode)
self.addSubnode(self.barNode)
self.shimmerClippingNode.addSubnode(self.shimmerNode)
self.addSubnode(self.shimmerClippingNode)
updateInHierarchy = { [weak self] value in
guard let strongSelf = self else {
return
}
if strongSelf.inHierarchyValue != value {
strongSelf.inHierarchyValue = value
strongSelf.updateAnimations()
}
}
animationStep = { [weak self] in
self?.update()
}
}
func updateTheme(theme: PresentationTheme) {
self.barNode.image = generateStretchableFilledCircleImage(diameter: 3.0, color: theme.list.itemAccentColor)
self.shimmerNode.image = generateImage(CGSize(width: 100.0, height: 3.0), opaque: false, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
let foregroundColor = theme.list.plainBackgroundColor.withAlphaComponent(0.4)
let transparentColor = foregroundColor.withAlphaComponent(0.0).cgColor
let peakColor = foregroundColor.cgColor
var locations: [CGFloat] = [0.0, 0.5, 1.0]
let colors: [CGColor] = [transparentColor, peakColor, transparentColor]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: CGGradientDrawingOptions())
})
}
func updateProgress(value: CGFloat, completion: @escaping () -> Void = {}) {
if self.currentProgress.isEqual(to: value) {
self.currentProgressAnimation = nil
completion()
} else {
self.currentProgressAnimation = (self.currentProgress, value, CACurrentMediaTime(), completion)
}
}
private func updateAnimations() {
let shouldAnimate = self.inHierarchyValue
if shouldAnimate != self.shouldAnimate {
self.shouldAnimate = shouldAnimate
self.animator.isPaused = !shouldAnimate
}
}
private func update() {
if let (fromValue, toValue, startTime, completion) = self.currentProgressAnimation {
let duration: Double = 0.15
let timestamp = CACurrentMediaTime()
let t = CGFloat((timestamp - startTime) / duration)
if t >= 1.0 {
self.currentProgress = toValue
self.currentProgressAnimation = nil
completion()
} else {
let clippedT = max(0.0, t)
self.currentProgress = (1.0 - clippedT) * fromValue + clippedT * toValue
}
var progressWidth: CGFloat = self.bounds.width * self.currentProgress
if progressWidth < 6.0 {
progressWidth = 0.0
}
let progressFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: progressWidth, height: 3.0))
self.barNode.frame = progressFrame
self.shimmerClippingNode.frame = progressFrame
}
self.shimmerPhase += 3.5
let shimmerWidth: CGFloat = 160.0
let shimmerOffset = self.shimmerPhase.remainder(dividingBy: self.bounds.width + shimmerWidth / 2.0)
self.shimmerNode.frame = CGRect(origin: CGPoint(x: shimmerOffset - shimmerWidth / 2.0, y: 0.0), size: CGSize(width: shimmerWidth, height: 3.0))
}
}

View File

@ -14,11 +14,19 @@ import PhotoResources
import WebsiteType import WebsiteType
import UrlHandling import UrlHandling
private let iconFont = Font.medium(22.0) private let iconFont = Font.with(size: 30.0, design: .round, traits: [.bold])
private let iconTextBackgroundImage = generateStretchableFilledCircleImage(radius: 2.0, color: UIColor(rgb: 0xdfdfdf)) private let iconTextBackgroundImage = generateStretchableFilledCircleImage(radius: 6.0, color: UIColor(rgb: 0xFF9500))
final class ListMessageSnippetItemNode: ListMessageNode { final class ListMessageSnippetItemNode: ListMessageNode {
private let contextSourceNode: ContextExtractedContentContainingNode
private let containerNode: ContextControllerSourceNode
private let extractedBackgroundImageNode: ASImageNode
private let offsetContainerNode: ASDisplayNode
private var extractedRect: CGRect?
private var nonExtractedRect: CGRect?
private let highlightedBackgroundNode: ASDisplayNode private let highlightedBackgroundNode: ASDisplayNode
private let separatorNode: ASDisplayNode private let separatorNode: ASDisplayNode
@ -36,20 +44,25 @@ final class ListMessageSnippetItemNode: ListMessageNode {
private var currentIconImageRepresentation: TelegramMediaImageRepresentation? private var currentIconImageRepresentation: TelegramMediaImageRepresentation?
private var currentMedia: Media? private var currentMedia: Media?
private var currentPrimaryUrl: String? var currentPrimaryUrl: String?
private var currentIsInstantView: Bool? private var currentIsInstantView: Bool?
private var appliedItem: ListMessageItem? private var appliedItem: ListMessageItem?
override var canBeLongTapped: Bool {
return true
}
public required init() { public required init() {
self.contextSourceNode = ContextExtractedContentContainingNode()
self.containerNode = ContextControllerSourceNode()
self.separatorNode = ASDisplayNode() self.separatorNode = ASDisplayNode()
self.separatorNode.displaysAsynchronously = false self.separatorNode.displaysAsynchronously = false
self.separatorNode.isLayerBacked = true self.separatorNode.isLayerBacked = true
self.extractedBackgroundImageNode = ASImageNode()
self.extractedBackgroundImageNode.displaysAsynchronously = false
self.extractedBackgroundImageNode.alpha = 0.0
self.offsetContainerNode = ASDisplayNode()
self.highlightedBackgroundNode = ASDisplayNode() self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true self.highlightedBackgroundNode.isLayerBacked = true
@ -80,11 +93,49 @@ final class ListMessageSnippetItemNode: ListMessageNode {
super.init() super.init()
self.addSubnode(self.separatorNode) self.addSubnode(self.separatorNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.descriptionNode) self.containerNode.addSubnode(self.contextSourceNode)
self.addSubnode(self.linkNode) self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode
self.addSubnode(self.instantViewIconNode) self.addSubnode(self.containerNode)
self.addSubnode(self.iconImageNode)
self.contextSourceNode.contentNode.addSubnode(self.extractedBackgroundImageNode)
self.contextSourceNode.contentNode.addSubnode(self.offsetContainerNode)
self.offsetContainerNode.addSubnode(self.titleNode)
self.offsetContainerNode.addSubnode(self.descriptionNode)
self.offsetContainerNode.addSubnode(self.linkNode)
self.offsetContainerNode.addSubnode(self.instantViewIconNode)
self.offsetContainerNode.addSubnode(self.iconImageNode)
self.containerNode.activated = { [weak self] gesture, _ in
guard let strongSelf = self, let item = strongSelf.item else {
return
}
item.controllerInteraction.openMessageContextMenu(item.message, false, strongSelf.contextSourceNode, strongSelf.contextSourceNode.bounds, gesture)
}
self.contextSourceNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, transition in
guard let strongSelf = self, let item = strongSelf.item else {
return
}
if isExtracted {
strongSelf.extractedBackgroundImageNode.image = generateStretchableFilledCircleImage(diameter: 28.0, color: item.theme.list.plainBackgroundColor)
}
if let extractedRect = strongSelf.extractedRect, let nonExtractedRect = strongSelf.nonExtractedRect {
let rect = isExtracted ? extractedRect : nonExtractedRect
transition.updateFrame(node: strongSelf.extractedBackgroundImageNode, frame: rect)
}
transition.updateSublayerTransformOffset(layer: strongSelf.offsetContainerNode.layer, offset: CGPoint(x: isExtracted ? 12.0 : 0.0, y: 0.0))
transition.updateAlpha(node: strongSelf.extractedBackgroundImageNode, alpha: isExtracted ? 1.0 : 0.0, completion: { _ in
if !isExtracted {
self?.extractedBackgroundImageNode.image = nil
}
})
}
} }
required public init?(coder aDecoder: NSCoder) { required public init?(coder aDecoder: NSCoder) {
@ -155,7 +206,7 @@ final class ListMessageSnippetItemNode: ListMessageNode {
updatedTheme = item.theme updatedTheme = item.theme
} }
let titleFont = Font.medium(floor(item.fontSize.baseDisplaySize * 16.0 / 17.0)) let titleFont = Font.semibold(floor(item.fontSize.baseDisplaySize * 16.0 / 17.0))
let descriptionFont = Font.regular(floor(item.fontSize.baseDisplaySize * 14.0 / 17.0)) let descriptionFont = Font.regular(floor(item.fontSize.baseDisplaySize * 14.0 / 17.0))
let leftInset: CGFloat = 65.0 + params.leftInset let leftInset: CGFloat = 65.0 + params.leftInset
@ -216,7 +267,7 @@ final class ListMessageSnippetItemNode: ListMessageNode {
let mutableDescriptionText = NSMutableAttributedString() let mutableDescriptionText = NSMutableAttributedString()
if let text = content.text { if let text = content.text {
mutableDescriptionText.append(NSAttributedString(string: text + "\n", font: descriptionFont, textColor: item.theme.list.itemPrimaryTextColor)) mutableDescriptionText.append(NSAttributedString(string: text + "\n", font: descriptionFont, textColor: item.theme.list.itemSecondaryTextColor))
} }
let plainUrlString = NSAttributedString(string: content.displayUrl, font: descriptionFont, textColor: item.theme.list.itemAccentColor) let plainUrlString = NSAttributedString(string: content.displayUrl, font: descriptionFont, textColor: item.theme.list.itemAccentColor)
@ -262,6 +313,7 @@ final class ListMessageSnippetItemNode: ListMessageNode {
range.length = nsString.length - range.location range.length = nsString.length - range.location
} }
var urlString = nsString.substring(with: range) var urlString = nsString.substring(with: range)
let rawUrlString = urlString
var parsedUrl = URL(string: urlString) var parsedUrl = URL(string: urlString)
if parsedUrl == nil || parsedUrl!.host == nil || parsedUrl!.host!.isEmpty { if parsedUrl == nil || parsedUrl!.host == nil || parsedUrl!.host!.isEmpty {
urlString = "http://" + urlString urlString = "http://" + urlString
@ -269,13 +321,18 @@ final class ListMessageSnippetItemNode: ListMessageNode {
} }
if let url = parsedUrl, let host = url.host { if let url = parsedUrl, let host = url.host {
primaryUrl = urlString primaryUrl = urlString
if url.path.hasPrefix("/addstickers/") {
title = NSAttributedString(string: urlString, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor)
iconText = NSAttributedString(string: "S", font: iconFont, textColor: UIColor.white)
} else {
iconText = NSAttributedString(string: host[..<host.index(after: host.startIndex)].uppercased(), font: iconFont, textColor: UIColor.white) iconText = NSAttributedString(string: host[..<host.index(after: host.startIndex)].uppercased(), font: iconFont, textColor: UIColor.white)
title = NSAttributedString(string: host, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor) title = NSAttributedString(string: host, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor)
}
let mutableDescriptionText = NSMutableAttributedString() let mutableDescriptionText = NSMutableAttributedString()
if item.message.text != urlString { if item.message.text != rawUrlString {
mutableDescriptionText.append(NSAttributedString(string: item.message.text + "\n", font: descriptionFont, textColor: item.theme.list.itemPrimaryTextColor)) mutableDescriptionText.append(NSAttributedString(string: item.message.text + "\n", font: descriptionFont, textColor: item.theme.list.itemSecondaryTextColor))
} }
let urlAttributedString = NSMutableAttributedString() let urlAttributedString = NSMutableAttributedString()
@ -296,11 +353,11 @@ final class ListMessageSnippetItemNode: ListMessageNode {
} }
} }
let (titleNodeLayout, titleNodeApply) = titleNodeMakeLayout(TextNodeLayoutArguments(attributedString: title, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - params.rightInset, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let (titleNodeLayout, titleNodeApply) = titleNodeMakeLayout(TextNodeLayoutArguments(attributedString: title, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .middle, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - params.rightInset - 16.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (descriptionNodeLayout, descriptionNodeApply) = descriptionNodeMakeLayout(TextNodeLayoutArguments(attributedString: descriptionText, backgroundColor: nil, maximumNumberOfLines: 3, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - params.rightInset - 12.0, height: CGFloat.infinity), alignment: .natural, lineSpacing: 0.3, cutout: nil, insets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0))) let (descriptionNodeLayout, descriptionNodeApply) = descriptionNodeMakeLayout(TextNodeLayoutArguments(attributedString: descriptionText, backgroundColor: nil, maximumNumberOfLines: 3, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - params.rightInset - 16.0 - 8.0, height: CGFloat.infinity), alignment: .natural, lineSpacing: 0.3, cutout: nil, insets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0)))
let (linkNodeLayout, linkNodeApply) = linkNodeMakeLayout(TextNodeLayoutArguments(attributedString: linkText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - params.rightInset - 12.0, height: CGFloat.infinity), alignment: .natural, lineSpacing: 0.3, cutout: isInstantView ? TextNodeCutout(topLeft: CGSize(width: 14.0, height: 8.0)) : nil, insets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0))) let (linkNodeLayout, linkNodeApply) = linkNodeMakeLayout(TextNodeLayoutArguments(attributedString: linkText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - params.rightInset - 16.0 - 8.0, height: CGFloat.infinity), alignment: .natural, lineSpacing: 0.3, cutout: isInstantView ? TextNodeCutout(topLeft: CGSize(width: 14.0, height: 8.0)) : nil, insets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0)))
var instantViewImage: UIImage? var instantViewImage: UIImage?
if isInstantView { if isInstantView {
instantViewImage = PresentationResourcesChat.sharedMediaInstantViewIcon(item.theme) instantViewImage = PresentationResourcesChat.sharedMediaInstantViewIcon(item.theme)
@ -310,8 +367,8 @@ final class ListMessageSnippetItemNode: ListMessageNode {
var iconImageApply: (() -> Void)? var iconImageApply: (() -> Void)?
if let iconImageReferenceAndRepresentation = iconImageReferenceAndRepresentation { if let iconImageReferenceAndRepresentation = iconImageReferenceAndRepresentation {
let iconSize = CGSize(width: 42.0, height: 42.0) let iconSize = CGSize(width: 40.0, height: 40.0)
let imageCorners = ImageCorners(topLeft: .Corner(2.0), topRight: .Corner(2.0), bottomLeft: .Corner(2.0), bottomRight: .Corner(2.0)) let imageCorners = ImageCorners(radius: 6.0)
let arguments = TransformImageArguments(corners: imageCorners, imageSize: iconImageReferenceAndRepresentation.1.dimensions.cgSize.aspectFilled(iconSize), boundingSize: iconSize, intrinsicInsets: UIEdgeInsets(), emptyColor: item.theme.list.mediaPlaceholderColor) let arguments = TransformImageArguments(corners: imageCorners, imageSize: iconImageReferenceAndRepresentation.1.dimensions.cgSize.aspectFilled(iconSize), boundingSize: iconSize, intrinsicInsets: UIEdgeInsets(), emptyColor: item.theme.list.mediaPlaceholderColor)
iconImageApply = iconImageLayout(arguments) iconImageApply = iconImageLayout(arguments)
} }
@ -335,7 +392,8 @@ final class ListMessageSnippetItemNode: ListMessageNode {
insets.top += header.height insets.top += header.height
} }
return (ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: contentHeight), insets: insets), { animation in let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: contentHeight), insets: insets)
return (nodeLayout, { animation in
if let strongSelf = self { if let strongSelf = self {
let transition: ContainedViewLayoutTransition let transition: ContainedViewLayoutTransition
if animation.isAnimated { if animation.isAnimated {
@ -344,6 +402,23 @@ final class ListMessageSnippetItemNode: ListMessageNode {
transition = .immediate transition = .immediate
} }
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: nodeLayout.contentSize)
strongSelf.contextSourceNode.frame = CGRect(origin: CGPoint(), size: nodeLayout.contentSize)
strongSelf.contextSourceNode.contentNode.frame = CGRect(origin: CGPoint(), size: nodeLayout.contentSize)
strongSelf.offsetContainerNode.frame = CGRect(origin: CGPoint(), size: nodeLayout.contentSize)
let nonExtractedRect = CGRect(origin: CGPoint(), size: CGSize(width: nodeLayout.contentSize.width - 16.0, height: nodeLayout.contentSize.height))
let extractedRect = CGRect(origin: CGPoint(), size: nodeLayout.contentSize).insetBy(dx: 16.0, dy: 0.0)
strongSelf.extractedRect = extractedRect
strongSelf.nonExtractedRect = nonExtractedRect
if strongSelf.contextSourceNode.isExtractedToContextPreview {
strongSelf.extractedBackgroundImageNode.frame = extractedRect
} else {
strongSelf.extractedBackgroundImageNode.frame = nonExtractedRect
}
strongSelf.contextSourceNode.contentRect = extractedRect
strongSelf.appliedItem = item strongSelf.appliedItem = item
strongSelf.currentMedia = selectedMedia strongSelf.currentMedia = selectedMedia
strongSelf.currentPrimaryUrl = primaryUrl strongSelf.currentPrimaryUrl = primaryUrl
@ -380,7 +455,7 @@ final class ListMessageSnippetItemNode: ListMessageNode {
transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset, y: 9.0), size: titleNodeLayout.size)) transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset, y: 9.0), size: titleNodeLayout.size))
let _ = titleNodeApply() let _ = titleNodeApply()
let descriptionFrame = CGRect(origin: CGPoint(x: leftOffset + leftInset - 1.0, y: strongSelf.titleNode.frame.maxY + 3.0), size: descriptionNodeLayout.size) let descriptionFrame = CGRect(origin: CGPoint(x: leftOffset + leftInset, y: strongSelf.titleNode.frame.maxY + 1.0), size: descriptionNodeLayout.size)
transition.updateFrame(node: strongSelf.descriptionNode, frame: descriptionFrame) transition.updateFrame(node: strongSelf.descriptionNode, frame: descriptionFrame)
let _ = descriptionNodeApply() let _ = descriptionNodeApply()
@ -393,8 +468,8 @@ final class ListMessageSnippetItemNode: ListMessageNode {
transition.updateFrame(node: strongSelf.instantViewIconNode, frame: CGRect(origin: linkFrame.origin.offsetBy(dx: 0.0, dy: 4.0), size: image.size)) transition.updateFrame(node: strongSelf.instantViewIconNode, frame: CGRect(origin: linkFrame.origin.offsetBy(dx: 0.0, dy: 4.0), size: image.size))
} }
let iconFrame = CGRect(origin: CGPoint(x: params.leftInset + leftOffset + 9.0, y: 12.0), size: CGSize(width: 42.0, height: 42.0)) let iconFrame = CGRect(origin: CGPoint(x: params.leftInset + leftOffset + 12.0, y: 12.0), size: CGSize(width: 40.0, height: 40.0))
transition.updateFrame(node: strongSelf.iconTextNode, frame: CGRect(origin: CGPoint(x: iconFrame.minX + floor((42.0 - iconTextLayout.size.width) / 2.0), y: iconFrame.minY + floor((42.0 - iconTextLayout.size.height) / 2.0) + 3.0), size: iconTextLayout.size)) transition.updateFrame(node: strongSelf.iconTextNode, frame: CGRect(origin: CGPoint(x: iconFrame.minX + floorToScreenPixels((iconFrame.width - iconTextLayout.size.width) / 2.0), y: iconFrame.minY + floorToScreenPixels((iconFrame.height - iconTextLayout.size.height) / 2.0) + 2.0), size: iconTextLayout.size))
let _ = iconTextApply() let _ = iconTextApply()
@ -406,7 +481,7 @@ final class ListMessageSnippetItemNode: ListMessageNode {
} }
if strongSelf.iconImageNode.supernode == nil { if strongSelf.iconImageNode.supernode == nil {
strongSelf.addSubnode(strongSelf.iconImageNode) strongSelf.offsetContainerNode.addSubnode(strongSelf.iconImageNode)
strongSelf.iconImageNode.frame = iconFrame strongSelf.iconImageNode.frame = iconFrame
} else { } else {
transition.updateFrame(node: strongSelf.iconImageNode, frame: iconFrame) transition.updateFrame(node: strongSelf.iconImageNode, frame: iconFrame)
@ -427,15 +502,18 @@ final class ListMessageSnippetItemNode: ListMessageNode {
if strongSelf.iconTextBackgroundNode.supernode == nil { if strongSelf.iconTextBackgroundNode.supernode == nil {
strongSelf.iconTextBackgroundNode.image = applyIconTextBackgroundImage strongSelf.iconTextBackgroundNode.image = applyIconTextBackgroundImage
strongSelf.addSubnode(strongSelf.iconTextBackgroundNode) strongSelf.offsetContainerNode.addSubnode(strongSelf.iconTextBackgroundNode)
strongSelf.iconTextBackgroundNode.frame = iconFrame strongSelf.iconTextBackgroundNode.frame = iconFrame
} else { } else {
transition.updateFrame(node: strongSelf.iconTextBackgroundNode, frame: iconFrame) transition.updateFrame(node: strongSelf.iconTextBackgroundNode, frame: iconFrame)
} }
if strongSelf.iconTextNode.supernode == nil { if strongSelf.iconTextNode.supernode == nil {
strongSelf.addSubnode(strongSelf.iconTextNode) strongSelf.offsetContainerNode.addSubnode(strongSelf.iconTextNode)
} }
} }
strongSelf.iconTextBackgroundNode.isHidden = iconText == nil
strongSelf.iconTextNode.isHidden = iconText == nil
} }
}) })
} }
@ -598,7 +676,7 @@ final class ListMessageSnippetItemNode: ListMessageNode {
} else { } else {
linkHighlightingNode = LinkHighlightingNode(color: item.message.effectivelyIncoming(item.context.account.peerId) ? item.theme.chat.message.incoming.linkHighlightColor : item.theme.chat.message.outgoing.linkHighlightColor) linkHighlightingNode = LinkHighlightingNode(color: item.message.effectivelyIncoming(item.context.account.peerId) ? item.theme.chat.message.incoming.linkHighlightColor : item.theme.chat.message.outgoing.linkHighlightColor)
self.linkHighlightingNode = linkHighlightingNode self.linkHighlightingNode = linkHighlightingNode
self.insertSubnode(linkHighlightingNode, belowSubnode: self.linkNode) self.offsetContainerNode.insertSubnode(linkHighlightingNode, belowSubnode: self.linkNode)
} }
linkHighlightingNode.frame = self.linkNode.frame.offsetBy(dx: 0.0, dy: 0.0) linkHighlightingNode.frame = self.linkNode.frame.offsetBy(dx: 0.0, dy: 0.0)
linkHighlightingNode.updateRects(rects.map { $0.insetBy(dx: -1.0, dy: -1.0) }) linkHighlightingNode.updateRects(rects.map { $0.insetBy(dx: -1.0, dy: -1.0) })
@ -610,10 +688,4 @@ final class ListMessageSnippetItemNode: ListMessageNode {
} }
} }
} }
override func longTapped() {
if let item = self.item {
item.controllerInteraction.openMessageContextMenu(item.message, false, self, self.bounds, nil)
}
}
} }

View File

@ -77,6 +77,8 @@ final class PeerInfoGroupsInCommonPaneNode: ASDisplayNode, PeerInfoPaneNode {
return self.ready.get() return self.ready.get()
} }
let shouldReceiveExpandProgressUpdates: Bool = false
private var disposable: Disposable? private var disposable: Disposable?
init(context: AccountContext, peerId: PeerId, chatControllerInteraction: ChatControllerInteraction, openPeerContextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void, groupsInCommonContext: GroupsInCommonContext) { init(context: AccountContext, peerId: PeerId, chatControllerInteraction: ChatControllerInteraction, openPeerContextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void, groupsInCommonContext: GroupsInCommonContext) {
@ -127,7 +129,7 @@ final class PeerInfoGroupsInCommonPaneNode: ASDisplayNode, PeerInfoPaneNode {
} }
} }
func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
let isFirstLayout = self.currentParams == nil let isFirstLayout = self.currentParams == nil
self.currentParams = (size, isScrollingLockedAtTop, presentationData) self.currentParams = (size, isScrollingLockedAtTop, presentationData)

View File

@ -5,10 +5,27 @@ import SyncCore
import SwiftSignalKit import SwiftSignalKit
import Postbox import Postbox
import TelegramPresentationData import TelegramPresentationData
import PresentationDataUtils
import AccountContext import AccountContext
import ContextUI import ContextUI
import PhotoResources import PhotoResources
import TelegramUIPreferences import TelegramUIPreferences
import UniversalMediaPlayer
import TelegramBaseController
import OverlayStatusController
private final class PassthroughContainerNode: ASDisplayNode {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let subnodes = self.subnodes {
for subnode in subnodes {
if let result = subnode.view.hitTest(self.view.convert(point, to: subnode.view), with: event) {
return result
}
}
}
return nil
}
}
final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode { final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode {
private let context: AccountContext private let context: AccountContext
@ -17,7 +34,7 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode {
private let listNode: ChatHistoryListNode private let listNode: ChatHistoryListNode
private var currentParams: (size: CGSize, isScrollingLockedAtTop: Bool, presentationData: PresentationData)? private var currentParams: (size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData)?
private let ready = Promise<Bool>() private let ready = Promise<Bool>()
private var didSetReady: Bool = false private var didSetReady: Bool = false
@ -25,6 +42,8 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode {
return self.ready.get() return self.ready.get()
} }
let shouldReceiveExpandProgressUpdates: Bool
private let selectedMessagesPromise = Promise<Set<MessageId>?>(nil) private let selectedMessagesPromise = Promise<Set<MessageId>?>(nil)
private var selectedMessages: Set<MessageId>? { private var selectedMessages: Set<MessageId>? {
didSet { didSet {
@ -35,6 +54,13 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode {
} }
private var hiddenMediaDisposable: Disposable? private var hiddenMediaDisposable: Disposable?
private var mediaStatusDisposable: Disposable?
private var playlistPreloadDisposable: Disposable?
private var playlistStateAndType: (SharedMediaPlaylistItem, SharedMediaPlaylistItem?, SharedMediaPlaylistItem?, MusicPlaybackSettingsOrder, MediaManagerPlayerType, Account)?
private var mediaAccessoryPanelContainer: PassthroughContainerNode
private var mediaAccessoryPanel: (MediaNavigationAccessoryPanel, MediaManagerPlayerType)?
private var dismissingPanel: ASDisplayNode?
init(context: AccountContext, chatControllerInteraction: ChatControllerInteraction, peerId: PeerId, tagMask: MessageTags) { init(context: AccountContext, chatControllerInteraction: ChatControllerInteraction, peerId: PeerId, tagMask: MessageTags) {
self.context = context self.context = context
@ -45,19 +71,82 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode {
self.selectedMessagesPromise.set(.single(self.selectedMessages)) self.selectedMessagesPromise.set(.single(self.selectedMessages))
self.listNode = ChatHistoryListNode(context: context, chatLocation: .peer(peerId), tagMask: tagMask, subject: nil, controllerInteraction: chatControllerInteraction, selectedMessages: self.selectedMessagesPromise.get(), mode: .list(search: false, reversed: false, displayHeaders: .allButLast)) self.listNode = ChatHistoryListNode(context: context, chatLocation: .peer(peerId), tagMask: tagMask, subject: nil, controllerInteraction: chatControllerInteraction, selectedMessages: self.selectedMessagesPromise.get(), mode: .list(search: false, reversed: false, displayHeaders: .allButLast))
self.listNode.defaultToSynchronousTransactionWhileScrolling = true
if tagMask == .music {
self.shouldReceiveExpandProgressUpdates = true
} else {
self.shouldReceiveExpandProgressUpdates = false
}
self.mediaAccessoryPanelContainer = PassthroughContainerNode()
self.mediaAccessoryPanelContainer.clipsToBounds = true
super.init() super.init()
self.listNode.preloadPages = true self.listNode.preloadPages = true
self.addSubnode(self.listNode) self.addSubnode(self.listNode)
self.addSubnode(self.mediaAccessoryPanelContainer)
self.ready.set(self.listNode.historyState.get() self.ready.set(self.listNode.historyState.get()
|> take(1) |> take(1)
|> map { _ -> Bool in true }) |> map { _ -> Bool in true })
if tagMask == .music || tagMask == .voiceOrInstantVideo {
self.mediaStatusDisposable = (context.sharedContext.mediaManager.globalMediaPlayerState
|> mapToSignal { playlistStateAndType -> Signal<(Account, SharedMediaPlayerItemPlaybackState, MediaManagerPlayerType)?, NoError> in
if let (account, state, type) = playlistStateAndType {
switch state {
case let .state(state):
if let playlistId = state.playlistId as? PeerMessagesMediaPlaylistId, case .peer(peerId) = playlistId {
switch type {
case .voice:
if tagMask != .voiceOrInstantVideo {
return .single(nil) |> delay(0.2, queue: .mainQueue())
}
case .music:
if tagMask != .music {
return .single(nil) |> delay(0.2, queue: .mainQueue())
}
}
return .single((account, state, type))
} else {
return .single(nil) |> delay(0.2, queue: .mainQueue())
}
case .loading:
return .single(nil) |> delay(0.2, queue: .mainQueue())
}
} else {
return .single(nil)
}
}
|> deliverOnMainQueue).start(next: { [weak self] playlistStateAndType in
guard let strongSelf = self else {
return
}
if !arePlaylistItemsEqual(strongSelf.playlistStateAndType?.0, playlistStateAndType?.1.item) ||
!arePlaylistItemsEqual(strongSelf.playlistStateAndType?.1, playlistStateAndType?.1.previousItem) ||
!arePlaylistItemsEqual(strongSelf.playlistStateAndType?.2, playlistStateAndType?.1.nextItem) ||
strongSelf.playlistStateAndType?.3 != playlistStateAndType?.1.order || strongSelf.playlistStateAndType?.4 != playlistStateAndType?.2 {
if let playlistStateAndType = playlistStateAndType {
strongSelf.playlistStateAndType = (playlistStateAndType.1.item, playlistStateAndType.1.previousItem, playlistStateAndType.1.nextItem, playlistStateAndType.1.order, playlistStateAndType.2, playlistStateAndType.0)
} else {
strongSelf.playlistStateAndType = nil
}
if let (size, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) = strongSelf.currentParams {
strongSelf.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, presentationData: presentationData, synchronous: true, transition: .animated(duration: 0.4, curve: .spring))
}
}
})
}
} }
deinit { deinit {
self.hiddenMediaDisposable?.dispose() self.hiddenMediaDisposable?.dispose()
self.mediaStatusDisposable?.dispose()
self.playlistPreloadDisposable?.dispose()
} }
func scrollToTop() -> Bool { func scrollToTop() -> Bool {
@ -71,12 +160,211 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode {
} }
} }
func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
self.currentParams = (size, isScrollingLockedAtTop, presentationData) self.currentParams = (size, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData)
var topPanelHeight: CGFloat = 0.0
if let (item, previousItem, nextItem, order, type, _) = self.playlistStateAndType {
let panelHeight = MediaNavigationAccessoryHeaderNode.minimizedHeight
topPanelHeight = floor(panelHeight * expandProgress)
let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: topPanelHeight - panelHeight), size: CGSize(width: size.width, height: panelHeight))
if let (mediaAccessoryPanel, mediaType) = self.mediaAccessoryPanel, mediaType == type {
transition.updateFrame(layer: mediaAccessoryPanel.layer, frame: panelFrame)
mediaAccessoryPanel.updateLayout(size: panelFrame.size, leftInset: sideInset, rightInset: sideInset, transition: transition)
switch order {
case .regular:
mediaAccessoryPanel.containerNode.headerNode.playbackItems = (item, previousItem, nextItem)
case .reversed:
mediaAccessoryPanel.containerNode.headerNode.playbackItems = (item, nextItem, previousItem)
case .random:
mediaAccessoryPanel.containerNode.headerNode.playbackItems = (item, nil, nil)
}
let delayedStatus = self.context.sharedContext.mediaManager.globalMediaPlayerState
|> mapToSignal { value -> Signal<(Account, SharedMediaPlayerItemPlaybackStateOrLoading, MediaManagerPlayerType)?, NoError> in
guard let value = value else {
return .single(nil)
}
switch value.1 {
case .state:
return .single(value)
case .loading:
return .single(value) |> delay(0.1, queue: .mainQueue())
}
}
mediaAccessoryPanel.containerNode.headerNode.playbackStatus = delayedStatus
|> map { state -> MediaPlayerStatus in
if let stateOrLoading = state?.1, case let .state(state) = stateOrLoading {
return state.status
} else {
return MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: .paused, soundEnabled: true)
}
}
} else {
if let (mediaAccessoryPanel, _) = self.mediaAccessoryPanel {
self.mediaAccessoryPanel = nil
self.dismissingPanel = mediaAccessoryPanel
mediaAccessoryPanel.animateOut(transition: transition, completion: { [weak self, weak mediaAccessoryPanel] in
mediaAccessoryPanel?.removeFromSupernode()
if let strongSelf = self, strongSelf.dismissingPanel === mediaAccessoryPanel {
strongSelf.dismissingPanel = nil
}
})
}
let mediaAccessoryPanel = MediaNavigationAccessoryPanel(context: self.context)
mediaAccessoryPanel.containerNode.headerNode.displayScrubber = type != .voice
mediaAccessoryPanel.close = { [weak self] in
if let strongSelf = self, let (_, _, _, _, type, _) = strongSelf.playlistStateAndType {
strongSelf.context.sharedContext.mediaManager.setPlaylist(nil, type: type, control: SharedMediaPlayerControlAction.playback(.pause))
}
}
mediaAccessoryPanel.toggleRate = {
[weak self] in
guard let strongSelf = self else {
return
}
let _ = (strongSelf.context.sharedContext.accountManager.transaction { transaction -> AudioPlaybackRate in
let settings = transaction.getSharedData(ApplicationSpecificSharedDataKeys.musicPlaybackSettings) as? MusicPlaybackSettings ?? MusicPlaybackSettings.defaultSettings
let nextRate: AudioPlaybackRate
switch settings.voicePlaybackRate {
case .x1:
nextRate = .x2
case .x2:
nextRate = .x1
}
transaction.updateSharedData(ApplicationSpecificSharedDataKeys.musicPlaybackSettings, { _ in
return settings.withUpdatedVoicePlaybackRate(nextRate)
})
return nextRate
}
|> deliverOnMainQueue).start(next: { baseRate in
guard let strongSelf = self, let (_, _, _, _, type, _) = strongSelf.playlistStateAndType else {
return
}
strongSelf.context.sharedContext.mediaManager.playlistControl(.setBaseRate(baseRate), type: type)
})
}
mediaAccessoryPanel.togglePlayPause = { [weak self] in
if let strongSelf = self, let (_, _, _, _, type, _) = strongSelf.playlistStateAndType {
strongSelf.context.sharedContext.mediaManager.playlistControl(.playback(.togglePlayPause), type: type)
}
}
mediaAccessoryPanel.playPrevious = { [weak self] in
if let strongSelf = self, let (_, _, _, _, type, _) = strongSelf.playlistStateAndType {
strongSelf.context.sharedContext.mediaManager.playlistControl(.next, type: type)
}
}
mediaAccessoryPanel.playNext = { [weak self] in
if let strongSelf = self, let (_, _, _, _, type, _) = strongSelf.playlistStateAndType {
strongSelf.context.sharedContext.mediaManager.playlistControl(.previous, type: type)
}
}
mediaAccessoryPanel.tapAction = { [weak self] in
guard let strongSelf = self, let _ = strongSelf.chatControllerInteraction.navigationController(), let (state, _, _, order, type, account) = strongSelf.playlistStateAndType else {
return
}
if let id = state.id as? PeerMessagesMediaPlaylistItemId {
if type == .music {
let signal = strongSelf.context.sharedContext.messageFromPreloadedChatHistoryViewForLocation(id: id.messageId, location: ChatHistoryLocationInput(content: .InitialSearch(location: .id(id.messageId), count: 60), id: 0), account: account, chatLocation: .peer(id.messageId.peerId), tagMask: MessageTags.music)
var cancelImpl: (() -> Void)?
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
let progressSignal = Signal<Never, NoError> { subscriber in
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
cancelImpl?()
}))
self?.chatControllerInteraction.presentController(controller, nil)
return ActionDisposable { [weak controller] in
Queue.mainQueue().async() {
controller?.dismiss()
}
}
}
|> runOn(Queue.mainQueue())
|> delay(0.15, queue: Queue.mainQueue())
let progressDisposable = MetaDisposable()
var progressStarted = false
strongSelf.playlistPreloadDisposable?.dispose()
strongSelf.playlistPreloadDisposable = (signal
|> afterDisposed {
Queue.mainQueue().async {
progressDisposable.dispose()
}
}
|> deliverOnMainQueue).start(next: { index in
guard let strongSelf = self else {
return
}
if let _ = index.0 {
let controllerContext: AccountContext
if account.id == strongSelf.context.account.id {
controllerContext = strongSelf.context
} else {
controllerContext = strongSelf.context.sharedContext.makeTempAccountContext(account: account)
}
let controller = strongSelf.context.sharedContext.makeOverlayAudioPlayerController(context: controllerContext, peerId: id.messageId.peerId, type: type, initialMessageId: id.messageId, initialOrder: order, parentNavigationController: strongSelf.chatControllerInteraction.navigationController())
strongSelf.view.window?.endEditing(true)
strongSelf.chatControllerInteraction.presentController(controller, nil)
} else if index.1 {
if !progressStarted {
progressStarted = true
progressDisposable.set(progressSignal.start())
}
}
}, completed: {
})
cancelImpl = {
self?.playlistPreloadDisposable?.dispose()
}
} else {
strongSelf.context.sharedContext.navigateToChat(accountId: strongSelf.context.account.id, peerId: id.messageId.peerId, messageId: id.messageId)
}
}
}
mediaAccessoryPanel.frame = panelFrame
if let dismissingPanel = self.dismissingPanel {
self.mediaAccessoryPanelContainer.insertSubnode(mediaAccessoryPanel, aboveSubnode: dismissingPanel)
} else {
self.mediaAccessoryPanelContainer.addSubnode(mediaAccessoryPanel)
}
self.mediaAccessoryPanel = (mediaAccessoryPanel, type)
mediaAccessoryPanel.updateLayout(size: panelFrame.size, leftInset: sideInset, rightInset: sideInset, transition: .immediate)
switch order {
case .regular:
mediaAccessoryPanel.containerNode.headerNode.playbackItems = (item, previousItem, nextItem)
case .reversed:
mediaAccessoryPanel.containerNode.headerNode.playbackItems = (item, nextItem, previousItem)
case .random:
mediaAccessoryPanel.containerNode.headerNode.playbackItems = (item, nil, nil)
}
mediaAccessoryPanel.containerNode.headerNode.playbackStatus = self.context.sharedContext.mediaManager.globalMediaPlayerState
|> map { state -> MediaPlayerStatus in
if let stateOrLoading = state?.1, case let .state(state) = stateOrLoading {
return state.status
} else {
return MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: .paused, soundEnabled: true)
}
}
mediaAccessoryPanel.animateIn(transition: transition)
}
} else if let (mediaAccessoryPanel, _) = self.mediaAccessoryPanel {
self.mediaAccessoryPanel = nil
self.dismissingPanel = mediaAccessoryPanel
mediaAccessoryPanel.animateOut(transition: transition, completion: { [weak self, weak mediaAccessoryPanel] in
mediaAccessoryPanel?.removeFromSupernode()
if let strongSelf = self, strongSelf.dismissingPanel === mediaAccessoryPanel {
strongSelf.dismissingPanel = nil
}
})
}
transition.updateFrame(node: self.mediaAccessoryPanelContainer, frame: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: MediaNavigationAccessoryHeaderNode.minimizedHeight)))
transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(), size: size)) transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(), size: size))
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
self.listNode.updateLayout(transition: transition, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), duration: duration, curve: curve)) self.listNode.updateLayout(transition: transition, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: UIEdgeInsets(top: topPanelHeight, left: sideInset, bottom: bottomInset, right: sideInset), duration: duration, curve: curve))
if isScrollingLockedAtTop { if isScrollingLockedAtTop {
switch self.listNode.visibleContentOffset() { switch self.listNode.visibleContentOffset() {
case .known(0.0), .none: case .known(0.0), .none:

View File

@ -118,6 +118,8 @@ final class PeerInfoMembersPaneNode: ASDisplayNode, PeerInfoPaneNode {
return self.ready.get() return self.ready.get()
} }
let shouldReceiveExpandProgressUpdates: Bool = false
private var disposable: Disposable? private var disposable: Disposable?
init(context: AccountContext, peerId: PeerId, membersContext: PeerInfoMembersContext, action: @escaping (PeerInfoMember, PeerMembersListAction) -> Void) { init(context: AccountContext, peerId: PeerId, membersContext: PeerInfoMembersContext, action: @escaping (PeerInfoMember, PeerMembersListAction) -> Void) {
@ -170,7 +172,7 @@ final class PeerInfoMembersPaneNode: ASDisplayNode, PeerInfoPaneNode {
} }
} }
func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
let isFirstLayout = self.currentParams == nil let isFirstLayout = self.currentParams == nil
self.currentParams = (size, isScrollingLockedAtTop, presentationData) self.currentParams = (size, isScrollingLockedAtTop, presentationData)

View File

@ -449,7 +449,7 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
return self._itemInteraction! return self._itemInteraction!
} }
private var currentParams: (size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, presentationData: PresentationData)? private var currentParams: (size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData)?
private let ready = Promise<Bool>() private let ready = Promise<Bool>()
private var didSetReady: Bool = false private var didSetReady: Bool = false
@ -457,6 +457,8 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
return self.ready.get() return self.ready.get()
} }
let shouldReceiveExpandProgressUpdates: Bool = false
private let listDisposable = MetaDisposable() private let listDisposable = MetaDisposable()
private var hiddenMediaDisposable: Disposable? private var hiddenMediaDisposable: Disposable?
private var mediaItems: [VisualMediaItem] = [] private var mediaItems: [VisualMediaItem] = []
@ -482,7 +484,7 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
self._itemInteraction = VisualMediaItemInteraction( self._itemInteraction = VisualMediaItemInteraction(
openMessage: { [weak self] message in openMessage: { [weak self] message in
self?.chatControllerInteraction.openMessage(message, .default) let _ = self?.chatControllerInteraction.openMessage(message, .default)
}, },
openMessageContextActions: { [weak self] message, sourceNode, sourceRect, gesture in openMessageContextActions: { [weak self] message, sourceNode, sourceRect, gesture in
self?.chatControllerInteraction.openMessageContextActions(message, sourceNode, sourceRect, gesture) self?.chatControllerInteraction.openMessageContextActions(message, sourceNode, sourceRect, gesture)
@ -559,8 +561,8 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
let wasFirstHistoryView = self.isFirstHistoryView let wasFirstHistoryView = self.isFirstHistoryView
self.isFirstHistoryView = false self.isFirstHistoryView = false
if let (size, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, presentationData) = self.currentParams { if let (size, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) = self.currentParams {
self.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, presentationData: presentationData, synchronous: wasFirstHistoryView, transition: .immediate) self.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, presentationData: presentationData, synchronous: wasFirstHistoryView, transition: .immediate)
if !self.didSetReady { if !self.didSetReady {
self.didSetReady = true self.didSetReady = true
self.ready.set(.single(true)) self.ready.set(.single(true))
@ -666,8 +668,8 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
} }
} }
func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
self.currentParams = (size, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, presentationData) self.currentParams = (size, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData)
transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: size)) transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: size))
@ -704,7 +706,7 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
} }
func scrollViewDidScroll(_ scrollView: UIScrollView) { func scrollViewDidScroll(_ scrollView: UIScrollView) {
if let (size, sideInset, bottomInset, visibleHeight, _, presentationData) = self.currentParams { if let (size, sideInset, bottomInset, visibleHeight, _, _, presentationData) = self.currentParams {
self.updateVisibleItems(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, theme: presentationData.theme, strings: presentationData.strings, synchronousLoad: false) self.updateVisibleItems(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, theme: presentationData.theme, strings: presentationData.strings, synchronousLoad: false)
if scrollView.contentOffset.y >= scrollView.contentSize.height - scrollView.bounds.height * 2.0, let currentView = self.currentView, currentView.earlierId != nil { if scrollView.contentOffset.y >= scrollView.contentSize.height - scrollView.bounds.height * 2.0, let currentView = self.currentView, currentView.earlierId != nil {

View File

@ -113,7 +113,7 @@ private func peerInfoAvailableMediaPanes(context: AccountContext, peerId: PeerId
(.photoOrVideo, .media), (.photoOrVideo, .media),
(.file, .files), (.file, .files),
(.music, .music), (.music, .music),
//(.voiceOrInstantVideo, .voice), (.voiceOrInstantVideo, .voice),
(.webPage, .links) (.webPage, .links)
] ]
enum PaneState { enum PaneState {

View File

@ -12,8 +12,9 @@ import ContextUI
protocol PeerInfoPaneNode: ASDisplayNode { protocol PeerInfoPaneNode: ASDisplayNode {
var isReady: Signal<Bool, NoError> { get } var isReady: Signal<Bool, NoError> { get }
var shouldReceiveExpandProgressUpdates: Bool { get }
func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition)
func scrollToTop() -> Bool func scrollToTop() -> Bool
func transferVelocity(_ velocity: CGFloat) func transferVelocity(_ velocity: CGFloat)
func cancelPreviewGestures() func cancelPreviewGestures()
@ -28,21 +29,21 @@ final class PeerInfoPaneWrapper {
let key: PeerInfoPaneKey let key: PeerInfoPaneKey
let node: PeerInfoPaneNode let node: PeerInfoPaneNode
var isAnimatingOut: Bool = false var isAnimatingOut: Bool = false
private var appliedParams: (CGSize, CGFloat, CGFloat, CGFloat, Bool, PresentationData)? private var appliedParams: (CGSize, CGFloat, CGFloat, CGFloat, Bool, CGFloat, PresentationData)?
init(key: PeerInfoPaneKey, node: PeerInfoPaneNode) { init(key: PeerInfoPaneKey, node: PeerInfoPaneNode) {
self.key = key self.key = key
self.node = node self.node = node
} }
func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
if let (currentSize, currentSideInset, currentBottomInset, visibleHeight, currentIsScrollingLockedAtTop, currentPresentationData) = self.appliedParams { if let (currentSize, currentSideInset, currentBottomInset, visibleHeight, currentIsScrollingLockedAtTop, currentExpandProgress, currentPresentationData) = self.appliedParams {
if currentSize == size && currentSideInset == sideInset && currentBottomInset == bottomInset, currentIsScrollingLockedAtTop == isScrollingLockedAtTop && currentPresentationData === presentationData { if currentSize == size && currentSideInset == sideInset && currentBottomInset == bottomInset, currentIsScrollingLockedAtTop == isScrollingLockedAtTop && currentExpandProgress == expandProgress && currentPresentationData === presentationData {
return return
} }
} }
self.appliedParams = (size, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, presentationData) self.appliedParams = (size, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData)
self.node.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, presentationData: presentationData, synchronous: synchronous, transition: transition) self.node.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, presentationData: presentationData, synchronous: synchronous, transition: transition)
} }
} }
@ -734,14 +735,14 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat
) )
self.pendingPanes[key] = pane self.pendingPanes[key] = pane
pane.pane.node.frame = paneFrame pane.pane.node.frame = paneFrame
pane.pane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: expansionFraction < 1.0 - CGFloat.ulpOfOne, presentationData: presentationData, synchronous: true, transition: .immediate) pane.pane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: expansionFraction < 1.0 - CGFloat.ulpOfOne, expandProgress: expansionFraction, presentationData: presentationData, synchronous: true, transition: .immediate)
leftScope = true leftScope = true
} }
} }
for (key, pane) in self.pendingPanes { for (key, pane) in self.pendingPanes {
pane.pane.node.frame = paneFrame pane.pane.node.frame = paneFrame
pane.pane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: expansionFraction < 1.0 - CGFloat.ulpOfOne, presentationData: presentationData, synchronous: self.currentPaneKey == nil, transition: .immediate) pane.pane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: expansionFraction < 1.0 - CGFloat.ulpOfOne, expandProgress: expansionFraction, presentationData: presentationData, synchronous: self.currentPaneKey == nil, transition: .immediate)
if pane.isReady { if pane.isReady {
self.pendingPanes.removeValue(forKey: key) self.pendingPanes.removeValue(forKey: key)
@ -819,7 +820,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat
paneCompletion() paneCompletion()
}) })
} }
pane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: expansionFraction < 1.0 - CGFloat.ulpOfOne, presentationData: presentationData, synchronous: paneWasAdded, transition: paneTransition) pane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: expansionFraction < 1.0 - CGFloat.ulpOfOne, expandProgress: expansionFraction, presentationData: presentationData, synchronous: paneWasAdded, transition: paneTransition)
} }
} }
@ -836,7 +837,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat
case .links: case .links:
title = presentationData.strings.PeerInfo_PaneLinks title = presentationData.strings.PeerInfo_PaneLinks
case .voice: case .voice:
title = presentationData.strings.PeerInfo_PaneVoice title = presentationData.strings.PeerInfo_PaneVoiceAndVideo
case .music: case .music:
title = presentationData.strings.PeerInfo_PaneAudio title = presentationData.strings.PeerInfo_PaneAudio
case .groupsInCommon: case .groupsInCommon:
@ -850,7 +851,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat
for (_, pane) in self.pendingPanes { for (_, pane) in self.pendingPanes {
let paneTransition: ContainedViewLayoutTransition = .immediate let paneTransition: ContainedViewLayoutTransition = .immediate
paneTransition.updateFrame(node: pane.pane.node, frame: paneFrame) paneTransition.updateFrame(node: pane.pane.node, frame: paneFrame)
pane.pane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: expansionFraction < 1.0 - CGFloat.ulpOfOne, presentationData: presentationData, synchronous: true, transition: paneTransition) pane.pane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: expansionFraction < 1.0 - CGFloat.ulpOfOne, expandProgress: expansionFraction, presentationData: presentationData, synchronous: true, transition: paneTransition)
} }
if !self.didSetIsReady && data != nil { if !self.didSetIsReady && data != nil {
if let currentPaneKey = self.currentPaneKey, let currentPane = self.currentPanes[currentPaneKey] { if let currentPaneKey = self.currentPaneKey, let currentPane = self.currentPanes[currentPaneKey] {

View File

@ -1205,59 +1205,131 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
self?.openPeer(peerId: id, navigation: navigation) self?.openPeer(peerId: id, navigation: navigation)
} }
}, openPeerMention: { _ in }, openPeerMention: { _ in
}, openMessageContextMenu: { [weak self] message, _, _, _, _ in }, openMessageContextMenu: { [weak self] message, _, node, frame, anyRecognizer in
guard let strongSelf = self else { guard let strongSelf = self, let node = node as? ContextExtractedContentContainingNode else {
return return
} }
let _ = storedMessageFromSearch(account: strongSelf.context.account, message: message).start() let _ = storedMessageFromSearch(account: strongSelf.context.account, message: message).start()
var linkForCopying: String?
var currentSupernode: ASDisplayNode? = node
while true {
if currentSupernode == nil {
break
} else if let currentSupernode = currentSupernode as? ListMessageSnippetItemNode {
linkForCopying = currentSupernode.currentPrimaryUrl
break
} else {
currentSupernode = currentSupernode?.supernode
}
}
let gesture: ContextGesture? = anyRecognizer as? ContextGesture
let _ = (chatAvailableMessageActionsImpl(postbox: strongSelf.context.account.postbox, accountPeerId: strongSelf.context.account.peerId, messageIds: [message.id]) let _ = (chatAvailableMessageActionsImpl(postbox: strongSelf.context.account.postbox, accountPeerId: strongSelf.context.account.peerId, messageIds: [message.id])
|> deliverOnMainQueue).start(next: { actions in |> deliverOnMainQueue).start(next: { actions in
guard let strongSelf = self else {
return
}
var messageIds = Set<MessageId>() var items: [ContextMenuItem] = []
messageIds.insert(message.id)
if let linkForCopying = linkForCopying {
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_ContextMenuCopyLink, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) }, action: { c, _ in
c.dismiss(completion: {})
UIPasteboard.general.string = linkForCopying
})))
}
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_ContextMenuForward, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) }, action: { c, _ in
c.dismiss(completion: {
if let strongSelf = self { if let strongSelf = self {
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) strongSelf.forwardMessages(messageIds: Set([message.id]))
var items: [ActionSheetButtonItem] = [] }
})
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.SharedMedia_ViewInChat, color: .accent, action: { [weak actionSheet] in })))
actionSheet?.dismissAnimated() items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.SharedMedia_ViewInChat, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor) }, action: { c, _ in
c.dismiss(completion: {
if let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController { if let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController {
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(strongSelf.peerId), subject: .message(message.id))) strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(strongSelf.peerId), subject: .message(message.id)))
} }
})) })
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_ContextMenuForward, color: .accent, action: { [weak actionSheet] in })))
actionSheet?.dismissAnimated()
if let strongSelf = self {
strongSelf.forwardMessages(messageIds: messageIds)
}
}))
if actions.options.contains(.deleteLocally) || actions.options.contains(.deleteGlobally) { if actions.options.contains(.deleteLocally) || actions.options.contains(.deleteGlobally) {
items.append( ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_ContextMenuDelete, color: .destructive, action: { [weak actionSheet] in let context = strongSelf.context
actionSheet?.dismissAnimated() let presentationData = strongSelf.presentationData
if let strongSelf = self { let peerId = strongSelf.peerId
strongSelf.deleteMessages(messageIds: Set(messageIds)) items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_ContextMenuDelete, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { c, _ in
c.setItems(context.account.postbox.transaction { transaction -> [ContextMenuItem] in
var items: [ContextMenuItem] = []
let messageIds = [message.id]
if let peer = transaction.getPeer(message.id.peerId) {
var personalPeerName: String?
var isChannel = false
if let user = peer as? TelegramUser {
personalPeerName = user.compactDisplayTitle
} else if let channel = peer as? TelegramChannel, case .broadcast = channel.info {
isChannel = true
} }
}))
if actions.options.contains(.deleteGlobally) {
let globalTitle: String
if isChannel {
globalTitle = presentationData.strings.Conversation_DeleteMessagesForMe
} else if let personalPeerName = personalPeerName {
globalTitle = presentationData.strings.Conversation_DeleteMessagesFor(personalPeerName).0
} else {
globalTitle = presentationData.strings.Conversation_DeleteMessagesForEveryone
}
items.append(.action(ContextMenuActionItem(text: globalTitle, textColor: .destructive, icon: { _ in nil }, action: { c, f in
c.dismiss(completion: {
if let strongSelf = self {
strongSelf.headerNode.navigationButtonContainer.performAction?(.selectionDone)
let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: Array(messageIds), type: .forEveryone).start()
}
})
})))
}
if actions.options.contains(.deleteLocally) {
var localOptionText = presentationData.strings.Conversation_DeleteMessagesForMe
if context.account.peerId == peerId {
if messageIds.count == 1 {
localOptionText = presentationData.strings.Conversation_Moderate_Delete
} else {
localOptionText = presentationData.strings.Conversation_DeleteManyMessages
}
}
items.append(.action(ContextMenuActionItem(text: localOptionText, textColor: .destructive, icon: { _ in nil }, action: { c, f in
c.dismiss(completion: {
if let strongSelf = self {
strongSelf.headerNode.navigationButtonContainer.performAction?(.selectionDone)
let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: Array(messageIds), type: .forLocalPeer).start()
}
})
})))
}
}
return items
})
})))
} }
if strongSelf.searchDisplayController == nil { if strongSelf.searchDisplayController == nil {
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_ContextMenuMore, color: .accent, action: { [weak actionSheet] in items.append(.separator)
actionSheet?.dismissAnimated()
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_ContextMenuMore, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/More"), color: theme.contextMenu.primaryColor) }, action: { c, _ in
c.dismiss(completion: {
if let strongSelf = self { if let strongSelf = self {
strongSelf.chatInterfaceInteraction.toggleMessagesSelection([message.id], true) strongSelf.chatInterfaceInteraction.toggleMessagesSelection([message.id], true)
strongSelf.expandTabs() strongSelf.expandTabs()
} }
}))
}
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
}) })
])]) })))
strongSelf.view.endEditing(true)
strongSelf.controller?.present(actionSheet, in: .window(.root))
} }
let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(MessageContextExtractedContentSource(sourceNode: node)), items: .single(items), reactionItems: [], recognizer: nil, gesture: gesture)
strongSelf.controller?.window?.presentInGlobalOverlay(controller)
}) })
}, openMessageContextActions: { [weak self] message, node, rect, gesture in }, openMessageContextActions: { [weak self] message, node, rect, gesture in
guard let strongSelf = self else { guard let strongSelf = self else {
@ -1446,9 +1518,10 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
}, updateInputState: { _ in }, updateInputState: { _ in
}, updateInputMode: { _ in }, updateInputMode: { _ in
}, openMessageShareMenu: { _ in }, openMessageShareMenu: { _ in
}, presentController: { _, _ in }, presentController: { [weak self] c, a in
}, navigationController: { self?.controller?.present(c, in: .window(.root), with: a)
return nil }, navigationController: { [weak self] in
return self?.controller?.navigationController as? NavigationController
}, chatControllerNode: { }, chatControllerNode: {
return nil return nil
}, reactionContainerNode: { }, reactionContainerNode: {
@ -4537,3 +4610,22 @@ private final class ContextControllerContentSourceImpl: ContextControllerContent
self.controller.didAppearInContextPreview() self.controller.didAppearInContextPreview()
} }
} }
private final class MessageContextExtractedContentSource: ContextExtractedContentSource {
let keepInPlace: Bool = false
let ignoreContentTouches: Bool = true
private let sourceNode: ContextExtractedContentContainingNode
init(sourceNode: ContextExtractedContentContainingNode) {
self.sourceNode = sourceNode
}
func takeView() -> ContextControllerTakeViewInfo? {
return ContextControllerTakeViewInfo(contentContainingNode: self.sourceNode, contentAreaInScreenSpace: UIScreen.main.bounds)
}
func putBack() -> ContextControllerPutBackViewInfo? {
return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds)
}
}