Peer media redesign

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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