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.PaneMedia" = "Media";
|
||||||
"PeerInfo.PaneFiles" = "Files";
|
"PeerInfo.PaneFiles" = "Files";
|
||||||
"PeerInfo.PaneLinks" = "Links";
|
"PeerInfo.PaneLinks" = "Links";
|
||||||
"PeerInfo.PaneVoice" = "Voice Messages";
|
"PeerInfo.PaneVoiceAndVideo" = "Voice";
|
||||||
"PeerInfo.PaneAudio" = "Audio";
|
"PeerInfo.PaneAudio" = "Audio";
|
||||||
"PeerInfo.PaneGroups" = "Groups";
|
"PeerInfo.PaneGroups" = "Groups";
|
||||||
"PeerInfo.PaneMembers" = "Members";
|
"PeerInfo.PaneMembers" = "Members";
|
||||||
|
@ -228,6 +228,8 @@ public final class ContextGesture: UIGestureRecognizer, UIGestureRecognizerDeleg
|
|||||||
self.delayTimer?.invalidate()
|
self.delayTimer?.invalidate()
|
||||||
self.animator?.invalidate()
|
self.animator?.invalidate()
|
||||||
self.state = .failed
|
self.state = .failed
|
||||||
|
} else {
|
||||||
|
self.state = .failed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,6 +131,19 @@ public enum GeneralScrollDirection {
|
|||||||
case down
|
case down
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func cancelContextGestures(view: UIView) {
|
||||||
|
if let gestureRecognizers = view.gestureRecognizers {
|
||||||
|
for gesture in gestureRecognizers {
|
||||||
|
if let gesture = gesture as? ContextGesture {
|
||||||
|
gesture.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for subview in view.subviews {
|
||||||
|
cancelContextGestures(view: subview)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGestureRecognizerDelegate {
|
open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGestureRecognizerDelegate {
|
||||||
public final let scroller: ListViewScroller
|
public final let scroller: ListViewScroller
|
||||||
private final var visibleSize: CGSize = CGSize()
|
private final var visibleSize: CGSize = CGSize()
|
||||||
@ -666,6 +679,10 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
|||||||
self.scrolledToItem = nil
|
self.scrolledToItem = nil
|
||||||
|
|
||||||
self.beganInteractiveDragging()
|
self.beganInteractiveDragging()
|
||||||
|
|
||||||
|
for itemNode in self.itemNodes {
|
||||||
|
cancelContextGestures(view: itemNode.view)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
||||||
@ -739,8 +756,10 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
|||||||
self.decelerationAnimator?.isPaused = false
|
self.decelerationAnimator?.isPaused = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var defaultToSynchronousTransactionWhileScrolling: Bool = false
|
||||||
|
|
||||||
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||||
self.updateScrollViewDidScroll(scrollView, synchronous: false)
|
self.updateScrollViewDidScroll(scrollView, synchronous: self.defaultToSynchronousTransactionWhileScrolling)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var generalAccumulatedDeltaY: CGFloat = 0.0
|
private var generalAccumulatedDeltaY: CGFloat = 0.0
|
||||||
@ -3606,8 +3625,12 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
|||||||
var updatedState = state
|
var updatedState = state
|
||||||
var updatedOperations = operations
|
var updatedOperations = operations
|
||||||
updatedState.removeInvisibleNodes(&updatedOperations)
|
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)
|
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 editableControlLayout = ItemListEditableControlNode.asyncLayout(self.editableControlNode)
|
||||||
let reorderControlLayout = ItemListEditableReorderControlNode.asyncLayout(self.reorderControlNode)
|
let reorderControlLayout = ItemListEditableReorderControlNode.asyncLayout(self.reorderControlNode)
|
||||||
|
|
||||||
var previousThumbnailItem = self.currentThumbnailItem
|
let previousThumbnailItem = self.currentThumbnailItem
|
||||||
var currentDisabledOverlayNode = self.disabledOverlayNode
|
var currentDisabledOverlayNode = self.disabledOverlayNode
|
||||||
|
|
||||||
let currentItem = self.layoutParams?.0
|
let currentItem = self.layoutParams?.0
|
||||||
|
@ -1279,14 +1279,14 @@ public func gifPaneVideoThumbnail(account: Account, videoReference: FileMediaRef
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func mediaGridMessageVideo(postbox: Postbox, videoReference: FileMediaReference, onlyFullSize: Bool = false, synchronousLoad: Bool = false, autoFetchFullSizeThumbnail: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> {
|
public func mediaGridMessageVideo(postbox: Postbox, videoReference: FileMediaReference, onlyFullSize: Bool = false, synchronousLoad: Bool = false, autoFetchFullSizeThumbnail: Bool = false, overlayColor: UIColor? = nil) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> {
|
||||||
return internalMediaGridMessageVideo(postbox: postbox, videoReference: videoReference, onlyFullSize: onlyFullSize, synchronousLoad: synchronousLoad, autoFetchFullSizeThumbnail: autoFetchFullSizeThumbnail)
|
return internalMediaGridMessageVideo(postbox: postbox, videoReference: videoReference, onlyFullSize: onlyFullSize, synchronousLoad: synchronousLoad, autoFetchFullSizeThumbnail: autoFetchFullSizeThumbnail, overlayColor: overlayColor)
|
||||||
|> map {
|
|> map {
|
||||||
return $0.1
|
return $0.1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func internalMediaGridMessageVideo(postbox: Postbox, videoReference: FileMediaReference, imageReference: ImageMediaReference? = nil, onlyFullSize: Bool = false, synchronousLoad: Bool = false, autoFetchFullSizeThumbnail: Bool = false) -> Signal<(() -> CGSize?, (TransformImageArguments) -> DrawingContext?), NoError> {
|
public func internalMediaGridMessageVideo(postbox: Postbox, videoReference: FileMediaReference, imageReference: ImageMediaReference? = nil, onlyFullSize: Bool = false, synchronousLoad: Bool = false, autoFetchFullSizeThumbnail: Bool = false, overlayColor: UIColor? = nil) -> Signal<(() -> CGSize?, (TransformImageArguments) -> DrawingContext?), NoError> {
|
||||||
let signal: Signal<Tuple3<Data?, Tuple2<Data, String>?, Bool>, NoError>
|
let signal: Signal<Tuple3<Data?, Tuple2<Data, String>?, Bool>, NoError>
|
||||||
if let imageReference = imageReference {
|
if let imageReference = imageReference {
|
||||||
signal = chatMessagePhotoDatas(postbox: postbox, photoReference: imageReference, tryAdditionalRepresentations: true, synchronousLoad: synchronousLoad)
|
signal = chatMessagePhotoDatas(postbox: postbox, photoReference: imageReference, tryAdditionalRepresentations: true, synchronousLoad: synchronousLoad)
|
||||||
@ -1480,6 +1480,14 @@ public func internalMediaGridMessageVideo(postbox: Postbox, videoReference: File
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let overlayColor = overlayColor {
|
||||||
|
context.withFlippedContext { c in
|
||||||
|
c.setBlendMode(.normal)
|
||||||
|
c.setFillColor(overlayColor.cgColor)
|
||||||
|
c.fill(arguments.drawingRect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
addCorners(context, arguments: arguments)
|
addCorners(context, arguments: arguments)
|
||||||
|
|
||||||
return context
|
return context
|
||||||
@ -2400,7 +2408,7 @@ private func drawAlbumArtPlaceholder(into c: CGContext, arguments: TransformImag
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func playerAlbumArt(postbox: Postbox, fileReference: FileMediaReference?, albumArt: SharedMediaPlaybackAlbumArt?, thumbnail: Bool) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> {
|
public func playerAlbumArt(postbox: Postbox, fileReference: FileMediaReference?, albumArt: SharedMediaPlaybackAlbumArt?, thumbnail: Bool, overlayColor: UIColor? = nil, emptyColor: UIColor? = nil) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> {
|
||||||
var fileArtworkData: Signal<Data?, NoError> = .single(nil)
|
var fileArtworkData: Signal<Data?, NoError> = .single(nil)
|
||||||
if let fileReference = fileReference {
|
if let fileReference = fileReference {
|
||||||
let size = thumbnail ? CGSize(width: 48.0, height: 48.0) : CGSize(width: 320.0, height: 320.0)
|
let size = thumbnail ? CGSize(width: 48.0, height: 48.0) : CGSize(width: 320.0, height: 320.0)
|
||||||
@ -2471,10 +2479,22 @@ public func playerAlbumArt(postbox: Postbox, fileReference: FileMediaReference?,
|
|||||||
let imageSize = sourceImage.size.aspectFilled(arguments.drawingRect.size)
|
let imageSize = sourceImage.size.aspectFilled(arguments.drawingRect.size)
|
||||||
context.withFlippedContext { c in
|
context.withFlippedContext { c in
|
||||||
c.draw(cgImage, in: CGRect(origin: CGPoint(x: floor((arguments.drawingRect.size.width - imageSize.width) / 2.0), y: floor((arguments.drawingRect.size.height - imageSize.height) / 2.0)), size: imageSize))
|
c.draw(cgImage, in: CGRect(origin: CGPoint(x: floor((arguments.drawingRect.size.width - imageSize.width) / 2.0), y: floor((arguments.drawingRect.size.height - imageSize.height) / 2.0)), size: imageSize))
|
||||||
|
if let overlayColor = overlayColor {
|
||||||
|
c.setFillColor(overlayColor.cgColor)
|
||||||
|
c.fill(arguments.drawingRect)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
context.withFlippedContext { c in
|
if let emptyColor = emptyColor {
|
||||||
drawAlbumArtPlaceholder(into: c, arguments: arguments, thumbnail: thumbnail)
|
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 final class SemanticStatusNode: ASControlNode {
|
||||||
public var backgroundNodeColor: UIColor {
|
public var backgroundNodeColor: UIColor {
|
||||||
didSet {
|
didSet {
|
||||||
self.setNeedsDisplay()
|
if !self.backgroundNodeColor.isEqual(oldValue) {
|
||||||
|
self.setNeedsDisplay()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public var foregroundNodeColor: UIColor {
|
public var foregroundNodeColor: UIColor {
|
||||||
didSet {
|
didSet {
|
||||||
self.setNeedsDisplay()
|
if !self.foregroundNodeColor.isEqual(oldValue) {
|
||||||
|
self.setNeedsDisplay()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var animator: ConstantDisplayLinkAnimator?
|
private var animator: ConstantDisplayLinkAnimator?
|
||||||
|
|
||||||
|
private var hasState: Bool = false
|
||||||
public private(set) var state: SemanticStatusNodeState
|
public private(set) var state: SemanticStatusNodeState
|
||||||
private var transtionContext: SemanticStatusNodeTransitionContext?
|
private var transtionContext: SemanticStatusNodeTransitionContext?
|
||||||
private var stateContext: SemanticStatusNodeStateContext
|
private var stateContext: SemanticStatusNodeStateContext
|
||||||
@ -505,8 +510,11 @@ public final class SemanticStatusNode: ASControlNode {
|
|||||||
|
|
||||||
public func transitionToState(_ state: SemanticStatusNodeState, animated: Bool = true, synchronous: Bool = false, completion: @escaping () -> Void = {}) {
|
public func transitionToState(_ state: SemanticStatusNodeState, animated: Bool = true, synchronous: Bool = false, completion: @escaping () -> Void = {}) {
|
||||||
var animated = animated
|
var animated = animated
|
||||||
|
if !self.hasState {
|
||||||
|
self.hasState = true
|
||||||
|
animated = false
|
||||||
|
}
|
||||||
if self.state != state {
|
if self.state != state {
|
||||||
let fromState = self.state
|
|
||||||
self.state = state
|
self.state = state
|
||||||
let previousStateContext = self.stateContext
|
let previousStateContext = self.stateContext
|
||||||
self.stateContext = self.state.context(current: self.stateContext)
|
self.stateContext = self.state.context(current: self.stateContext)
|
||||||
|
@ -7,9 +7,9 @@ import SyncCore
|
|||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
import AccountContext
|
import AccountContext
|
||||||
|
|
||||||
final class MediaNavigationAccessoryContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
|
public final class MediaNavigationAccessoryContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
|
||||||
let backgroundNode: ASDisplayNode
|
public let backgroundNode: ASDisplayNode
|
||||||
let headerNode: MediaNavigationAccessoryHeaderNode
|
public let headerNode: MediaNavigationAccessoryHeaderNode
|
||||||
|
|
||||||
private let currentHeaderHeight: CGFloat = MediaNavigationAccessoryHeaderNode.minimizedHeight
|
private let currentHeaderHeight: CGFloat = MediaNavigationAccessoryHeaderNode.minimizedHeight
|
||||||
|
|
||||||
@ -44,7 +44,7 @@ final class MediaNavigationAccessoryContainerNode: ASDisplayNode, UIGestureRecog
|
|||||||
self.headerNode.updateLayout(size: CGSize(width: size.width, height: headerHeight), leftInset: leftInset, rightInset: rightInset, transition: transition)
|
self.headerNode.updateLayout(size: CGSize(width: size.width, height: headerHeight), leftInset: leftInset, rightInset: rightInset, transition: transition)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||||
if !self.headerNode.frame.contains(point) {
|
if !self.headerNode.frame.contains(point) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -127,8 +127,8 @@ private func generateMaskImage(color: UIColor) -> UIImage? {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollViewDelegate {
|
public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollViewDelegate {
|
||||||
static let minimizedHeight: CGFloat = 37.0
|
public static let minimizedHeight: CGFloat = 37.0
|
||||||
|
|
||||||
private var theme: PresentationTheme
|
private var theme: PresentationTheme
|
||||||
private var strings: PresentationStrings
|
private var strings: PresentationStrings
|
||||||
@ -156,7 +156,7 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollViewDeleg
|
|||||||
|
|
||||||
private var validLayout: (CGSize, CGFloat, CGFloat)?
|
private var validLayout: (CGSize, CGFloat, CGFloat)?
|
||||||
|
|
||||||
var displayScrubber: Bool = true {
|
public var displayScrubber: Bool = true {
|
||||||
didSet {
|
didSet {
|
||||||
self.scrubbingNode.isHidden = !self.displayScrubber
|
self.scrubbingNode.isHidden = !self.displayScrubber
|
||||||
}
|
}
|
||||||
@ -166,14 +166,14 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollViewDeleg
|
|||||||
|
|
||||||
private var tapRecognizer: UITapGestureRecognizer?
|
private var tapRecognizer: UITapGestureRecognizer?
|
||||||
|
|
||||||
var tapAction: (() -> Void)?
|
public var tapAction: (() -> Void)?
|
||||||
var close: (() -> Void)?
|
public var close: (() -> Void)?
|
||||||
var toggleRate: (() -> Void)?
|
public var toggleRate: (() -> Void)?
|
||||||
var togglePlayPause: (() -> Void)?
|
public var togglePlayPause: (() -> Void)?
|
||||||
var playPrevious: (() -> Void)?
|
public var playPrevious: (() -> Void)?
|
||||||
var playNext: (() -> Void)?
|
public var playNext: (() -> Void)?
|
||||||
|
|
||||||
var playbackBaseRate: AudioPlaybackRate? = nil {
|
public var playbackBaseRate: AudioPlaybackRate? = nil {
|
||||||
didSet {
|
didSet {
|
||||||
guard self.playbackBaseRate != oldValue, let playbackBaseRate = self.playbackBaseRate else {
|
guard self.playbackBaseRate != oldValue, let playbackBaseRate = self.playbackBaseRate else {
|
||||||
return
|
return
|
||||||
@ -193,13 +193,13 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollViewDeleg
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var playbackStatus: Signal<MediaPlayerStatus, NoError>? {
|
public var playbackStatus: Signal<MediaPlayerStatus, NoError>? {
|
||||||
didSet {
|
didSet {
|
||||||
self.scrubbingNode.status = self.playbackStatus
|
self.scrubbingNode.status = self.playbackStatus
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var playbackItems: (SharedMediaPlaylistItem?, SharedMediaPlaylistItem?, SharedMediaPlaylistItem?)? {
|
public var playbackItems: (SharedMediaPlaylistItem?, SharedMediaPlaylistItem?, SharedMediaPlaylistItem?)? {
|
||||||
didSet {
|
didSet {
|
||||||
if !arePlaylistItemsEqual(self.playbackItems?.0, oldValue?.0) || !arePlaylistItemsEqual(self.playbackItems?.1, oldValue?.1) || !arePlaylistItemsEqual(self.playbackItems?.2, oldValue?.2), let layout = validLayout {
|
if !arePlaylistItemsEqual(self.playbackItems?.0, oldValue?.0) || !arePlaylistItemsEqual(self.playbackItems?.1, oldValue?.1) || !arePlaylistItemsEqual(self.playbackItems?.2, oldValue?.2), let layout = validLayout {
|
||||||
self.updateLayout(size: layout.0, leftInset: layout.1, rightInset: layout.2, transition: .immediate)
|
self.updateLayout(size: layout.0, leftInset: layout.1, rightInset: layout.2, transition: .immediate)
|
||||||
@ -207,7 +207,7 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollViewDeleg
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init(presentationData: PresentationData) {
|
public init(presentationData: PresentationData) {
|
||||||
self.theme = presentationData.theme
|
self.theme = presentationData.theme
|
||||||
self.strings = presentationData.strings
|
self.strings = presentationData.strings
|
||||||
self.dateTimeFormat = presentationData.dateTimeFormat
|
self.dateTimeFormat = presentationData.dateTimeFormat
|
||||||
@ -346,7 +346,7 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollViewDeleg
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func didLoad() {
|
override public func didLoad() {
|
||||||
super.didLoad()
|
super.didLoad()
|
||||||
|
|
||||||
self.view.disablesInteractiveTransitionGestureRecognizer = true
|
self.view.disablesInteractiveTransitionGestureRecognizer = true
|
||||||
@ -361,7 +361,7 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollViewDeleg
|
|||||||
self.view.addGestureRecognizer(tapRecognizer)
|
self.view.addGestureRecognizer(tapRecognizer)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updatePresentationData(_ presentationData: PresentationData) {
|
public func updatePresentationData(_ presentationData: PresentationData) {
|
||||||
self.theme = presentationData.theme
|
self.theme = presentationData.theme
|
||||||
self.strings = presentationData.strings
|
self.strings = presentationData.strings
|
||||||
self.nameDisplayOrder = presentationData.nameDisplayOrder
|
self.nameDisplayOrder = presentationData.nameDisplayOrder
|
||||||
@ -390,17 +390,17 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollViewDeleg
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||||
if scrollView.isDecelerating {
|
if scrollView.isDecelerating {
|
||||||
self.changeTrack()
|
self.changeTrack()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||||||
self.changeTrack()
|
self.changeTrack()
|
||||||
}
|
}
|
||||||
|
|
||||||
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
||||||
guard !decelerate else {
|
guard !decelerate else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -418,7 +418,7 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollViewDeleg
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) {
|
public func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||||
self.validLayout = (size, leftInset, rightInset)
|
self.validLayout = (size, leftInset, rightInset)
|
||||||
|
|
||||||
let minHeight = MediaNavigationAccessoryHeaderNode.minimizedHeight
|
let minHeight = MediaNavigationAccessoryHeaderNode.minimizedHeight
|
||||||
@ -472,19 +472,19 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollViewDeleg
|
|||||||
self.accessibilityAreaNode.frame = CGRect(origin: CGPoint(x: self.actionButton.frame.maxX, y: 0.0), size: CGSize(width: self.rateButton.frame.minX - self.actionButton.frame.maxX, height: minHeight))
|
self.accessibilityAreaNode.frame = CGRect(origin: CGPoint(x: self.actionButton.frame.maxX, y: 0.0), size: CGSize(width: self.rateButton.frame.minX - self.actionButton.frame.maxX, height: minHeight))
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func closeButtonPressed() {
|
@objc public func closeButtonPressed() {
|
||||||
self.close?()
|
self.close?()
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func rateButtonPressed() {
|
@objc public func rateButtonPressed() {
|
||||||
self.toggleRate?()
|
self.toggleRate?()
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func actionButtonPressed() {
|
@objc public func actionButtonPressed() {
|
||||||
self.togglePlayPause?()
|
self.togglePlayPause?()
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
@objc public func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||||
if case .ended = recognizer.state {
|
if case .ended = recognizer.state {
|
||||||
self.tapAction?()
|
self.tapAction?()
|
||||||
}
|
}
|
||||||
|
@ -6,17 +6,17 @@ import TelegramCore
|
|||||||
import SyncCore
|
import SyncCore
|
||||||
import AccountContext
|
import AccountContext
|
||||||
|
|
||||||
final class MediaNavigationAccessoryPanel: ASDisplayNode {
|
public final class MediaNavigationAccessoryPanel: ASDisplayNode {
|
||||||
let containerNode: MediaNavigationAccessoryContainerNode
|
public let containerNode: MediaNavigationAccessoryContainerNode
|
||||||
|
|
||||||
var close: (() -> Void)?
|
public var close: (() -> Void)?
|
||||||
var toggleRate: (() -> Void)?
|
public var toggleRate: (() -> Void)?
|
||||||
var togglePlayPause: (() -> Void)?
|
public var togglePlayPause: (() -> Void)?
|
||||||
var tapAction: (() -> Void)?
|
public var tapAction: (() -> Void)?
|
||||||
var playPrevious: (() -> Void)?
|
public var playPrevious: (() -> Void)?
|
||||||
var playNext: (() -> Void)?
|
public var playNext: (() -> Void)?
|
||||||
|
|
||||||
init(context: AccountContext) {
|
public init(context: AccountContext) {
|
||||||
self.containerNode = MediaNavigationAccessoryContainerNode(context: context)
|
self.containerNode = MediaNavigationAccessoryContainerNode(context: context)
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
@ -53,12 +53,12 @@ final class MediaNavigationAccessoryPanel: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) {
|
public func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||||
transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(), size: size))
|
transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(), size: size))
|
||||||
self.containerNode.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, transition: transition)
|
self.containerNode.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, transition: transition)
|
||||||
}
|
}
|
||||||
|
|
||||||
func animateIn(transition: ContainedViewLayoutTransition) {
|
public func animateIn(transition: ContainedViewLayoutTransition) {
|
||||||
self.clipsToBounds = true
|
self.clipsToBounds = true
|
||||||
let contentPosition = self.containerNode.layer.position
|
let contentPosition = self.containerNode.layer.position
|
||||||
transition.animatePosition(node: self.containerNode, from: CGPoint(x: contentPosition.x, y: contentPosition.y - 37.0), completion: { [weak self] _ in
|
transition.animatePosition(node: self.containerNode, from: CGPoint(x: contentPosition.x, y: contentPosition.y - 37.0), completion: { [weak self] _ in
|
||||||
@ -66,7 +66,7 @@ final class MediaNavigationAccessoryPanel: ASDisplayNode {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func animateOut(transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
|
public func animateOut(transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
|
||||||
self.clipsToBounds = true
|
self.clipsToBounds = true
|
||||||
let contentPosition = self.containerNode.layer.position
|
let contentPosition = self.containerNode.layer.position
|
||||||
transition.animatePosition(node: self.containerNode, to: CGPoint(x: contentPosition.x, y: contentPosition.y - 37.0), removeOnCompletion: false, completion: { [weak self] _ in
|
transition.animatePosition(node: self.containerNode, to: CGPoint(x: contentPosition.x, y: contentPosition.y - 37.0), removeOnCompletion: false, completion: { [weak self] _ in
|
||||||
@ -75,7 +75,7 @@ final class MediaNavigationAccessoryPanel: ASDisplayNode {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||||
return self.containerNode.hitTest(point, with: event)
|
return self.containerNode.hitTest(point, with: event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -72,7 +72,7 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder {
|
|||||||
public var tempVoicePlaylistEnded: (() -> Void)?
|
public var tempVoicePlaylistEnded: (() -> Void)?
|
||||||
public var tempVoicePlaylistItemChanged: ((SharedMediaPlaylistItem?, SharedMediaPlaylistItem?) -> Void)?
|
public var tempVoicePlaylistItemChanged: ((SharedMediaPlaylistItem?, SharedMediaPlaylistItem?) -> Void)?
|
||||||
|
|
||||||
private var mediaAccessoryPanel: (MediaNavigationAccessoryPanel, MediaManagerPlayerType)?
|
public var mediaAccessoryPanel: (MediaNavigationAccessoryPanel, MediaManagerPlayerType)?
|
||||||
|
|
||||||
private var locationBroadcastMode: LocationBroadcastNavigationAccessoryPanelMode?
|
private var locationBroadcastMode: LocationBroadcastNavigationAccessoryPanelMode?
|
||||||
private var locationBroadcastPeers: [Peer]?
|
private var locationBroadcastPeers: [Peer]?
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -577,19 +577,47 @@ public struct PresentationResourcesChat {
|
|||||||
|
|
||||||
public static func sharedMediaFileDownloadStartIcon(_ theme: PresentationTheme) -> UIImage? {
|
public static func sharedMediaFileDownloadStartIcon(_ theme: PresentationTheme) -> UIImage? {
|
||||||
return theme.image(PresentationResourceKey.sharedMediaFileDownloadStartIcon.rawValue, { theme in
|
return theme.image(PresentationResourceKey.sharedMediaFileDownloadStartIcon.rawValue, { theme in
|
||||||
return generateTintedImage(image: UIImage(bundleImageName: "List Menu/ListDownloadStartIcon"), color: theme.list.itemAccentColor)
|
return generateImage(CGSize(width: 12.0, height: 12.0), rotatedContext: { size, context in
|
||||||
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||||
|
|
||||||
|
context.setStrokeColor(theme.list.itemAccentColor.cgColor)
|
||||||
|
context.setLineWidth(1.67)
|
||||||
|
context.setLineCap(.round)
|
||||||
|
context.setLineJoin(.round)
|
||||||
|
|
||||||
|
context.translateBy(x: 2.0, y: 1.0)
|
||||||
|
|
||||||
|
context.move(to: CGPoint(x: 4.0, y: 0.0))
|
||||||
|
context.addLine(to: CGPoint(x: 4.0, y: 10.0))
|
||||||
|
context.strokePath()
|
||||||
|
|
||||||
|
context.move(to: CGPoint(x: 0.0, y: 6.0))
|
||||||
|
context.addLine(to: CGPoint(x: 4.0, y: 10.0))
|
||||||
|
context.addLine(to: CGPoint(x: 8.0, y: 6.0))
|
||||||
|
context.strokePath()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func sharedMediaFileDownloadPauseIcon(_ theme: PresentationTheme) -> UIImage? {
|
public static func sharedMediaFileDownloadPauseIcon(_ theme: PresentationTheme) -> UIImage? {
|
||||||
return theme.image(PresentationResourceKey.sharedMediaFileDownloadPauseIcon.rawValue, { theme in
|
return theme.image(PresentationResourceKey.sharedMediaFileDownloadPauseIcon.rawValue, { theme in
|
||||||
return generateImage(CGSize(width: 11.0, height: 11.0), contextGenerator: { size, context in
|
return generateImage(CGSize(width: 12.0, height: 12.0), rotatedContext: { size, context in
|
||||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||||
|
|
||||||
context.setFillColor(theme.list.itemAccentColor.cgColor)
|
context.setStrokeColor(theme.list.itemAccentColor.cgColor)
|
||||||
|
context.setLineWidth(1.67)
|
||||||
|
context.setLineCap(.round)
|
||||||
|
context.setLineJoin(.round)
|
||||||
|
|
||||||
context.fill(CGRect(x: 2.0, y: 0.0, width: 2.0, height: 11.0 - 1.0))
|
context.translateBy(x: 2.0, y: 2.0)
|
||||||
context.fill(CGRect(x: 2.0 + 2.0 + 2.0, y: 0.0, width: 2.0, height: 11.0 - 1.0))
|
|
||||||
|
context.move(to: CGPoint(x: 0.0, y: 0.0))
|
||||||
|
context.addLine(to: CGPoint(x: 8.0, y: 8.0))
|
||||||
|
context.strokePath()
|
||||||
|
|
||||||
|
context.move(to: CGPoint(x: 8.0, y: 0.0))
|
||||||
|
context.addLine(to: CGPoint(x: 0.0, y: 8.0))
|
||||||
|
context.strokePath()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
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)
|
return generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/MinimizedPlay"), color: theme.chat.inputPanel.actionControlForegroundColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension AudioWaveformNode: CustomMediaPlayerScrubbingForegroundNode {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode {
|
final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode {
|
||||||
private let deleteButton: HighlightableButtonNode
|
private let deleteButton: HighlightableButtonNode
|
||||||
let sendButton: HighlightTrackingButtonNode
|
let sendButton: HighlightTrackingButtonNode
|
||||||
|
@ -6,6 +6,7 @@ import AsyncDisplayKit
|
|||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
import TelegramUIPreferences
|
import TelegramUIPreferences
|
||||||
import TelegramStringFormatting
|
import TelegramStringFormatting
|
||||||
|
import ListSectionHeaderNode
|
||||||
|
|
||||||
private let timezoneOffset: Int32 = {
|
private let timezoneOffset: Int32 = {
|
||||||
let nowTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
|
let nowTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
|
||||||
@ -16,7 +17,7 @@ private let timezoneOffset: Int32 = {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
func listMessageDateHeaderId(timestamp: Int32) -> Int64 {
|
func listMessageDateHeaderId(timestamp: Int32) -> Int64 {
|
||||||
var unclippedValue: Int64 = min(Int64(Int32.max), Int64(timestamp) + Int64(timezoneOffset))
|
let unclippedValue: Int64 = min(Int64(Int32.max), Int64(timestamp) + Int64(timezoneOffset))
|
||||||
|
|
||||||
var time: time_t = time_t(Int32(clamping: unclippedValue))
|
var time: time_t = time_t(Int32(clamping: unclippedValue))
|
||||||
var timeinfo: tm = tm()
|
var timeinfo: tm = tm()
|
||||||
@ -65,7 +66,7 @@ final class ListMessageDateHeader: ListViewItemHeader {
|
|||||||
|
|
||||||
let stickDirection: ListViewItemHeaderStickDirection = .top
|
let stickDirection: ListViewItemHeaderStickDirection = .top
|
||||||
|
|
||||||
let height: CGFloat = 36.0
|
let height: CGFloat = 28.0
|
||||||
|
|
||||||
func node() -> ListViewItemHeaderNode {
|
func node() -> ListViewItemHeaderNode {
|
||||||
return ListMessageDateHeaderNode(theme: self.theme, strings: self.strings, fontSize: self.fontSize, roundedTimestamp: self.roundedTimestamp, month: self.month, year: self.year)
|
return ListMessageDateHeaderNode(theme: self.theme, strings: self.strings, fontSize: self.fontSize, roundedTimestamp: self.roundedTimestamp, month: self.month, year: self.year)
|
||||||
@ -78,51 +79,39 @@ final class ListMessageDateHeader: ListViewItemHeader {
|
|||||||
final class ListMessageDateHeaderNode: ListViewItemHeaderNode {
|
final class ListMessageDateHeaderNode: ListViewItemHeaderNode {
|
||||||
var theme: PresentationTheme
|
var theme: PresentationTheme
|
||||||
var strings: PresentationStrings
|
var strings: PresentationStrings
|
||||||
var fontSize: PresentationFontSize
|
let headerNode: ListSectionHeaderNode
|
||||||
let titleNode: ASTextNode
|
|
||||||
let backgroundNode: ASDisplayNode
|
let month: Int32
|
||||||
|
let year: Int32
|
||||||
|
|
||||||
init(theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize, roundedTimestamp: Int32, month: Int32, year: Int32) {
|
init(theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize, roundedTimestamp: Int32, month: Int32, year: Int32) {
|
||||||
self.theme = theme
|
self.theme = theme
|
||||||
self.strings = strings
|
self.strings = strings
|
||||||
self.fontSize = fontSize
|
self.month = month
|
||||||
|
self.year = year
|
||||||
|
|
||||||
self.backgroundNode = ASDisplayNode()
|
self.headerNode = ListSectionHeaderNode(theme: theme)
|
||||||
self.backgroundNode.isLayerBacked = true
|
|
||||||
self.backgroundNode.backgroundColor = theme.list.plainBackgroundColor.withAlphaComponent(0.9)
|
|
||||||
|
|
||||||
self.titleNode = ASTextNode()
|
|
||||||
self.titleNode.isUserInteractionEnabled = false
|
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
let dateText = stringForMonth(strings: strings, month: month, ofYear: year)
|
self.addSubnode(self.headerNode)
|
||||||
|
|
||||||
let sectionTitleFont = Font.regular(floor(fontSize.baseDisplaySize * 14.0 / 17.0))
|
self.headerNode.title = stringForMonth(strings: strings, month: month, ofYear: year).uppercased()
|
||||||
|
|
||||||
self.addSubnode(self.backgroundNode)
|
|
||||||
self.addSubnode(self.titleNode)
|
|
||||||
self.titleNode.attributedText = NSAttributedString(string: dateText, font: sectionTitleFont, textColor: theme.list.itemPrimaryTextColor)
|
|
||||||
self.titleNode.maximumNumberOfLines = 1
|
|
||||||
self.titleNode.truncationMode = .byTruncatingTail
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
|
func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
|
||||||
self.theme = theme
|
self.theme = theme
|
||||||
if let attributedString = self.titleNode.attributedText?.mutableCopy() as? NSMutableAttributedString {
|
self.headerNode.updateTheme(theme: theme)
|
||||||
attributedString.addAttribute(NSAttributedString.Key.foregroundColor, value: theme.list.itemPrimaryTextColor, range: NSMakeRange(0, attributedString.length))
|
|
||||||
self.titleNode.attributedText = attributedString
|
|
||||||
}
|
|
||||||
|
|
||||||
self.strings = strings
|
self.strings = strings
|
||||||
|
self.headerNode.title = stringForMonth(strings: strings, month: self.month, ofYear: self.year).uppercased()
|
||||||
|
|
||||||
self.backgroundNode.backgroundColor = theme.list.plainBackgroundColor.withAlphaComponent(0.9)
|
|
||||||
self.setNeedsLayout()
|
self.setNeedsLayout()
|
||||||
}
|
}
|
||||||
|
|
||||||
override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) {
|
override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) {
|
||||||
let titleSize = self.titleNode.measure(CGSize(width: size.width - leftInset - rightInset - 24.0, height: CGFloat.greatestFiniteMagnitude))
|
let headerFrame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: size.width, height: size.height + UIScreenPixel))
|
||||||
self.titleNode.frame = CGRect(origin: CGPoint(x: leftInset + 12.0, y: floor((size.height - titleSize.height) / 2.0)), size: titleSize)
|
self.headerNode.frame = headerFrame
|
||||||
self.backgroundNode.frame = CGRect(origin: CGPoint(), size: size)
|
self.headerNode.updateLayout(size: headerFrame.size, leftInset: leftInset, rightInset: rightInset)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,9 +13,11 @@ import AccountContext
|
|||||||
import TelegramStringFormatting
|
import TelegramStringFormatting
|
||||||
import AccountContext
|
import AccountContext
|
||||||
import RadialStatusNode
|
import RadialStatusNode
|
||||||
|
import SemanticStatusNode
|
||||||
import PhotoResources
|
import PhotoResources
|
||||||
import MusicAlbumArtResources
|
import MusicAlbumArtResources
|
||||||
import UniversalMediaPlayer
|
import UniversalMediaPlayer
|
||||||
|
import ContextUI
|
||||||
|
|
||||||
private let extensionImageCache = Atomic<[UInt32: UIImage]>(value: [:])
|
private let extensionImageCache = Atomic<[UInt32: UIImage]>(value: [:])
|
||||||
|
|
||||||
@ -41,50 +43,19 @@ private let extensionColorsMap: [String: (UInt32, UInt32)] = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
private func generateExtensionImage(colors: (UInt32, UInt32)) -> UIImage? {
|
private func generateExtensionImage(colors: (UInt32, UInt32)) -> UIImage? {
|
||||||
return generateImage(CGSize(width: 42.0, height: 42.0), contextGenerator: { size, context in
|
return generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in
|
||||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||||
|
|
||||||
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
|
|
||||||
context.scaleBy(x: 1.0, y: -1.0)
|
|
||||||
context.translateBy(x: -size.width / 2.0 + 1.0, y: -size.height / 2.0 + 1.0)
|
|
||||||
|
|
||||||
let radius: CGFloat = 2.0
|
|
||||||
let cornerSize: CGFloat = 10.0
|
|
||||||
let size = CGSize(width: 42.0, height: 42.0)
|
|
||||||
|
|
||||||
context.setFillColor(UIColor(rgb: colors.0).cgColor)
|
context.setFillColor(UIColor(rgb: colors.0).cgColor)
|
||||||
|
let _ = try? drawSvgPath(context, path: "M6,0 L26.7573593,0 C27.5530088,-8.52837125e-16 28.3160705,0.316070521 28.8786797,0.878679656 L39.1213203,11.1213203 C39.6839295,11.6839295 40,12.4469912 40,13.2426407 L40,34 C40,37.3137085 37.3137085,40 34,40 L6,40 C2.6862915,40 4.05812251e-16,37.3137085 0,34 L0,6 C-4.05812251e-16,2.6862915 2.6862915,6.08718376e-16 6,0 Z ")
|
||||||
|
|
||||||
context.beginPath()
|
context.beginPath()
|
||||||
context.move(to: CGPoint(x: 0.0, y: radius))
|
let _ = try? drawSvgPath(context, path: "M6,0 L26.7573593,0 C27.5530088,-8.52837125e-16 28.3160705,0.316070521 28.8786797,0.878679656 L39.1213203,11.1213203 C39.6839295,11.6839295 40,12.4469912 40,13.2426407 L40,34 C40,37.3137085 37.3137085,40 34,40 L6,40 C2.6862915,40 4.05812251e-16,37.3137085 0,34 L0,6 C-4.05812251e-16,2.6862915 2.6862915,6.08718376e-16 6,0 ")
|
||||||
if !radius.isZero {
|
context.clip()
|
||||||
context.addArc(tangent1End: CGPoint(x: 0.0, y: 0.0), tangent2End: CGPoint(x: radius, y: 0.0), radius: radius)
|
|
||||||
}
|
|
||||||
context.addLine(to: CGPoint(x: size.width - cornerSize, y: 0.0))
|
|
||||||
context.addLine(to: CGPoint(x: size.width - cornerSize + cornerSize / 4.0, y: cornerSize - cornerSize / 4.0))
|
|
||||||
context.addLine(to: CGPoint(x: size.width, y: cornerSize))
|
|
||||||
context.addLine(to: CGPoint(x: size.width, y: size.height - radius))
|
|
||||||
if !radius.isZero {
|
|
||||||
context.addArc(tangent1End: CGPoint(x: size.width, y: size.height), tangent2End: CGPoint(x: size.width - radius, y: size.height), radius: radius)
|
|
||||||
}
|
|
||||||
context.addLine(to: CGPoint(x: radius, y: size.height))
|
|
||||||
|
|
||||||
if !radius.isZero {
|
context.setFillColor(UIColor(rgb: colors.0).withMultipliedBrightnessBy(0.85).cgColor)
|
||||||
context.addArc(tangent1End: CGPoint(x: 0.0, y: size.height), tangent2End: CGPoint(x: 0.0, y: size.height - radius), radius: radius)
|
context.translateBy(x: 40.0 - 14.0, y: 0.0)
|
||||||
}
|
let _ = try? drawSvgPath(context, path: "M-1,0 L14,0 L14,15 L14,14 C14,12.8954305 13.1045695,12 12,12 L4,12 C2.8954305,12 2,11.1045695 2,10 L2,2 C2,0.8954305 1.1045695,-2.02906125e-16 0,0 L-1,0 L-1,0 Z ")
|
||||||
context.closePath()
|
|
||||||
context.fillPath()
|
|
||||||
|
|
||||||
context.setFillColor(UIColor(rgb: colors.1).cgColor)
|
|
||||||
context.beginPath()
|
|
||||||
context.move(to: CGPoint(x: size.width - cornerSize, y: 0.0))
|
|
||||||
context.addLine(to: CGPoint(x: size.width, y: cornerSize))
|
|
||||||
context.addLine(to: CGPoint(x: size.width - cornerSize + radius, y: cornerSize))
|
|
||||||
|
|
||||||
if !radius.isZero {
|
|
||||||
context.addArc(tangent1End: CGPoint(x: size.width - cornerSize, y: cornerSize), tangent2End: CGPoint(x: size.width - cornerSize, y: cornerSize - radius), radius: radius)
|
|
||||||
}
|
|
||||||
|
|
||||||
context.closePath()
|
|
||||||
context.fillPath()
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,7 +86,7 @@ private func extensionImage(fileExtension: String?) -> UIImage? {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private let extensionFont = Font.medium(13.0)
|
private let extensionFont = Font.with(size: 15.0, design: .round, traits: [.bold])
|
||||||
|
|
||||||
private struct FetchControls {
|
private struct FetchControls {
|
||||||
let fetch: () -> Void
|
let fetch: () -> Void
|
||||||
@ -151,11 +122,16 @@ private enum FileIconImage: Equatable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AudioWaveformNode: CustomMediaPlayerScrubbingForegroundNode {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
final class ListMessageFileItemNode: ListMessageNode {
|
final class ListMessageFileItemNode: ListMessageNode {
|
||||||
|
private let contextSourceNode: ContextExtractedContentContainingNode
|
||||||
|
private let containerNode: ContextControllerSourceNode
|
||||||
|
private let extractedBackgroundImageNode: ASImageNode
|
||||||
|
|
||||||
|
private var extractedRect: CGRect?
|
||||||
|
private var nonExtractedRect: CGRect?
|
||||||
|
|
||||||
|
private let offsetContainerNode: ASDisplayNode
|
||||||
|
|
||||||
private let highlightedBackgroundNode: ASDisplayNode
|
private let highlightedBackgroundNode: ASDisplayNode
|
||||||
private let separatorNode: ASDisplayNode
|
private let separatorNode: ASDisplayNode
|
||||||
|
|
||||||
@ -168,12 +144,7 @@ final class ListMessageFileItemNode: ListMessageNode {
|
|||||||
private let extensionIconNode: ASImageNode
|
private let extensionIconNode: ASImageNode
|
||||||
private let extensionIconText: TextNode
|
private let extensionIconText: TextNode
|
||||||
private let iconImageNode: TransformImageNode
|
private let iconImageNode: TransformImageNode
|
||||||
private let statusButtonNode: HighlightTrackingButtonNode
|
private let iconStatusNode: SemanticStatusNode
|
||||||
private let statusNode: RadialStatusNode
|
|
||||||
|
|
||||||
private var waveformNode: AudioWaveformNode?
|
|
||||||
private var waveformForegroundNode: AudioWaveformNode?
|
|
||||||
private var waveformScrubbingNode: MediaPlayerScrubbingNode?
|
|
||||||
|
|
||||||
private var currentIconImage: FileIconImage?
|
private var currentIconImage: FileIconImage?
|
||||||
private var currentMedia: Media?
|
private var currentMedia: Media?
|
||||||
@ -187,10 +158,7 @@ final class ListMessageFileItemNode: ListMessageNode {
|
|||||||
private let playbackStatus = Promise<MediaPlayerStatus>()
|
private let playbackStatus = Promise<MediaPlayerStatus>()
|
||||||
|
|
||||||
private var downloadStatusIconNode: ASImageNode
|
private var downloadStatusIconNode: ASImageNode
|
||||||
private var linearProgressNode: ASDisplayNode
|
private var linearProgressNode: LinearProgressNode?
|
||||||
|
|
||||||
private let progressNode: RadialProgressNode
|
|
||||||
private var playbackOverlayNode: ListMessagePlaybackOverlayNode?
|
|
||||||
|
|
||||||
private var context: AccountContext?
|
private var context: AccountContext?
|
||||||
private (set) var message: Message?
|
private (set) var message: Message?
|
||||||
@ -200,15 +168,20 @@ final class ListMessageFileItemNode: ListMessageNode {
|
|||||||
private var contentSizeValue: CGSize?
|
private var contentSizeValue: CGSize?
|
||||||
private var currentLeftOffset: CGFloat = 0.0
|
private var currentLeftOffset: CGFloat = 0.0
|
||||||
|
|
||||||
override var canBeLongTapped: Bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
public required init() {
|
public required init() {
|
||||||
|
self.contextSourceNode = ContextExtractedContentContainingNode()
|
||||||
|
self.containerNode = ContextControllerSourceNode()
|
||||||
|
|
||||||
self.separatorNode = ASDisplayNode()
|
self.separatorNode = ASDisplayNode()
|
||||||
self.separatorNode.displaysAsynchronously = false
|
self.separatorNode.displaysAsynchronously = false
|
||||||
self.separatorNode.isLayerBacked = true
|
self.separatorNode.isLayerBacked = true
|
||||||
|
|
||||||
|
self.extractedBackgroundImageNode = ASImageNode()
|
||||||
|
self.extractedBackgroundImageNode.displaysAsynchronously = false
|
||||||
|
self.extractedBackgroundImageNode.alpha = 0.0
|
||||||
|
|
||||||
|
self.offsetContainerNode = ASDisplayNode()
|
||||||
|
|
||||||
self.highlightedBackgroundNode = ASDisplayNode()
|
self.highlightedBackgroundNode = ASDisplayNode()
|
||||||
self.highlightedBackgroundNode.isLayerBacked = true
|
self.highlightedBackgroundNode.isLayerBacked = true
|
||||||
|
|
||||||
@ -234,45 +207,60 @@ final class ListMessageFileItemNode: ListMessageNode {
|
|||||||
self.iconImageNode.displaysAsynchronously = false
|
self.iconImageNode.displaysAsynchronously = false
|
||||||
self.iconImageNode.contentAnimations = .subsequentUpdates
|
self.iconImageNode.contentAnimations = .subsequentUpdates
|
||||||
|
|
||||||
self.statusButtonNode = HighlightTrackingButtonNode()
|
self.iconStatusNode = SemanticStatusNode(backgroundNodeColor: .clear, foregroundNodeColor: .white)
|
||||||
self.statusNode = RadialStatusNode(backgroundNodeColor: .clear)
|
self.iconStatusNode.isUserInteractionEnabled = false
|
||||||
self.statusNode.isUserInteractionEnabled = false
|
|
||||||
|
|
||||||
self.downloadStatusIconNode = ASImageNode()
|
self.downloadStatusIconNode = ASImageNode()
|
||||||
self.downloadStatusIconNode.isLayerBacked = true
|
self.downloadStatusIconNode.isLayerBacked = true
|
||||||
self.downloadStatusIconNode.displaysAsynchronously = false
|
self.downloadStatusIconNode.displaysAsynchronously = false
|
||||||
self.downloadStatusIconNode.displayWithoutProcessing = true
|
self.downloadStatusIconNode.displayWithoutProcessing = true
|
||||||
|
|
||||||
self.progressNode = RadialProgressNode(theme: RadialProgressTheme(backgroundColor: .black, foregroundColor: .white, icon: nil))
|
|
||||||
//self.progressNode.isLayerBacked = true
|
|
||||||
|
|
||||||
self.linearProgressNode = ASDisplayNode()
|
|
||||||
self.linearProgressNode.isLayerBacked = true
|
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
self.addSubnode(self.separatorNode)
|
self.addSubnode(self.separatorNode)
|
||||||
self.addSubnode(self.titleNode)
|
|
||||||
self.addSubnode(self.progressNode)
|
|
||||||
self.addSubnode(self.descriptionNode)
|
|
||||||
self.addSubnode(self.descriptionProgressNode)
|
|
||||||
self.addSubnode(self.extensionIconNode)
|
|
||||||
self.addSubnode(self.extensionIconText)
|
|
||||||
self.addSubnode(self.statusNode)
|
|
||||||
self.addSubnode(self.statusButtonNode)
|
|
||||||
|
|
||||||
self.statusButtonNode.highligthedChanged = { [weak self] highlighted in
|
self.containerNode.addSubnode(self.contextSourceNode)
|
||||||
if let strongSelf = self {
|
self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode
|
||||||
if highlighted {
|
self.addSubnode(self.containerNode)
|
||||||
strongSelf.statusNode.layer.removeAnimation(forKey: "opacity")
|
|
||||||
strongSelf.statusNode.alpha = 0.4
|
self.contextSourceNode.contentNode.addSubnode(self.extractedBackgroundImageNode)
|
||||||
} else {
|
self.contextSourceNode.contentNode.addSubnode(self.offsetContainerNode)
|
||||||
strongSelf.statusNode.alpha = 1.0
|
self.offsetContainerNode.addSubnode(self.titleNode)
|
||||||
strongSelf.statusNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
self.offsetContainerNode.addSubnode(self.descriptionNode)
|
||||||
}
|
self.offsetContainerNode.addSubnode(self.descriptionProgressNode)
|
||||||
|
self.offsetContainerNode.addSubnode(self.extensionIconNode)
|
||||||
|
self.offsetContainerNode.addSubnode(self.extensionIconText)
|
||||||
|
self.offsetContainerNode.addSubnode(self.iconStatusNode)
|
||||||
|
|
||||||
|
self.containerNode.activated = { [weak self] gesture, _ in
|
||||||
|
guard let strongSelf = self, let item = strongSelf.item else {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
item.controllerInteraction.openMessageContextMenu(item.message, false, strongSelf.contextSourceNode, strongSelf.contextSourceNode.bounds, gesture)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.contextSourceNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, transition in
|
||||||
|
guard let strongSelf = self, let item = strongSelf.item else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if isExtracted {
|
||||||
|
strongSelf.extractedBackgroundImageNode.image = generateStretchableFilledCircleImage(diameter: 28.0, color: item.theme.list.plainBackgroundColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let extractedRect = strongSelf.extractedRect, let nonExtractedRect = strongSelf.nonExtractedRect {
|
||||||
|
let rect = isExtracted ? extractedRect : nonExtractedRect
|
||||||
|
transition.updateFrame(node: strongSelf.extractedBackgroundImageNode, frame: rect)
|
||||||
|
}
|
||||||
|
|
||||||
|
transition.updateSublayerTransformOffset(layer: strongSelf.offsetContainerNode.layer, offset: CGPoint(x: isExtracted ? 12.0 : 0.0, y: 0.0))
|
||||||
|
transition.updateAlpha(node: strongSelf.extractedBackgroundImageNode, alpha: isExtracted ? 1.0 : 0.0, completion: { _ in
|
||||||
|
if !isExtracted {
|
||||||
|
self?.extractedBackgroundImageNode.image = nil
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
self.statusButtonNode.addTarget(self, action: #selector(self.statusPressed), forControlEvents: .touchUpInside)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
@ -331,9 +319,9 @@ final class ListMessageFileItemNode: ListMessageNode {
|
|||||||
updatedTheme = item.theme
|
updatedTheme = item.theme
|
||||||
}
|
}
|
||||||
|
|
||||||
let titleFont = Font.medium(floor(item.fontSize.baseDisplaySize * 16.0 / 17.0))
|
let titleFont = Font.semibold(floor(item.fontSize.baseDisplaySize * 16.0 / 17.0))
|
||||||
let audioTitleFont = Font.regular(floor(item.fontSize.baseDisplaySize * 16.0 / 17.0))
|
let audioTitleFont = Font.semibold(floor(item.fontSize.baseDisplaySize * 16.0 / 17.0))
|
||||||
let descriptionFont = Font.regular(floor(item.fontSize.baseDisplaySize * 13.0 / 17.0))
|
let descriptionFont = Font.regular(floor(item.fontSize.baseDisplaySize * 14.0 / 17.0))
|
||||||
|
|
||||||
var leftInset: CGFloat = 65.0 + params.leftInset
|
var leftInset: CGFloat = 65.0 + params.leftInset
|
||||||
let rightInset: CGFloat = 8.0 + params.rightInset
|
let rightInset: CGFloat = 8.0 + params.rightInset
|
||||||
@ -356,7 +344,6 @@ final class ListMessageFileItemNode: ListMessageNode {
|
|||||||
var updatedStatusSignal: Signal<FileMediaResourceStatus, NoError>?
|
var updatedStatusSignal: Signal<FileMediaResourceStatus, NoError>?
|
||||||
var updatedPlaybackStatusSignal: Signal<MediaPlayerStatus, NoError>?
|
var updatedPlaybackStatusSignal: Signal<MediaPlayerStatus, NoError>?
|
||||||
var updatedFetchControls: FetchControls?
|
var updatedFetchControls: FetchControls?
|
||||||
var waveform: AudioWaveform?
|
|
||||||
|
|
||||||
var isAudio = false
|
var isAudio = false
|
||||||
var isVoice = false
|
var isVoice = false
|
||||||
@ -372,7 +359,7 @@ final class ListMessageFileItemNode: ListMessageNode {
|
|||||||
isInstantVideo = file.isInstantVideo
|
isInstantVideo = file.isInstantVideo
|
||||||
|
|
||||||
for attribute in file.attributes {
|
for attribute in file.attributes {
|
||||||
if case let .Audio(voice, _, title, performer, waveformValue) = attribute {
|
if case let .Audio(voice, duration, title, performer, _) = attribute {
|
||||||
isAudio = true
|
isAudio = true
|
||||||
isVoice = voice
|
isVoice = voice
|
||||||
|
|
||||||
@ -380,7 +367,7 @@ final class ListMessageFileItemNode: ListMessageNode {
|
|||||||
|
|
||||||
let descriptionString: String
|
let descriptionString: String
|
||||||
if let performer = performer {
|
if let performer = performer {
|
||||||
descriptionString = performer
|
descriptionString = "\(stringForDuration(Int32(duration))) • \(performer)"
|
||||||
} else if let size = file.size {
|
} else if let size = file.size {
|
||||||
descriptionString = dataSizeString(size, decimalSeparator: item.dateTimeFormat.decimalSeparator)
|
descriptionString = dataSizeString(size, decimalSeparator: item.dateTimeFormat.decimalSeparator)
|
||||||
} else {
|
} else {
|
||||||
@ -394,16 +381,39 @@ final class ListMessageFileItemNode: ListMessageNode {
|
|||||||
} else {
|
} else {
|
||||||
titleText = NSAttributedString(string: " ", font: audioTitleFont, textColor: item.theme.list.itemPrimaryTextColor)
|
titleText = NSAttributedString(string: " ", font: audioTitleFont, textColor: item.theme.list.itemPrimaryTextColor)
|
||||||
descriptionText = NSAttributedString(string: item.message.author?.displayTitle(strings: item.strings, displayOrder: .firstLast) ?? " ", font: descriptionFont, textColor: item.theme.list.itemSecondaryTextColor)
|
descriptionText = NSAttributedString(string: item.message.author?.displayTitle(strings: item.strings, displayOrder: .firstLast) ?? " ", font: descriptionFont, textColor: item.theme.list.itemSecondaryTextColor)
|
||||||
waveformValue?.withDataNoCopy { data in
|
|
||||||
waveform = AudioWaveform(bitstream: data, bitsPerSample: 5)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if isInstantVideo {
|
if isInstantVideo || isVoice {
|
||||||
titleText = NSAttributedString(string: item.strings.Message_VideoMessage, font: audioTitleFont, textColor: item.theme.list.itemPrimaryTextColor)
|
let authorName: String
|
||||||
descriptionText = NSAttributedString(string: item.message.author?.displayTitle(strings: item.strings, displayOrder: .firstLast) ?? " ", font: descriptionFont, textColor: item.theme.list.itemSecondaryTextColor)
|
if let author = message.forwardInfo?.author {
|
||||||
|
if author.id == item.context.account.peerId {
|
||||||
|
authorName = item.strings.DialogList_You
|
||||||
|
} else {
|
||||||
|
authorName = author.displayTitle(strings: item.strings, displayOrder: .firstLast)
|
||||||
|
}
|
||||||
|
} else if let signature = message.forwardInfo?.authorSignature {
|
||||||
|
authorName = signature
|
||||||
|
} else if let author = message.author {
|
||||||
|
if author.id == item.context.account.peerId {
|
||||||
|
authorName = item.strings.DialogList_You
|
||||||
|
} else {
|
||||||
|
authorName = author.displayTitle(strings: item.strings, displayOrder: .firstLast)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
authorName = " "
|
||||||
|
}
|
||||||
|
titleText = NSAttributedString(string: authorName, font: audioTitleFont, textColor: item.theme.list.itemPrimaryTextColor)
|
||||||
|
let dateString = stringForFullDate(timestamp: item.message.timestamp, strings: item.strings, dateTimeFormat: item.dateTimeFormat)
|
||||||
|
let descriptionString: String
|
||||||
|
if let duration = file.duration {
|
||||||
|
descriptionString = "\(stringForDuration(Int32(duration))) • \(dateString)"
|
||||||
|
} else {
|
||||||
|
descriptionString = dateString
|
||||||
|
}
|
||||||
|
|
||||||
|
descriptionText = NSAttributedString(string: descriptionString, font: descriptionFont, textColor: item.theme.list.itemSecondaryTextColor)
|
||||||
iconImage = .roundVideo(file)
|
iconImage = .roundVideo(file)
|
||||||
} else if !isAudio {
|
} else if !isAudio {
|
||||||
let fileName: String = file.fileName ?? ""
|
let fileName: String = file.fileName ?? ""
|
||||||
@ -437,10 +447,7 @@ final class ListMessageFileItemNode: ListMessageNode {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if isAudio && !isVoice {
|
|
||||||
leftInset += 14.0
|
|
||||||
}
|
|
||||||
|
|
||||||
var mediaUpdated = false
|
var mediaUpdated = false
|
||||||
if let currentMedia = currentMedia {
|
if let currentMedia = currentMedia {
|
||||||
@ -492,7 +499,7 @@ final class ListMessageFileItemNode: ListMessageNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let (titleNodeLayout, titleNodeApply) = titleNodeMakeLayout(TextNodeLayoutArguments(attributedString: titleText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 40.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
let (titleNodeLayout, titleNodeApply) = titleNodeMakeLayout(TextNodeLayoutArguments(attributedString: titleText, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .middle, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 40.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||||
|
|
||||||
let (descriptionNodeLayout, descriptionNodeApply) = descriptionNodeMakeLayout(TextNodeLayoutArguments(attributedString: descriptionText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 12.0 - 40.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
let (descriptionNodeLayout, descriptionNodeApply) = descriptionNodeMakeLayout(TextNodeLayoutArguments(attributedString: descriptionText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 12.0 - 40.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||||
|
|
||||||
@ -502,18 +509,18 @@ final class ListMessageFileItemNode: ListMessageNode {
|
|||||||
if let iconImage = iconImage {
|
if let iconImage = iconImage {
|
||||||
switch iconImage {
|
switch iconImage {
|
||||||
case let .imageRepresentation(_, representation):
|
case let .imageRepresentation(_, representation):
|
||||||
let iconSize = CGSize(width: 42.0, height: 42.0)
|
let iconSize = CGSize(width: 40.0, height: 40.0)
|
||||||
let imageCorners = ImageCorners(topLeft: .Corner(4.0), topRight: .Corner(4.0), bottomLeft: .Corner(4.0), bottomRight: .Corner(4.0))
|
let imageCorners = ImageCorners(radius: 6.0)
|
||||||
let arguments = TransformImageArguments(corners: imageCorners, imageSize: representation.dimensions.cgSize.aspectFilled(iconSize), boundingSize: iconSize, intrinsicInsets: UIEdgeInsets(), emptyColor: item.theme.list.mediaPlaceholderColor)
|
let arguments = TransformImageArguments(corners: imageCorners, imageSize: representation.dimensions.cgSize.aspectFilled(iconSize), boundingSize: iconSize, intrinsicInsets: UIEdgeInsets(), emptyColor: item.theme.list.mediaPlaceholderColor)
|
||||||
iconImageApply = iconImageLayout(arguments)
|
iconImageApply = iconImageLayout(arguments)
|
||||||
case .albumArt:
|
case .albumArt:
|
||||||
let iconSize = CGSize(width: 46.0, height: 46.0)
|
let iconSize = CGSize(width: 40.0, height: 40.0)
|
||||||
let imageCorners = ImageCorners(topLeft: .Corner(4.0), topRight: .Corner(4.0), bottomLeft: .Corner(4.0), bottomRight: .Corner(4.0))
|
let imageCorners = ImageCorners(radius: iconSize.width / 2.0)
|
||||||
let arguments = TransformImageArguments(corners: imageCorners, imageSize: iconSize, boundingSize: iconSize, intrinsicInsets: UIEdgeInsets(), emptyColor: item.theme.list.mediaPlaceholderColor)
|
let arguments = TransformImageArguments(corners: imageCorners, imageSize: iconSize, boundingSize: iconSize, intrinsicInsets: UIEdgeInsets(), emptyColor: item.theme.list.mediaPlaceholderColor)
|
||||||
iconImageApply = iconImageLayout(arguments)
|
iconImageApply = iconImageLayout(arguments)
|
||||||
case let .roundVideo(file):
|
case let .roundVideo(file):
|
||||||
let iconSize = CGSize(width: 42.0, height: 42.0)
|
let iconSize = CGSize(width: 40.0, height: 40.0)
|
||||||
let imageCorners = ImageCorners(topLeft: .Corner(iconSize.width / 2.0), topRight: .Corner(iconSize.width / 2.0), bottomLeft: .Corner(iconSize.width / 2.0), bottomRight: .Corner(iconSize.width / 2.0))
|
let imageCorners = ImageCorners(radius: iconSize.width / 2.0)
|
||||||
let arguments = TransformImageArguments(corners: imageCorners, imageSize: (file.dimensions ?? PixelDimensions(width: 320, height: 320)).cgSize.aspectFilled(iconSize), boundingSize: iconSize, intrinsicInsets: UIEdgeInsets(), emptyColor: item.theme.list.mediaPlaceholderColor)
|
let arguments = TransformImageArguments(corners: imageCorners, imageSize: (file.dimensions ?? PixelDimensions(width: 320, height: 320)).cgSize.aspectFilled(iconSize), boundingSize: iconSize, intrinsicInsets: UIEdgeInsets(), emptyColor: item.theme.list.mediaPlaceholderColor)
|
||||||
iconImageApply = iconImageLayout(arguments)
|
iconImageApply = iconImageLayout(arguments)
|
||||||
}
|
}
|
||||||
@ -525,9 +532,9 @@ final class ListMessageFileItemNode: ListMessageNode {
|
|||||||
case let .imageRepresentation(file, representation):
|
case let .imageRepresentation(file, representation):
|
||||||
updateIconImageSignal = chatWebpageSnippetFile(account: item.context.account, fileReference: .message(message: MessageReference(message), media: file), representation: representation)
|
updateIconImageSignal = chatWebpageSnippetFile(account: item.context.account, fileReference: .message(message: MessageReference(message), media: file), representation: representation)
|
||||||
case let .albumArt(file, albumArt):
|
case let .albumArt(file, albumArt):
|
||||||
updateIconImageSignal = playerAlbumArt(postbox: item.context.account.postbox, fileReference: .message(message: MessageReference(message), media: file), albumArt: albumArt, thumbnail: true)
|
updateIconImageSignal = playerAlbumArt(postbox: item.context.account.postbox, fileReference: .message(message: MessageReference(message), media: file), albumArt: albumArt, thumbnail: true, overlayColor: UIColor(white: 0.0, alpha: 0.3), emptyColor: item.theme.list.itemAccentColor)
|
||||||
case let .roundVideo(file):
|
case let .roundVideo(file):
|
||||||
updateIconImageSignal = mediaGridMessageVideo(postbox: item.context.account.postbox, videoReference: FileMediaReference.message(message: MessageReference(message), media: file), autoFetchFullSizeThumbnail: true)
|
updateIconImageSignal = mediaGridMessageVideo(postbox: item.context.account.postbox, videoReference: FileMediaReference.message(message: MessageReference(message), media: file), autoFetchFullSizeThumbnail: true, overlayColor: UIColor(white: 0.0, alpha: 0.3))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
updateIconImageSignal = .complete()
|
updateIconImageSignal = .complete()
|
||||||
@ -550,6 +557,23 @@ final class ListMessageFileItemNode: ListMessageNode {
|
|||||||
transition = .immediate
|
transition = .immediate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: nodeLayout.contentSize)
|
||||||
|
strongSelf.contextSourceNode.frame = CGRect(origin: CGPoint(), size: nodeLayout.contentSize)
|
||||||
|
strongSelf.offsetContainerNode.frame = CGRect(origin: CGPoint(), size: nodeLayout.contentSize)
|
||||||
|
strongSelf.contextSourceNode.contentNode.frame = CGRect(origin: CGPoint(), size: nodeLayout.contentSize)
|
||||||
|
|
||||||
|
let nonExtractedRect = CGRect(origin: CGPoint(), size: CGSize(width: nodeLayout.contentSize.width - 16.0, height: nodeLayout.contentSize.height))
|
||||||
|
let extractedRect = CGRect(origin: CGPoint(), size: nodeLayout.contentSize).insetBy(dx: 16.0, dy: 0.0)
|
||||||
|
strongSelf.extractedRect = extractedRect
|
||||||
|
strongSelf.nonExtractedRect = nonExtractedRect
|
||||||
|
|
||||||
|
if strongSelf.contextSourceNode.isExtractedToContextPreview {
|
||||||
|
strongSelf.extractedBackgroundImageNode.frame = extractedRect
|
||||||
|
} else {
|
||||||
|
strongSelf.extractedBackgroundImageNode.frame = nonExtractedRect
|
||||||
|
}
|
||||||
|
strongSelf.contextSourceNode.contentRect = extractedRect
|
||||||
|
|
||||||
strongSelf.currentMedia = selectedMedia
|
strongSelf.currentMedia = selectedMedia
|
||||||
strongSelf.message = message
|
strongSelf.message = message
|
||||||
strongSelf.context = item.context
|
strongSelf.context = item.context
|
||||||
@ -561,9 +585,7 @@ final class ListMessageFileItemNode: ListMessageNode {
|
|||||||
if let _ = updatedTheme {
|
if let _ = updatedTheme {
|
||||||
strongSelf.separatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor
|
strongSelf.separatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor
|
||||||
strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor
|
strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor
|
||||||
|
strongSelf.linearProgressNode?.updateTheme(theme: item.theme)
|
||||||
strongSelf.progressNode.updateTheme(RadialProgressTheme(backgroundColor: item.theme.list.itemAccentColor, foregroundColor: item.theme.list.plainBackgroundColor, icon: nil))
|
|
||||||
strongSelf.linearProgressNode.backgroundColor = item.theme.list.itemAccentColor
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let (selectionWidth, selectionApply) = selectionNodeWidthAndApply {
|
if let (selectionWidth, selectionApply) = selectionNodeWidthAndApply {
|
||||||
@ -572,7 +594,7 @@ final class ListMessageFileItemNode: ListMessageNode {
|
|||||||
if selectionNode !== strongSelf.selectionNode {
|
if selectionNode !== strongSelf.selectionNode {
|
||||||
strongSelf.selectionNode?.removeFromSupernode()
|
strongSelf.selectionNode?.removeFromSupernode()
|
||||||
strongSelf.selectionNode = selectionNode
|
strongSelf.selectionNode = selectionNode
|
||||||
strongSelf.addSubnode(selectionNode)
|
strongSelf.contextSourceNode.contentNode.addSubnode(selectionNode)
|
||||||
selectionNode.frame = selectionFrame
|
selectionNode.frame = selectionFrame
|
||||||
transition.animatePosition(node: selectionNode, from: CGPoint(x: -selectionFrame.size.width / 2.0, y: selectionFrame.midY))
|
transition.animatePosition(node: selectionNode, from: CGPoint(x: -selectionFrame.size.width / 2.0, y: selectionFrame.midY))
|
||||||
} else {
|
} else {
|
||||||
@ -589,7 +611,7 @@ final class ListMessageFileItemNode: ListMessageNode {
|
|||||||
transition.updateFrame(node: strongSelf.separatorNode, frame: CGRect(origin: CGPoint(x: leftInset + leftOffset, y: nodeLayout.contentSize.height - UIScreenPixel), size: CGSize(width: params.width - leftInset - leftOffset, height: UIScreenPixel)))
|
transition.updateFrame(node: strongSelf.separatorNode, frame: CGRect(origin: CGPoint(x: leftInset + leftOffset, y: nodeLayout.contentSize.height - UIScreenPixel), size: CGSize(width: params.width - leftInset - leftOffset, height: UIScreenPixel)))
|
||||||
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel - nodeLayout.insets.top), size: CGSize(width: params.width, height: nodeLayout.size.height + UIScreenPixel))
|
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel - nodeLayout.insets.top), size: CGSize(width: params.width, height: nodeLayout.size.height + UIScreenPixel))
|
||||||
|
|
||||||
transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset, y: 8.0), size: titleNodeLayout.size))
|
transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset, y: 9.0), size: titleNodeLayout.size))
|
||||||
let _ = titleNodeApply()
|
let _ = titleNodeApply()
|
||||||
|
|
||||||
var descriptionOffset: CGFloat = 0.0
|
var descriptionOffset: CGFloat = 0.0
|
||||||
@ -607,67 +629,27 @@ final class ListMessageFileItemNode: ListMessageNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
transition.updateFrame(node: strongSelf.descriptionNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset + descriptionOffset, y: strongSelf.titleNode.frame.maxY + 3.0), size: descriptionNodeLayout.size))
|
transition.updateFrame(node: strongSelf.descriptionNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset + descriptionOffset, y: strongSelf.titleNode.frame.maxY + 1.0), size: descriptionNodeLayout.size))
|
||||||
let _ = descriptionNodeApply()
|
let _ = descriptionNodeApply()
|
||||||
|
|
||||||
let iconFrame: CGRect
|
let iconFrame: CGRect
|
||||||
if isAudio {
|
if isAudio {
|
||||||
let iconSize = CGSize(width: 48.0, height: 48.0)
|
let iconSize = CGSize(width: 40.0, height: 40.0)
|
||||||
iconFrame = CGRect(origin: CGPoint(x: params.leftInset + leftOffset + 12.0, y: 5.0), size: iconSize)
|
iconFrame = CGRect(origin: CGPoint(x: params.leftInset + leftOffset + 12.0, y: 8.0), size: iconSize)
|
||||||
} else {
|
} else {
|
||||||
let iconSize = CGSize(width: 42.0, height: 42.0)
|
let iconSize = CGSize(width: 40.0, height: 40.0)
|
||||||
iconFrame = CGRect(origin: CGPoint(x: params.leftInset + leftOffset + 12.0, y: 8.0), size: iconSize)
|
iconFrame = CGRect(origin: CGPoint(x: params.leftInset + leftOffset + 12.0, y: 8.0), size: iconSize)
|
||||||
}
|
}
|
||||||
transition.updateFrame(node: strongSelf.extensionIconNode, frame: iconFrame)
|
transition.updateFrame(node: strongSelf.extensionIconNode, frame: iconFrame)
|
||||||
strongSelf.extensionIconNode.image = extensionIconImage
|
strongSelf.extensionIconNode.image = extensionIconImage
|
||||||
transition.updateFrame(node: strongSelf.extensionIconText, frame: CGRect(origin: CGPoint(x: leftOffset + 12.0 + floor((42.0 - extensionTextLayout.size.width) / 2.0), y: 8.0 + floor((42.0 - extensionTextLayout.size.height) / 2.0)), size: extensionTextLayout.size))
|
transition.updateFrame(node: strongSelf.extensionIconText, frame: CGRect(origin: CGPoint(x: iconFrame.minX + floor((iconFrame.width - extensionTextLayout.size.width) / 2.0), y: iconFrame.minY + 2.0 + floor((iconFrame.height - extensionTextLayout.size.height) / 2.0)), size: extensionTextLayout.size))
|
||||||
|
|
||||||
|
transition.updateFrame(node: strongSelf.iconStatusNode, frame: iconFrame)
|
||||||
|
|
||||||
let _ = extensionTextApply()
|
let _ = extensionTextApply()
|
||||||
|
|
||||||
strongSelf.currentIconImage = iconImage
|
strongSelf.currentIconImage = iconImage
|
||||||
|
|
||||||
if isVoice {
|
|
||||||
let waveformNode: AudioWaveformNode
|
|
||||||
let waveformForegroundNode: AudioWaveformNode
|
|
||||||
let waveformScrubbingNode: MediaPlayerScrubbingNode
|
|
||||||
if let current = strongSelf.waveformNode {
|
|
||||||
waveformNode = current
|
|
||||||
} else {
|
|
||||||
waveformNode = AudioWaveformNode()
|
|
||||||
waveformNode.isLayerBacked = true
|
|
||||||
strongSelf.waveformNode = waveformNode
|
|
||||||
strongSelf.addSubnode(waveformNode)
|
|
||||||
}
|
|
||||||
if let current = strongSelf.waveformForegroundNode {
|
|
||||||
waveformForegroundNode = current
|
|
||||||
} else {
|
|
||||||
waveformForegroundNode = AudioWaveformNode()
|
|
||||||
waveformForegroundNode.isLayerBacked = true
|
|
||||||
strongSelf.waveformForegroundNode = waveformForegroundNode
|
|
||||||
strongSelf.addSubnode(waveformForegroundNode)
|
|
||||||
}
|
|
||||||
if let current = strongSelf.waveformScrubbingNode {
|
|
||||||
waveformScrubbingNode = current
|
|
||||||
} else {
|
|
||||||
waveformScrubbingNode = MediaPlayerScrubbingNode(content: .custom(backgroundNode: waveformNode, foregroundContentNode: waveformForegroundNode))
|
|
||||||
waveformScrubbingNode.hitTestSlop = UIEdgeInsets(top: -10.0, left: 0.0, bottom: -10.0, right: 0.0)
|
|
||||||
waveformScrubbingNode.seek = { timestamp in
|
|
||||||
if let strongSelf = self, let context = strongSelf.context, let message = strongSelf.message, let type = peerMessageMediaPlayerType(message) {
|
|
||||||
context.sharedContext.mediaManager.playlistControl(.seek(timestamp), type: type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
waveformScrubbingNode.enableScrubbing = false
|
|
||||||
waveformScrubbingNode.status = strongSelf.playbackStatus.get()
|
|
||||||
strongSelf.waveformScrubbingNode = waveformScrubbingNode
|
|
||||||
strongSelf.addSubnode(waveformScrubbingNode)
|
|
||||||
}
|
|
||||||
|
|
||||||
transition.updateFrame(node: waveformScrubbingNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset, y: 10.0), size: CGSize(width: params.width - leftInset - 16.0, height: 12.0)))
|
|
||||||
|
|
||||||
waveformNode.setup(color: item.theme.list.controlSecondaryColor, waveform: waveform)
|
|
||||||
waveformForegroundNode.setup(color: item.theme.list.itemAccentColor, waveform: waveform)
|
|
||||||
}
|
|
||||||
|
|
||||||
if let iconImageApply = iconImageApply {
|
if let iconImageApply = iconImageApply {
|
||||||
if let updateImageSignal = updateIconImageSignal {
|
if let updateImageSignal = updateIconImageSignal {
|
||||||
strongSelf.iconImageNode.setSignal(updateImageSignal)
|
strongSelf.iconImageNode.setSignal(updateImageSignal)
|
||||||
@ -675,7 +657,7 @@ final class ListMessageFileItemNode: ListMessageNode {
|
|||||||
|
|
||||||
transition.updateFrame(node: strongSelf.iconImageNode, frame: iconFrame)
|
transition.updateFrame(node: strongSelf.iconImageNode, frame: iconFrame)
|
||||||
if strongSelf.iconImageNode.supernode == nil {
|
if strongSelf.iconImageNode.supernode == nil {
|
||||||
strongSelf.addSubnode(strongSelf.iconImageNode)
|
strongSelf.offsetContainerNode.insertSubnode(strongSelf.iconImageNode, belowSubnode: strongSelf.iconStatusNode)
|
||||||
}
|
}
|
||||||
|
|
||||||
iconImageApply()
|
iconImageApply()
|
||||||
@ -690,22 +672,13 @@ final class ListMessageFileItemNode: ListMessageNode {
|
|||||||
strongSelf.iconImageNode.removeFromSupernode()
|
strongSelf.iconImageNode.removeFromSupernode()
|
||||||
|
|
||||||
if strongSelf.extensionIconNode.supernode == nil {
|
if strongSelf.extensionIconNode.supernode == nil {
|
||||||
strongSelf.addSubnode(strongSelf.extensionIconNode)
|
strongSelf.offsetContainerNode.insertSubnode(strongSelf.extensionIconNode, belowSubnode: strongSelf.iconStatusNode)
|
||||||
}
|
}
|
||||||
if strongSelf.extensionIconText.supernode == nil {
|
if strongSelf.extensionIconText.supernode == nil {
|
||||||
strongSelf.addSubnode(strongSelf.extensionIconText)
|
strongSelf.offsetContainerNode.insertSubnode(strongSelf.extensionIconText, belowSubnode: strongSelf.iconStatusNode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let playbackOverlayNode = strongSelf.playbackOverlayNode {
|
|
||||||
transition.updateFrame(node: playbackOverlayNode, frame: iconFrame)
|
|
||||||
}
|
|
||||||
|
|
||||||
let statusSize = CGSize(width: 28.0, height: 28.0)
|
|
||||||
transition.updateFrame(node: strongSelf.statusNode, frame: CGRect(origin: CGPoint(x: params.width - params.rightInset - rightInset - statusSize.width + leftOffset, y: floor((nodeLayout.contentSize.height - statusSize.height) / 2.0)), size: statusSize))
|
|
||||||
|
|
||||||
strongSelf.statusButtonNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - rightInset - 40.0 + leftOffset, y: 0.0), size: CGSize(width: 40.0, height: nodeLayout.contentSize.height))
|
|
||||||
|
|
||||||
if let updatedStatusSignal = updatedStatusSignal {
|
if let updatedStatusSignal = updatedStatusSignal {
|
||||||
strongSelf.statusDisposable.set((updatedStatusSignal
|
strongSelf.statusDisposable.set((updatedStatusSignal
|
||||||
|> deliverOnMainQueue).start(next: { [weak strongSelf] fileStatus in
|
|> deliverOnMainQueue).start(next: { [weak strongSelf] fileStatus in
|
||||||
@ -717,10 +690,7 @@ final class ListMessageFileItemNode: ListMessageNode {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
transition.updateFrame(node: strongSelf.downloadStatusIconNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset, y: strongSelf.descriptionNode.frame.minY + floor((strongSelf.descriptionNode.frame.height - 11.0) / 2.0)), size: CGSize(width: 11.0, height: 11.0)))
|
transition.updateFrame(node: strongSelf.downloadStatusIconNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset, y: strongSelf.descriptionNode.frame.minY + floor((strongSelf.descriptionNode.frame.height - 12.0) / 2.0)), size: CGSize(width: 12.0, height: 12.0)))
|
||||||
|
|
||||||
let progressSize: CGFloat = 40.0
|
|
||||||
transition.updateFrame(node: strongSelf.progressNode, frame: CGRect(origin: CGPoint(x: leftOffset + params.leftInset + floor((leftInset - params.leftInset - progressSize) / 2.0), y: floor((nodeLayout.contentSize.height - progressSize) / 2.0)), size: CGSize(width: progressSize, height: progressSize)))
|
|
||||||
|
|
||||||
if let updatedFetchControls = updatedFetchControls {
|
if let updatedFetchControls = updatedFetchControls {
|
||||||
let _ = strongSelf.fetchControls.swap(updatedFetchControls)
|
let _ = strongSelf.fetchControls.swap(updatedFetchControls)
|
||||||
@ -757,74 +727,44 @@ final class ListMessageFileItemNode: ListMessageNode {
|
|||||||
isInstantVideo = file.isInstantVideo
|
isInstantVideo = file.isInstantVideo
|
||||||
}
|
}
|
||||||
|
|
||||||
self.progressNode.isHidden = !isVoice
|
var iconStatusState: SemanticStatusNodeState = .none
|
||||||
|
var iconStatusBackgroundColor: UIColor = .clear
|
||||||
|
var iconStatusForegroundColor: UIColor = .white
|
||||||
|
|
||||||
|
if isVoice {
|
||||||
|
iconStatusBackgroundColor = item.theme.list.itemAccentColor
|
||||||
|
iconStatusForegroundColor = item.theme.list.itemCheckColors.foregroundColor
|
||||||
|
}
|
||||||
|
|
||||||
var enableScrubbing = false
|
|
||||||
var musicIsPlaying: Bool?
|
|
||||||
var statusState: RadialStatusNodeState = .none
|
|
||||||
if !isAudio && !isInstantVideo {
|
if !isAudio && !isInstantVideo {
|
||||||
self.updateProgressFrame(size: contentSize, leftInset: layoutParams.leftInset, rightInset: layoutParams.rightInset, transition: .immediate)
|
self.updateProgressFrame(size: contentSize, leftInset: layoutParams.leftInset, rightInset: layoutParams.rightInset, transition: .immediate)
|
||||||
} else {
|
} else {
|
||||||
if !isVoice && !isInstantVideo {
|
|
||||||
switch fetchStatus {
|
|
||||||
case let .Fetching(_, progress):
|
|
||||||
let adjustedProgress = max(progress, 0.027)
|
|
||||||
statusState = .cloudProgress(color: item.theme.list.itemAccentColor, strokeBackgroundColor: item.theme.list.itemAccentColor.withAlphaComponent(0.5), lineWidth: 2.0, value: CGFloat(adjustedProgress))
|
|
||||||
case .Local:
|
|
||||||
break
|
|
||||||
case .Remote:
|
|
||||||
if let image = PresentationResourcesItemList.cloudFetchIcon(item.theme) {
|
|
||||||
statusState = .customIcon(image)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.statusNode.transitionToState(statusState, completion: {})
|
|
||||||
self.statusButtonNode.isUserInteractionEnabled = statusState != .none
|
|
||||||
|
|
||||||
switch status {
|
switch status {
|
||||||
case let .fetchStatus(fetchStatus):
|
case let .fetchStatus(fetchStatus):
|
||||||
switch fetchStatus {
|
switch fetchStatus {
|
||||||
case let .Fetching(_, progress):
|
case .Fetching:
|
||||||
let adjustedProgress = max(progress, 0.027)
|
break
|
||||||
self.progressNode.state = .Fetching(progress: adjustedProgress)
|
|
||||||
case .Local:
|
case .Local:
|
||||||
if isAudio {
|
if isAudio || isInstantVideo {
|
||||||
self.progressNode.state = .Play
|
iconStatusState = .play
|
||||||
} else {
|
|
||||||
self.progressNode.state = .Icon
|
|
||||||
}
|
}
|
||||||
case .Remote:
|
case .Remote:
|
||||||
if isAudio {
|
if isAudio || isInstantVideo {
|
||||||
self.progressNode.state = .Play
|
iconStatusState = .play
|
||||||
} else {
|
|
||||||
self.progressNode.state = .Remote
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case let .playbackStatus(playbackStatus):
|
case let .playbackStatus(playbackStatus):
|
||||||
enableScrubbing = true
|
|
||||||
switch playbackStatus {
|
switch playbackStatus {
|
||||||
case .playing:
|
case .playing:
|
||||||
musicIsPlaying = true
|
iconStatusState = .pause
|
||||||
self.progressNode.state = .Pause
|
case .paused:
|
||||||
case .paused:
|
iconStatusState = .play
|
||||||
musicIsPlaying = false
|
|
||||||
self.progressNode.state = .Play
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.waveformScrubbingNode?.enableScrubbing = enableScrubbing
|
self.iconStatusNode.backgroundNodeColor = iconStatusBackgroundColor
|
||||||
if let musicIsPlaying = musicIsPlaying, !isVoice, !isInstantVideo {
|
self.iconStatusNode.foregroundNodeColor = iconStatusForegroundColor
|
||||||
if self.playbackOverlayNode == nil {
|
self.iconStatusNode.transitionToState(iconStatusState)
|
||||||
let playbackOverlayNode = ListMessagePlaybackOverlayNode()
|
|
||||||
playbackOverlayNode.frame = self.iconImageNode.frame
|
|
||||||
self.playbackOverlayNode = playbackOverlayNode
|
|
||||||
self.addSubnode(playbackOverlayNode)
|
|
||||||
}
|
|
||||||
self.playbackOverlayNode?.isPlaying = musicIsPlaying
|
|
||||||
} else if let playbackOverlayNode = self.playbackOverlayNode {
|
|
||||||
self.playbackOverlayNode = nil
|
|
||||||
playbackOverlayNode.removeFromSupernode()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
|
override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
|
||||||
@ -903,35 +843,54 @@ final class ListMessageFileItemNode: ListMessageNode {
|
|||||||
|
|
||||||
switch maybeFetchStatus {
|
switch maybeFetchStatus {
|
||||||
case let .Fetching(_, progress):
|
case let .Fetching(_, progress):
|
||||||
let progressFrame = CGRect(x: self.currentLeftOffset + leftInset + 65.0, y: size.height - 2.0, width: floor((size.width - 65.0 - leftInset - rightInset) * CGFloat(progress)), height: 2.0)
|
let progressFrame = CGRect(x: self.currentLeftOffset + leftInset + 65.0, y: size.height - 2.0, width: floor((size.width - 65.0 - leftInset - rightInset)), height: 3.0)
|
||||||
if self.linearProgressNode.supernode == nil {
|
let linearProgressNode: LinearProgressNode
|
||||||
self.addSubnode(self.linearProgressNode)
|
if let current = self.linearProgressNode {
|
||||||
|
linearProgressNode = current
|
||||||
|
} else {
|
||||||
|
linearProgressNode = LinearProgressNode()
|
||||||
|
linearProgressNode.updateTheme(theme: item.theme)
|
||||||
|
self.linearProgressNode = linearProgressNode
|
||||||
|
self.addSubnode(linearProgressNode)
|
||||||
}
|
}
|
||||||
transition.updateFrame(node: self.linearProgressNode, frame: progressFrame)
|
transition.updateFrame(node: linearProgressNode, frame: progressFrame)
|
||||||
|
linearProgressNode.updateProgress(value: CGFloat(progress), completion: {})
|
||||||
|
|
||||||
if self.downloadStatusIconNode.supernode == nil {
|
if self.downloadStatusIconNode.supernode == nil {
|
||||||
self.addSubnode(self.downloadStatusIconNode)
|
self.offsetContainerNode.addSubnode(self.downloadStatusIconNode)
|
||||||
}
|
}
|
||||||
self.downloadStatusIconNode.image = PresentationResourcesChat.sharedMediaFileDownloadPauseIcon(item.theme)
|
self.downloadStatusIconNode.image = PresentationResourcesChat.sharedMediaFileDownloadPauseIcon(item.theme)
|
||||||
case .Local:
|
case .Local:
|
||||||
if self.linearProgressNode.supernode != nil {
|
if let linearProgressNode = self.linearProgressNode {
|
||||||
self.linearProgressNode.removeFromSupernode()
|
self.linearProgressNode = nil
|
||||||
|
linearProgressNode.updateProgress(value: 1.0, completion: { [weak linearProgressNode] in
|
||||||
|
linearProgressNode?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { _ in
|
||||||
|
linearProgressNode?.removeFromSupernode()
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
if self.downloadStatusIconNode.supernode != nil {
|
if self.downloadStatusIconNode.supernode != nil {
|
||||||
self.downloadStatusIconNode.removeFromSupernode()
|
self.downloadStatusIconNode.removeFromSupernode()
|
||||||
}
|
}
|
||||||
self.downloadStatusIconNode.image = nil
|
self.downloadStatusIconNode.image = nil
|
||||||
case .Remote:
|
case .Remote:
|
||||||
if self.linearProgressNode.supernode != nil {
|
if let linearProgressNode = self.linearProgressNode {
|
||||||
self.linearProgressNode.removeFromSupernode()
|
self.linearProgressNode = nil
|
||||||
|
linearProgressNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak linearProgressNode] _ in
|
||||||
|
linearProgressNode?.removeFromSupernode()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
if self.downloadStatusIconNode.supernode == nil {
|
if self.downloadStatusIconNode.supernode == nil {
|
||||||
self.addSubnode(self.downloadStatusIconNode)
|
self.offsetContainerNode.addSubnode(self.downloadStatusIconNode)
|
||||||
}
|
}
|
||||||
self.downloadStatusIconNode.image = PresentationResourcesChat.sharedMediaFileDownloadStartIcon(item.theme)
|
self.downloadStatusIconNode.image = PresentationResourcesChat.sharedMediaFileDownloadStartIcon(item.theme)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if self.linearProgressNode.supernode != nil {
|
if let linearProgressNode = self.linearProgressNode {
|
||||||
self.linearProgressNode.removeFromSupernode()
|
self.linearProgressNode = nil
|
||||||
|
linearProgressNode.layer.animateAlpha(from: 1.0, to: 1.0, duration: 0.2, removeOnCompletion: false, completion: { [weak linearProgressNode] _ in
|
||||||
|
linearProgressNode?.removeFromSupernode()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
if self.downloadStatusIconNode.supernode != nil {
|
if self.downloadStatusIconNode.supernode != nil {
|
||||||
self.downloadStatusIconNode.removeFromSupernode()
|
self.downloadStatusIconNode.removeFromSupernode()
|
||||||
@ -1002,12 +961,6 @@ final class ListMessageFileItemNode: ListMessageNode {
|
|||||||
return super.hitTest(point, with: event)
|
return super.hitTest(point, with: event)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func longTapped() {
|
|
||||||
if let item = self.item {
|
|
||||||
item.controllerInteraction.openMessageContextMenu(item.message, false, self, self.bounds, nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func statusPressed() {
|
@objc private func statusPressed() {
|
||||||
guard let _ = self.item, let fetchStatus = self.fetchStatus else {
|
guard let _ = self.item, let fetchStatus = self.fetchStatus else {
|
||||||
return
|
return
|
||||||
@ -1027,3 +980,129 @@ final class ListMessageFileItemNode: ListMessageNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final class LinearProgressNode: ASDisplayNode {
|
||||||
|
private let trackingNode: HierarchyTrackingNode
|
||||||
|
private let barNode: ASImageNode
|
||||||
|
private let shimmerNode: ASImageNode
|
||||||
|
private let shimmerClippingNode: ASDisplayNode
|
||||||
|
|
||||||
|
private var currentProgress: CGFloat = 0.0
|
||||||
|
private var currentProgressAnimation: (from: CGFloat, to: CGFloat, startTime: Double, completion: () -> Void)?
|
||||||
|
|
||||||
|
private var shimmerPhase: CGFloat = 0.0
|
||||||
|
|
||||||
|
private var inHierarchyValue: Bool = false
|
||||||
|
private var shouldAnimate: Bool = false
|
||||||
|
|
||||||
|
private let animator: ConstantDisplayLinkAnimator
|
||||||
|
|
||||||
|
override init() {
|
||||||
|
var updateInHierarchy: ((Bool) -> Void)?
|
||||||
|
self.trackingNode = HierarchyTrackingNode { value in
|
||||||
|
updateInHierarchy?(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
var animationStep: (() -> Void)?
|
||||||
|
self.animator = ConstantDisplayLinkAnimator {
|
||||||
|
animationStep?()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
self.barNode = ASImageNode()
|
||||||
|
self.barNode.isLayerBacked = true
|
||||||
|
|
||||||
|
self.shimmerNode = ASImageNode()
|
||||||
|
self.shimmerNode.contentMode = .scaleToFill
|
||||||
|
self.shimmerClippingNode = ASDisplayNode()
|
||||||
|
self.shimmerClippingNode.clipsToBounds = true
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
self.addSubnode(trackingNode)
|
||||||
|
self.addSubnode(self.barNode)
|
||||||
|
|
||||||
|
self.shimmerClippingNode.addSubnode(self.shimmerNode)
|
||||||
|
self.addSubnode(self.shimmerClippingNode)
|
||||||
|
|
||||||
|
updateInHierarchy = { [weak self] value in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if strongSelf.inHierarchyValue != value {
|
||||||
|
strongSelf.inHierarchyValue = value
|
||||||
|
strongSelf.updateAnimations()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
animationStep = { [weak self] in
|
||||||
|
self?.update()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateTheme(theme: PresentationTheme) {
|
||||||
|
self.barNode.image = generateStretchableFilledCircleImage(diameter: 3.0, color: theme.list.itemAccentColor)
|
||||||
|
self.shimmerNode.image = generateImage(CGSize(width: 100.0, height: 3.0), opaque: false, rotatedContext: { size, context in
|
||||||
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||||
|
|
||||||
|
let foregroundColor = theme.list.plainBackgroundColor.withAlphaComponent(0.4)
|
||||||
|
|
||||||
|
let transparentColor = foregroundColor.withAlphaComponent(0.0).cgColor
|
||||||
|
let peakColor = foregroundColor.cgColor
|
||||||
|
|
||||||
|
var locations: [CGFloat] = [0.0, 0.5, 1.0]
|
||||||
|
let colors: [CGColor] = [transparentColor, peakColor, transparentColor]
|
||||||
|
|
||||||
|
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||||
|
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
|
||||||
|
|
||||||
|
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: CGGradientDrawingOptions())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateProgress(value: CGFloat, completion: @escaping () -> Void = {}) {
|
||||||
|
if self.currentProgress.isEqual(to: value) {
|
||||||
|
self.currentProgressAnimation = nil
|
||||||
|
completion()
|
||||||
|
} else {
|
||||||
|
self.currentProgressAnimation = (self.currentProgress, value, CACurrentMediaTime(), completion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateAnimations() {
|
||||||
|
let shouldAnimate = self.inHierarchyValue
|
||||||
|
if shouldAnimate != self.shouldAnimate {
|
||||||
|
self.shouldAnimate = shouldAnimate
|
||||||
|
self.animator.isPaused = !shouldAnimate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func update() {
|
||||||
|
if let (fromValue, toValue, startTime, completion) = self.currentProgressAnimation {
|
||||||
|
let duration: Double = 0.15
|
||||||
|
let timestamp = CACurrentMediaTime()
|
||||||
|
let t = CGFloat((timestamp - startTime) / duration)
|
||||||
|
if t >= 1.0 {
|
||||||
|
self.currentProgress = toValue
|
||||||
|
self.currentProgressAnimation = nil
|
||||||
|
completion()
|
||||||
|
} else {
|
||||||
|
let clippedT = max(0.0, t)
|
||||||
|
self.currentProgress = (1.0 - clippedT) * fromValue + clippedT * toValue
|
||||||
|
}
|
||||||
|
|
||||||
|
var progressWidth: CGFloat = self.bounds.width * self.currentProgress
|
||||||
|
if progressWidth < 6.0 {
|
||||||
|
progressWidth = 0.0
|
||||||
|
}
|
||||||
|
let progressFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: progressWidth, height: 3.0))
|
||||||
|
self.barNode.frame = progressFrame
|
||||||
|
self.shimmerClippingNode.frame = progressFrame
|
||||||
|
}
|
||||||
|
|
||||||
|
self.shimmerPhase += 3.5
|
||||||
|
let shimmerWidth: CGFloat = 160.0
|
||||||
|
let shimmerOffset = self.shimmerPhase.remainder(dividingBy: self.bounds.width + shimmerWidth / 2.0)
|
||||||
|
self.shimmerNode.frame = CGRect(origin: CGPoint(x: shimmerOffset - shimmerWidth / 2.0, y: 0.0), size: CGSize(width: shimmerWidth, height: 3.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -14,11 +14,19 @@ import PhotoResources
|
|||||||
import WebsiteType
|
import WebsiteType
|
||||||
import UrlHandling
|
import UrlHandling
|
||||||
|
|
||||||
private let iconFont = Font.medium(22.0)
|
private let iconFont = Font.with(size: 30.0, design: .round, traits: [.bold])
|
||||||
|
|
||||||
private let iconTextBackgroundImage = generateStretchableFilledCircleImage(radius: 2.0, color: UIColor(rgb: 0xdfdfdf))
|
private let iconTextBackgroundImage = generateStretchableFilledCircleImage(radius: 6.0, color: UIColor(rgb: 0xFF9500))
|
||||||
|
|
||||||
final class ListMessageSnippetItemNode: ListMessageNode {
|
final class ListMessageSnippetItemNode: ListMessageNode {
|
||||||
|
private let contextSourceNode: ContextExtractedContentContainingNode
|
||||||
|
private let containerNode: ContextControllerSourceNode
|
||||||
|
private let extractedBackgroundImageNode: ASImageNode
|
||||||
|
private let offsetContainerNode: ASDisplayNode
|
||||||
|
|
||||||
|
private var extractedRect: CGRect?
|
||||||
|
private var nonExtractedRect: CGRect?
|
||||||
|
|
||||||
private let highlightedBackgroundNode: ASDisplayNode
|
private let highlightedBackgroundNode: ASDisplayNode
|
||||||
private let separatorNode: ASDisplayNode
|
private let separatorNode: ASDisplayNode
|
||||||
|
|
||||||
@ -36,20 +44,25 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
|||||||
|
|
||||||
private var currentIconImageRepresentation: TelegramMediaImageRepresentation?
|
private var currentIconImageRepresentation: TelegramMediaImageRepresentation?
|
||||||
private var currentMedia: Media?
|
private var currentMedia: Media?
|
||||||
private var currentPrimaryUrl: String?
|
var currentPrimaryUrl: String?
|
||||||
private var currentIsInstantView: Bool?
|
private var currentIsInstantView: Bool?
|
||||||
|
|
||||||
private var appliedItem: ListMessageItem?
|
private var appliedItem: ListMessageItem?
|
||||||
|
|
||||||
override var canBeLongTapped: Bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
public required init() {
|
public required init() {
|
||||||
|
self.contextSourceNode = ContextExtractedContentContainingNode()
|
||||||
|
self.containerNode = ContextControllerSourceNode()
|
||||||
|
|
||||||
self.separatorNode = ASDisplayNode()
|
self.separatorNode = ASDisplayNode()
|
||||||
self.separatorNode.displaysAsynchronously = false
|
self.separatorNode.displaysAsynchronously = false
|
||||||
self.separatorNode.isLayerBacked = true
|
self.separatorNode.isLayerBacked = true
|
||||||
|
|
||||||
|
self.extractedBackgroundImageNode = ASImageNode()
|
||||||
|
self.extractedBackgroundImageNode.displaysAsynchronously = false
|
||||||
|
self.extractedBackgroundImageNode.alpha = 0.0
|
||||||
|
|
||||||
|
self.offsetContainerNode = ASDisplayNode()
|
||||||
|
|
||||||
self.highlightedBackgroundNode = ASDisplayNode()
|
self.highlightedBackgroundNode = ASDisplayNode()
|
||||||
self.highlightedBackgroundNode.isLayerBacked = true
|
self.highlightedBackgroundNode.isLayerBacked = true
|
||||||
|
|
||||||
@ -80,11 +93,49 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
|||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
self.addSubnode(self.separatorNode)
|
self.addSubnode(self.separatorNode)
|
||||||
self.addSubnode(self.titleNode)
|
|
||||||
self.addSubnode(self.descriptionNode)
|
self.containerNode.addSubnode(self.contextSourceNode)
|
||||||
self.addSubnode(self.linkNode)
|
self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode
|
||||||
self.addSubnode(self.instantViewIconNode)
|
self.addSubnode(self.containerNode)
|
||||||
self.addSubnode(self.iconImageNode)
|
|
||||||
|
self.contextSourceNode.contentNode.addSubnode(self.extractedBackgroundImageNode)
|
||||||
|
self.contextSourceNode.contentNode.addSubnode(self.offsetContainerNode)
|
||||||
|
self.offsetContainerNode.addSubnode(self.titleNode)
|
||||||
|
self.offsetContainerNode.addSubnode(self.descriptionNode)
|
||||||
|
self.offsetContainerNode.addSubnode(self.linkNode)
|
||||||
|
self.offsetContainerNode.addSubnode(self.instantViewIconNode)
|
||||||
|
self.offsetContainerNode.addSubnode(self.iconImageNode)
|
||||||
|
|
||||||
|
self.containerNode.activated = { [weak self] gesture, _ in
|
||||||
|
guard let strongSelf = self, let item = strongSelf.item else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
item.controllerInteraction.openMessageContextMenu(item.message, false, strongSelf.contextSourceNode, strongSelf.contextSourceNode.bounds, gesture)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.contextSourceNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, transition in
|
||||||
|
guard let strongSelf = self, let item = strongSelf.item else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if isExtracted {
|
||||||
|
strongSelf.extractedBackgroundImageNode.image = generateStretchableFilledCircleImage(diameter: 28.0, color: item.theme.list.plainBackgroundColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let extractedRect = strongSelf.extractedRect, let nonExtractedRect = strongSelf.nonExtractedRect {
|
||||||
|
let rect = isExtracted ? extractedRect : nonExtractedRect
|
||||||
|
transition.updateFrame(node: strongSelf.extractedBackgroundImageNode, frame: rect)
|
||||||
|
}
|
||||||
|
|
||||||
|
transition.updateSublayerTransformOffset(layer: strongSelf.offsetContainerNode.layer, offset: CGPoint(x: isExtracted ? 12.0 : 0.0, y: 0.0))
|
||||||
|
|
||||||
|
transition.updateAlpha(node: strongSelf.extractedBackgroundImageNode, alpha: isExtracted ? 1.0 : 0.0, completion: { _ in
|
||||||
|
if !isExtracted {
|
||||||
|
self?.extractedBackgroundImageNode.image = nil
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
required public init?(coder aDecoder: NSCoder) {
|
required public init?(coder aDecoder: NSCoder) {
|
||||||
@ -155,7 +206,7 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
|||||||
updatedTheme = item.theme
|
updatedTheme = item.theme
|
||||||
}
|
}
|
||||||
|
|
||||||
let titleFont = Font.medium(floor(item.fontSize.baseDisplaySize * 16.0 / 17.0))
|
let titleFont = Font.semibold(floor(item.fontSize.baseDisplaySize * 16.0 / 17.0))
|
||||||
let descriptionFont = Font.regular(floor(item.fontSize.baseDisplaySize * 14.0 / 17.0))
|
let descriptionFont = Font.regular(floor(item.fontSize.baseDisplaySize * 14.0 / 17.0))
|
||||||
|
|
||||||
let leftInset: CGFloat = 65.0 + params.leftInset
|
let leftInset: CGFloat = 65.0 + params.leftInset
|
||||||
@ -216,7 +267,7 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
|||||||
|
|
||||||
let mutableDescriptionText = NSMutableAttributedString()
|
let mutableDescriptionText = NSMutableAttributedString()
|
||||||
if let text = content.text {
|
if let text = content.text {
|
||||||
mutableDescriptionText.append(NSAttributedString(string: text + "\n", font: descriptionFont, textColor: item.theme.list.itemPrimaryTextColor))
|
mutableDescriptionText.append(NSAttributedString(string: text + "\n", font: descriptionFont, textColor: item.theme.list.itemSecondaryTextColor))
|
||||||
}
|
}
|
||||||
|
|
||||||
let plainUrlString = NSAttributedString(string: content.displayUrl, font: descriptionFont, textColor: item.theme.list.itemAccentColor)
|
let plainUrlString = NSAttributedString(string: content.displayUrl, font: descriptionFont, textColor: item.theme.list.itemAccentColor)
|
||||||
@ -262,6 +313,7 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
|||||||
range.length = nsString.length - range.location
|
range.length = nsString.length - range.location
|
||||||
}
|
}
|
||||||
var urlString = nsString.substring(with: range)
|
var urlString = nsString.substring(with: range)
|
||||||
|
let rawUrlString = urlString
|
||||||
var parsedUrl = URL(string: urlString)
|
var parsedUrl = URL(string: urlString)
|
||||||
if parsedUrl == nil || parsedUrl!.host == nil || parsedUrl!.host!.isEmpty {
|
if parsedUrl == nil || parsedUrl!.host == nil || parsedUrl!.host!.isEmpty {
|
||||||
urlString = "http://" + urlString
|
urlString = "http://" + urlString
|
||||||
@ -269,13 +321,18 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
|||||||
}
|
}
|
||||||
if let url = parsedUrl, let host = url.host {
|
if let url = parsedUrl, let host = url.host {
|
||||||
primaryUrl = urlString
|
primaryUrl = urlString
|
||||||
|
if url.path.hasPrefix("/addstickers/") {
|
||||||
iconText = NSAttributedString(string: host[..<host.index(after: host.startIndex)].uppercased(), font: iconFont, textColor: UIColor.white)
|
title = NSAttributedString(string: urlString, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor)
|
||||||
|
|
||||||
title = NSAttributedString(string: host, 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()
|
let mutableDescriptionText = NSMutableAttributedString()
|
||||||
if item.message.text != urlString {
|
if item.message.text != rawUrlString {
|
||||||
mutableDescriptionText.append(NSAttributedString(string: item.message.text + "\n", font: descriptionFont, textColor: item.theme.list.itemPrimaryTextColor))
|
mutableDescriptionText.append(NSAttributedString(string: item.message.text + "\n", font: descriptionFont, textColor: item.theme.list.itemSecondaryTextColor))
|
||||||
}
|
}
|
||||||
|
|
||||||
let urlAttributedString = NSMutableAttributedString()
|
let urlAttributedString = NSMutableAttributedString()
|
||||||
@ -296,11 +353,11 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let (titleNodeLayout, titleNodeApply) = titleNodeMakeLayout(TextNodeLayoutArguments(attributedString: title, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - params.rightInset, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
let (titleNodeLayout, titleNodeApply) = titleNodeMakeLayout(TextNodeLayoutArguments(attributedString: title, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .middle, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - params.rightInset - 16.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||||
|
|
||||||
let (descriptionNodeLayout, descriptionNodeApply) = descriptionNodeMakeLayout(TextNodeLayoutArguments(attributedString: descriptionText, backgroundColor: nil, maximumNumberOfLines: 3, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - params.rightInset - 12.0, height: CGFloat.infinity), alignment: .natural, lineSpacing: 0.3, cutout: nil, insets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0)))
|
let (descriptionNodeLayout, descriptionNodeApply) = descriptionNodeMakeLayout(TextNodeLayoutArguments(attributedString: descriptionText, backgroundColor: nil, maximumNumberOfLines: 3, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - params.rightInset - 16.0 - 8.0, height: CGFloat.infinity), alignment: .natural, lineSpacing: 0.3, cutout: nil, insets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0)))
|
||||||
|
|
||||||
let (linkNodeLayout, linkNodeApply) = linkNodeMakeLayout(TextNodeLayoutArguments(attributedString: linkText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - params.rightInset - 12.0, height: CGFloat.infinity), alignment: .natural, lineSpacing: 0.3, cutout: isInstantView ? TextNodeCutout(topLeft: CGSize(width: 14.0, height: 8.0)) : nil, insets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0)))
|
let (linkNodeLayout, linkNodeApply) = linkNodeMakeLayout(TextNodeLayoutArguments(attributedString: linkText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - params.rightInset - 16.0 - 8.0, height: CGFloat.infinity), alignment: .natural, lineSpacing: 0.3, cutout: isInstantView ? TextNodeCutout(topLeft: CGSize(width: 14.0, height: 8.0)) : nil, insets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0)))
|
||||||
var instantViewImage: UIImage?
|
var instantViewImage: UIImage?
|
||||||
if isInstantView {
|
if isInstantView {
|
||||||
instantViewImage = PresentationResourcesChat.sharedMediaInstantViewIcon(item.theme)
|
instantViewImage = PresentationResourcesChat.sharedMediaInstantViewIcon(item.theme)
|
||||||
@ -310,8 +367,8 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
|||||||
|
|
||||||
var iconImageApply: (() -> Void)?
|
var iconImageApply: (() -> Void)?
|
||||||
if let iconImageReferenceAndRepresentation = iconImageReferenceAndRepresentation {
|
if let iconImageReferenceAndRepresentation = iconImageReferenceAndRepresentation {
|
||||||
let iconSize = CGSize(width: 42.0, height: 42.0)
|
let iconSize = CGSize(width: 40.0, height: 40.0)
|
||||||
let imageCorners = ImageCorners(topLeft: .Corner(2.0), topRight: .Corner(2.0), bottomLeft: .Corner(2.0), bottomRight: .Corner(2.0))
|
let imageCorners = ImageCorners(radius: 6.0)
|
||||||
let arguments = TransformImageArguments(corners: imageCorners, imageSize: iconImageReferenceAndRepresentation.1.dimensions.cgSize.aspectFilled(iconSize), boundingSize: iconSize, intrinsicInsets: UIEdgeInsets(), emptyColor: item.theme.list.mediaPlaceholderColor)
|
let arguments = TransformImageArguments(corners: imageCorners, imageSize: iconImageReferenceAndRepresentation.1.dimensions.cgSize.aspectFilled(iconSize), boundingSize: iconSize, intrinsicInsets: UIEdgeInsets(), emptyColor: item.theme.list.mediaPlaceholderColor)
|
||||||
iconImageApply = iconImageLayout(arguments)
|
iconImageApply = iconImageLayout(arguments)
|
||||||
}
|
}
|
||||||
@ -335,7 +392,8 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
|||||||
insets.top += header.height
|
insets.top += header.height
|
||||||
}
|
}
|
||||||
|
|
||||||
return (ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: contentHeight), insets: insets), { animation in
|
let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: contentHeight), insets: insets)
|
||||||
|
return (nodeLayout, { animation in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
let transition: ContainedViewLayoutTransition
|
let transition: ContainedViewLayoutTransition
|
||||||
if animation.isAnimated {
|
if animation.isAnimated {
|
||||||
@ -344,6 +402,23 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
|||||||
transition = .immediate
|
transition = .immediate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: nodeLayout.contentSize)
|
||||||
|
strongSelf.contextSourceNode.frame = CGRect(origin: CGPoint(), size: nodeLayout.contentSize)
|
||||||
|
strongSelf.contextSourceNode.contentNode.frame = CGRect(origin: CGPoint(), size: nodeLayout.contentSize)
|
||||||
|
strongSelf.offsetContainerNode.frame = CGRect(origin: CGPoint(), size: nodeLayout.contentSize)
|
||||||
|
|
||||||
|
let nonExtractedRect = CGRect(origin: CGPoint(), size: CGSize(width: nodeLayout.contentSize.width - 16.0, height: nodeLayout.contentSize.height))
|
||||||
|
let extractedRect = CGRect(origin: CGPoint(), size: nodeLayout.contentSize).insetBy(dx: 16.0, dy: 0.0)
|
||||||
|
strongSelf.extractedRect = extractedRect
|
||||||
|
strongSelf.nonExtractedRect = nonExtractedRect
|
||||||
|
|
||||||
|
if strongSelf.contextSourceNode.isExtractedToContextPreview {
|
||||||
|
strongSelf.extractedBackgroundImageNode.frame = extractedRect
|
||||||
|
} else {
|
||||||
|
strongSelf.extractedBackgroundImageNode.frame = nonExtractedRect
|
||||||
|
}
|
||||||
|
strongSelf.contextSourceNode.contentRect = extractedRect
|
||||||
|
|
||||||
strongSelf.appliedItem = item
|
strongSelf.appliedItem = item
|
||||||
strongSelf.currentMedia = selectedMedia
|
strongSelf.currentMedia = selectedMedia
|
||||||
strongSelf.currentPrimaryUrl = primaryUrl
|
strongSelf.currentPrimaryUrl = primaryUrl
|
||||||
@ -380,7 +455,7 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
|||||||
transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset, y: 9.0), size: titleNodeLayout.size))
|
transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset, y: 9.0), size: titleNodeLayout.size))
|
||||||
let _ = titleNodeApply()
|
let _ = titleNodeApply()
|
||||||
|
|
||||||
let descriptionFrame = CGRect(origin: CGPoint(x: leftOffset + leftInset - 1.0, y: strongSelf.titleNode.frame.maxY + 3.0), size: descriptionNodeLayout.size)
|
let descriptionFrame = CGRect(origin: CGPoint(x: leftOffset + leftInset, y: strongSelf.titleNode.frame.maxY + 1.0), size: descriptionNodeLayout.size)
|
||||||
transition.updateFrame(node: strongSelf.descriptionNode, frame: descriptionFrame)
|
transition.updateFrame(node: strongSelf.descriptionNode, frame: descriptionFrame)
|
||||||
let _ = descriptionNodeApply()
|
let _ = descriptionNodeApply()
|
||||||
|
|
||||||
@ -393,8 +468,8 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
|||||||
transition.updateFrame(node: strongSelf.instantViewIconNode, frame: CGRect(origin: linkFrame.origin.offsetBy(dx: 0.0, dy: 4.0), size: image.size))
|
transition.updateFrame(node: strongSelf.instantViewIconNode, frame: CGRect(origin: linkFrame.origin.offsetBy(dx: 0.0, dy: 4.0), size: image.size))
|
||||||
}
|
}
|
||||||
|
|
||||||
let iconFrame = CGRect(origin: CGPoint(x: params.leftInset + leftOffset + 9.0, y: 12.0), size: CGSize(width: 42.0, height: 42.0))
|
let iconFrame = CGRect(origin: CGPoint(x: params.leftInset + leftOffset + 12.0, y: 12.0), size: CGSize(width: 40.0, height: 40.0))
|
||||||
transition.updateFrame(node: strongSelf.iconTextNode, frame: CGRect(origin: CGPoint(x: iconFrame.minX + floor((42.0 - iconTextLayout.size.width) / 2.0), y: iconFrame.minY + floor((42.0 - iconTextLayout.size.height) / 2.0) + 3.0), size: iconTextLayout.size))
|
transition.updateFrame(node: strongSelf.iconTextNode, frame: CGRect(origin: CGPoint(x: iconFrame.minX + floorToScreenPixels((iconFrame.width - iconTextLayout.size.width) / 2.0), y: iconFrame.minY + floorToScreenPixels((iconFrame.height - iconTextLayout.size.height) / 2.0) + 2.0), size: iconTextLayout.size))
|
||||||
|
|
||||||
let _ = iconTextApply()
|
let _ = iconTextApply()
|
||||||
|
|
||||||
@ -406,7 +481,7 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if strongSelf.iconImageNode.supernode == nil {
|
if strongSelf.iconImageNode.supernode == nil {
|
||||||
strongSelf.addSubnode(strongSelf.iconImageNode)
|
strongSelf.offsetContainerNode.addSubnode(strongSelf.iconImageNode)
|
||||||
strongSelf.iconImageNode.frame = iconFrame
|
strongSelf.iconImageNode.frame = iconFrame
|
||||||
} else {
|
} else {
|
||||||
transition.updateFrame(node: strongSelf.iconImageNode, frame: iconFrame)
|
transition.updateFrame(node: strongSelf.iconImageNode, frame: iconFrame)
|
||||||
@ -427,15 +502,18 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
|||||||
|
|
||||||
if strongSelf.iconTextBackgroundNode.supernode == nil {
|
if strongSelf.iconTextBackgroundNode.supernode == nil {
|
||||||
strongSelf.iconTextBackgroundNode.image = applyIconTextBackgroundImage
|
strongSelf.iconTextBackgroundNode.image = applyIconTextBackgroundImage
|
||||||
strongSelf.addSubnode(strongSelf.iconTextBackgroundNode)
|
strongSelf.offsetContainerNode.addSubnode(strongSelf.iconTextBackgroundNode)
|
||||||
strongSelf.iconTextBackgroundNode.frame = iconFrame
|
strongSelf.iconTextBackgroundNode.frame = iconFrame
|
||||||
} else {
|
} else {
|
||||||
transition.updateFrame(node: strongSelf.iconTextBackgroundNode, frame: iconFrame)
|
transition.updateFrame(node: strongSelf.iconTextBackgroundNode, frame: iconFrame)
|
||||||
}
|
}
|
||||||
if strongSelf.iconTextNode.supernode == nil {
|
if strongSelf.iconTextNode.supernode == nil {
|
||||||
strongSelf.addSubnode(strongSelf.iconTextNode)
|
strongSelf.offsetContainerNode.addSubnode(strongSelf.iconTextNode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
strongSelf.iconTextBackgroundNode.isHidden = iconText == nil
|
||||||
|
strongSelf.iconTextNode.isHidden = iconText == nil
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -598,7 +676,7 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
|||||||
} else {
|
} else {
|
||||||
linkHighlightingNode = LinkHighlightingNode(color: item.message.effectivelyIncoming(item.context.account.peerId) ? item.theme.chat.message.incoming.linkHighlightColor : item.theme.chat.message.outgoing.linkHighlightColor)
|
linkHighlightingNode = LinkHighlightingNode(color: item.message.effectivelyIncoming(item.context.account.peerId) ? item.theme.chat.message.incoming.linkHighlightColor : item.theme.chat.message.outgoing.linkHighlightColor)
|
||||||
self.linkHighlightingNode = linkHighlightingNode
|
self.linkHighlightingNode = linkHighlightingNode
|
||||||
self.insertSubnode(linkHighlightingNode, belowSubnode: self.linkNode)
|
self.offsetContainerNode.insertSubnode(linkHighlightingNode, belowSubnode: self.linkNode)
|
||||||
}
|
}
|
||||||
linkHighlightingNode.frame = self.linkNode.frame.offsetBy(dx: 0.0, dy: 0.0)
|
linkHighlightingNode.frame = self.linkNode.frame.offsetBy(dx: 0.0, dy: 0.0)
|
||||||
linkHighlightingNode.updateRects(rects.map { $0.insetBy(dx: -1.0, dy: -1.0) })
|
linkHighlightingNode.updateRects(rects.map { $0.insetBy(dx: -1.0, dy: -1.0) })
|
||||||
@ -610,10 +688,4 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func longTapped() {
|
|
||||||
if let item = self.item {
|
|
||||||
item.controllerInteraction.openMessageContextMenu(item.message, false, self, self.bounds, nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -77,6 +77,8 @@ final class PeerInfoGroupsInCommonPaneNode: ASDisplayNode, PeerInfoPaneNode {
|
|||||||
return self.ready.get()
|
return self.ready.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let shouldReceiveExpandProgressUpdates: Bool = false
|
||||||
|
|
||||||
private var disposable: Disposable?
|
private var disposable: Disposable?
|
||||||
|
|
||||||
init(context: AccountContext, peerId: PeerId, chatControllerInteraction: ChatControllerInteraction, openPeerContextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void, groupsInCommonContext: GroupsInCommonContext) {
|
init(context: AccountContext, peerId: PeerId, chatControllerInteraction: ChatControllerInteraction, openPeerContextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void, groupsInCommonContext: GroupsInCommonContext) {
|
||||||
@ -127,7 +129,7 @@ final class PeerInfoGroupsInCommonPaneNode: ASDisplayNode, PeerInfoPaneNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
|
func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
|
||||||
let isFirstLayout = self.currentParams == nil
|
let isFirstLayout = self.currentParams == nil
|
||||||
self.currentParams = (size, isScrollingLockedAtTop, presentationData)
|
self.currentParams = (size, isScrollingLockedAtTop, presentationData)
|
||||||
|
|
||||||
|
@ -5,10 +5,27 @@ import SyncCore
|
|||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import Postbox
|
import Postbox
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
|
import PresentationDataUtils
|
||||||
import AccountContext
|
import AccountContext
|
||||||
import ContextUI
|
import ContextUI
|
||||||
import PhotoResources
|
import PhotoResources
|
||||||
import TelegramUIPreferences
|
import TelegramUIPreferences
|
||||||
|
import UniversalMediaPlayer
|
||||||
|
import TelegramBaseController
|
||||||
|
import OverlayStatusController
|
||||||
|
|
||||||
|
private final class PassthroughContainerNode: ASDisplayNode {
|
||||||
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||||
|
if let subnodes = self.subnodes {
|
||||||
|
for subnode in subnodes {
|
||||||
|
if let result = subnode.view.hitTest(self.view.convert(point, to: subnode.view), with: event) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode {
|
final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode {
|
||||||
private let context: AccountContext
|
private let context: AccountContext
|
||||||
@ -17,7 +34,7 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode {
|
|||||||
|
|
||||||
private let listNode: ChatHistoryListNode
|
private let listNode: ChatHistoryListNode
|
||||||
|
|
||||||
private var currentParams: (size: CGSize, isScrollingLockedAtTop: Bool, presentationData: PresentationData)?
|
private var currentParams: (size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData)?
|
||||||
|
|
||||||
private let ready = Promise<Bool>()
|
private let ready = Promise<Bool>()
|
||||||
private var didSetReady: Bool = false
|
private var didSetReady: Bool = false
|
||||||
@ -25,6 +42,8 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode {
|
|||||||
return self.ready.get()
|
return self.ready.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let shouldReceiveExpandProgressUpdates: Bool
|
||||||
|
|
||||||
private let selectedMessagesPromise = Promise<Set<MessageId>?>(nil)
|
private let selectedMessagesPromise = Promise<Set<MessageId>?>(nil)
|
||||||
private var selectedMessages: Set<MessageId>? {
|
private var selectedMessages: Set<MessageId>? {
|
||||||
didSet {
|
didSet {
|
||||||
@ -35,6 +54,13 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var hiddenMediaDisposable: Disposable?
|
private var hiddenMediaDisposable: Disposable?
|
||||||
|
private var mediaStatusDisposable: Disposable?
|
||||||
|
private var playlistPreloadDisposable: Disposable?
|
||||||
|
|
||||||
|
private var playlistStateAndType: (SharedMediaPlaylistItem, SharedMediaPlaylistItem?, SharedMediaPlaylistItem?, MusicPlaybackSettingsOrder, MediaManagerPlayerType, Account)?
|
||||||
|
private var mediaAccessoryPanelContainer: PassthroughContainerNode
|
||||||
|
private var mediaAccessoryPanel: (MediaNavigationAccessoryPanel, MediaManagerPlayerType)?
|
||||||
|
private var dismissingPanel: ASDisplayNode?
|
||||||
|
|
||||||
init(context: AccountContext, chatControllerInteraction: ChatControllerInteraction, peerId: PeerId, tagMask: MessageTags) {
|
init(context: AccountContext, chatControllerInteraction: ChatControllerInteraction, peerId: PeerId, tagMask: MessageTags) {
|
||||||
self.context = context
|
self.context = context
|
||||||
@ -45,19 +71,82 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode {
|
|||||||
self.selectedMessagesPromise.set(.single(self.selectedMessages))
|
self.selectedMessagesPromise.set(.single(self.selectedMessages))
|
||||||
|
|
||||||
self.listNode = ChatHistoryListNode(context: context, chatLocation: .peer(peerId), tagMask: tagMask, subject: nil, controllerInteraction: chatControllerInteraction, selectedMessages: self.selectedMessagesPromise.get(), mode: .list(search: false, reversed: false, displayHeaders: .allButLast))
|
self.listNode = ChatHistoryListNode(context: context, chatLocation: .peer(peerId), tagMask: tagMask, subject: nil, controllerInteraction: chatControllerInteraction, selectedMessages: self.selectedMessagesPromise.get(), mode: .list(search: false, reversed: false, displayHeaders: .allButLast))
|
||||||
|
self.listNode.defaultToSynchronousTransactionWhileScrolling = true
|
||||||
|
|
||||||
|
if tagMask == .music {
|
||||||
|
self.shouldReceiveExpandProgressUpdates = true
|
||||||
|
} else {
|
||||||
|
self.shouldReceiveExpandProgressUpdates = false
|
||||||
|
}
|
||||||
|
|
||||||
|
self.mediaAccessoryPanelContainer = PassthroughContainerNode()
|
||||||
|
self.mediaAccessoryPanelContainer.clipsToBounds = true
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
self.listNode.preloadPages = true
|
self.listNode.preloadPages = true
|
||||||
self.addSubnode(self.listNode)
|
self.addSubnode(self.listNode)
|
||||||
|
self.addSubnode(self.mediaAccessoryPanelContainer)
|
||||||
|
|
||||||
self.ready.set(self.listNode.historyState.get()
|
self.ready.set(self.listNode.historyState.get()
|
||||||
|> take(1)
|
|> take(1)
|
||||||
|> map { _ -> Bool in true })
|
|> map { _ -> Bool in true })
|
||||||
|
|
||||||
|
if tagMask == .music || tagMask == .voiceOrInstantVideo {
|
||||||
|
self.mediaStatusDisposable = (context.sharedContext.mediaManager.globalMediaPlayerState
|
||||||
|
|> mapToSignal { playlistStateAndType -> Signal<(Account, SharedMediaPlayerItemPlaybackState, MediaManagerPlayerType)?, NoError> in
|
||||||
|
if let (account, state, type) = playlistStateAndType {
|
||||||
|
switch state {
|
||||||
|
case let .state(state):
|
||||||
|
if let playlistId = state.playlistId as? PeerMessagesMediaPlaylistId, case .peer(peerId) = playlistId {
|
||||||
|
switch type {
|
||||||
|
case .voice:
|
||||||
|
if tagMask != .voiceOrInstantVideo {
|
||||||
|
return .single(nil) |> delay(0.2, queue: .mainQueue())
|
||||||
|
}
|
||||||
|
case .music:
|
||||||
|
if tagMask != .music {
|
||||||
|
return .single(nil) |> delay(0.2, queue: .mainQueue())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return .single((account, state, type))
|
||||||
|
} else {
|
||||||
|
return .single(nil) |> delay(0.2, queue: .mainQueue())
|
||||||
|
}
|
||||||
|
case .loading:
|
||||||
|
return .single(nil) |> delay(0.2, queue: .mainQueue())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return .single(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] playlistStateAndType in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !arePlaylistItemsEqual(strongSelf.playlistStateAndType?.0, playlistStateAndType?.1.item) ||
|
||||||
|
!arePlaylistItemsEqual(strongSelf.playlistStateAndType?.1, playlistStateAndType?.1.previousItem) ||
|
||||||
|
!arePlaylistItemsEqual(strongSelf.playlistStateAndType?.2, playlistStateAndType?.1.nextItem) ||
|
||||||
|
strongSelf.playlistStateAndType?.3 != playlistStateAndType?.1.order || strongSelf.playlistStateAndType?.4 != playlistStateAndType?.2 {
|
||||||
|
|
||||||
|
if let playlistStateAndType = playlistStateAndType {
|
||||||
|
strongSelf.playlistStateAndType = (playlistStateAndType.1.item, playlistStateAndType.1.previousItem, playlistStateAndType.1.nextItem, playlistStateAndType.1.order, playlistStateAndType.2, playlistStateAndType.0)
|
||||||
|
} else {
|
||||||
|
strongSelf.playlistStateAndType = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if let (size, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) = strongSelf.currentParams {
|
||||||
|
strongSelf.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, presentationData: presentationData, synchronous: true, transition: .animated(duration: 0.4, curve: .spring))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
self.hiddenMediaDisposable?.dispose()
|
self.hiddenMediaDisposable?.dispose()
|
||||||
|
self.mediaStatusDisposable?.dispose()
|
||||||
|
self.playlistPreloadDisposable?.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
func scrollToTop() -> Bool {
|
func scrollToTop() -> Bool {
|
||||||
@ -71,12 +160,211 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
|
func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
|
||||||
self.currentParams = (size, isScrollingLockedAtTop, presentationData)
|
self.currentParams = (size, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData)
|
||||||
|
|
||||||
|
var topPanelHeight: CGFloat = 0.0
|
||||||
|
if let (item, previousItem, nextItem, order, type, _) = self.playlistStateAndType {
|
||||||
|
let panelHeight = MediaNavigationAccessoryHeaderNode.minimizedHeight
|
||||||
|
topPanelHeight = floor(panelHeight * expandProgress)
|
||||||
|
let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: topPanelHeight - panelHeight), size: CGSize(width: size.width, height: panelHeight))
|
||||||
|
if let (mediaAccessoryPanel, mediaType) = self.mediaAccessoryPanel, mediaType == type {
|
||||||
|
transition.updateFrame(layer: mediaAccessoryPanel.layer, frame: panelFrame)
|
||||||
|
mediaAccessoryPanel.updateLayout(size: panelFrame.size, leftInset: sideInset, rightInset: sideInset, transition: transition)
|
||||||
|
switch order {
|
||||||
|
case .regular:
|
||||||
|
mediaAccessoryPanel.containerNode.headerNode.playbackItems = (item, previousItem, nextItem)
|
||||||
|
case .reversed:
|
||||||
|
mediaAccessoryPanel.containerNode.headerNode.playbackItems = (item, nextItem, previousItem)
|
||||||
|
case .random:
|
||||||
|
mediaAccessoryPanel.containerNode.headerNode.playbackItems = (item, nil, nil)
|
||||||
|
}
|
||||||
|
let delayedStatus = self.context.sharedContext.mediaManager.globalMediaPlayerState
|
||||||
|
|> mapToSignal { value -> Signal<(Account, SharedMediaPlayerItemPlaybackStateOrLoading, MediaManagerPlayerType)?, NoError> in
|
||||||
|
guard let value = value else {
|
||||||
|
return .single(nil)
|
||||||
|
}
|
||||||
|
switch value.1 {
|
||||||
|
case .state:
|
||||||
|
return .single(value)
|
||||||
|
case .loading:
|
||||||
|
return .single(value) |> delay(0.1, queue: .mainQueue())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaAccessoryPanel.containerNode.headerNode.playbackStatus = delayedStatus
|
||||||
|
|> map { state -> MediaPlayerStatus in
|
||||||
|
if let stateOrLoading = state?.1, case let .state(state) = stateOrLoading {
|
||||||
|
return state.status
|
||||||
|
} else {
|
||||||
|
return MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: .paused, soundEnabled: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let (mediaAccessoryPanel, _) = self.mediaAccessoryPanel {
|
||||||
|
self.mediaAccessoryPanel = nil
|
||||||
|
self.dismissingPanel = mediaAccessoryPanel
|
||||||
|
mediaAccessoryPanel.animateOut(transition: transition, completion: { [weak self, weak mediaAccessoryPanel] in
|
||||||
|
mediaAccessoryPanel?.removeFromSupernode()
|
||||||
|
if let strongSelf = self, strongSelf.dismissingPanel === mediaAccessoryPanel {
|
||||||
|
strongSelf.dismissingPanel = nil
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let mediaAccessoryPanel = MediaNavigationAccessoryPanel(context: self.context)
|
||||||
|
mediaAccessoryPanel.containerNode.headerNode.displayScrubber = type != .voice
|
||||||
|
mediaAccessoryPanel.close = { [weak self] in
|
||||||
|
if let strongSelf = self, let (_, _, _, _, type, _) = strongSelf.playlistStateAndType {
|
||||||
|
strongSelf.context.sharedContext.mediaManager.setPlaylist(nil, type: type, control: SharedMediaPlayerControlAction.playback(.pause))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mediaAccessoryPanel.toggleRate = {
|
||||||
|
[weak self] in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let _ = (strongSelf.context.sharedContext.accountManager.transaction { transaction -> AudioPlaybackRate in
|
||||||
|
let settings = transaction.getSharedData(ApplicationSpecificSharedDataKeys.musicPlaybackSettings) as? MusicPlaybackSettings ?? MusicPlaybackSettings.defaultSettings
|
||||||
|
|
||||||
|
let nextRate: AudioPlaybackRate
|
||||||
|
switch settings.voicePlaybackRate {
|
||||||
|
case .x1:
|
||||||
|
nextRate = .x2
|
||||||
|
case .x2:
|
||||||
|
nextRate = .x1
|
||||||
|
}
|
||||||
|
transaction.updateSharedData(ApplicationSpecificSharedDataKeys.musicPlaybackSettings, { _ in
|
||||||
|
return settings.withUpdatedVoicePlaybackRate(nextRate)
|
||||||
|
})
|
||||||
|
return nextRate
|
||||||
|
}
|
||||||
|
|> deliverOnMainQueue).start(next: { baseRate in
|
||||||
|
guard let strongSelf = self, let (_, _, _, _, type, _) = strongSelf.playlistStateAndType else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
strongSelf.context.sharedContext.mediaManager.playlistControl(.setBaseRate(baseRate), type: type)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
mediaAccessoryPanel.togglePlayPause = { [weak self] in
|
||||||
|
if let strongSelf = self, let (_, _, _, _, type, _) = strongSelf.playlistStateAndType {
|
||||||
|
strongSelf.context.sharedContext.mediaManager.playlistControl(.playback(.togglePlayPause), type: type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mediaAccessoryPanel.playPrevious = { [weak self] in
|
||||||
|
if let strongSelf = self, let (_, _, _, _, type, _) = strongSelf.playlistStateAndType {
|
||||||
|
strongSelf.context.sharedContext.mediaManager.playlistControl(.next, type: type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mediaAccessoryPanel.playNext = { [weak self] in
|
||||||
|
if let strongSelf = self, let (_, _, _, _, type, _) = strongSelf.playlistStateAndType {
|
||||||
|
strongSelf.context.sharedContext.mediaManager.playlistControl(.previous, type: type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mediaAccessoryPanel.tapAction = { [weak self] in
|
||||||
|
guard let strongSelf = self, let _ = strongSelf.chatControllerInteraction.navigationController(), let (state, _, _, order, type, account) = strongSelf.playlistStateAndType else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if let id = state.id as? PeerMessagesMediaPlaylistItemId {
|
||||||
|
if type == .music {
|
||||||
|
let signal = strongSelf.context.sharedContext.messageFromPreloadedChatHistoryViewForLocation(id: id.messageId, location: ChatHistoryLocationInput(content: .InitialSearch(location: .id(id.messageId), count: 60), id: 0), account: account, chatLocation: .peer(id.messageId.peerId), tagMask: MessageTags.music)
|
||||||
|
|
||||||
|
var cancelImpl: (() -> Void)?
|
||||||
|
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
||||||
|
let progressSignal = Signal<Never, NoError> { subscriber in
|
||||||
|
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
|
||||||
|
cancelImpl?()
|
||||||
|
}))
|
||||||
|
self?.chatControllerInteraction.presentController(controller, nil)
|
||||||
|
return ActionDisposable { [weak controller] in
|
||||||
|
Queue.mainQueue().async() {
|
||||||
|
controller?.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|> runOn(Queue.mainQueue())
|
||||||
|
|> delay(0.15, queue: Queue.mainQueue())
|
||||||
|
let progressDisposable = MetaDisposable()
|
||||||
|
var progressStarted = false
|
||||||
|
strongSelf.playlistPreloadDisposable?.dispose()
|
||||||
|
strongSelf.playlistPreloadDisposable = (signal
|
||||||
|
|> afterDisposed {
|
||||||
|
Queue.mainQueue().async {
|
||||||
|
progressDisposable.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|> deliverOnMainQueue).start(next: { index in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if let _ = index.0 {
|
||||||
|
let controllerContext: AccountContext
|
||||||
|
if account.id == strongSelf.context.account.id {
|
||||||
|
controllerContext = strongSelf.context
|
||||||
|
} else {
|
||||||
|
controllerContext = strongSelf.context.sharedContext.makeTempAccountContext(account: account)
|
||||||
|
}
|
||||||
|
let controller = strongSelf.context.sharedContext.makeOverlayAudioPlayerController(context: controllerContext, peerId: id.messageId.peerId, type: type, initialMessageId: id.messageId, initialOrder: order, parentNavigationController: strongSelf.chatControllerInteraction.navigationController())
|
||||||
|
strongSelf.view.window?.endEditing(true)
|
||||||
|
strongSelf.chatControllerInteraction.presentController(controller, nil)
|
||||||
|
} else if index.1 {
|
||||||
|
if !progressStarted {
|
||||||
|
progressStarted = true
|
||||||
|
progressDisposable.set(progressSignal.start())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, completed: {
|
||||||
|
})
|
||||||
|
cancelImpl = {
|
||||||
|
self?.playlistPreloadDisposable?.dispose()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
strongSelf.context.sharedContext.navigateToChat(accountId: strongSelf.context.account.id, peerId: id.messageId.peerId, messageId: id.messageId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mediaAccessoryPanel.frame = panelFrame
|
||||||
|
if let dismissingPanel = self.dismissingPanel {
|
||||||
|
self.mediaAccessoryPanelContainer.insertSubnode(mediaAccessoryPanel, aboveSubnode: dismissingPanel)
|
||||||
|
} else {
|
||||||
|
self.mediaAccessoryPanelContainer.addSubnode(mediaAccessoryPanel)
|
||||||
|
}
|
||||||
|
self.mediaAccessoryPanel = (mediaAccessoryPanel, type)
|
||||||
|
mediaAccessoryPanel.updateLayout(size: panelFrame.size, leftInset: sideInset, rightInset: sideInset, transition: .immediate)
|
||||||
|
switch order {
|
||||||
|
case .regular:
|
||||||
|
mediaAccessoryPanel.containerNode.headerNode.playbackItems = (item, previousItem, nextItem)
|
||||||
|
case .reversed:
|
||||||
|
mediaAccessoryPanel.containerNode.headerNode.playbackItems = (item, nextItem, previousItem)
|
||||||
|
case .random:
|
||||||
|
mediaAccessoryPanel.containerNode.headerNode.playbackItems = (item, nil, nil)
|
||||||
|
}
|
||||||
|
mediaAccessoryPanel.containerNode.headerNode.playbackStatus = self.context.sharedContext.mediaManager.globalMediaPlayerState
|
||||||
|
|> map { state -> MediaPlayerStatus in
|
||||||
|
if let stateOrLoading = state?.1, case let .state(state) = stateOrLoading {
|
||||||
|
return state.status
|
||||||
|
} else {
|
||||||
|
return MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: .paused, soundEnabled: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mediaAccessoryPanel.animateIn(transition: transition)
|
||||||
|
}
|
||||||
|
} else if let (mediaAccessoryPanel, _) = self.mediaAccessoryPanel {
|
||||||
|
self.mediaAccessoryPanel = nil
|
||||||
|
self.dismissingPanel = mediaAccessoryPanel
|
||||||
|
mediaAccessoryPanel.animateOut(transition: transition, completion: { [weak self, weak mediaAccessoryPanel] in
|
||||||
|
mediaAccessoryPanel?.removeFromSupernode()
|
||||||
|
if let strongSelf = self, strongSelf.dismissingPanel === mediaAccessoryPanel {
|
||||||
|
strongSelf.dismissingPanel = nil
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
transition.updateFrame(node: self.mediaAccessoryPanelContainer, frame: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: MediaNavigationAccessoryHeaderNode.minimizedHeight)))
|
||||||
|
|
||||||
transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(), size: size))
|
transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(), size: size))
|
||||||
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
|
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
|
||||||
self.listNode.updateLayout(transition: transition, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), duration: duration, curve: curve))
|
self.listNode.updateLayout(transition: transition, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: UIEdgeInsets(top: topPanelHeight, left: sideInset, bottom: bottomInset, right: sideInset), duration: duration, curve: curve))
|
||||||
if isScrollingLockedAtTop {
|
if isScrollingLockedAtTop {
|
||||||
switch self.listNode.visibleContentOffset() {
|
switch self.listNode.visibleContentOffset() {
|
||||||
case .known(0.0), .none:
|
case .known(0.0), .none:
|
||||||
|
@ -118,6 +118,8 @@ final class PeerInfoMembersPaneNode: ASDisplayNode, PeerInfoPaneNode {
|
|||||||
return self.ready.get()
|
return self.ready.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let shouldReceiveExpandProgressUpdates: Bool = false
|
||||||
|
|
||||||
private var disposable: Disposable?
|
private var disposable: Disposable?
|
||||||
|
|
||||||
init(context: AccountContext, peerId: PeerId, membersContext: PeerInfoMembersContext, action: @escaping (PeerInfoMember, PeerMembersListAction) -> Void) {
|
init(context: AccountContext, peerId: PeerId, membersContext: PeerInfoMembersContext, action: @escaping (PeerInfoMember, PeerMembersListAction) -> Void) {
|
||||||
@ -170,7 +172,7 @@ final class PeerInfoMembersPaneNode: ASDisplayNode, PeerInfoPaneNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
|
func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
|
||||||
let isFirstLayout = self.currentParams == nil
|
let isFirstLayout = self.currentParams == nil
|
||||||
self.currentParams = (size, isScrollingLockedAtTop, presentationData)
|
self.currentParams = (size, isScrollingLockedAtTop, presentationData)
|
||||||
|
|
||||||
|
@ -449,7 +449,7 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
|
|||||||
return self._itemInteraction!
|
return self._itemInteraction!
|
||||||
}
|
}
|
||||||
|
|
||||||
private var currentParams: (size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, presentationData: PresentationData)?
|
private var currentParams: (size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData)?
|
||||||
|
|
||||||
private let ready = Promise<Bool>()
|
private let ready = Promise<Bool>()
|
||||||
private var didSetReady: Bool = false
|
private var didSetReady: Bool = false
|
||||||
@ -457,6 +457,8 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
|
|||||||
return self.ready.get()
|
return self.ready.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let shouldReceiveExpandProgressUpdates: Bool = false
|
||||||
|
|
||||||
private let listDisposable = MetaDisposable()
|
private let listDisposable = MetaDisposable()
|
||||||
private var hiddenMediaDisposable: Disposable?
|
private var hiddenMediaDisposable: Disposable?
|
||||||
private var mediaItems: [VisualMediaItem] = []
|
private var mediaItems: [VisualMediaItem] = []
|
||||||
@ -482,7 +484,7 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
|
|||||||
|
|
||||||
self._itemInteraction = VisualMediaItemInteraction(
|
self._itemInteraction = VisualMediaItemInteraction(
|
||||||
openMessage: { [weak self] message in
|
openMessage: { [weak self] message in
|
||||||
self?.chatControllerInteraction.openMessage(message, .default)
|
let _ = self?.chatControllerInteraction.openMessage(message, .default)
|
||||||
},
|
},
|
||||||
openMessageContextActions: { [weak self] message, sourceNode, sourceRect, gesture in
|
openMessageContextActions: { [weak self] message, sourceNode, sourceRect, gesture in
|
||||||
self?.chatControllerInteraction.openMessageContextActions(message, sourceNode, sourceRect, gesture)
|
self?.chatControllerInteraction.openMessageContextActions(message, sourceNode, sourceRect, gesture)
|
||||||
@ -559,8 +561,8 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
|
|||||||
let wasFirstHistoryView = self.isFirstHistoryView
|
let wasFirstHistoryView = self.isFirstHistoryView
|
||||||
self.isFirstHistoryView = false
|
self.isFirstHistoryView = false
|
||||||
|
|
||||||
if let (size, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, presentationData) = self.currentParams {
|
if let (size, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) = self.currentParams {
|
||||||
self.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, presentationData: presentationData, synchronous: wasFirstHistoryView, transition: .immediate)
|
self.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, presentationData: presentationData, synchronous: wasFirstHistoryView, transition: .immediate)
|
||||||
if !self.didSetReady {
|
if !self.didSetReady {
|
||||||
self.didSetReady = true
|
self.didSetReady = true
|
||||||
self.ready.set(.single(true))
|
self.ready.set(.single(true))
|
||||||
@ -666,8 +668,8 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
|
func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
|
||||||
self.currentParams = (size, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, presentationData)
|
self.currentParams = (size, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData)
|
||||||
|
|
||||||
transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: size))
|
transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: size))
|
||||||
|
|
||||||
@ -704,7 +706,7 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
|
|||||||
}
|
}
|
||||||
|
|
||||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||||
if let (size, sideInset, bottomInset, visibleHeight, _, presentationData) = self.currentParams {
|
if let (size, sideInset, bottomInset, visibleHeight, _, _, presentationData) = self.currentParams {
|
||||||
self.updateVisibleItems(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, theme: presentationData.theme, strings: presentationData.strings, synchronousLoad: false)
|
self.updateVisibleItems(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, theme: presentationData.theme, strings: presentationData.strings, synchronousLoad: false)
|
||||||
|
|
||||||
if scrollView.contentOffset.y >= scrollView.contentSize.height - scrollView.bounds.height * 2.0, let currentView = self.currentView, currentView.earlierId != nil {
|
if scrollView.contentOffset.y >= scrollView.contentSize.height - scrollView.bounds.height * 2.0, let currentView = self.currentView, currentView.earlierId != nil {
|
||||||
|
@ -113,7 +113,7 @@ private func peerInfoAvailableMediaPanes(context: AccountContext, peerId: PeerId
|
|||||||
(.photoOrVideo, .media),
|
(.photoOrVideo, .media),
|
||||||
(.file, .files),
|
(.file, .files),
|
||||||
(.music, .music),
|
(.music, .music),
|
||||||
//(.voiceOrInstantVideo, .voice),
|
(.voiceOrInstantVideo, .voice),
|
||||||
(.webPage, .links)
|
(.webPage, .links)
|
||||||
]
|
]
|
||||||
enum PaneState {
|
enum PaneState {
|
||||||
|
@ -12,8 +12,9 @@ import ContextUI
|
|||||||
|
|
||||||
protocol PeerInfoPaneNode: ASDisplayNode {
|
protocol PeerInfoPaneNode: ASDisplayNode {
|
||||||
var isReady: Signal<Bool, NoError> { get }
|
var isReady: Signal<Bool, NoError> { get }
|
||||||
|
var shouldReceiveExpandProgressUpdates: Bool { get }
|
||||||
|
|
||||||
func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition)
|
func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition)
|
||||||
func scrollToTop() -> Bool
|
func scrollToTop() -> Bool
|
||||||
func transferVelocity(_ velocity: CGFloat)
|
func transferVelocity(_ velocity: CGFloat)
|
||||||
func cancelPreviewGestures()
|
func cancelPreviewGestures()
|
||||||
@ -28,21 +29,21 @@ final class PeerInfoPaneWrapper {
|
|||||||
let key: PeerInfoPaneKey
|
let key: PeerInfoPaneKey
|
||||||
let node: PeerInfoPaneNode
|
let node: PeerInfoPaneNode
|
||||||
var isAnimatingOut: Bool = false
|
var isAnimatingOut: Bool = false
|
||||||
private var appliedParams: (CGSize, CGFloat, CGFloat, CGFloat, Bool, PresentationData)?
|
private var appliedParams: (CGSize, CGFloat, CGFloat, CGFloat, Bool, CGFloat, PresentationData)?
|
||||||
|
|
||||||
init(key: PeerInfoPaneKey, node: PeerInfoPaneNode) {
|
init(key: PeerInfoPaneKey, node: PeerInfoPaneNode) {
|
||||||
self.key = key
|
self.key = key
|
||||||
self.node = node
|
self.node = node
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
|
func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
|
||||||
if let (currentSize, currentSideInset, currentBottomInset, visibleHeight, currentIsScrollingLockedAtTop, currentPresentationData) = self.appliedParams {
|
if let (currentSize, currentSideInset, currentBottomInset, visibleHeight, currentIsScrollingLockedAtTop, currentExpandProgress, currentPresentationData) = self.appliedParams {
|
||||||
if currentSize == size && currentSideInset == sideInset && currentBottomInset == bottomInset, currentIsScrollingLockedAtTop == isScrollingLockedAtTop && currentPresentationData === presentationData {
|
if currentSize == size && currentSideInset == sideInset && currentBottomInset == bottomInset, currentIsScrollingLockedAtTop == isScrollingLockedAtTop && currentExpandProgress == expandProgress && currentPresentationData === presentationData {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.appliedParams = (size, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, presentationData)
|
self.appliedParams = (size, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData)
|
||||||
self.node.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, presentationData: presentationData, synchronous: synchronous, transition: transition)
|
self.node.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, presentationData: presentationData, synchronous: synchronous, transition: transition)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -734,14 +735,14 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat
|
|||||||
)
|
)
|
||||||
self.pendingPanes[key] = pane
|
self.pendingPanes[key] = pane
|
||||||
pane.pane.node.frame = paneFrame
|
pane.pane.node.frame = paneFrame
|
||||||
pane.pane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: expansionFraction < 1.0 - CGFloat.ulpOfOne, presentationData: presentationData, synchronous: true, transition: .immediate)
|
pane.pane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: expansionFraction < 1.0 - CGFloat.ulpOfOne, expandProgress: expansionFraction, presentationData: presentationData, synchronous: true, transition: .immediate)
|
||||||
leftScope = true
|
leftScope = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (key, pane) in self.pendingPanes {
|
for (key, pane) in self.pendingPanes {
|
||||||
pane.pane.node.frame = paneFrame
|
pane.pane.node.frame = paneFrame
|
||||||
pane.pane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: expansionFraction < 1.0 - CGFloat.ulpOfOne, presentationData: presentationData, synchronous: self.currentPaneKey == nil, transition: .immediate)
|
pane.pane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: expansionFraction < 1.0 - CGFloat.ulpOfOne, expandProgress: expansionFraction, presentationData: presentationData, synchronous: self.currentPaneKey == nil, transition: .immediate)
|
||||||
|
|
||||||
if pane.isReady {
|
if pane.isReady {
|
||||||
self.pendingPanes.removeValue(forKey: key)
|
self.pendingPanes.removeValue(forKey: key)
|
||||||
@ -819,7 +820,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat
|
|||||||
paneCompletion()
|
paneCompletion()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
pane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: expansionFraction < 1.0 - CGFloat.ulpOfOne, presentationData: presentationData, synchronous: paneWasAdded, transition: paneTransition)
|
pane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: expansionFraction < 1.0 - CGFloat.ulpOfOne, expandProgress: expansionFraction, presentationData: presentationData, synchronous: paneWasAdded, transition: paneTransition)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -836,7 +837,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat
|
|||||||
case .links:
|
case .links:
|
||||||
title = presentationData.strings.PeerInfo_PaneLinks
|
title = presentationData.strings.PeerInfo_PaneLinks
|
||||||
case .voice:
|
case .voice:
|
||||||
title = presentationData.strings.PeerInfo_PaneVoice
|
title = presentationData.strings.PeerInfo_PaneVoiceAndVideo
|
||||||
case .music:
|
case .music:
|
||||||
title = presentationData.strings.PeerInfo_PaneAudio
|
title = presentationData.strings.PeerInfo_PaneAudio
|
||||||
case .groupsInCommon:
|
case .groupsInCommon:
|
||||||
@ -850,7 +851,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat
|
|||||||
for (_, pane) in self.pendingPanes {
|
for (_, pane) in self.pendingPanes {
|
||||||
let paneTransition: ContainedViewLayoutTransition = .immediate
|
let paneTransition: ContainedViewLayoutTransition = .immediate
|
||||||
paneTransition.updateFrame(node: pane.pane.node, frame: paneFrame)
|
paneTransition.updateFrame(node: pane.pane.node, frame: paneFrame)
|
||||||
pane.pane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: expansionFraction < 1.0 - CGFloat.ulpOfOne, presentationData: presentationData, synchronous: true, transition: paneTransition)
|
pane.pane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: expansionFraction < 1.0 - CGFloat.ulpOfOne, expandProgress: expansionFraction, presentationData: presentationData, synchronous: true, transition: paneTransition)
|
||||||
}
|
}
|
||||||
if !self.didSetIsReady && data != nil {
|
if !self.didSetIsReady && data != nil {
|
||||||
if let currentPaneKey = self.currentPaneKey, let currentPane = self.currentPanes[currentPaneKey] {
|
if let currentPaneKey = self.currentPaneKey, let currentPane = self.currentPanes[currentPaneKey] {
|
||||||
|
@ -1205,59 +1205,131 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
|||||||
self?.openPeer(peerId: id, navigation: navigation)
|
self?.openPeer(peerId: id, navigation: navigation)
|
||||||
}
|
}
|
||||||
}, openPeerMention: { _ in
|
}, openPeerMention: { _ in
|
||||||
}, openMessageContextMenu: { [weak self] message, _, _, _, _ in
|
}, openMessageContextMenu: { [weak self] message, _, node, frame, anyRecognizer in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self, let node = node as? ContextExtractedContentContainingNode else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let _ = storedMessageFromSearch(account: strongSelf.context.account, message: message).start()
|
let _ = storedMessageFromSearch(account: strongSelf.context.account, message: message).start()
|
||||||
|
|
||||||
|
var linkForCopying: String?
|
||||||
|
var currentSupernode: ASDisplayNode? = node
|
||||||
|
while true {
|
||||||
|
if currentSupernode == nil {
|
||||||
|
break
|
||||||
|
} else if let currentSupernode = currentSupernode as? ListMessageSnippetItemNode {
|
||||||
|
linkForCopying = currentSupernode.currentPrimaryUrl
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
currentSupernode = currentSupernode?.supernode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let gesture: ContextGesture? = anyRecognizer as? ContextGesture
|
||||||
let _ = (chatAvailableMessageActionsImpl(postbox: strongSelf.context.account.postbox, accountPeerId: strongSelf.context.account.peerId, messageIds: [message.id])
|
let _ = (chatAvailableMessageActionsImpl(postbox: strongSelf.context.account.postbox, accountPeerId: strongSelf.context.account.peerId, messageIds: [message.id])
|
||||||
|> deliverOnMainQueue).start(next: { actions in
|
|> deliverOnMainQueue).start(next: { actions in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var messageIds = Set<MessageId>()
|
var items: [ContextMenuItem] = []
|
||||||
messageIds.insert(message.id)
|
|
||||||
|
|
||||||
if let strongSelf = self {
|
if let linkForCopying = linkForCopying {
|
||||||
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)
|
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
|
||||||
var items: [ActionSheetButtonItem] = []
|
c.dismiss(completion: {})
|
||||||
|
UIPasteboard.general.string = linkForCopying
|
||||||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.SharedMedia_ViewInChat, color: .accent, action: { [weak actionSheet] in
|
})))
|
||||||
actionSheet?.dismissAnimated()
|
}
|
||||||
|
|
||||||
|
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.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 {
|
if let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController {
|
||||||
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(strongSelf.peerId), subject: .message(message.id)))
|
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(strongSelf.peerId), subject: .message(message.id)))
|
||||||
}
|
}
|
||||||
}))
|
})
|
||||||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_ContextMenuForward, color: .accent, action: { [weak actionSheet] in
|
})))
|
||||||
actionSheet?.dismissAnimated()
|
if actions.options.contains(.deleteLocally) || actions.options.contains(.deleteGlobally) {
|
||||||
if let strongSelf = self {
|
let context = strongSelf.context
|
||||||
strongSelf.forwardMessages(messageIds: messageIds)
|
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
|
||||||
if actions.options.contains(.deleteLocally) || actions.options.contains(.deleteGlobally) {
|
c.setItems(context.account.postbox.transaction { transaction -> [ContextMenuItem] in
|
||||||
items.append( ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_ContextMenuDelete, color: .destructive, action: { [weak actionSheet] in
|
var items: [ContextMenuItem] = []
|
||||||
actionSheet?.dismissAnimated()
|
let messageIds = [message.id]
|
||||||
if let strongSelf = self {
|
|
||||||
strongSelf.deleteMessages(messageIds: Set(messageIds))
|
if let peer = transaction.getPeer(message.id.peerId) {
|
||||||
|
var personalPeerName: String?
|
||||||
|
var isChannel = false
|
||||||
|
if let user = peer as? TelegramUser {
|
||||||
|
personalPeerName = user.compactDisplayTitle
|
||||||
|
} else if let channel = peer as? TelegramChannel, case .broadcast = channel.info {
|
||||||
|
isChannel = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if actions.options.contains(.deleteGlobally) {
|
||||||
|
let globalTitle: String
|
||||||
|
if isChannel {
|
||||||
|
globalTitle = presentationData.strings.Conversation_DeleteMessagesForMe
|
||||||
|
} else if let personalPeerName = personalPeerName {
|
||||||
|
globalTitle = presentationData.strings.Conversation_DeleteMessagesFor(personalPeerName).0
|
||||||
|
} else {
|
||||||
|
globalTitle = presentationData.strings.Conversation_DeleteMessagesForEveryone
|
||||||
|
}
|
||||||
|
items.append(.action(ContextMenuActionItem(text: globalTitle, textColor: .destructive, icon: { _ in nil }, action: { c, f in
|
||||||
|
c.dismiss(completion: {
|
||||||
|
if let strongSelf = self {
|
||||||
|
strongSelf.headerNode.navigationButtonContainer.performAction?(.selectionDone)
|
||||||
|
let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: Array(messageIds), type: .forEveryone).start()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
if actions.options.contains(.deleteLocally) {
|
||||||
|
var localOptionText = presentationData.strings.Conversation_DeleteMessagesForMe
|
||||||
|
if context.account.peerId == peerId {
|
||||||
|
if messageIds.count == 1 {
|
||||||
|
localOptionText = presentationData.strings.Conversation_Moderate_Delete
|
||||||
|
} else {
|
||||||
|
localOptionText = presentationData.strings.Conversation_DeleteManyMessages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
items.append(.action(ContextMenuActionItem(text: localOptionText, textColor: .destructive, icon: { _ in nil }, action: { c, f in
|
||||||
|
c.dismiss(completion: {
|
||||||
|
if let strongSelf = self {
|
||||||
|
strongSelf.headerNode.navigationButtonContainer.performAction?(.selectionDone)
|
||||||
|
let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: Array(messageIds), type: .forLocalPeer).start()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}))
|
|
||||||
}
|
return items
|
||||||
if strongSelf.searchDisplayController == nil {
|
})
|
||||||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_ContextMenuMore, color: .accent, action: { [weak actionSheet] in
|
})))
|
||||||
actionSheet?.dismissAnimated()
|
}
|
||||||
|
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 {
|
if let strongSelf = self {
|
||||||
strongSelf.chatInterfaceInteraction.toggleMessagesSelection([message.id], true)
|
strongSelf.chatInterfaceInteraction.toggleMessagesSelection([message.id], true)
|
||||||
strongSelf.expandTabs()
|
strongSelf.expandTabs()
|
||||||
}
|
}
|
||||||
}))
|
|
||||||
}
|
|
||||||
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
|
|
||||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
|
||||||
actionSheet?.dismissAnimated()
|
|
||||||
})
|
})
|
||||||
])])
|
})))
|
||||||
strongSelf.view.endEditing(true)
|
|
||||||
strongSelf.controller?.present(actionSheet, in: .window(.root))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(MessageContextExtractedContentSource(sourceNode: node)), items: .single(items), reactionItems: [], recognizer: nil, gesture: gesture)
|
||||||
|
strongSelf.controller?.window?.presentInGlobalOverlay(controller)
|
||||||
})
|
})
|
||||||
}, openMessageContextActions: { [weak self] message, node, rect, gesture in
|
}, openMessageContextActions: { [weak self] message, node, rect, gesture in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
@ -1446,9 +1518,10 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
|||||||
}, updateInputState: { _ in
|
}, updateInputState: { _ in
|
||||||
}, updateInputMode: { _ in
|
}, updateInputMode: { _ in
|
||||||
}, openMessageShareMenu: { _ in
|
}, openMessageShareMenu: { _ in
|
||||||
}, presentController: { _, _ in
|
}, presentController: { [weak self] c, a in
|
||||||
}, navigationController: {
|
self?.controller?.present(c, in: .window(.root), with: a)
|
||||||
return nil
|
}, navigationController: { [weak self] in
|
||||||
|
return self?.controller?.navigationController as? NavigationController
|
||||||
}, chatControllerNode: {
|
}, chatControllerNode: {
|
||||||
return nil
|
return nil
|
||||||
}, reactionContainerNode: {
|
}, reactionContainerNode: {
|
||||||
@ -4537,3 +4610,22 @@ private final class ContextControllerContentSourceImpl: ContextControllerContent
|
|||||||
self.controller.didAppearInContextPreview()
|
self.controller.didAppearInContextPreview()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final class MessageContextExtractedContentSource: ContextExtractedContentSource {
|
||||||
|
let keepInPlace: Bool = false
|
||||||
|
let ignoreContentTouches: Bool = true
|
||||||
|
|
||||||
|
private let sourceNode: ContextExtractedContentContainingNode
|
||||||
|
|
||||||
|
init(sourceNode: ContextExtractedContentContainingNode) {
|
||||||
|
self.sourceNode = sourceNode
|
||||||
|
}
|
||||||
|
|
||||||
|
func takeView() -> ContextControllerTakeViewInfo? {
|
||||||
|
return ContextControllerTakeViewInfo(contentContainingNode: self.sourceNode, contentAreaInScreenSpace: UIScreen.main.bounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
func putBack() -> ContextControllerPutBackViewInfo? {
|
||||||
|
return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user