mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Peer media redesign
This commit is contained in:
parent
890db9606c
commit
84848c6e2e
@ -5338,7 +5338,7 @@ Any member of this group will be able to see messages in the channel.";
|
||||
"PeerInfo.PaneMedia" = "Media";
|
||||
"PeerInfo.PaneFiles" = "Files";
|
||||
"PeerInfo.PaneLinks" = "Links";
|
||||
"PeerInfo.PaneVoice" = "Voice Messages";
|
||||
"PeerInfo.PaneVoiceAndVideo" = "Voice";
|
||||
"PeerInfo.PaneAudio" = "Audio";
|
||||
"PeerInfo.PaneGroups" = "Groups";
|
||||
"PeerInfo.PaneMembers" = "Members";
|
||||
|
@ -228,6 +228,8 @@ public final class ContextGesture: UIGestureRecognizer, UIGestureRecognizerDeleg
|
||||
self.delayTimer?.invalidate()
|
||||
self.animator?.invalidate()
|
||||
self.state = .failed
|
||||
} else {
|
||||
self.state = .failed
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -131,6 +131,19 @@ public enum GeneralScrollDirection {
|
||||
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 {
|
||||
public final let scroller: ListViewScroller
|
||||
private final var visibleSize: CGSize = CGSize()
|
||||
@ -666,6 +679,10 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
||||
self.scrolledToItem = nil
|
||||
|
||||
self.beganInteractiveDragging()
|
||||
|
||||
for itemNode in self.itemNodes {
|
||||
cancelContextGestures(view: itemNode.view)
|
||||
}
|
||||
}
|
||||
|
||||
public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
||||
@ -739,8 +756,10 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
||||
self.decelerationAnimator?.isPaused = false
|
||||
}
|
||||
|
||||
public var defaultToSynchronousTransactionWhileScrolling: Bool = false
|
||||
|
||||
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
self.updateScrollViewDidScroll(scrollView, synchronous: false)
|
||||
self.updateScrollViewDidScroll(scrollView, synchronous: self.defaultToSynchronousTransactionWhileScrolling)
|
||||
}
|
||||
|
||||
private var generalAccumulatedDeltaY: CGFloat = 0.0
|
||||
@ -3606,8 +3625,12 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
||||
var updatedState = state
|
||||
var updatedOperations = operations
|
||||
updatedState.removeInvisibleNodes(&updatedOperations)
|
||||
self.dispatchOnVSync {
|
||||
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.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -264,7 +264,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode {
|
||||
let editableControlLayout = ItemListEditableControlNode.asyncLayout(self.editableControlNode)
|
||||
let reorderControlLayout = ItemListEditableReorderControlNode.asyncLayout(self.reorderControlNode)
|
||||
|
||||
var previousThumbnailItem = self.currentThumbnailItem
|
||||
let previousThumbnailItem = self.currentThumbnailItem
|
||||
var currentDisabledOverlayNode = self.disabledOverlayNode
|
||||
|
||||
let currentItem = self.layoutParams?.0
|
||||
|
@ -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> {
|
||||
return internalMediaGridMessageVideo(postbox: postbox, videoReference: videoReference, onlyFullSize: onlyFullSize, synchronousLoad: synchronousLoad, autoFetchFullSizeThumbnail: autoFetchFullSizeThumbnail)
|
||||
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, overlayColor: overlayColor)
|
||||
|> map {
|
||||
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>
|
||||
if let imageReference = imageReference {
|
||||
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)
|
||||
|
||||
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)
|
||||
if let fileReference = fileReference {
|
||||
let size = thumbnail ? CGSize(width: 48.0, height: 48.0) : CGSize(width: 320.0, height: 320.0)
|
||||
@ -2471,10 +2479,22 @@ public func playerAlbumArt(postbox: Postbox, fileReference: FileMediaReference?,
|
||||
let imageSize = sourceImage.size.aspectFilled(arguments.drawingRect.size)
|
||||
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))
|
||||
if let overlayColor = overlayColor {
|
||||
c.setFillColor(overlayColor.cgColor)
|
||||
c.fill(arguments.drawingRect)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
context.withFlippedContext { c in
|
||||
drawAlbumArtPlaceholder(into: c, arguments: arguments, thumbnail: thumbnail)
|
||||
if let emptyColor = emptyColor {
|
||||
context.withFlippedContext { c in
|
||||
let rect = arguments.drawingRect
|
||||
c.setFillColor(emptyColor.cgColor)
|
||||
c.fill(rect)
|
||||
}
|
||||
} else {
|
||||
context.withFlippedContext { c in
|
||||
drawAlbumArtPlaceholder(into: c, arguments: arguments, thumbnail: thumbnail)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -440,18 +440,23 @@ private final class SemanticStatusNodeTransitionContext {
|
||||
public final class SemanticStatusNode: ASControlNode {
|
||||
public var backgroundNodeColor: UIColor {
|
||||
didSet {
|
||||
self.setNeedsDisplay()
|
||||
if !self.backgroundNodeColor.isEqual(oldValue) {
|
||||
self.setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var foregroundNodeColor: UIColor {
|
||||
didSet {
|
||||
self.setNeedsDisplay()
|
||||
if !self.foregroundNodeColor.isEqual(oldValue) {
|
||||
self.setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var animator: ConstantDisplayLinkAnimator?
|
||||
|
||||
private var hasState: Bool = false
|
||||
public private(set) var state: SemanticStatusNodeState
|
||||
private var transtionContext: SemanticStatusNodeTransitionContext?
|
||||
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 = {}) {
|
||||
var animated = animated
|
||||
if !self.hasState {
|
||||
self.hasState = true
|
||||
animated = false
|
||||
}
|
||||
if self.state != state {
|
||||
let fromState = self.state
|
||||
self.state = state
|
||||
let previousStateContext = self.stateContext
|
||||
self.stateContext = self.state.context(current: self.stateContext)
|
||||
|
@ -7,9 +7,9 @@ import SyncCore
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
|
||||
final class MediaNavigationAccessoryContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
|
||||
let backgroundNode: ASDisplayNode
|
||||
let headerNode: MediaNavigationAccessoryHeaderNode
|
||||
public final class MediaNavigationAccessoryContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
|
||||
public let backgroundNode: ASDisplayNode
|
||||
public let headerNode: MediaNavigationAccessoryHeaderNode
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
return nil
|
||||
}
|
||||
|
@ -127,8 +127,8 @@ private func generateMaskImage(color: UIColor) -> UIImage? {
|
||||
})
|
||||
}
|
||||
|
||||
final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
static let minimizedHeight: CGFloat = 37.0
|
||||
public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
public static let minimizedHeight: CGFloat = 37.0
|
||||
|
||||
private var theme: PresentationTheme
|
||||
private var strings: PresentationStrings
|
||||
@ -156,7 +156,7 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollViewDeleg
|
||||
|
||||
private var validLayout: (CGSize, CGFloat, CGFloat)?
|
||||
|
||||
var displayScrubber: Bool = true {
|
||||
public var displayScrubber: Bool = true {
|
||||
didSet {
|
||||
self.scrubbingNode.isHidden = !self.displayScrubber
|
||||
}
|
||||
@ -166,14 +166,14 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollViewDeleg
|
||||
|
||||
private var tapRecognizer: UITapGestureRecognizer?
|
||||
|
||||
var tapAction: (() -> Void)?
|
||||
var close: (() -> Void)?
|
||||
var toggleRate: (() -> Void)?
|
||||
var togglePlayPause: (() -> Void)?
|
||||
var playPrevious: (() -> Void)?
|
||||
var playNext: (() -> Void)?
|
||||
public var tapAction: (() -> Void)?
|
||||
public var close: (() -> Void)?
|
||||
public var toggleRate: (() -> Void)?
|
||||
public var togglePlayPause: (() -> Void)?
|
||||
public var playPrevious: (() -> Void)?
|
||||
public var playNext: (() -> Void)?
|
||||
|
||||
var playbackBaseRate: AudioPlaybackRate? = nil {
|
||||
public var playbackBaseRate: AudioPlaybackRate? = nil {
|
||||
didSet {
|
||||
guard self.playbackBaseRate != oldValue, let playbackBaseRate = self.playbackBaseRate else {
|
||||
return
|
||||
@ -193,13 +193,13 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollViewDeleg
|
||||
}
|
||||
}
|
||||
|
||||
var playbackStatus: Signal<MediaPlayerStatus, NoError>? {
|
||||
public var playbackStatus: Signal<MediaPlayerStatus, NoError>? {
|
||||
didSet {
|
||||
self.scrubbingNode.status = self.playbackStatus
|
||||
}
|
||||
}
|
||||
|
||||
var playbackItems: (SharedMediaPlaylistItem?, SharedMediaPlaylistItem?, SharedMediaPlaylistItem?)? {
|
||||
public var playbackItems: (SharedMediaPlaylistItem?, SharedMediaPlaylistItem?, SharedMediaPlaylistItem?)? {
|
||||
didSet {
|
||||
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)
|
||||
@ -207,7 +207,7 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollViewDeleg
|
||||
}
|
||||
}
|
||||
|
||||
init(presentationData: PresentationData) {
|
||||
public init(presentationData: PresentationData) {
|
||||
self.theme = presentationData.theme
|
||||
self.strings = presentationData.strings
|
||||
self.dateTimeFormat = presentationData.dateTimeFormat
|
||||
@ -346,7 +346,7 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollViewDeleg
|
||||
}
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
override public func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.view.disablesInteractiveTransitionGestureRecognizer = true
|
||||
@ -361,7 +361,7 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollViewDeleg
|
||||
self.view.addGestureRecognizer(tapRecognizer)
|
||||
}
|
||||
|
||||
func updatePresentationData(_ presentationData: PresentationData) {
|
||||
public func updatePresentationData(_ presentationData: PresentationData) {
|
||||
self.theme = presentationData.theme
|
||||
self.strings = presentationData.strings
|
||||
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 {
|
||||
self.changeTrack()
|
||||
}
|
||||
}
|
||||
|
||||
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||||
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||||
self.changeTrack()
|
||||
}
|
||||
|
||||
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
||||
public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
||||
guard !decelerate else {
|
||||
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)
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
@objc func closeButtonPressed() {
|
||||
@objc public func closeButtonPressed() {
|
||||
self.close?()
|
||||
}
|
||||
|
||||
@objc func rateButtonPressed() {
|
||||
@objc public func rateButtonPressed() {
|
||||
self.toggleRate?()
|
||||
}
|
||||
|
||||
@objc func actionButtonPressed() {
|
||||
@objc public func actionButtonPressed() {
|
||||
self.togglePlayPause?()
|
||||
}
|
||||
|
||||
@objc func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
@objc public func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
self.tapAction?()
|
||||
}
|
||||
|
@ -6,17 +6,17 @@ import TelegramCore
|
||||
import SyncCore
|
||||
import AccountContext
|
||||
|
||||
final class MediaNavigationAccessoryPanel: ASDisplayNode {
|
||||
let containerNode: MediaNavigationAccessoryContainerNode
|
||||
public final class MediaNavigationAccessoryPanel: ASDisplayNode {
|
||||
public let containerNode: MediaNavigationAccessoryContainerNode
|
||||
|
||||
var close: (() -> Void)?
|
||||
var toggleRate: (() -> Void)?
|
||||
var togglePlayPause: (() -> Void)?
|
||||
var tapAction: (() -> Void)?
|
||||
var playPrevious: (() -> Void)?
|
||||
var playNext: (() -> Void)?
|
||||
public var close: (() -> Void)?
|
||||
public var toggleRate: (() -> Void)?
|
||||
public var togglePlayPause: (() -> Void)?
|
||||
public var tapAction: (() -> Void)?
|
||||
public var playPrevious: (() -> Void)?
|
||||
public var playNext: (() -> Void)?
|
||||
|
||||
init(context: AccountContext) {
|
||||
public init(context: AccountContext) {
|
||||
self.containerNode = MediaNavigationAccessoryContainerNode(context: context)
|
||||
|
||||
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))
|
||||
self.containerNode.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, transition: transition)
|
||||
}
|
||||
|
||||
func animateIn(transition: ContainedViewLayoutTransition) {
|
||||
public func animateIn(transition: ContainedViewLayoutTransition) {
|
||||
self.clipsToBounds = true
|
||||
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
|
||||
@ -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
|
||||
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
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -72,7 +72,7 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder {
|
||||
public var tempVoicePlaylistEnded: (() -> Void)?
|
||||
public var tempVoicePlaylistItemChanged: ((SharedMediaPlaylistItem?, SharedMediaPlaylistItem?) -> Void)?
|
||||
|
||||
private var mediaAccessoryPanel: (MediaNavigationAccessoryPanel, MediaManagerPlayerType)?
|
||||
public var mediaAccessoryPanel: (MediaNavigationAccessoryPanel, MediaManagerPlayerType)?
|
||||
|
||||
private var locationBroadcastMode: LocationBroadcastNavigationAccessoryPanelMode?
|
||||
private var locationBroadcastPeers: [Peer]?
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -577,19 +577,47 @@ public struct PresentationResourcesChat {
|
||||
|
||||
public static func sharedMediaFileDownloadStartIcon(_ theme: PresentationTheme) -> UIImage? {
|
||||
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? {
|
||||
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.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.fill(CGRect(x: 2.0 + 2.0 + 2.0, y: 0.0, width: 2.0, height: 11.0 - 1.0))
|
||||
context.translateBy(x: 2.0, y: 2.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()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
Binary file not shown.
@ -19,6 +19,10 @@ private func generatePlayIcon(_ theme: PresentationTheme) -> UIImage? {
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/MinimizedPlay"), color: theme.chat.inputPanel.actionControlForegroundColor)
|
||||
}
|
||||
|
||||
extension AudioWaveformNode: CustomMediaPlayerScrubbingForegroundNode {
|
||||
|
||||
}
|
||||
|
||||
final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode {
|
||||
private let deleteButton: HighlightableButtonNode
|
||||
let sendButton: HighlightTrackingButtonNode
|
||||
|
@ -6,6 +6,7 @@ import AsyncDisplayKit
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import TelegramStringFormatting
|
||||
import ListSectionHeaderNode
|
||||
|
||||
private let timezoneOffset: Int32 = {
|
||||
let nowTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
|
||||
@ -16,7 +17,7 @@ private let timezoneOffset: Int32 = {
|
||||
}()
|
||||
|
||||
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 timeinfo: tm = tm()
|
||||
@ -65,7 +66,7 @@ final class ListMessageDateHeader: ListViewItemHeader {
|
||||
|
||||
let stickDirection: ListViewItemHeaderStickDirection = .top
|
||||
|
||||
let height: CGFloat = 36.0
|
||||
let height: CGFloat = 28.0
|
||||
|
||||
func node() -> ListViewItemHeaderNode {
|
||||
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 {
|
||||
var theme: PresentationTheme
|
||||
var strings: PresentationStrings
|
||||
var fontSize: PresentationFontSize
|
||||
let titleNode: ASTextNode
|
||||
let backgroundNode: ASDisplayNode
|
||||
let headerNode: ListSectionHeaderNode
|
||||
|
||||
let month: Int32
|
||||
let year: Int32
|
||||
|
||||
init(theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize, roundedTimestamp: Int32, month: Int32, year: Int32) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.fontSize = fontSize
|
||||
self.month = month
|
||||
self.year = year
|
||||
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
self.backgroundNode.backgroundColor = theme.list.plainBackgroundColor.withAlphaComponent(0.9)
|
||||
|
||||
self.titleNode = ASTextNode()
|
||||
self.titleNode.isUserInteractionEnabled = false
|
||||
self.headerNode = ListSectionHeaderNode(theme: theme)
|
||||
|
||||
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.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
|
||||
self.headerNode.title = stringForMonth(strings: strings, month: month, ofYear: year).uppercased()
|
||||
}
|
||||
|
||||
func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
|
||||
self.theme = theme
|
||||
if let attributedString = self.titleNode.attributedText?.mutableCopy() as? NSMutableAttributedString {
|
||||
attributedString.addAttribute(NSAttributedString.Key.foregroundColor, value: theme.list.itemPrimaryTextColor, range: NSMakeRange(0, attributedString.length))
|
||||
self.titleNode.attributedText = attributedString
|
||||
}
|
||||
self.headerNode.updateTheme(theme: theme)
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
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))
|
||||
self.titleNode.frame = CGRect(origin: CGPoint(x: leftInset + 12.0, y: floor((size.height - titleSize.height) / 2.0)), size: titleSize)
|
||||
self.backgroundNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
let headerFrame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: size.width, height: size.height + UIScreenPixel))
|
||||
self.headerNode.frame = headerFrame
|
||||
self.headerNode.updateLayout(size: headerFrame.size, leftInset: leftInset, rightInset: rightInset)
|
||||
}
|
||||
}
|
||||
|
@ -13,9 +13,11 @@ import AccountContext
|
||||
import TelegramStringFormatting
|
||||
import AccountContext
|
||||
import RadialStatusNode
|
||||
import SemanticStatusNode
|
||||
import PhotoResources
|
||||
import MusicAlbumArtResources
|
||||
import UniversalMediaPlayer
|
||||
import ContextUI
|
||||
|
||||
private let extensionImageCache = Atomic<[UInt32: UIImage]>(value: [:])
|
||||
|
||||
@ -41,50 +43,19 @@ private let extensionColorsMap: [String: (UInt32, UInt32)] = [
|
||||
]
|
||||
|
||||
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.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)
|
||||
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.move(to: CGPoint(x: 0.0, y: radius))
|
||||
if !radius.isZero {
|
||||
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))
|
||||
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 ")
|
||||
context.clip()
|
||||
|
||||
if !radius.isZero {
|
||||
context.addArc(tangent1End: CGPoint(x: 0.0, y: size.height), tangent2End: CGPoint(x: 0.0, y: size.height - radius), radius: radius)
|
||||
}
|
||||
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()
|
||||
context.setFillColor(UIColor(rgb: colors.0).withMultipliedBrightnessBy(0.85).cgColor)
|
||||
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 ")
|
||||
})
|
||||
}
|
||||
|
||||
@ -115,7 +86,7 @@ private func extensionImage(fileExtension: String?) -> UIImage? {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
private let extensionFont = Font.medium(13.0)
|
||||
private let extensionFont = Font.with(size: 15.0, design: .round, traits: [.bold])
|
||||
|
||||
private struct FetchControls {
|
||||
let fetch: () -> Void
|
||||
@ -151,11 +122,16 @@ private enum FileIconImage: Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
extension AudioWaveformNode: CustomMediaPlayerScrubbingForegroundNode {
|
||||
|
||||
}
|
||||
|
||||
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 separatorNode: ASDisplayNode
|
||||
|
||||
@ -168,12 +144,7 @@ final class ListMessageFileItemNode: ListMessageNode {
|
||||
private let extensionIconNode: ASImageNode
|
||||
private let extensionIconText: TextNode
|
||||
private let iconImageNode: TransformImageNode
|
||||
private let statusButtonNode: HighlightTrackingButtonNode
|
||||
private let statusNode: RadialStatusNode
|
||||
|
||||
private var waveformNode: AudioWaveformNode?
|
||||
private var waveformForegroundNode: AudioWaveformNode?
|
||||
private var waveformScrubbingNode: MediaPlayerScrubbingNode?
|
||||
private let iconStatusNode: SemanticStatusNode
|
||||
|
||||
private var currentIconImage: FileIconImage?
|
||||
private var currentMedia: Media?
|
||||
@ -187,10 +158,7 @@ final class ListMessageFileItemNode: ListMessageNode {
|
||||
private let playbackStatus = Promise<MediaPlayerStatus>()
|
||||
|
||||
private var downloadStatusIconNode: ASImageNode
|
||||
private var linearProgressNode: ASDisplayNode
|
||||
|
||||
private let progressNode: RadialProgressNode
|
||||
private var playbackOverlayNode: ListMessagePlaybackOverlayNode?
|
||||
private var linearProgressNode: LinearProgressNode?
|
||||
|
||||
private var context: AccountContext?
|
||||
private (set) var message: Message?
|
||||
@ -200,15 +168,20 @@ final class ListMessageFileItemNode: ListMessageNode {
|
||||
private var contentSizeValue: CGSize?
|
||||
private var currentLeftOffset: CGFloat = 0.0
|
||||
|
||||
override var canBeLongTapped: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
public required init() {
|
||||
self.contextSourceNode = ContextExtractedContentContainingNode()
|
||||
self.containerNode = ContextControllerSourceNode()
|
||||
|
||||
self.separatorNode = ASDisplayNode()
|
||||
self.separatorNode.displaysAsynchronously = false
|
||||
self.separatorNode.isLayerBacked = true
|
||||
|
||||
self.extractedBackgroundImageNode = ASImageNode()
|
||||
self.extractedBackgroundImageNode.displaysAsynchronously = false
|
||||
self.extractedBackgroundImageNode.alpha = 0.0
|
||||
|
||||
self.offsetContainerNode = ASDisplayNode()
|
||||
|
||||
self.highlightedBackgroundNode = ASDisplayNode()
|
||||
self.highlightedBackgroundNode.isLayerBacked = true
|
||||
|
||||
@ -234,45 +207,60 @@ final class ListMessageFileItemNode: ListMessageNode {
|
||||
self.iconImageNode.displaysAsynchronously = false
|
||||
self.iconImageNode.contentAnimations = .subsequentUpdates
|
||||
|
||||
self.statusButtonNode = HighlightTrackingButtonNode()
|
||||
self.statusNode = RadialStatusNode(backgroundNodeColor: .clear)
|
||||
self.statusNode.isUserInteractionEnabled = false
|
||||
self.iconStatusNode = SemanticStatusNode(backgroundNodeColor: .clear, foregroundNodeColor: .white)
|
||||
self.iconStatusNode.isUserInteractionEnabled = false
|
||||
|
||||
self.downloadStatusIconNode = ASImageNode()
|
||||
self.downloadStatusIconNode.isLayerBacked = true
|
||||
self.downloadStatusIconNode.displaysAsynchronously = false
|
||||
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()
|
||||
|
||||
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
|
||||
if let strongSelf = self {
|
||||
if highlighted {
|
||||
strongSelf.statusNode.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.statusNode.alpha = 0.4
|
||||
} else {
|
||||
strongSelf.statusNode.alpha = 1.0
|
||||
strongSelf.statusNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||
}
|
||||
self.containerNode.addSubnode(self.contextSourceNode)
|
||||
self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode
|
||||
self.addSubnode(self.containerNode)
|
||||
|
||||
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.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 {
|
||||
@ -331,9 +319,9 @@ final class ListMessageFileItemNode: ListMessageNode {
|
||||
updatedTheme = item.theme
|
||||
}
|
||||
|
||||
let titleFont = Font.medium(floor(item.fontSize.baseDisplaySize * 16.0 / 17.0))
|
||||
let audioTitleFont = Font.regular(floor(item.fontSize.baseDisplaySize * 16.0 / 17.0))
|
||||
let descriptionFont = Font.regular(floor(item.fontSize.baseDisplaySize * 13.0 / 17.0))
|
||||
let titleFont = Font.semibold(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 * 14.0 / 17.0))
|
||||
|
||||
var leftInset: CGFloat = 65.0 + params.leftInset
|
||||
let rightInset: CGFloat = 8.0 + params.rightInset
|
||||
@ -356,7 +344,6 @@ final class ListMessageFileItemNode: ListMessageNode {
|
||||
var updatedStatusSignal: Signal<FileMediaResourceStatus, NoError>?
|
||||
var updatedPlaybackStatusSignal: Signal<MediaPlayerStatus, NoError>?
|
||||
var updatedFetchControls: FetchControls?
|
||||
var waveform: AudioWaveform?
|
||||
|
||||
var isAudio = false
|
||||
var isVoice = false
|
||||
@ -372,7 +359,7 @@ final class ListMessageFileItemNode: ListMessageNode {
|
||||
isInstantVideo = file.isInstantVideo
|
||||
|
||||
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
|
||||
isVoice = voice
|
||||
|
||||
@ -380,7 +367,7 @@ final class ListMessageFileItemNode: ListMessageNode {
|
||||
|
||||
let descriptionString: String
|
||||
if let performer = performer {
|
||||
descriptionString = performer
|
||||
descriptionString = "\(stringForDuration(Int32(duration))) • \(performer)"
|
||||
} else if let size = file.size {
|
||||
descriptionString = dataSizeString(size, decimalSeparator: item.dateTimeFormat.decimalSeparator)
|
||||
} else {
|
||||
@ -394,16 +381,39 @@ final class ListMessageFileItemNode: ListMessageNode {
|
||||
} else {
|
||||
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)
|
||||
waveformValue?.withDataNoCopy { data in
|
||||
waveform = AudioWaveform(bitstream: data, bitsPerSample: 5)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if isInstantVideo {
|
||||
titleText = NSAttributedString(string: item.strings.Message_VideoMessage, 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)
|
||||
if isInstantVideo || isVoice {
|
||||
let authorName: String
|
||||
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)
|
||||
} else if !isAudio {
|
||||
let fileName: String = file.fileName ?? ""
|
||||
@ -437,10 +447,7 @@ final class ListMessageFileItemNode: ListMessageNode {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if isAudio && !isVoice {
|
||||
leftInset += 14.0
|
||||
}
|
||||
|
||||
|
||||
var mediaUpdated = false
|
||||
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()))
|
||||
|
||||
@ -502,18 +509,18 @@ final class ListMessageFileItemNode: ListMessageNode {
|
||||
if let iconImage = iconImage {
|
||||
switch iconImage {
|
||||
case let .imageRepresentation(_, representation):
|
||||
let iconSize = CGSize(width: 42.0, height: 42.0)
|
||||
let imageCorners = ImageCorners(topLeft: .Corner(4.0), topRight: .Corner(4.0), bottomLeft: .Corner(4.0), bottomRight: .Corner(4.0))
|
||||
let iconSize = CGSize(width: 40.0, height: 40.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)
|
||||
iconImageApply = iconImageLayout(arguments)
|
||||
case .albumArt:
|
||||
let iconSize = CGSize(width: 46.0, height: 46.0)
|
||||
let imageCorners = ImageCorners(topLeft: .Corner(4.0), topRight: .Corner(4.0), bottomLeft: .Corner(4.0), bottomRight: .Corner(4.0))
|
||||
let iconSize = CGSize(width: 40.0, height: 40.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)
|
||||
iconImageApply = iconImageLayout(arguments)
|
||||
case let .roundVideo(file):
|
||||
let iconSize = CGSize(width: 42.0, height: 42.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 iconSize = CGSize(width: 40.0, height: 40.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)
|
||||
iconImageApply = iconImageLayout(arguments)
|
||||
}
|
||||
@ -525,9 +532,9 @@ final class ListMessageFileItemNode: ListMessageNode {
|
||||
case let .imageRepresentation(file, representation):
|
||||
updateIconImageSignal = chatWebpageSnippetFile(account: item.context.account, fileReference: .message(message: MessageReference(message), media: file), representation: representation)
|
||||
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):
|
||||
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 {
|
||||
updateIconImageSignal = .complete()
|
||||
@ -550,6 +557,23 @@ final class ListMessageFileItemNode: ListMessageNode {
|
||||
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.message = message
|
||||
strongSelf.context = item.context
|
||||
@ -561,9 +585,7 @@ final class ListMessageFileItemNode: ListMessageNode {
|
||||
if let _ = updatedTheme {
|
||||
strongSelf.separatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor
|
||||
strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor
|
||||
|
||||
strongSelf.progressNode.updateTheme(RadialProgressTheme(backgroundColor: item.theme.list.itemAccentColor, foregroundColor: item.theme.list.plainBackgroundColor, icon: nil))
|
||||
strongSelf.linearProgressNode.backgroundColor = item.theme.list.itemAccentColor
|
||||
strongSelf.linearProgressNode?.updateTheme(theme: item.theme)
|
||||
}
|
||||
|
||||
if let (selectionWidth, selectionApply) = selectionNodeWidthAndApply {
|
||||
@ -572,7 +594,7 @@ final class ListMessageFileItemNode: ListMessageNode {
|
||||
if selectionNode !== strongSelf.selectionNode {
|
||||
strongSelf.selectionNode?.removeFromSupernode()
|
||||
strongSelf.selectionNode = selectionNode
|
||||
strongSelf.addSubnode(selectionNode)
|
||||
strongSelf.contextSourceNode.contentNode.addSubnode(selectionNode)
|
||||
selectionNode.frame = selectionFrame
|
||||
transition.animatePosition(node: selectionNode, from: CGPoint(x: -selectionFrame.size.width / 2.0, y: selectionFrame.midY))
|
||||
} 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)))
|
||||
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()
|
||||
|
||||
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 iconFrame: CGRect
|
||||
if isAudio {
|
||||
let iconSize = CGSize(width: 48.0, height: 48.0)
|
||||
iconFrame = CGRect(origin: CGPoint(x: params.leftInset + leftOffset + 12.0, y: 5.0), size: iconSize)
|
||||
let iconSize = CGSize(width: 40.0, height: 40.0)
|
||||
iconFrame = CGRect(origin: CGPoint(x: params.leftInset + leftOffset + 12.0, y: 8.0), size: iconSize)
|
||||
} 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)
|
||||
}
|
||||
transition.updateFrame(node: strongSelf.extensionIconNode, frame: iconFrame)
|
||||
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()
|
||||
|
||||
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 updateImageSignal = updateIconImageSignal {
|
||||
strongSelf.iconImageNode.setSignal(updateImageSignal)
|
||||
@ -675,7 +657,7 @@ final class ListMessageFileItemNode: ListMessageNode {
|
||||
|
||||
transition.updateFrame(node: strongSelf.iconImageNode, frame: iconFrame)
|
||||
if strongSelf.iconImageNode.supernode == nil {
|
||||
strongSelf.addSubnode(strongSelf.iconImageNode)
|
||||
strongSelf.offsetContainerNode.insertSubnode(strongSelf.iconImageNode, belowSubnode: strongSelf.iconStatusNode)
|
||||
}
|
||||
|
||||
iconImageApply()
|
||||
@ -690,22 +672,13 @@ final class ListMessageFileItemNode: ListMessageNode {
|
||||
strongSelf.iconImageNode.removeFromSupernode()
|
||||
|
||||
if strongSelf.extensionIconNode.supernode == nil {
|
||||
strongSelf.addSubnode(strongSelf.extensionIconNode)
|
||||
strongSelf.offsetContainerNode.insertSubnode(strongSelf.extensionIconNode, belowSubnode: strongSelf.iconStatusNode)
|
||||
}
|
||||
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 {
|
||||
strongSelf.statusDisposable.set((updatedStatusSignal
|
||||
|> 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)))
|
||||
|
||||
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)))
|
||||
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)))
|
||||
|
||||
if let updatedFetchControls = updatedFetchControls {
|
||||
let _ = strongSelf.fetchControls.swap(updatedFetchControls)
|
||||
@ -757,74 +727,44 @@ final class ListMessageFileItemNode: ListMessageNode {
|
||||
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 {
|
||||
self.updateProgressFrame(size: contentSize, leftInset: layoutParams.leftInset, rightInset: layoutParams.rightInset, transition: .immediate)
|
||||
} 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 {
|
||||
case let .fetchStatus(fetchStatus):
|
||||
switch fetchStatus {
|
||||
case let .Fetching(_, progress):
|
||||
let adjustedProgress = max(progress, 0.027)
|
||||
self.progressNode.state = .Fetching(progress: adjustedProgress)
|
||||
case .Fetching:
|
||||
break
|
||||
case .Local:
|
||||
if isAudio {
|
||||
self.progressNode.state = .Play
|
||||
} else {
|
||||
self.progressNode.state = .Icon
|
||||
if isAudio || isInstantVideo {
|
||||
iconStatusState = .play
|
||||
}
|
||||
case .Remote:
|
||||
if isAudio {
|
||||
self.progressNode.state = .Play
|
||||
} else {
|
||||
self.progressNode.state = .Remote
|
||||
if isAudio || isInstantVideo {
|
||||
iconStatusState = .play
|
||||
}
|
||||
}
|
||||
case let .playbackStatus(playbackStatus):
|
||||
enableScrubbing = true
|
||||
switch playbackStatus {
|
||||
case .playing:
|
||||
musicIsPlaying = true
|
||||
self.progressNode.state = .Pause
|
||||
case .paused:
|
||||
musicIsPlaying = false
|
||||
self.progressNode.state = .Play
|
||||
case .playing:
|
||||
iconStatusState = .pause
|
||||
case .paused:
|
||||
iconStatusState = .play
|
||||
}
|
||||
}
|
||||
}
|
||||
self.waveformScrubbingNode?.enableScrubbing = enableScrubbing
|
||||
if let musicIsPlaying = musicIsPlaying, !isVoice, !isInstantVideo {
|
||||
if self.playbackOverlayNode == nil {
|
||||
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()
|
||||
}
|
||||
self.iconStatusNode.backgroundNodeColor = iconStatusBackgroundColor
|
||||
self.iconStatusNode.foregroundNodeColor = iconStatusForegroundColor
|
||||
self.iconStatusNode.transitionToState(iconStatusState)
|
||||
}
|
||||
|
||||
override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
|
||||
@ -903,35 +843,54 @@ final class ListMessageFileItemNode: ListMessageNode {
|
||||
|
||||
switch maybeFetchStatus {
|
||||
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)
|
||||
if self.linearProgressNode.supernode == nil {
|
||||
self.addSubnode(self.linearProgressNode)
|
||||
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)
|
||||
let linearProgressNode: 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 {
|
||||
self.addSubnode(self.downloadStatusIconNode)
|
||||
self.offsetContainerNode.addSubnode(self.downloadStatusIconNode)
|
||||
}
|
||||
self.downloadStatusIconNode.image = PresentationResourcesChat.sharedMediaFileDownloadPauseIcon(item.theme)
|
||||
case .Local:
|
||||
if self.linearProgressNode.supernode != nil {
|
||||
self.linearProgressNode.removeFromSupernode()
|
||||
if let linearProgressNode = self.linearProgressNode {
|
||||
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 {
|
||||
self.downloadStatusIconNode.removeFromSupernode()
|
||||
}
|
||||
self.downloadStatusIconNode.image = nil
|
||||
case .Remote:
|
||||
if self.linearProgressNode.supernode != nil {
|
||||
self.linearProgressNode.removeFromSupernode()
|
||||
if let linearProgressNode = self.linearProgressNode {
|
||||
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 {
|
||||
self.addSubnode(self.downloadStatusIconNode)
|
||||
self.offsetContainerNode.addSubnode(self.downloadStatusIconNode)
|
||||
}
|
||||
self.downloadStatusIconNode.image = PresentationResourcesChat.sharedMediaFileDownloadStartIcon(item.theme)
|
||||
}
|
||||
} else {
|
||||
if self.linearProgressNode.supernode != nil {
|
||||
self.linearProgressNode.removeFromSupernode()
|
||||
if let linearProgressNode = self.linearProgressNode {
|
||||
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 {
|
||||
self.downloadStatusIconNode.removeFromSupernode()
|
||||
@ -1002,12 +961,6 @@ final class ListMessageFileItemNode: ListMessageNode {
|
||||
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() {
|
||||
guard let _ = self.item, let fetchStatus = self.fetchStatus else {
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
@ -14,11 +14,19 @@ import PhotoResources
|
||||
import WebsiteType
|
||||
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 {
|
||||
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 separatorNode: ASDisplayNode
|
||||
|
||||
@ -36,20 +44,25 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
||||
|
||||
private var currentIconImageRepresentation: TelegramMediaImageRepresentation?
|
||||
private var currentMedia: Media?
|
||||
private var currentPrimaryUrl: String?
|
||||
var currentPrimaryUrl: String?
|
||||
private var currentIsInstantView: Bool?
|
||||
|
||||
private var appliedItem: ListMessageItem?
|
||||
|
||||
override var canBeLongTapped: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
public required init() {
|
||||
self.contextSourceNode = ContextExtractedContentContainingNode()
|
||||
self.containerNode = ContextControllerSourceNode()
|
||||
|
||||
self.separatorNode = ASDisplayNode()
|
||||
self.separatorNode.displaysAsynchronously = false
|
||||
self.separatorNode.isLayerBacked = true
|
||||
|
||||
self.extractedBackgroundImageNode = ASImageNode()
|
||||
self.extractedBackgroundImageNode.displaysAsynchronously = false
|
||||
self.extractedBackgroundImageNode.alpha = 0.0
|
||||
|
||||
self.offsetContainerNode = ASDisplayNode()
|
||||
|
||||
self.highlightedBackgroundNode = ASDisplayNode()
|
||||
self.highlightedBackgroundNode.isLayerBacked = true
|
||||
|
||||
@ -80,11 +93,49 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.separatorNode)
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.descriptionNode)
|
||||
self.addSubnode(self.linkNode)
|
||||
self.addSubnode(self.instantViewIconNode)
|
||||
self.addSubnode(self.iconImageNode)
|
||||
|
||||
self.containerNode.addSubnode(self.contextSourceNode)
|
||||
self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode
|
||||
self.addSubnode(self.containerNode)
|
||||
|
||||
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) {
|
||||
@ -155,7 +206,7 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
||||
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 leftInset: CGFloat = 65.0 + params.leftInset
|
||||
@ -216,7 +267,7 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
||||
|
||||
let mutableDescriptionText = NSMutableAttributedString()
|
||||
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)
|
||||
@ -262,6 +313,7 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
||||
range.length = nsString.length - range.location
|
||||
}
|
||||
var urlString = nsString.substring(with: range)
|
||||
let rawUrlString = urlString
|
||||
var parsedUrl = URL(string: urlString)
|
||||
if parsedUrl == nil || parsedUrl!.host == nil || parsedUrl!.host!.isEmpty {
|
||||
urlString = "http://" + urlString
|
||||
@ -269,13 +321,18 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
||||
}
|
||||
if let url = parsedUrl, let host = url.host {
|
||||
primaryUrl = urlString
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
title = NSAttributedString(string: host, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor)
|
||||
}
|
||||
let mutableDescriptionText = NSMutableAttributedString()
|
||||
if item.message.text != urlString {
|
||||
mutableDescriptionText.append(NSAttributedString(string: item.message.text + "\n", font: descriptionFont, textColor: item.theme.list.itemPrimaryTextColor))
|
||||
if item.message.text != rawUrlString {
|
||||
mutableDescriptionText.append(NSAttributedString(string: item.message.text + "\n", font: descriptionFont, textColor: item.theme.list.itemSecondaryTextColor))
|
||||
}
|
||||
|
||||
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?
|
||||
if isInstantView {
|
||||
instantViewImage = PresentationResourcesChat.sharedMediaInstantViewIcon(item.theme)
|
||||
@ -310,8 +367,8 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
||||
|
||||
var iconImageApply: (() -> Void)?
|
||||
if let iconImageReferenceAndRepresentation = iconImageReferenceAndRepresentation {
|
||||
let iconSize = CGSize(width: 42.0, height: 42.0)
|
||||
let imageCorners = ImageCorners(topLeft: .Corner(2.0), topRight: .Corner(2.0), bottomLeft: .Corner(2.0), bottomRight: .Corner(2.0))
|
||||
let iconSize = CGSize(width: 40.0, height: 40.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)
|
||||
iconImageApply = iconImageLayout(arguments)
|
||||
}
|
||||
@ -335,7 +392,8 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
||||
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 {
|
||||
let transition: ContainedViewLayoutTransition
|
||||
if animation.isAnimated {
|
||||
@ -344,6 +402,23 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
||||
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.currentMedia = selectedMedia
|
||||
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))
|
||||
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)
|
||||
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))
|
||||
}
|
||||
|
||||
let iconFrame = CGRect(origin: CGPoint(x: params.leftInset + leftOffset + 9.0, y: 12.0), size: CGSize(width: 42.0, height: 42.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))
|
||||
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 + 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()
|
||||
|
||||
@ -406,7 +481,7 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
||||
}
|
||||
|
||||
if strongSelf.iconImageNode.supernode == nil {
|
||||
strongSelf.addSubnode(strongSelf.iconImageNode)
|
||||
strongSelf.offsetContainerNode.addSubnode(strongSelf.iconImageNode)
|
||||
strongSelf.iconImageNode.frame = iconFrame
|
||||
} else {
|
||||
transition.updateFrame(node: strongSelf.iconImageNode, frame: iconFrame)
|
||||
@ -427,15 +502,18 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
||||
|
||||
if strongSelf.iconTextBackgroundNode.supernode == nil {
|
||||
strongSelf.iconTextBackgroundNode.image = applyIconTextBackgroundImage
|
||||
strongSelf.addSubnode(strongSelf.iconTextBackgroundNode)
|
||||
strongSelf.offsetContainerNode.addSubnode(strongSelf.iconTextBackgroundNode)
|
||||
strongSelf.iconTextBackgroundNode.frame = iconFrame
|
||||
} else {
|
||||
transition.updateFrame(node: strongSelf.iconTextBackgroundNode, frame: iconFrame)
|
||||
}
|
||||
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 {
|
||||
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.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.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -77,6 +77,8 @@ final class PeerInfoGroupsInCommonPaneNode: ASDisplayNode, PeerInfoPaneNode {
|
||||
return self.ready.get()
|
||||
}
|
||||
|
||||
let shouldReceiveExpandProgressUpdates: Bool = false
|
||||
|
||||
private var disposable: Disposable?
|
||||
|
||||
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
|
||||
self.currentParams = (size, isScrollingLockedAtTop, presentationData)
|
||||
|
||||
|
@ -5,10 +5,27 @@ import SyncCore
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramPresentationData
|
||||
import PresentationDataUtils
|
||||
import AccountContext
|
||||
import ContextUI
|
||||
import PhotoResources
|
||||
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 {
|
||||
private let context: AccountContext
|
||||
@ -17,7 +34,7 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode {
|
||||
|
||||
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 var didSetReady: Bool = false
|
||||
@ -25,6 +42,8 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode {
|
||||
return self.ready.get()
|
||||
}
|
||||
|
||||
let shouldReceiveExpandProgressUpdates: Bool
|
||||
|
||||
private let selectedMessagesPromise = Promise<Set<MessageId>?>(nil)
|
||||
private var selectedMessages: Set<MessageId>? {
|
||||
didSet {
|
||||
@ -35,6 +54,13 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode {
|
||||
}
|
||||
|
||||
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) {
|
||||
self.context = context
|
||||
@ -45,19 +71,82 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode {
|
||||
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.defaultToSynchronousTransactionWhileScrolling = true
|
||||
|
||||
if tagMask == .music {
|
||||
self.shouldReceiveExpandProgressUpdates = true
|
||||
} else {
|
||||
self.shouldReceiveExpandProgressUpdates = false
|
||||
}
|
||||
|
||||
self.mediaAccessoryPanelContainer = PassthroughContainerNode()
|
||||
self.mediaAccessoryPanelContainer.clipsToBounds = true
|
||||
|
||||
super.init()
|
||||
|
||||
self.listNode.preloadPages = true
|
||||
self.addSubnode(self.listNode)
|
||||
self.addSubnode(self.mediaAccessoryPanelContainer)
|
||||
|
||||
self.ready.set(self.listNode.historyState.get()
|
||||
|> take(1)
|
||||
|> 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 {
|
||||
self.hiddenMediaDisposable?.dispose()
|
||||
self.mediaStatusDisposable?.dispose()
|
||||
self.playlistPreloadDisposable?.dispose()
|
||||
}
|
||||
|
||||
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) {
|
||||
self.currentParams = (size, isScrollingLockedAtTop, presentationData)
|
||||
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, 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))
|
||||
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 {
|
||||
switch self.listNode.visibleContentOffset() {
|
||||
case .known(0.0), .none:
|
||||
|
@ -118,6 +118,8 @@ final class PeerInfoMembersPaneNode: ASDisplayNode, PeerInfoPaneNode {
|
||||
return self.ready.get()
|
||||
}
|
||||
|
||||
let shouldReceiveExpandProgressUpdates: Bool = false
|
||||
|
||||
private var disposable: Disposable?
|
||||
|
||||
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
|
||||
self.currentParams = (size, isScrollingLockedAtTop, presentationData)
|
||||
|
||||
|
@ -449,7 +449,7 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
|
||||
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 var didSetReady: Bool = false
|
||||
@ -457,6 +457,8 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
|
||||
return self.ready.get()
|
||||
}
|
||||
|
||||
let shouldReceiveExpandProgressUpdates: Bool = false
|
||||
|
||||
private let listDisposable = MetaDisposable()
|
||||
private var hiddenMediaDisposable: Disposable?
|
||||
private var mediaItems: [VisualMediaItem] = []
|
||||
@ -482,7 +484,7 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
|
||||
|
||||
self._itemInteraction = VisualMediaItemInteraction(
|
||||
openMessage: { [weak self] message in
|
||||
self?.chatControllerInteraction.openMessage(message, .default)
|
||||
let _ = self?.chatControllerInteraction.openMessage(message, .default)
|
||||
},
|
||||
openMessageContextActions: { [weak self] message, sourceNode, sourceRect, gesture in
|
||||
self?.chatControllerInteraction.openMessageContextActions(message, sourceNode, sourceRect, gesture)
|
||||
@ -559,8 +561,8 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
|
||||
let wasFirstHistoryView = self.isFirstHistoryView
|
||||
self.isFirstHistoryView = false
|
||||
|
||||
if let (size, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, presentationData) = self.currentParams {
|
||||
self.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, presentationData: presentationData, synchronous: wasFirstHistoryView, transition: .immediate)
|
||||
if let (size, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) = self.currentParams {
|
||||
self.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, presentationData: presentationData, synchronous: wasFirstHistoryView, transition: .immediate)
|
||||
if !self.didSetReady {
|
||||
self.didSetReady = 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) {
|
||||
self.currentParams = (size, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, presentationData)
|
||||
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, expandProgress, presentationData)
|
||||
|
||||
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) {
|
||||
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)
|
||||
|
||||
if scrollView.contentOffset.y >= scrollView.contentSize.height - scrollView.bounds.height * 2.0, let currentView = self.currentView, currentView.earlierId != nil {
|
||||
|
@ -113,7 +113,7 @@ private func peerInfoAvailableMediaPanes(context: AccountContext, peerId: PeerId
|
||||
(.photoOrVideo, .media),
|
||||
(.file, .files),
|
||||
(.music, .music),
|
||||
//(.voiceOrInstantVideo, .voice),
|
||||
(.voiceOrInstantVideo, .voice),
|
||||
(.webPage, .links)
|
||||
]
|
||||
enum PaneState {
|
||||
|
@ -12,8 +12,9 @@ import ContextUI
|
||||
|
||||
protocol PeerInfoPaneNode: ASDisplayNode {
|
||||
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 transferVelocity(_ velocity: CGFloat)
|
||||
func cancelPreviewGestures()
|
||||
@ -28,21 +29,21 @@ final class PeerInfoPaneWrapper {
|
||||
let key: PeerInfoPaneKey
|
||||
let node: PeerInfoPaneNode
|
||||
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) {
|
||||
self.key = key
|
||||
self.node = node
|
||||
}
|
||||
|
||||
func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
|
||||
if let (currentSize, currentSideInset, currentBottomInset, visibleHeight, currentIsScrollingLockedAtTop, currentPresentationData) = self.appliedParams {
|
||||
if currentSize == size && currentSideInset == sideInset && currentBottomInset == bottomInset, currentIsScrollingLockedAtTop == isScrollingLockedAtTop && currentPresentationData === presentationData {
|
||||
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, currentExpandProgress, currentPresentationData) = self.appliedParams {
|
||||
if currentSize == size && currentSideInset == sideInset && currentBottomInset == bottomInset, currentIsScrollingLockedAtTop == isScrollingLockedAtTop && currentExpandProgress == expandProgress && currentPresentationData === presentationData {
|
||||
return
|
||||
}
|
||||
}
|
||||
self.appliedParams = (size, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, presentationData)
|
||||
self.node.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, presentationData: presentationData, synchronous: synchronous, transition: transition)
|
||||
self.appliedParams = (size, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData)
|
||||
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
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
for (key, pane) in self.pendingPanes {
|
||||
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 {
|
||||
self.pendingPanes.removeValue(forKey: key)
|
||||
@ -819,7 +820,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat
|
||||
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:
|
||||
title = presentationData.strings.PeerInfo_PaneLinks
|
||||
case .voice:
|
||||
title = presentationData.strings.PeerInfo_PaneVoice
|
||||
title = presentationData.strings.PeerInfo_PaneVoiceAndVideo
|
||||
case .music:
|
||||
title = presentationData.strings.PeerInfo_PaneAudio
|
||||
case .groupsInCommon:
|
||||
@ -850,7 +851,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat
|
||||
for (_, pane) in self.pendingPanes {
|
||||
let paneTransition: ContainedViewLayoutTransition = .immediate
|
||||
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 let currentPaneKey = self.currentPaneKey, let currentPane = self.currentPanes[currentPaneKey] {
|
||||
|
@ -1205,59 +1205,131 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
||||
self?.openPeer(peerId: id, navigation: navigation)
|
||||
}
|
||||
}, openPeerMention: { _ in
|
||||
}, openMessageContextMenu: { [weak self] message, _, _, _, _ in
|
||||
guard let strongSelf = self else {
|
||||
}, openMessageContextMenu: { [weak self] message, _, node, frame, anyRecognizer in
|
||||
guard let strongSelf = self, let node = node as? ContextExtractedContentContainingNode else {
|
||||
return
|
||||
}
|
||||
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])
|
||||
|> deliverOnMainQueue).start(next: { actions in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
var messageIds = Set<MessageId>()
|
||||
messageIds.insert(message.id)
|
||||
var items: [ContextMenuItem] = []
|
||||
|
||||
if let strongSelf = self {
|
||||
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)
|
||||
var items: [ActionSheetButtonItem] = []
|
||||
|
||||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.SharedMedia_ViewInChat, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
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 {
|
||||
strongSelf.forwardMessages(messageIds: Set([message.id]))
|
||||
}
|
||||
})
|
||||
})))
|
||||
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 {
|
||||
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) {
|
||||
items.append( ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_ContextMenuDelete, color: .destructive, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
if let strongSelf = self {
|
||||
strongSelf.deleteMessages(messageIds: Set(messageIds))
|
||||
})
|
||||
})))
|
||||
if actions.options.contains(.deleteLocally) || actions.options.contains(.deleteGlobally) {
|
||||
let context = strongSelf.context
|
||||
let presentationData = strongSelf.presentationData
|
||||
let peerId = strongSelf.peerId
|
||||
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()
|
||||
}
|
||||
})
|
||||
})))
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
if strongSelf.searchDisplayController == nil {
|
||||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_ContextMenuMore, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
|
||||
return items
|
||||
})
|
||||
})))
|
||||
}
|
||||
if strongSelf.searchDisplayController == nil {
|
||||
items.append(.separator)
|
||||
|
||||
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 {
|
||||
strongSelf.chatInterfaceInteraction.toggleMessagesSelection([message.id], true)
|
||||
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
|
||||
guard let strongSelf = self else {
|
||||
@ -1446,9 +1518,10 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
||||
}, updateInputState: { _ in
|
||||
}, updateInputMode: { _ in
|
||||
}, openMessageShareMenu: { _ in
|
||||
}, presentController: { _, _ in
|
||||
}, navigationController: {
|
||||
return nil
|
||||
}, presentController: { [weak self] c, a in
|
||||
self?.controller?.present(c, in: .window(.root), with: a)
|
||||
}, navigationController: { [weak self] in
|
||||
return self?.controller?.navigationController as? NavigationController
|
||||
}, chatControllerNode: {
|
||||
return nil
|
||||
}, reactionContainerNode: {
|
||||
@ -4537,3 +4610,22 @@ private final class ContextControllerContentSourceImpl: ContextControllerContent
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user