Various Improvements

This commit is contained in:
Ilya Laktyushin 2021-03-17 09:19:57 +04:00
parent 86e27f0706
commit 0f9d40016f
27 changed files with 594 additions and 161 deletions

View File

@ -154,6 +154,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
private let tabContainerNode: ChatListFilterTabContainerNode private let tabContainerNode: ChatListFilterTabContainerNode
private var tabContainerData: ([ChatListFilterTabEntry], Bool)? private var tabContainerData: ([ChatListFilterTabEntry], Bool)?
private var didSetupTabs = false
public override func updateNavigationCustomData(_ data: Any?, progress: CGFloat, transition: ContainedViewLayoutTransition) { public override func updateNavigationCustomData(_ data: Any?, progress: CGFloat, transition: ContainedViewLayoutTransition) {
if self.isNodeLoaded { if self.isNodeLoaded {
self.chatListDisplayNode.containerNode.updateSelectedChatLocation(data: data as? ChatLocation, progress: progress, transition: transition) self.chatListDisplayNode.containerNode.updateSelectedChatLocation(data: data as? ChatLocation, progress: progress, transition: transition)
@ -1590,17 +1592,21 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
let isEmpty = resolvedItems.count <= 1 || displayTabsAtBottom let isEmpty = resolvedItems.count <= 1 || displayTabsAtBottom
let animated = strongSelf.didSetupTabs
strongSelf.didSetupTabs = true
if wasEmpty != isEmpty, strongSelf.displayNavigationBar { if wasEmpty != isEmpty, strongSelf.displayNavigationBar {
strongSelf.navigationBar?.setSecondaryContentNode(isEmpty ? nil : strongSelf.tabContainerNode) strongSelf.navigationBar?.setSecondaryContentNode(isEmpty ? nil : strongSelf.tabContainerNode, animated: false)
if let parentController = strongSelf.parent as? TabBarController { if let parentController = strongSelf.parent as? TabBarController {
parentController.navigationBar?.setSecondaryContentNode(isEmpty ? nil : strongSelf.tabContainerNode) parentController.navigationBar?.setSecondaryContentNode(isEmpty ? nil : strongSelf.tabContainerNode, animated: animated)
} }
} }
if let layout = strongSelf.validLayout { if let layout = strongSelf.validLayout {
if wasEmpty != isEmpty { if wasEmpty != isEmpty {
strongSelf.containerLayoutUpdated(layout, transition: .immediate) let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate
(strongSelf.parent as? TabBarController)?.updateLayout() strongSelf.containerLayoutUpdated(layout, transition: transition)
(strongSelf.parent as? TabBarController)?.updateLayout(transition: transition)
} else { } else {
strongSelf.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: resolvedItems, selectedFilter: selectedEntryId, isReordering: strongSelf.chatListDisplayNode.isReorderingFilters || (strongSelf.chatListDisplayNode.containerNode.currentItemNode.currentState.editing && !strongSelf.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: false, transitionFraction: strongSelf.chatListDisplayNode.containerNode.transitionFraction, presentationData: strongSelf.presentationData, transition: .animated(duration: 0.4, curve: .spring)) strongSelf.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: resolvedItems, selectedFilter: selectedEntryId, isReordering: strongSelf.chatListDisplayNode.isReorderingFilters || (strongSelf.chatListDisplayNode.containerNode.currentItemNode.currentState.editing && !strongSelf.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: false, transitionFraction: strongSelf.chatListDisplayNode.containerNode.transitionFraction, presentationData: strongSelf.presentationData, transition: .animated(duration: 0.4, curve: .spring))
strongSelf.chatListDisplayNode.inlineTabContainerNode.update(size: CGSize(width: layout.size.width, height: 40.0), sideInset: layout.safeInsets.left, filters: resolvedItems, selectedFilter: selectedEntryId, isReordering: strongSelf.chatListDisplayNode.isReorderingFilters || (strongSelf.chatListDisplayNode.containerNode.currentItemNode.currentState.editing && !strongSelf.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: false, transitionFraction: strongSelf.chatListDisplayNode.containerNode.transitionFraction, presentationData: strongSelf.presentationData, transition: .animated(duration: 0.4, curve: .spring)) strongSelf.chatListDisplayNode.inlineTabContainerNode.update(size: CGSize(width: layout.size.width, height: 40.0), sideInset: layout.safeInsets.left, filters: resolvedItems, selectedFilter: selectedEntryId, isReordering: strongSelf.chatListDisplayNode.isReorderingFilters || (strongSelf.chatListDisplayNode.containerNode.currentItemNode.currentState.editing && !strongSelf.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: false, transitionFraction: strongSelf.chatListDisplayNode.containerNode.transitionFraction, presentationData: strongSelf.presentationData, transition: .animated(duration: 0.4, curve: .spring))

View File

@ -28,6 +28,7 @@ swift_library(
"//submodules/OverlayStatusController:OverlayStatusController", "//submodules/OverlayStatusController:OverlayStatusController",
"//submodules/PresentationDataUtils:PresentationDataUtils", "//submodules/PresentationDataUtils:PresentationDataUtils",
"//submodules/UrlEscaping:UrlEscaping", "//submodules/UrlEscaping:UrlEscaping",
"//submodules/ManagedAnimationNode:ManagedAnimationNode"
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -20,6 +20,7 @@ import LocalizedPeerData
import TextSelectionNode import TextSelectionNode
import UrlEscaping import UrlEscaping
import UndoUI import UndoUI
import ManagedAnimationNode
private let deleteImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: .white) private let deleteImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: .white)
private let actionImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: .white) private let actionImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: .white)
@ -27,8 +28,6 @@ private let editImage = generateTintedImage(image: UIImage(bundleImageName: "Med
private let backwardImage = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/BackwardButton"), color: .white) private let backwardImage = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/BackwardButton"), color: .white)
private let forwardImage = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/ForwardButton"), color: .white) private let forwardImage = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/ForwardButton"), color: .white)
private let pauseImage = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/PauseButton"), color: .white)
private let playImage = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/PlayButton"), color: .white)
private let cloudFetchIcon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/FileCloudFetch"), color: UIColor.white) private let cloudFetchIcon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/FileCloudFetch"), color: UIColor.white)
@ -130,6 +129,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
private let backwardButton: HighlightableButtonNode private let backwardButton: HighlightableButtonNode
private let forwardButton: HighlightableButtonNode private let forwardButton: HighlightableButtonNode
private let playbackControlButton: HighlightableButtonNode private let playbackControlButton: HighlightableButtonNode
private let playPauseIconNode: PlayPauseIconNode
private let statusButtonNode: HighlightTrackingButtonNode private let statusButtonNode: HighlightTrackingButtonNode
private let statusNode: RadialStatusNode private let statusNode: RadialStatusNode
@ -179,7 +179,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
self.forwardButton.isHidden = !seekable self.forwardButton.isHidden = !seekable
if status == .Local { if status == .Local {
self.playbackControlButton.isHidden = false self.playbackControlButton.isHidden = false
self.playbackControlButton.setImage(playImage, for: []) self.playPauseIconNode.enqueueState(.play, animated: true)
} else { } else {
self.playbackControlButton.isHidden = true self.playbackControlButton.isHidden = true
} }
@ -207,7 +207,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
self.backwardButton.isHidden = !seekable self.backwardButton.isHidden = !seekable
self.forwardButton.isHidden = !seekable self.forwardButton.isHidden = !seekable
self.playbackControlButton.isHidden = false self.playbackControlButton.isHidden = false
self.playbackControlButton.setImage(paused ? playImage : pauseImage, for: []) self.playPauseIconNode.enqueueState(paused && !self.wasPlaying ? .play : .pause, animated: true)
self.statusButtonNode.isHidden = true self.statusButtonNode.isHidden = true
self.statusNode.isHidden = true self.statusNode.isHidden = true
} }
@ -314,6 +314,8 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
self.playbackControlButton = HighlightableButtonNode() self.playbackControlButton = HighlightableButtonNode()
self.playbackControlButton.isHidden = true self.playbackControlButton.isHidden = true
self.playPauseIconNode = PlayPauseIconNode()
self.statusButtonNode = HighlightTrackingButtonNode() self.statusButtonNode = HighlightTrackingButtonNode()
self.statusNode = RadialStatusNode(backgroundNodeColor: .clear) self.statusNode = RadialStatusNode(backgroundNodeColor: .clear)
self.statusNode.isUserInteractionEnabled = false self.statusNode.isUserInteractionEnabled = false
@ -361,6 +363,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
self.contentNode.addSubnode(self.backwardButton) self.contentNode.addSubnode(self.backwardButton)
self.contentNode.addSubnode(self.forwardButton) self.contentNode.addSubnode(self.forwardButton)
self.contentNode.addSubnode(self.playbackControlButton) self.contentNode.addSubnode(self.playbackControlButton)
self.playbackControlButton.addSubnode(self.playPauseIconNode)
self.contentNode.addSubnode(self.statusNode) self.contentNode.addSubnode(self.statusNode)
self.contentNode.addSubnode(self.statusButtonNode) self.contentNode.addSubnode(self.statusButtonNode)
@ -767,6 +770,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
} }
self.playbackControlButton.frame = CGRect(origin: CGPoint(x: floor((width - 44.0) / 2.0), y: panelHeight - bottomInset - 44.0), size: CGSize(width: 44.0, height: 44.0)) self.playbackControlButton.frame = CGRect(origin: CGPoint(x: floor((width - 44.0) / 2.0), y: panelHeight - bottomInset - 44.0), size: CGSize(width: 44.0, height: 44.0))
self.playPauseIconNode.frame = self.playbackControlButton.bounds.offsetBy(dx: 2.0, dy: 2.0)
let statusSize = CGSize(width: 28.0, height: 28.0) let statusSize = CGSize(width: 28.0, height: 28.0)
transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: floor((width - statusSize.width) / 2.0), y: panelHeight - bottomInset - statusSize.height - 8.0), size: statusSize)) transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: floor((width - statusSize.width) / 2.0), y: panelHeight - bottomInset - statusSize.height - 8.0), size: statusSize))
@ -1079,6 +1083,11 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
} }
} }
let shareController = ShareController(context: strongSelf.context, subject: subject, preferredAction: preferredAction, forcedTheme: defaultDarkColorPresentationTheme) let shareController = ShareController(context: strongSelf.context, subject: subject, preferredAction: preferredAction, forcedTheme: defaultDarkColorPresentationTheme)
shareController.actionCompleted = { [weak self] in
if let strongSelf = self {
strongSelf.controllerInteraction?.presentController(UndoOverlayController(presentationData: presentationData, content: .mediaSaved(text: "Media saved to phone"), elevatedLayout: true, animateInAsReplacement: true, action: { _ in return false }), nil)
}
}
shareController.completed = { [weak self] peerIds in shareController.completed = { [weak self] peerIds in
if let strongSelf = self { if let strongSelf = self {
let _ = (strongSelf.context.account.postbox.transaction { transaction -> [Peer] in let _ = (strongSelf.context.account.postbox.transaction { transaction -> [Peer] in
@ -1140,6 +1149,11 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
let shareAction: ([Message]) -> Void = { messages in let shareAction: ([Message]) -> Void = { messages in
if let strongSelf = self { if let strongSelf = self {
let shareController = ShareController(context: strongSelf.context, subject: .messages(messages), preferredAction: preferredAction, forcedTheme: defaultDarkColorPresentationTheme) let shareController = ShareController(context: strongSelf.context, subject: .messages(messages), preferredAction: preferredAction, forcedTheme: defaultDarkColorPresentationTheme)
shareController.actionCompleted = { [weak self] in
if let strongSelf = self {
strongSelf.controllerInteraction?.presentController(UndoOverlayController(presentationData: presentationData, content: .mediaSaved(text: "Media saved to phone"), elevatedLayout: true, animateInAsReplacement: true, action: { _ in return false }), nil)
}
}
shareController.completed = { [weak self] peerIds in shareController.completed = { [weak self] peerIds in
if let strongSelf = self { if let strongSelf = self {
let _ = (strongSelf.context.account.postbox.transaction { transaction -> [Peer] in let _ = (strongSelf.context.account.postbox.transaction { transaction -> [Peer] in
@ -1257,6 +1271,11 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
} }
} }
let shareController = ShareController(context: self.context, subject: subject, preferredAction: preferredAction, forcedTheme: defaultDarkColorPresentationTheme) let shareController = ShareController(context: self.context, subject: subject, preferredAction: preferredAction, forcedTheme: defaultDarkColorPresentationTheme)
shareController.actionCompleted = { [weak self] in
if let strongSelf = self {
strongSelf.controllerInteraction?.presentController(UndoOverlayController(presentationData: presentationData, content: .mediaSaved(text: "Media saved to phone"), elevatedLayout: true, animateInAsReplacement: true, action: { _ in return false }), nil)
}
}
shareController.completed = { [weak self] peerIds in shareController.completed = { [weak self] peerIds in
if let strongSelf = self { if let strongSelf = self {
let _ = (strongSelf.context.account.postbox.transaction { transaction -> [Peer] in let _ = (strongSelf.context.account.postbox.transaction { transaction -> [Peer] in
@ -1381,3 +1400,53 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
} }
} }
} }
private enum PlayPauseIconNodeState: Equatable {
case play
case pause
}
private final class PlayPauseIconNode: ManagedAnimationNode {
private let duration: Double = 0.4
private var iconState: PlayPauseIconNodeState = .pause
init() {
super.init(size: CGSize(width: 40.0, height: 40.0))
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 41), duration: 0.01))
}
func enqueueState(_ state: PlayPauseIconNodeState, animated: Bool) {
guard self.iconState != state else {
return
}
let previousState = self.iconState
self.iconState = state
switch previousState {
case .pause:
switch state {
case .play:
if animated {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 83), duration: self.duration))
} else {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01))
}
case .pause:
break
}
case .play:
switch state {
case .pause:
if animated {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 41), duration: self.duration))
} else {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 41), duration: 0.01))
}
case .play:
break
}
}
}
}

View File

@ -484,7 +484,13 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
private var previousPlaying: Bool? private var previousPlaying: Bool?
private func setupControlsTimer() { private func setupControlsTimer() {
return
let timer = SwiftSignalKit.Timer(timeout: 3.0, repeat: false, completion: { [weak self] in
self?.updateControlsVisibility(false)
self?.controlsTimer = nil
}, queue: Queue.mainQueue())
timer.start()
self.controlsTimer = timer
} }
func setupItem(_ item: UniversalVideoGalleryItem) { func setupItem(_ item: UniversalVideoGalleryItem) {
@ -713,13 +719,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
if strongSelf.isCentral && playing && strongSelf.previousPlaying != true && !disablePlayerControls { if strongSelf.isCentral && playing && strongSelf.previousPlaying != true && !disablePlayerControls {
strongSelf.controlsTimer?.invalidate() strongSelf.controlsTimer?.invalidate()
strongSelf.setupControlsTimer()
let timer = SwiftSignalKit.Timer(timeout: 3.0, repeat: false, completion: { [weak self] in
self?.updateControlsVisibility(false)
self?.controlsTimer = nil
}, queue: Queue.mainQueue())
timer.start()
strongSelf.controlsTimer = timer
} else if !playing { } else if !playing {
strongSelf.controlsTimer?.invalidate() strongSelf.controlsTimer?.invalidate()
strongSelf.controlsTimer = nil strongSelf.controlsTimer = nil

View File

@ -18,6 +18,7 @@ swift_library(
"//submodules/TelegramUIPreferences:TelegramUIPreferences", "//submodules/TelegramUIPreferences:TelegramUIPreferences",
"//submodules/PresentationDataUtils:PresentationDataUtils", "//submodules/PresentationDataUtils:PresentationDataUtils",
"//submodules/LocationResources:LocationResources", "//submodules/LocationResources:LocationResources",
"//submodules/ShimmerEffect:ShimmerEffect",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -9,11 +9,12 @@ import SyncCore
import TelegramPresentationData import TelegramPresentationData
import ItemListUI import ItemListUI
import LocationResources import LocationResources
import ShimmerEffect
public final class ItemListVenueItem: ListViewItem, ItemListItem { public final class ItemListVenueItem: ListViewItem, ItemListItem {
let presentationData: ItemListPresentationData let presentationData: ItemListPresentationData
let account: Account let account: Account
let venue: TelegramMediaMap let venue: TelegramMediaMap?
let title: String? let title: String?
let subtitle: String? let subtitle: String?
let style: ItemListStyle let style: ItemListStyle
@ -23,7 +24,7 @@ public final class ItemListVenueItem: ListViewItem, ItemListItem {
public let sectionId: ItemListSectionId public let sectionId: ItemListSectionId
let header: ListViewItemHeader? let header: ListViewItemHeader?
public init(presentationData: ItemListPresentationData, account: Account, venue: TelegramMediaMap, title: String? = nil, subtitle: String? = nil, sectionId: ItemListSectionId = 0, style: ItemListStyle, action: (() -> Void)?, infoAction: (() -> Void)? = nil, header: ListViewItemHeader? = nil) { public init(presentationData: ItemListPresentationData, account: Account, venue: TelegramMediaMap?, title: String? = nil, subtitle: String? = nil, sectionId: ItemListSectionId = 0, style: ItemListStyle, action: (() -> Void)?, infoAction: (() -> Void)? = nil, header: ListViewItemHeader? = nil) {
self.presentationData = presentationData self.presentationData = presentationData
self.account = account self.account = account
self.venue = venue self.venue = venue
@ -117,6 +118,9 @@ public class ItemListVenueItemNode: ListViewItemNode, ItemListItemNode {
private let addressNode: TextNode private let addressNode: TextNode
private let infoButton: HighlightableButtonNode private let infoButton: HighlightableButtonNode
private var placeholderNode: ShimmerEffectNode?
private var absoluteLocation: (CGRect, CGSize)?
private var item: ItemListVenueItem? private var item: ItemListVenueItem?
private var layoutParams: (ItemListVenueItem, ListViewItemLayoutParams, ItemListNeighbors, Bool, Bool)? private var layoutParams: (ItemListVenueItem, ListViewItemLayoutParams, ItemListNeighbors, Bool, Bool)?
@ -170,6 +174,15 @@ public class ItemListVenueItemNode: ListViewItemNode, ItemListItemNode {
self.infoButton.addTarget(self, action: #selector(self.infoPressed), forControlEvents: .touchUpInside) self.infoButton.addTarget(self, action: #selector(self.infoPressed), forControlEvents: .touchUpInside)
} }
override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
var rect = rect
rect.origin.y += self.insets.top
self.absoluteLocation = (rect, containerSize)
if let shimmerNode = self.placeholderNode {
shimmerNode.updateAbsoluteRect(rect, within: containerSize)
}
}
public func asyncLayout() -> (_ item: ItemListVenueItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors, _ firstWithHeader: Bool, _ last: Bool) -> (ListViewItemNodeLayout, () -> Void) { public func asyncLayout() -> (_ item: ItemListVenueItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors, _ firstWithHeader: Bool, _ last: Bool) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeAddressLayout = TextNode.asyncLayout(self.addressNode) let makeAddressLayout = TextNode.asyncLayout(self.addressNode)
@ -188,27 +201,27 @@ public class ItemListVenueItemNode: ListViewItemNode, ItemListItemNode {
updatedTheme = item.presentationData.theme updatedTheme = item.presentationData.theme
} }
let venueType = item.venue.venue?.type ?? "" let venueType = item.venue?.venue?.type ?? ""
if currentItem?.venue.venue?.type != venueType { if currentItem?.venue?.venue?.type != venueType {
updatedVenueType = venueType updatedVenueType = venueType
} }
let title: String let title: String
if let venueTitle = item.venue.venue?.title { if let venueTitle = item.venue?.venue?.title {
title = venueTitle title = venueTitle
} else if let customTitle = item.title { } else if let customTitle = item.title {
title = customTitle title = customTitle
} else { } else {
title = "" title = " "
} }
let subtitle: String let subtitle: String
if let address = item.venue.venue?.address { if let address = item.venue?.venue?.address {
subtitle = address subtitle = address
} else if let customSubtitle = item.subtitle { } else if let customSubtitle = item.subtitle {
subtitle = customSubtitle subtitle = customSubtitle
} else { } else {
subtitle = "" subtitle = " "
} }
let titleAttributedString = NSAttributedString(string: title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor) let titleAttributedString = NSAttributedString(string: title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor)
@ -285,7 +298,7 @@ public class ItemListVenueItemNode: ListViewItemNode, ItemListItemNode {
strongSelf.topStripeNode.removeFromSupernode() strongSelf.topStripeNode.removeFromSupernode()
} }
if strongSelf.bottomStripeNode.supernode == nil { if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0) strongSelf.addSubnode(strongSelf.bottomStripeNode)
} }
if strongSelf.maskNode.supernode != nil { if strongSelf.maskNode.supernode != nil {
strongSelf.maskNode.removeFromSupernode() strongSelf.maskNode.removeFromSupernode()
@ -350,6 +363,45 @@ public class ItemListVenueItemNode: ListViewItemNode, ItemListItemNode {
strongSelf.infoButton.isHidden = item.infoAction == nil strongSelf.infoButton.isHidden = item.infoAction == nil
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: contentSize.height + UIScreenPixel + UIScreenPixel)) strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: contentSize.height + UIScreenPixel + UIScreenPixel))
if item.venue == nil {
let shimmerNode: ShimmerEffectNode
if let current = strongSelf.placeholderNode {
shimmerNode = current
} else {
shimmerNode = ShimmerEffectNode()
strongSelf.placeholderNode = shimmerNode
if strongSelf.bottomStripeNode.supernode != nil {
strongSelf.insertSubnode(shimmerNode, belowSubnode: strongSelf.bottomStripeNode)
} else {
strongSelf.addSubnode(shimmerNode)
}
}
shimmerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
if let (rect, size) = strongSelf.absoluteLocation {
shimmerNode.updateAbsoluteRect(rect, within: size)
}
var shapes: [ShimmerEffectNode.Shape] = []
let titleLineWidth: CGFloat = 180.0
let subtitleLineWidth: CGFloat = 90.0
let lineDiameter: CGFloat = 10.0
let iconFrame = strongSelf.iconNode.frame
shapes.append(.circle(iconFrame))
let titleFrame = strongSelf.titleNode.frame
shapes.append(.roundedRectLine(startPoint: CGPoint(x: titleFrame.minX, y: titleFrame.minY + floor((titleFrame.height - lineDiameter) / 2.0)), width: titleLineWidth, diameter: lineDiameter))
let subtitleFrame = strongSelf.addressNode.frame
shapes.append(.roundedRectLine(startPoint: CGPoint(x: subtitleFrame.minX, y: subtitleFrame.minY + floor((subtitleFrame.height - lineDiameter) / 2.0)), width: subtitleLineWidth, diameter: lineDiameter))
shimmerNode.update(backgroundColor: item.presentationData.theme.list.itemBlocksBackgroundColor, foregroundColor: item.presentationData.theme.list.mediaPlaceholderColor, shimmeringColor: item.presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: shapes, size: layout.contentSize)
} else if let shimmerNode = strongSelf.placeholderNode {
strongSelf.placeholderNode = nil
shimmerNode.removeFromSupernode()
}
} }
}) })
} }

View File

@ -10,7 +10,6 @@ import SwiftSignalKit
import MergeLists import MergeLists
import ItemListUI import ItemListUI
import ItemListVenueItem import ItemListVenueItem
import ActivityIndicator
import TelegramPresentationData import TelegramPresentationData
import TelegramStringFormatting import TelegramStringFormatting
import AccountContext import AccountContext
@ -41,7 +40,7 @@ private enum LocationPickerEntry: Comparable, Identifiable {
case location(PresentationTheme, String, String, TelegramMediaMap?, CLLocationCoordinate2D?) case location(PresentationTheme, String, String, TelegramMediaMap?, CLLocationCoordinate2D?)
case liveLocation(PresentationTheme, String, String, CLLocationCoordinate2D?) case liveLocation(PresentationTheme, String, String, CLLocationCoordinate2D?)
case header(PresentationTheme, String) case header(PresentationTheme, String)
case venue(PresentationTheme, TelegramMediaMap, Int) case venue(PresentationTheme, TelegramMediaMap?, Int)
case attribution(PresentationTheme, LocationAttribution) case attribution(PresentationTheme, LocationAttribution)
var stableId: LocationPickerEntryId { var stableId: LocationPickerEntryId {
@ -52,8 +51,8 @@ private enum LocationPickerEntry: Comparable, Identifiable {
return .liveLocation return .liveLocation
case .header: case .header:
return .header return .header
case let .venue(_, venue, _): case let .venue(_, venue, index):
return .venue(venue.venue?.id ?? "") return .venue(venue?.venue?.id ?? "\(index)")
case .attribution: case .attribution:
return .attribution return .attribution
} }
@ -80,7 +79,7 @@ private enum LocationPickerEntry: Comparable, Identifiable {
return false return false
} }
case let .venue(lhsTheme, lhsVenue, lhsIndex): case let .venue(lhsTheme, lhsVenue, lhsIndex):
if case let .venue(rhsTheme, rhsVenue, rhsIndex) = rhs, lhsTheme === rhsTheme, lhsVenue.venue?.id == rhsVenue.venue?.id, lhsIndex == rhsIndex { if case let .venue(rhsTheme, rhsVenue, rhsIndex) = rhs, lhsTheme === rhsTheme, lhsVenue?.venue?.id == rhsVenue?.venue?.id, lhsIndex == rhsIndex {
return true return true
} else { } else {
return false return false
@ -158,9 +157,9 @@ private enum LocationPickerEntry: Comparable, Identifiable {
case let .header(_, title): case let .header(_, title):
return LocationSectionHeaderItem(presentationData: ItemListPresentationData(presentationData), title: title) return LocationSectionHeaderItem(presentationData: ItemListPresentationData(presentationData), title: title)
case let .venue(_, venue, _): case let .venue(_, venue, _):
let venueType = venue.venue?.type ?? "" let venueType = venue?.venue?.type ?? ""
return ItemListVenueItem(presentationData: ItemListPresentationData(presentationData), account: account, venue: venue, style: .plain, action: { return ItemListVenueItem(presentationData: ItemListPresentationData(presentationData), account: account, venue: venue, style: .plain, action: venue.flatMap { venue in
interaction?.sendVenue(venue) return { interaction?.sendVenue(venue) }
}, infoAction: ["home", "work"].contains(venueType) ? { }, infoAction: ["home", "work"].contains(venueType) ? {
interaction?.openHomeWorkInfo() interaction?.openHomeWorkInfo()
} : nil) } : nil)
@ -253,7 +252,6 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM
private let listNode: ListView private let listNode: ListView
private let emptyResultsTextNode: ImmediateTextNode private let emptyResultsTextNode: ImmediateTextNode
private let headerNode: LocationMapHeaderNode private let headerNode: LocationMapHeaderNode
private let activityIndicator: ActivityIndicator
private let shadeNode: ASDisplayNode private let shadeNode: ASDisplayNode
private let innerShadeNode: ASDisplayNode private let innerShadeNode: ASDisplayNode
@ -301,8 +299,6 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM
self.optionsNode = LocationOptionsNode(presentationData: presentationData, updateMapMode: interaction.updateMapMode) self.optionsNode = LocationOptionsNode(presentationData: presentationData, updateMapMode: interaction.updateMapMode)
self.activityIndicator = ActivityIndicator(type: .custom(self.presentationData.theme.list.itemSecondaryTextColor, 22.0, 1.0, false))
self.shadeNode = ASDisplayNode() self.shadeNode = ASDisplayNode()
self.shadeNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor self.shadeNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
self.shadeNode.alpha = 0.0 self.shadeNode.alpha = 0.0
@ -316,7 +312,6 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM
self.addSubnode(self.listNode) self.addSubnode(self.listNode)
self.addSubnode(self.headerNode) self.addSubnode(self.headerNode)
self.addSubnode(self.optionsNode) self.addSubnode(self.optionsNode)
self.listNode.addSubnode(self.activityIndicator)
self.listNode.addSubnode(self.emptyResultsTextNode) self.listNode.addSubnode(self.emptyResultsTextNode)
self.shadeNode.addSubnode(self.innerShadeNode) self.shadeNode.addSubnode(self.innerShadeNode)
self.addSubnode(self.shadeNode) self.addSubnode(self.shadeNode)
@ -504,8 +499,8 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM
entries.append(.header(presentationData.theme, presentationData.strings.Map_ChooseAPlace.uppercased())) entries.append(.header(presentationData.theme, presentationData.strings.Map_ChooseAPlace.uppercased()))
let displayedVenues = foundVenues != nil || state.searchingVenuesAround ? foundVenues : venues let displayedVenues = foundVenues != nil || state.searchingVenuesAround ? foundVenues : venues
if let venues = displayedVenues {
var index: Int = 0 var index: Int = 0
if let venues = displayedVenues {
var attribution: LocationAttribution? var attribution: LocationAttribution?
for venue in venues { for venue in venues {
if venue.venue?.provider == "foursquare" { if venue.venue?.provider == "foursquare" {
@ -519,6 +514,11 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM
if let attribution = attribution { if let attribution = attribution {
entries.append(.attribution(presentationData.theme, attribution)) entries.append(.attribution(presentationData.theme, attribution))
} }
} else {
for i in 0 ..< 8 {
entries.append(.venue(presentationData.theme, nil, index))
index += 1
}
} }
let previousEntries = previousEntries.swap(entries) let previousEntries = previousEntries.swap(entries)
let previousState = previousState.swap(state) let previousState = previousState.swap(state)
@ -637,7 +637,7 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM
let headerFrame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: max(0.0, offset + overlap))) let headerFrame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: max(0.0, offset + overlap)))
listTransition.updateFrame(node: strongSelf.headerNode, frame: headerFrame) listTransition.updateFrame(node: strongSelf.headerNode, frame: headerFrame)
strongSelf.headerNode.updateLayout(layout: layout, navigationBarHeight: navigationBarHeight, topPadding: strongSelf.state.displayingMapModeOptions ? 38.0 : 0.0, offset: 0.0, size: headerFrame.size, transition: listTransition) strongSelf.headerNode.updateLayout(layout: layout, navigationBarHeight: navigationBarHeight, topPadding: strongSelf.state.displayingMapModeOptions ? 38.0 : 0.0, offset: 0.0, size: headerFrame.size, transition: listTransition)
strongSelf.layoutActivityIndicator(transition: listTransition) strongSelf.layoutEmptyResultsPlaceholder(transition: listTransition)
} }
self.listNode.beganInteractiveDragging = { [weak self] in self.listNode.beganInteractiveDragging = { [weak self] in
@ -755,12 +755,11 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM
self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in
if let strongSelf = self { if let strongSelf = self {
strongSelf.activityIndicator.isHidden = !transition.isLoading
strongSelf.emptyResultsTextNode.isHidden = transition.isLoading || !transition.isEmpty strongSelf.emptyResultsTextNode.isHidden = transition.isLoading || !transition.isEmpty
strongSelf.emptyResultsTextNode.attributedText = NSAttributedString(string: strongSelf.presentationData.strings.Map_NoPlacesNearby, font: Font.regular(15.0), textColor: strongSelf.presentationData.theme.list.freeTextColor) strongSelf.emptyResultsTextNode.attributedText = NSAttributedString(string: strongSelf.presentationData.strings.Map_NoPlacesNearby, font: Font.regular(15.0), textColor: strongSelf.presentationData.theme.list.freeTextColor)
strongSelf.layoutActivityIndicator(transition: .immediate) strongSelf.layoutEmptyResultsPlaceholder(transition: .immediate)
} }
}) })
} }
@ -799,7 +798,7 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM
} }
} }
private func layoutActivityIndicator(transition: ContainedViewLayoutTransition) { private func layoutEmptyResultsPlaceholder(transition: ContainedViewLayoutTransition) {
guard let (layout, navigationHeight) = self.validLayout else { guard let (layout, navigationHeight) = self.validLayout else {
return return
} }
@ -812,10 +811,7 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM
headerHeight = topInset headerHeight = topInset
} }
let indicatorSize = self.activityIndicator.measure(CGSize(width: 100.0, height: 100.0))
let actionsInset: CGFloat = 148.0 let actionsInset: CGFloat = 148.0
transition.updateFrame(node: self.activityIndicator, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - indicatorSize.width) / 2.0), y: headerHeight + actionsInset + floor((layout.size.height - headerHeight - actionsInset - indicatorSize.height - layout.intrinsicInsets.bottom) / 2.0)), size: indicatorSize))
let padding: CGFloat = 16.0 let padding: CGFloat = 16.0
let emptyTextSize = self.emptyResultsTextNode.updateLayout(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0, height: CGFloat.greatestFiniteMagnitude)) let emptyTextSize = self.emptyResultsTextNode.updateLayout(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0, height: CGFloat.greatestFiniteMagnitude))
transition.updateFrame(node: self.emptyResultsTextNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - emptyTextSize.width) / 2.0), y: headerHeight + actionsInset + floor((layout.size.height - headerHeight - actionsInset - emptyTextSize.height - layout.intrinsicInsets.bottom) / 2.0)), size: emptyTextSize)) transition.updateFrame(node: self.emptyResultsTextNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - emptyTextSize.width) / 2.0), y: headerHeight + actionsInset + floor((layout.size.height - headerHeight - actionsInset - emptyTextSize.height - layout.intrinsicInsets.bottom) / 2.0)), size: emptyTextSize))
@ -875,7 +871,7 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM
self.innerShadeNode.frame = CGRect(x: 0.0, y: 4.0, width: layout.size.width, height: 10000.0) self.innerShadeNode.frame = CGRect(x: 0.0, y: 4.0, width: layout.size.width, height: 10000.0)
self.innerShadeNode.alpha = layout.intrinsicInsets.bottom > 0.0 ? 1.0 : 0.0 self.innerShadeNode.alpha = layout.intrinsicInsets.bottom > 0.0 ? 1.0 : 0.0
self.layoutActivityIndicator(transition: transition) self.layoutEmptyResultsPlaceholder(transition: transition)
if isFirstLayout { if isFirstLayout {
while !self.enqueuedTransitions.isEmpty { while !self.enqueuedTransitions.isEmpty {

View File

@ -137,6 +137,14 @@ open class ManagedAnimationNode: ASDisplayNode {
public var trackStack: [ManagedAnimationItem] = [] public var trackStack: [ManagedAnimationItem] = []
public var didTryAdvancingState = false public var didTryAdvancingState = false
public var customColor: UIColor? {
didSet {
if let customColor = self.customColor, oldValue?.rgb != customColor.rgb {
self.imageNode.image = generateTintedImage(image: self.imageNode.image, color: customColor)
}
}
}
public init(size: CGSize) { public init(size: CGSize) {
self.intrinsicSize = size self.intrinsicSize = size
@ -242,8 +250,12 @@ open class ManagedAnimationNode: ASDisplayNode {
if state.frameIndex != frameIndex { if state.frameIndex != frameIndex {
state.frameIndex = frameIndex state.frameIndex = frameIndex
if let image = state.draw() { if let image = state.draw() {
if let customColor = self.customColor {
self.imageNode.image = generateTintedImage(image: image, color: customColor)
} else {
self.imageNode.image = image self.imageNode.image = image
} }
}
for (callbackFrame, callback) in state.item.callbacks { for (callbackFrame, callback) in state.item.callbacks {
if !state.executedCallbacks.contains(callbackFrame) && frameIndex >= callbackFrame { if !state.executedCallbacks.contains(callbackFrame) && frameIndex >= callbackFrame {

View File

@ -281,7 +281,13 @@ private func channelMembersControllerEntries(context: AccountContext, presentati
if let peer = view.peers[view.peerId] as? TelegramChannel, peer.addressName == nil { if let peer = view.peers[view.peerId] as? TelegramChannel, peer.addressName == nil {
entries.append(.inviteLink(presentationData.theme, presentationData.strings.Channel_Members_InviteLink)) entries.append(.inviteLink(presentationData.theme, presentationData.strings.Channel_Members_InviteLink))
} }
entries.append(.addMemberInfo(presentationData.theme, isGroup ? presentationData.strings.Group_Members_AddMembersHelp : presentationData.strings.Channel_Members_AddMembersHelp)) if let peer = view.peers[view.peerId] as? TelegramChannel {
if peer.flags.contains(.isGigagroup) {
entries.append(.addMemberInfo(presentationData.theme, presentationData.strings.Group_Members_AddMembersHelp))
} else if case .broadcast = peer.info {
entries.append(.addMemberInfo(presentationData.theme, presentationData.strings.Channel_Members_AddMembersHelp))
}
}
} }

View File

@ -928,7 +928,7 @@ public class SearchBarNode: ASDisplayNode, UITextFieldDelegate {
let textBackgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minX + padding, y: verticalOffset + textBackgroundHeight), size: CGSize(width: contentFrame.width - padding * 2.0 - (self.hasCancelButton ? cancelButtonSize.width + 11.0 : 0.0), height: textBackgroundHeight)) let textBackgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minX + padding, y: verticalOffset + textBackgroundHeight), size: CGSize(width: contentFrame.width - padding * 2.0 - (self.hasCancelButton ? cancelButtonSize.width + 11.0 : 0.0), height: textBackgroundHeight))
transition.updateFrame(node: self.textBackgroundNode, frame: textBackgroundFrame) transition.updateFrame(node: self.textBackgroundNode, frame: textBackgroundFrame)
let textFrame = CGRect(origin: CGPoint(x: textBackgroundFrame.minX + 24.0, y: textBackgroundFrame.minY), size: CGSize(width: max(1.0, textBackgroundFrame.size.width - 24.0 - 20.0), height: textBackgroundFrame.size.height)) let textFrame = CGRect(origin: CGPoint(x: textBackgroundFrame.minX + 24.0, y: textBackgroundFrame.minY), size: CGSize(width: max(1.0, textBackgroundFrame.size.width - 24.0 - 27.0), height: textBackgroundFrame.size.height))
if let iconImage = self.iconNode.image { if let iconImage = self.iconNode.image {
let iconSize = iconImage.size let iconSize = iconImage.size

View File

@ -14,7 +14,6 @@ import AccountContext
import ShareController import ShareController
import SearchBarNode import SearchBarNode
import SearchUI import SearchUI
import ActivityIndicator
import UndoUI import UndoUI
private enum LanguageListSection: ItemListSectionId { private enum LanguageListSection: ItemListSectionId {
@ -33,12 +32,12 @@ private enum LanguageListEntryType {
} }
private enum LanguageListEntry: Comparable, Identifiable { private enum LanguageListEntry: Comparable, Identifiable {
case localization(index: Int, info: LocalizationInfo, type: LanguageListEntryType, selected: Bool, activity: Bool, revealed: Bool, editing: Bool) case localization(index: Int, info: LocalizationInfo?, type: LanguageListEntryType, selected: Bool, activity: Bool, revealed: Bool, editing: Bool)
var stableId: LanguageListEntryId { var stableId: LanguageListEntryId {
switch self { switch self {
case let .localization(_, info, _, _, _, _, _): case let .localization(index, info, _, _, _, _, _):
return .localization(info.languageCode) return .localization(info?.languageCode ?? "\(index)")
} }
} }
@ -56,8 +55,10 @@ private enum LanguageListEntry: Comparable, Identifiable {
func item(presentationData: PresentationData, searchMode: Bool, openSearch: @escaping () -> Void, selectLocalization: @escaping (LocalizationInfo) -> Void, setItemWithRevealedOptions: @escaping (String?, String?) -> Void, removeItem: @escaping (String) -> Void) -> ListViewItem { func item(presentationData: PresentationData, searchMode: Bool, openSearch: @escaping () -> Void, selectLocalization: @escaping (LocalizationInfo) -> Void, setItemWithRevealedOptions: @escaping (String?, String?) -> Void, removeItem: @escaping (String) -> Void) -> ListViewItem {
switch self { switch self {
case let .localization(_, info, type, selected, activity, revealed, editing): case let .localization(_, info, type, selected, activity, revealed, editing):
return LocalizationListItem(presentationData: ItemListPresentationData(presentationData), id: info.languageCode, title: info.title, subtitle: info.localizedTitle, checked: selected, activity: activity, editing: LocalizationListItemEditing(editable: !selected && !searchMode && !info.isOfficial, editing: editing, revealed: !selected && revealed, reorderable: false), sectionId: type == .official ? LanguageListSection.official.rawValue : LanguageListSection.unofficial.rawValue, alwaysPlain: searchMode, action: { return LocalizationListItem(presentationData: ItemListPresentationData(presentationData), id: info?.languageCode ?? "", title: info?.title ?? " ", subtitle: info?.localizedTitle ?? " ", checked: selected, activity: activity, loading: info == nil, editing: LocalizationListItemEditing(editable: !selected && !searchMode && !(info?.isOfficial ?? true), editing: editing, revealed: !selected && revealed, reorderable: false), sectionId: type == .official ? LanguageListSection.official.rawValue : LanguageListSection.unofficial.rawValue, alwaysPlain: searchMode, action: {
if let info = info {
selectLocalization(info) selectLocalization(info)
}
}, setItemWithRevealedOptions: setItemWithRevealedOptions, removeItem: removeItem) }, setItemWithRevealedOptions: setItemWithRevealedOptions, removeItem: removeItem)
} }
} }
@ -259,16 +260,17 @@ private struct LanguageListNodeTransition {
let firstTime: Bool let firstTime: Bool
let isLoading: Bool let isLoading: Bool
let animated: Bool let animated: Bool
let crossfade: Bool
} }
private func preparedLanguageListNodeTransition(presentationData: PresentationData, from fromEntries: [LanguageListEntry], to toEntries: [LanguageListEntry], openSearch: @escaping () -> Void, selectLocalization: @escaping (LocalizationInfo) -> Void, setItemWithRevealedOptions: @escaping (String?, String?) -> Void, removeItem: @escaping (String) -> Void, firstTime: Bool, isLoading: Bool, forceUpdate: Bool, animated: Bool) -> LanguageListNodeTransition { private func preparedLanguageListNodeTransition(presentationData: PresentationData, from fromEntries: [LanguageListEntry], to toEntries: [LanguageListEntry], openSearch: @escaping () -> Void, selectLocalization: @escaping (LocalizationInfo) -> Void, setItemWithRevealedOptions: @escaping (String?, String?) -> Void, removeItem: @escaping (String) -> Void, firstTime: Bool, isLoading: Bool, forceUpdate: Bool, animated: Bool, crossfade: Bool) -> LanguageListNodeTransition {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries, allUpdated: forceUpdate) let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries, allUpdated: forceUpdate)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, searchMode: false, openSearch: openSearch, selectLocalization: selectLocalization, setItemWithRevealedOptions: setItemWithRevealedOptions, removeItem: removeItem), directionHint: nil) } let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, searchMode: false, openSearch: openSearch, selectLocalization: selectLocalization, setItemWithRevealedOptions: setItemWithRevealedOptions, removeItem: removeItem), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, searchMode: false, openSearch: openSearch, selectLocalization: selectLocalization, setItemWithRevealedOptions: setItemWithRevealedOptions, removeItem: removeItem), directionHint: nil) } let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, searchMode: false, openSearch: openSearch, selectLocalization: selectLocalization, setItemWithRevealedOptions: setItemWithRevealedOptions, removeItem: removeItem), directionHint: nil) }
return LanguageListNodeTransition(deletions: deletions, insertions: insertions, updates: updates, firstTime: firstTime, isLoading: isLoading, animated: animated) return LanguageListNodeTransition(deletions: deletions, insertions: insertions, updates: updates, firstTime: firstTime, isLoading: isLoading, animated: animated, crossfade: crossfade)
} }
final class LocalizationListControllerNode: ViewControllerTracingNode { final class LocalizationListControllerNode: ViewControllerTracingNode {
@ -285,7 +287,6 @@ final class LocalizationListControllerNode: ViewControllerTracingNode {
private var containerLayout: (ContainerViewLayout, CGFloat)? private var containerLayout: (ContainerViewLayout, CGFloat)?
let listNode: ListView let listNode: ListView
private var queuedTransitions: [LanguageListNodeTransition] = [] private var queuedTransitions: [LanguageListNodeTransition] = []
private var activityIndicator: ActivityIndicator?
private var searchDisplayController: SearchDisplayController? private var searchDisplayController: SearchDisplayController?
private let presentationDataValue = Promise<PresentationData>() private let presentationDataValue = Promise<PresentationData>()
@ -365,6 +366,7 @@ final class LocalizationListControllerNode: ViewControllerTracingNode {
} }
let preferencesKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.localizationListState])) let preferencesKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.localizationListState]))
let previousState = Atomic<LocalizationListState?>(value: nil)
let previousEntriesHolder = Atomic<([LanguageListEntry], PresentationTheme, PresentationStrings)?>(value: nil) let previousEntriesHolder = Atomic<([LanguageListEntry], PresentationTheme, PresentationStrings)?>(value: nil)
self.listDisposable = combineLatest(queue: .mainQueue(), context.account.postbox.combinedView(keys: [preferencesKey]), context.sharedContext.accountManager.sharedData(keys: [SharedDataKeys.localizationSettings]), self.presentationDataValue.get(), self.applyingCode.get(), revealedCode.get(), self.isEditing.get()).start(next: { [weak self] view, sharedData, presentationData, applyingCode, revealedCode, isEditing in self.listDisposable = combineLatest(queue: .mainQueue(), context.account.postbox.combinedView(keys: [preferencesKey]), context.sharedContext.accountManager.sharedData(keys: [SharedDataKeys.localizationSettings]), self.presentationDataValue.get(), self.applyingCode.get(), revealedCode.get(), self.isEditing.get()).start(next: { [weak self] view, sharedData, presentationData, applyingCode, revealedCode, isEditing in
guard let strongSelf = self else { guard let strongSelf = self else {
@ -377,7 +379,9 @@ final class LocalizationListControllerNode: ViewControllerTracingNode {
activeLanguageCode = localizationSettings.primaryComponent.languageCode activeLanguageCode = localizationSettings.primaryComponent.languageCode
} }
var existingIds = Set<String>() var existingIds = Set<String>()
if let localizationListState = (view.views[preferencesKey] as? PreferencesView)?.values[PreferencesKeys.localizationListState] as? LocalizationListState, !localizationListState.availableOfficialLocalizations.isEmpty {
let localizationListState = (view.views[preferencesKey] as? PreferencesView)?.values[PreferencesKeys.localizationListState] as? LocalizationListState
if let localizationListState = localizationListState, !localizationListState.availableOfficialLocalizations.isEmpty {
strongSelf.currentListState = localizationListState strongSelf.currentListState = localizationListState
let availableSavedLocalizations = localizationListState.availableSavedLocalizations.filter({ info in !localizationListState.availableOfficialLocalizations.contains(where: { $0.languageCode == info.languageCode }) }) let availableSavedLocalizations = localizationListState.availableSavedLocalizations.filter({ info in !localizationListState.availableOfficialLocalizations.contains(where: { $0.languageCode == info.languageCode }) })
@ -402,9 +406,16 @@ final class LocalizationListControllerNode: ViewControllerTracingNode {
existingIds.insert(info.languageCode) existingIds.insert(info.languageCode)
entries.append(.localization(index: entries.count, info: info, type: .official, selected: info.languageCode == activeLanguageCode, activity: applyingCode == info.languageCode, revealed: revealedCode == info.languageCode, editing: false)) entries.append(.localization(index: entries.count, info: info, type: .official, selected: info.languageCode == activeLanguageCode, activity: applyingCode == info.languageCode, revealed: revealedCode == info.languageCode, editing: false))
} }
} else {
for _ in 0 ..< 15 {
entries.append(.localization(index: entries.count, info: nil, type: .official, selected: false, activity: false, revealed: false, editing: false))
} }
}
let previousState = previousState.swap(localizationListState)
let previousEntriesAndPresentationData = previousEntriesHolder.swap((entries, presentationData.theme, presentationData.strings)) let previousEntriesAndPresentationData = previousEntriesHolder.swap((entries, presentationData.theme, presentationData.strings))
let transition = preparedLanguageListNodeTransition(presentationData: presentationData, from: previousEntriesAndPresentationData?.0 ?? [], to: entries, openSearch: openSearch, selectLocalization: { [weak self] info in self?.selectLocalization(info) }, setItemWithRevealedOptions: setItemWithRevealedOptions, removeItem: removeItem, firstTime: previousEntriesAndPresentationData == nil, isLoading: entries.isEmpty, forceUpdate: previousEntriesAndPresentationData?.1 !== presentationData.theme || previousEntriesAndPresentationData?.2 !== presentationData.strings, animated: (previousEntriesAndPresentationData?.0.count ?? 0) >= entries.count) let transition = preparedLanguageListNodeTransition(presentationData: presentationData, from: previousEntriesAndPresentationData?.0 ?? [], to: entries, openSearch: openSearch, selectLocalization: { [weak self] info in self?.selectLocalization(info) }, setItemWithRevealedOptions: setItemWithRevealedOptions, removeItem: removeItem, firstTime: previousEntriesAndPresentationData == nil, isLoading: entries.isEmpty, forceUpdate: previousEntriesAndPresentationData?.1 !== presentationData.theme || previousEntriesAndPresentationData?.2 !== presentationData.strings, animated: (previousEntriesAndPresentationData?.0.count ?? 0) >= entries.count, crossfade: (previousState == nil) != (localizationListState == nil))
strongSelf.enqueueTransition(transition) strongSelf.enqueueTransition(transition)
}) })
self.updatedDisposable = synchronizedLocalizationListState(postbox: context.account.postbox, network: context.account.network).start() self.updatedDisposable = synchronizedLocalizationListState(postbox: context.account.postbox, network: context.account.network).start()
@ -444,11 +455,6 @@ final class LocalizationListControllerNode: ViewControllerTracingNode {
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
if let activityIndicator = self.activityIndicator {
let indicatorSize = activityIndicator.measure(CGSize(width: 100.0, height: 100.0))
transition.updateFrame(node: activityIndicator, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - indicatorSize.width) / 2.0), y: updateSizeAndInsets.insets.top + 50.0 + floor((layout.size.height - updateSizeAndInsets.insets.top - updateSizeAndInsets.insets.bottom - indicatorSize.height - 50.0) / 2.0)), size: indicatorSize))
}
if !hadValidLayout { if !hadValidLayout {
self.dequeueTransitions() self.dequeueTransitions()
} }
@ -463,7 +469,7 @@ final class LocalizationListControllerNode: ViewControllerTracingNode {
} }
private func dequeueTransitions() { private func dequeueTransitions() {
guard let (layout, navigationBarHeight) = self.containerLayout else { guard let _ = self.containerLayout else {
return return
} }
while !self.queuedTransitions.isEmpty { while !self.queuedTransitions.isEmpty {
@ -473,6 +479,8 @@ final class LocalizationListControllerNode: ViewControllerTracingNode {
if transition.firstTime { if transition.firstTime {
options.insert(.Synchronous) options.insert(.Synchronous)
options.insert(.LowLatency) options.insert(.LowLatency)
} else if transition.crossfade {
options.insert(.AnimateCrossfade)
} else if transition.animated { } else if transition.animated {
options.insert(.AnimateInsertion) options.insert(.AnimateInsertion)
} }
@ -482,17 +490,6 @@ final class LocalizationListControllerNode: ViewControllerTracingNode {
strongSelf.didSetReady = true strongSelf.didSetReady = true
strongSelf._ready.set(true) strongSelf._ready.set(true)
} }
if transition.isLoading, strongSelf.activityIndicator == nil {
let activityIndicator = ActivityIndicator(type: .custom(strongSelf.presentationData.theme.list.itemAccentColor, 22.0, 1.0, false))
strongSelf.activityIndicator = activityIndicator
strongSelf.insertSubnode(activityIndicator, aboveSubnode: strongSelf.listNode)
strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
} else if !transition.isLoading, let activityIndicator = strongSelf.activityIndicator {
strongSelf.activityIndicator = nil
activityIndicator.removeFromSupernode()
}
} }
}) })
} }

View File

@ -8,6 +8,7 @@ import ItemListUI
import PresentationDataUtils import PresentationDataUtils
import ActivityIndicator import ActivityIndicator
import ChatListSearchItemNode import ChatListSearchItemNode
import ShimmerEffect
struct LocalizationListItemEditing: Equatable { struct LocalizationListItemEditing: Equatable {
let editable: Bool let editable: Bool
@ -23,6 +24,7 @@ class LocalizationListItem: ListViewItem, ItemListItem {
let subtitle: String let subtitle: String
let checked: Bool let checked: Bool
let activity: Bool let activity: Bool
let loading: Bool
let editing: LocalizationListItemEditing let editing: LocalizationListItemEditing
let sectionId: ItemListSectionId let sectionId: ItemListSectionId
let alwaysPlain: Bool let alwaysPlain: Bool
@ -30,13 +32,14 @@ class LocalizationListItem: ListViewItem, ItemListItem {
let setItemWithRevealedOptions: (String?, String?) -> Void let setItemWithRevealedOptions: (String?, String?) -> Void
let removeItem: (String) -> Void let removeItem: (String) -> Void
init(presentationData: ItemListPresentationData, id: String, title: String, subtitle: String, checked: Bool, activity: Bool, editing: LocalizationListItemEditing, sectionId: ItemListSectionId, alwaysPlain: Bool, action: @escaping () -> Void, setItemWithRevealedOptions: @escaping (String?, String?) -> Void, removeItem: @escaping (String) -> Void) { init(presentationData: ItemListPresentationData, id: String, title: String, subtitle: String, checked: Bool, activity: Bool, loading: Bool, editing: LocalizationListItemEditing, sectionId: ItemListSectionId, alwaysPlain: Bool, action: @escaping () -> Void, setItemWithRevealedOptions: @escaping (String?, String?) -> Void, removeItem: @escaping (String) -> Void) {
self.presentationData = presentationData self.presentationData = presentationData
self.id = id self.id = id
self.title = title self.title = title
self.subtitle = subtitle self.subtitle = subtitle
self.checked = checked self.checked = checked
self.activity = activity self.activity = activity
self.loading = loading
self.editing = editing self.editing = editing
self.sectionId = sectionId self.sectionId = sectionId
self.alwaysPlain = alwaysPlain self.alwaysPlain = alwaysPlain
@ -108,6 +111,9 @@ class LocalizationListItemNode: ItemListRevealOptionsItemNode {
private var item: LocalizationListItem? private var item: LocalizationListItem?
private var layoutParams: (ListViewItemLayoutParams, ItemListNeighbors)? private var layoutParams: (ListViewItemLayoutParams, ItemListNeighbors)?
private var placeholderNode: ShimmerEffectNode?
private var absoluteLocation: (CGRect, CGSize)?
private var editableControlNode: ItemListEditableControlNode? private var editableControlNode: ItemListEditableControlNode?
private var reorderControlNode: ItemListEditableReorderControlNode? private var reorderControlNode: ItemListEditableReorderControlNode?
@ -117,7 +123,7 @@ class LocalizationListItemNode: ItemListRevealOptionsItemNode {
if self.editableControlNode != nil { if self.editableControlNode != nil {
return false return false
} }
if let _ = self.layoutParams?.0 { if let _ = self.layoutParams?.0, let item = self.item, !item.loading {
return super.canBeSelected return super.canBeSelected
} else { } else {
return false return false
@ -167,6 +173,15 @@ class LocalizationListItemNode: ItemListRevealOptionsItemNode {
self.addSubnode(self.activateArea) self.addSubnode(self.activateArea)
} }
override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
var rect = rect
rect.origin.y += self.insets.top
self.absoluteLocation = (rect, containerSize)
if let shimmerNode = self.placeholderNode {
shimmerNode.updateAbsoluteRect(rect, within: containerSize)
}
}
func asyncLayout() -> (_ item: LocalizationListItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) { func asyncLayout() -> (_ item: LocalizationListItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode) let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode)
@ -322,6 +337,42 @@ class LocalizationListItemNode: ItemListRevealOptionsItemNode {
strongSelf.setRevealOptions((left: [], right: [])) strongSelf.setRevealOptions((left: [], right: []))
} }
strongSelf.setRevealOptionsOpened(item.editing.revealed, animated: animated) strongSelf.setRevealOptionsOpened(item.editing.revealed, animated: animated)
if item.loading {
let shimmerNode: ShimmerEffectNode
if let current = strongSelf.placeholderNode {
shimmerNode = current
} else {
shimmerNode = ShimmerEffectNode()
strongSelf.placeholderNode = shimmerNode
if strongSelf.bottomStripeNode.supernode != nil {
strongSelf.insertSubnode(shimmerNode, belowSubnode: strongSelf.bottomStripeNode)
} else {
strongSelf.addSubnode(shimmerNode)
}
}
shimmerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
if let (rect, size) = strongSelf.absoluteLocation {
shimmerNode.updateAbsoluteRect(rect, within: size)
}
var shapes: [ShimmerEffectNode.Shape] = []
let titleLineWidth: CGFloat = 80.0
let subtitleLineWidth: CGFloat = 50.0
let lineDiameter: CGFloat = 10.0
let titleFrame = strongSelf.titleNode.frame
shapes.append(.roundedRectLine(startPoint: CGPoint(x: titleFrame.minX, y: titleFrame.minY + floor((titleFrame.height - lineDiameter) / 2.0)), width: titleLineWidth, diameter: lineDiameter))
let subtitleFrame = strongSelf.subtitleNode.frame
shapes.append(.roundedRectLine(startPoint: CGPoint(x: subtitleFrame.minX, y: subtitleFrame.minY + floor((subtitleFrame.height - lineDiameter) / 2.0)), width: subtitleLineWidth, diameter: lineDiameter))
shimmerNode.update(backgroundColor: item.presentationData.theme.list.itemBlocksBackgroundColor, foregroundColor: item.presentationData.theme.list.mediaPlaceholderColor, shimmeringColor: item.presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: shapes, size: layout.contentSize)
} else if let shimmerNode = strongSelf.placeholderNode {
strongSelf.placeholderNode = nil
shimmerNode.removeFromSupernode()
}
} }
}) })
} }

View File

@ -133,15 +133,17 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
var index: Int32 = 0 var index: Int32 = 0
var existingPeerIds: Set<PeerId> = Set() var existingPeerIds: Set<PeerId> = Set()
entries.append(SharePeerEntry(index: index, peer: RenderedPeer(peer: accountPeer), presence: nil, theme: theme, strings: strings)) entries.append(SharePeerEntry(index: index, peer: RenderedPeer(peer: accountPeer), presence: nil, theme: theme, strings: strings))
existingPeerIds.insert(accountPeer.id)
index += 1 index += 1
for peer in foundPeers.reversed() { for peer in foundPeers.reversed() {
if !existingPeerIds.contains(peer.peerId) {
entries.append(SharePeerEntry(index: index, peer: peer, presence: nil, theme: theme, strings: strings)) entries.append(SharePeerEntry(index: index, peer: peer, presence: nil, theme: theme, strings: strings))
existingPeerIds.insert(peer.peerId) existingPeerIds.insert(peer.peerId)
index += 1 index += 1
} }
}
for (peer, presence) in initialPeers { for (peer, presence) in initialPeers {
if !existingPeerIds.contains(peer.peerId) { if !existingPeerIds.contains(peer.peerId) {

View File

@ -21,6 +21,7 @@ swift_library(
"//submodules/PresentationDataUtils:PresentationDataUtils", "//submodules/PresentationDataUtils:PresentationDataUtils",
"//submodules/Markdown:Markdown", "//submodules/Markdown:Markdown",
"//submodules/TelegramCallsUI:TelegramCallsUI", "//submodules/TelegramCallsUI:TelegramCallsUI",
"//submodules/ManagedAnimationNode:ManagedAnimationNode",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -10,6 +10,7 @@ import TelegramUIPreferences
import UniversalMediaPlayer import UniversalMediaPlayer
import AccountContext import AccountContext
import TelegramStringFormatting import TelegramStringFormatting
import ManagedAnimationNode
private let titleFont = Font.regular(12.0) private let titleFont = Font.regular(12.0)
private let subtitleFont = Font.regular(10.0) private let subtitleFont = Font.regular(10.0)
@ -147,8 +148,7 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollVi
private let closeButton: HighlightableButtonNode private let closeButton: HighlightableButtonNode
private let actionButton: HighlightTrackingButtonNode private let actionButton: HighlightTrackingButtonNode
private let actionPauseNode: ASImageNode private let playPauseIconNode: PlayPauseIconNode
private let actionPlayNode: ASImageNode
private let rateButton: HighlightableButtonNode private let rateButton: HighlightableButtonNode
private let accessibilityAreaNode: AccessibilityAreaNode private let accessibilityAreaNode: AccessibilityAreaNode
@ -248,20 +248,8 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollVi
self.actionButton.hitTestSlop = UIEdgeInsets(top: -8.0, left: -8.0, bottom: -8.0, right: -8.0) self.actionButton.hitTestSlop = UIEdgeInsets(top: -8.0, left: -8.0, bottom: -8.0, right: -8.0)
self.actionButton.displaysAsynchronously = false self.actionButton.displaysAsynchronously = false
self.actionPauseNode = ASImageNode() self.playPauseIconNode = PlayPauseIconNode()
self.actionPauseNode.contentMode = .center self.playPauseIconNode.customColor = self.theme.rootController.navigationBar.accentTextColor
self.actionPauseNode.isLayerBacked = true
self.actionPauseNode.displaysAsynchronously = false
self.actionPauseNode.displayWithoutProcessing = true
self.actionPauseNode.image = PresentationResourcesRootController.navigationPlayerPauseIcon(self.theme)
self.actionPlayNode = ASImageNode()
self.actionPlayNode.contentMode = .center
self.actionPlayNode.isLayerBacked = true
self.actionPlayNode.displaysAsynchronously = false
self.actionPlayNode.displayWithoutProcessing = true
self.actionPlayNode.image = PresentationResourcesRootController.navigationPlayerPlayIcon(self.theme)
self.actionPlayNode.isHidden = true
self.scrubbingNode = MediaPlayerScrubbingNode(content: .standard(lineHeight: 2.0, lineCap: .square, scrubberHandle: .none, backgroundColor: .clear, foregroundColor: self.theme.rootController.navigationBar.accentTextColor, bufferingColor: self.theme.rootController.navigationBar.accentTextColor.withAlphaComponent(0.5), chapters: [])) self.scrubbingNode = MediaPlayerScrubbingNode(content: .standard(lineHeight: 2.0, lineCap: .square, scrubberHandle: .none, backgroundColor: .clear, foregroundColor: self.theme.rootController.navigationBar.accentTextColor, bufferingColor: self.theme.rootController.navigationBar.accentTextColor.withAlphaComponent(0.5), chapters: []))
@ -285,8 +273,7 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollVi
self.addSubnode(self.rateButton) self.addSubnode(self.rateButton)
self.addSubnode(self.accessibilityAreaNode) self.addSubnode(self.accessibilityAreaNode)
self.actionButton.addSubnode(self.actionPauseNode) self.actionButton.addSubnode(self.playPauseIconNode)
self.actionButton.addSubnode(self.actionPlayNode)
self.addSubnode(self.actionButton) self.addSubnode(self.actionButton)
self.closeButton.addTarget(self, action: #selector(self.closeButtonPressed), forControlEvents: .touchUpInside) self.closeButton.addTarget(self, action: #selector(self.closeButtonPressed), forControlEvents: .touchUpInside)
@ -341,8 +328,7 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollVi
} else { } else {
paused = true paused = true
} }
strongSelf.actionPlayNode.isHidden = !paused strongSelf.playPauseIconNode.enqueueState(paused ? .play : .pause, animated: true)
strongSelf.actionPauseNode.isHidden = paused
strongSelf.actionButton.accessibilityLabel = paused ? strongSelf.strings.VoiceOver_Media_PlaybackPlay : strongSelf.strings.VoiceOver_Media_PlaybackPause strongSelf.actionButton.accessibilityLabel = paused ? strongSelf.strings.VoiceOver_Media_PlaybackPlay : strongSelf.strings.VoiceOver_Media_PlaybackPause
} }
} }
@ -374,8 +360,7 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollVi
self.rightMaskNode.image = maskImage self.rightMaskNode.image = maskImage
self.closeButton.setImage(PresentationResourcesRootController.navigationPlayerCloseButton(self.theme), for: []) self.closeButton.setImage(PresentationResourcesRootController.navigationPlayerCloseButton(self.theme), for: [])
self.actionPlayNode.image = PresentationResourcesRootController.navigationPlayerPlayIcon(self.theme) self.playPauseIconNode.customColor = self.theme.rootController.navigationBar.accentTextColor
self.actionPauseNode.image = PresentationResourcesRootController.navigationPlayerPauseIcon(self.theme)
self.separatorNode.backgroundColor = self.theme.rootController.navigationBar.separatorColor self.separatorNode.backgroundColor = self.theme.rootController.navigationBar.separatorColor
self.scrubbingNode.updateContent(.standard(lineHeight: 2.0, lineCap: .square, scrubberHandle: .none, backgroundColor: .clear, foregroundColor: self.theme.rootController.navigationBar.accentTextColor, bufferingColor: self.theme.rootController.navigationBar.accentTextColor.withAlphaComponent(0.5), chapters: [])) self.scrubbingNode.updateContent(.standard(lineHeight: 2.0, lineCap: .square, scrubberHandle: .none, backgroundColor: .clear, foregroundColor: self.theme.rootController.navigationBar.accentTextColor, bufferingColor: self.theme.rootController.navigationBar.accentTextColor.withAlphaComponent(0.5), chapters: []))
@ -477,8 +462,7 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollVi
transition.updateFrame(node: self.closeButton, frame: CGRect(origin: CGPoint(x: bounds.size.width - 44.0 - rightInset, y: 0.0), size: CGSize(width: 44.0, height: minHeight))) transition.updateFrame(node: self.closeButton, frame: CGRect(origin: CGPoint(x: bounds.size.width - 44.0 - rightInset, y: 0.0), size: CGSize(width: 44.0, height: minHeight)))
let rateButtonSize = CGSize(width: 24.0, height: minHeight) let rateButtonSize = CGSize(width: 24.0, height: minHeight)
transition.updateFrame(node: self.rateButton, frame: CGRect(origin: CGPoint(x: bounds.size.width - 18.0 - closeButtonSize.width - 17.0 - rateButtonSize.width - rightInset, y: 0.0), size: rateButtonSize)) transition.updateFrame(node: self.rateButton, frame: CGRect(origin: CGPoint(x: bounds.size.width - 18.0 - closeButtonSize.width - 17.0 - rateButtonSize.width - rightInset, y: 0.0), size: rateButtonSize))
transition.updateFrame(node: self.actionPlayNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: 40.0, height: 37.0))) transition.updateFrame(node: self.playPauseIconNode, frame: CGRect(origin: CGPoint(x: 6.0, y: 4.0 + UIScreenPixel), size: CGSize(width: 28.0, height: 28.0)))
transition.updateFrame(node: self.actionPauseNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: 40.0, height: 37.0)))
transition.updateFrame(node: self.actionButton, frame: CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: 40.0, height: 37.0))) transition.updateFrame(node: self.actionButton, frame: CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: 40.0, height: 37.0)))
transition.updateFrame(node: self.scrubbingNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 37.0 - 2.0), size: CGSize(width: size.width, height: 2.0))) transition.updateFrame(node: self.scrubbingNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 37.0 - 2.0), size: CGSize(width: size.width, height: 2.0)))
@ -505,3 +489,53 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollVi
} }
} }
} }
private enum PlayPauseIconNodeState: Equatable {
case play
case pause
}
private final class PlayPauseIconNode: ManagedAnimationNode {
private let duration: Double = 0.4
private var iconState: PlayPauseIconNodeState = .pause
init() {
super.init(size: CGSize(width: 28.0, height: 28.0))
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 41), duration: 0.01))
}
func enqueueState(_ state: PlayPauseIconNodeState, animated: Bool) {
guard self.iconState != state else {
return
}
let previousState = self.iconState
self.iconState = state
switch previousState {
case .pause:
switch state {
case .play:
if animated {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 83), duration: self.duration))
} else {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01))
}
case .pause:
break
}
case .play:
switch state {
case .pause:
if animated {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 41), duration: self.duration))
} else {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 41), duration: 0.01))
}
case .play:
break
}
}
}
}

View File

@ -3139,7 +3139,7 @@ public final class VoiceChatController: ViewController {
muteState: memberMuteState, muteState: memberMuteState,
canManageCall: self.callState?.canManageCall ?? false, canManageCall: self.callState?.canManageCall ?? false,
volume: member.volume, volume: member.volume,
raisedHand: member.raiseHandRating != nil, raisedHand: member.hasRaiseHand,
displayRaisedHandStatus: self.displayedRaisedHands.contains(member.peer.id) displayRaisedHandStatus: self.displayedRaisedHands.contains(member.peer.id)
))) )))
index += 1 index += 1
@ -3416,6 +3416,12 @@ public final class VoiceChatController: ViewController {
} }
return result return result
} }
fileprivate func scrollToTop() {
if self.isExpanded {
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
}
}
} }
private let sharedContext: SharedAccountContext private let sharedContext: SharedAccountContext
@ -3474,6 +3480,10 @@ public final class VoiceChatController: ViewController {
return true return true
} }
|> filter { $0 }) |> filter { $0 })
self.scrollToTop = { [weak self] in
self?.controllerNode.scrollToTop()
}
} }
required init(coder aDecoder: NSCoder) { required init(coder aDecoder: NSCoder) {

View File

@ -158,7 +158,7 @@ private func currentDateTimeFormat() -> PresentationDateTimeFormat {
var dateSeparator = "/" var dateSeparator = "/"
var dateSuffix = "" var dateSuffix = ""
if let dateString = DateFormatter.dateFormat(fromTemplate: "MdY", options: 0, locale: locale) { if let dateString = DateFormatter.dateFormat(fromTemplate: "MdY", options: 0, locale: locale) {
for separator in [". ", ".", "/", "-", "/"] { for separator in [".", "/", "-", "/"] {
if dateString.contains(separator) { if dateString.contains(separator) {
if separator == ". " { if separator == ". " {
dateSuffix = "." dateSuffix = "."

View File

@ -38,7 +38,7 @@ private func titleAndColorForAction(_ action: SubscriberAction, theme: Presentat
} }
} }
private func actionForPeer(peer: Peer, interfaceState: ChatPresentationInterfaceState, isMuted: Bool) -> SubscriberAction? { private func actionForPeer(peer: Peer, interfaceState: ChatPresentationInterfaceState, isJoining: Bool, isMuted: Bool) -> SubscriberAction? {
if case .pinnedMessages = interfaceState.subject { if case .pinnedMessages = interfaceState.subject {
var canManagePin = false var canManagePin = false
if let channel = peer as? TelegramChannel { if let channel = peer as? TelegramChannel {
@ -64,6 +64,13 @@ private func actionForPeer(peer: Peer, interfaceState: ChatPresentationInterface
} }
} else { } else {
if let channel = peer as? TelegramChannel { if let channel = peer as? TelegramChannel {
if case .broadcast = channel.info, isJoining {
if isMuted {
return .unmuteNotifications
} else {
return .muteNotifications
}
}
switch channel.participationStatus { switch channel.participationStatus {
case .kicked: case .kicked:
return .kicked return .kicked
@ -102,10 +109,11 @@ final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode {
private let actionDisposable = MetaDisposable() private let actionDisposable = MetaDisposable()
private let badgeDisposable = MetaDisposable() private let badgeDisposable = MetaDisposable()
private var isJoining: Bool = false
private var presentationInterfaceState: ChatPresentationInterfaceState? private var presentationInterfaceState: ChatPresentationInterfaceState?
private var layoutData: (CGFloat, CGFloat, CGFloat)? private var layoutData: (CGFloat, CGFloat, CGFloat, UIEdgeInsets, CGFloat, Bool, LayoutMetrics)?
override init() { override init() {
self.button = HighlightableButtonNode() self.button = HighlightableButtonNode()
@ -168,14 +176,34 @@ final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode {
switch action { switch action {
case .join: case .join:
var delayActivity = false
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
delayActivity = true
}
if delayActivity {
Queue.mainQueue().after(1.5) {
if self.isJoining {
self.activityIndicator.isHidden = false self.activityIndicator.isHidden = false
self.activityIndicator.startAnimating() self.activityIndicator.startAnimating()
}
}
} else {
self.activityIndicator.isHidden = false
self.activityIndicator.startAnimating()
}
self.isJoining = true
if let (width, leftInset, rightInset, additionalSideInsets, maxHeight, isSecondary, metrics) = self.layoutData, let presentationInterfaceState = self.presentationInterfaceState {
let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, isSecondary: isSecondary, transition: .immediate, interfaceState: presentationInterfaceState, metrics: metrics, force: true)
}
self.actionDisposable.set((context.peerChannelMemberCategoriesContextsManager.join(account: context.account, peerId: peer.id, hash: nil) self.actionDisposable.set((context.peerChannelMemberCategoriesContextsManager.join(account: context.account, peerId: peer.id, hash: nil)
|> afterDisposed { [weak self] in |> afterDisposed { [weak self] in
Queue.mainQueue().async { Queue.mainQueue().async {
if let strongSelf = self { if let strongSelf = self {
strongSelf.activityIndicator.isHidden = true strongSelf.activityIndicator.isHidden = true
strongSelf.activityIndicator.stopAnimating() strongSelf.activityIndicator.stopAnimating()
strongSelf.isJoining = false
} }
} }
}).start(error: { [weak self] error in }).start(error: { [weak self] error in
@ -220,9 +248,13 @@ final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode {
} }
override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat {
self.layoutData = (width, leftInset, rightInset) return self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, isSecondary: isSecondary, transition: transition, interfaceState: interfaceState, metrics: metrics, force: false)
}
if self.presentationInterfaceState != interfaceState { private func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, force: Bool) -> CGFloat {
self.layoutData = (width, leftInset, rightInset, additionalSideInsets, maxHeight, isSecondary, metrics)
if self.presentationInterfaceState != interfaceState || force {
let previousState = self.presentationInterfaceState let previousState = self.presentationInterfaceState
self.presentationInterfaceState = interfaceState self.presentationInterfaceState = interfaceState
@ -231,16 +263,17 @@ final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode {
self.helpButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/Help"), color: interfaceState.theme.chat.inputPanel.panelControlAccentColor), for: .normal) self.helpButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/Help"), color: interfaceState.theme.chat.inputPanel.panelControlAccentColor), for: .normal)
} }
if let peer = interfaceState.renderedPeer?.peer, previousState?.renderedPeer?.peer == nil || !peer.isEqual(previousState!.renderedPeer!.peer!) || previousState?.theme !== interfaceState.theme || previousState?.strings !== interfaceState.strings || previousState?.peerIsMuted != interfaceState.peerIsMuted || previousState?.pinnedMessage != interfaceState.pinnedMessage { if let peer = interfaceState.renderedPeer?.peer, previousState?.renderedPeer?.peer == nil || !peer.isEqual(previousState!.renderedPeer!.peer!) || previousState?.theme !== interfaceState.theme || previousState?.strings !== interfaceState.strings || previousState?.peerIsMuted != interfaceState.peerIsMuted || previousState?.pinnedMessage != interfaceState.pinnedMessage || force {
if let action = actionForPeer(peer: peer, interfaceState: interfaceState, isMuted: interfaceState.peerIsMuted) { if let action = actionForPeer(peer: peer, interfaceState: interfaceState, isJoining: self.isJoining, isMuted: interfaceState.peerIsMuted) {
let previousAction = self.action let previousAction = self.action
self.action = action self.action = action
let (title, color) = titleAndColorForAction(action, theme: interfaceState.theme, strings: interfaceState.strings) let (title, color) = titleAndColorForAction(action, theme: interfaceState.theme, strings: interfaceState.strings)
var offset: CGFloat = 30.0 var offset: CGFloat = 30.0
if let previousAction = previousAction, previousAction == .muteNotifications && action == .unmuteNotifications || previousAction == .unmuteNotifications && action == .muteNotifications {
if previousAction == .muteNotifications { if let previousAction = previousAction, [.join, .muteNotifications].contains(previousAction) && action == .unmuteNotifications || [.join, .unmuteNotifications].contains(previousAction) && action == .muteNotifications {
if [.join, .muteNotifications].contains(previousAction) {
offset *= -1.0 offset *= -1.0
} }
if let snapshotView = self.button.view.snapshotContentTree() { if let snapshotView = self.button.view.snapshotContentTree() {

View File

@ -11,14 +11,7 @@ import UniversalMediaPlayer
import AppBundle import AppBundle
import ContextUI import ContextUI
import AnimationUI import AnimationUI
import ManagedAnimationNode
private func generatePauseIcon(_ theme: PresentationTheme) -> UIImage? {
return generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/MinimizedPause"), color: theme.chat.inputPanel.actionControlForegroundColor)
}
private func generatePlayIcon(_ theme: PresentationTheme) -> UIImage? {
return generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/MinimizedPlay"), color: theme.chat.inputPanel.actionControlForegroundColor)
}
extension AudioWaveformNode: CustomMediaPlayerScrubbingForegroundNode { extension AudioWaveformNode: CustomMediaPlayerScrubbingForegroundNode {
@ -30,7 +23,7 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode {
let sendButton: HighlightTrackingButtonNode let sendButton: HighlightTrackingButtonNode
private var sendButtonRadialStatusNode: ChatSendButtonRadialStatusNode? private var sendButtonRadialStatusNode: ChatSendButtonRadialStatusNode?
let playButton: HighlightableButtonNode let playButton: HighlightableButtonNode
let pauseButton: HighlightableButtonNode private let playPauseIconNode: PlayPauseIconNode
private let waveformButton: ASButtonNode private let waveformButton: ASButtonNode
let waveformBackgroundNode: ASImageNode let waveformBackgroundNode: ASImageNode
@ -76,13 +69,10 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode {
self.playButton = HighlightableButtonNode() self.playButton = HighlightableButtonNode()
self.playButton.displaysAsynchronously = false self.playButton.displaysAsynchronously = false
self.playButton.setImage(generatePlayIcon(theme), for: [])
self.playButton.isUserInteractionEnabled = false self.playPauseIconNode = PlayPauseIconNode()
self.pauseButton = HighlightableButtonNode() self.playPauseIconNode.enqueueState(.play, animated: false)
self.pauseButton.displaysAsynchronously = false self.playPauseIconNode.customColor = theme.chat.inputPanel.actionControlForegroundColor
self.pauseButton.setImage(generatePauseIcon(theme), for: [])
self.pauseButton.isHidden = true
self.pauseButton.isUserInteractionEnabled = false
self.waveformButton = ASButtonNode() self.waveformButton = ASButtonNode()
self.waveformButton.accessibilityTraits.insert(.startsMediaSession) self.waveformButton.accessibilityTraits.insert(.startsMediaSession)
@ -106,9 +96,9 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode {
self.addSubnode(self.sendButton) self.addSubnode(self.sendButton)
self.addSubnode(self.waveformScubberNode) self.addSubnode(self.waveformScubberNode)
self.addSubnode(self.playButton) self.addSubnode(self.playButton)
self.addSubnode(self.pauseButton)
self.addSubnode(self.durationLabel) self.addSubnode(self.durationLabel)
self.addSubnode(self.waveformButton) self.addSubnode(self.waveformButton)
self.playButton.addSubnode(self.playPauseIconNode)
self.sendButton.highligthedChanged = { [weak self] highlighted in self.sendButton.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self { if let strongSelf = self {
@ -168,6 +158,9 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode {
if let context = self.context { if let context = self.context {
let mediaManager = context.sharedContext.mediaManager let mediaManager = context.sharedContext.mediaManager
let mediaPlayer = MediaPlayer(audioSessionManager: mediaManager.audioSession, postbox: context.account.postbox, resourceReference: .standalone(resource: recordedMediaPreview.resource), streamable: .none, video: false, preferSoftwareDecoding: false, enableSound: true, fetchAutomatically: true) let mediaPlayer = MediaPlayer(audioSessionManager: mediaManager.audioSession, postbox: context.account.postbox, resourceReference: .standalone(resource: recordedMediaPreview.resource), streamable: .none, video: false, preferSoftwareDecoding: false, enableSound: true, fetchAutomatically: true)
mediaPlayer.actionAtEnd = .action{ [weak mediaPlayer] in
mediaPlayer?.seek(timestamp: 0.0)
}
self.mediaPlayer = mediaPlayer self.mediaPlayer = mediaPlayer
self.durationLabel.defaultDuration = Double(recordedMediaPreview.duration) self.durationLabel.defaultDuration = Double(recordedMediaPreview.duration)
self.durationLabel.status = mediaPlayer.status self.durationLabel.status = mediaPlayer.status
@ -177,11 +170,10 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode {
if let strongSelf = self { if let strongSelf = self {
switch status.status { switch status.status {
case .playing, .buffering(_, true, _, _): case .playing, .buffering(_, true, _, _):
strongSelf.playButton.isHidden = true strongSelf.playPauseIconNode.enqueueState(.pause, animated: true)
default: default:
strongSelf.playButton.isHidden = false strongSelf.playPauseIconNode.enqueueState(.play, animated: true)
} }
strongSelf.pauseButton.isHidden = !strongSelf.playButton.isHidden
} }
})) }))
} }
@ -223,7 +215,8 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode {
} }
transition.updateFrame(node: self.playButton, frame: CGRect(origin: CGPoint(x: leftInset + 52.0, y: 10.0), size: CGSize(width: 26.0, height: 26.0))) transition.updateFrame(node: self.playButton, frame: CGRect(origin: CGPoint(x: leftInset + 52.0, y: 10.0), size: CGSize(width: 26.0, height: 26.0)))
transition.updateFrame(node: self.pauseButton, frame: CGRect(origin: CGPoint(x: leftInset + 50.0, y: 10.0), size: CGSize(width: 26.0, height: 26.0))) self.playPauseIconNode.frame = CGRect(origin: CGPoint(x: -2.0, y: -1.0), size: CGSize(width: 26.0, height: 26.0))
let waveformBackgroundFrame = CGRect(origin: CGPoint(x: leftInset + 45.0, y: 7.0 - UIScreenPixel), size: CGSize(width: width - leftInset - rightInset - 90.0, height: 33.0)) let waveformBackgroundFrame = CGRect(origin: CGPoint(x: leftInset + 45.0, y: 7.0 - UIScreenPixel), size: CGSize(width: width - leftInset - rightInset - 90.0, height: 33.0))
transition.updateFrame(node: self.waveformBackgroundNode, frame: waveformBackgroundFrame) transition.updateFrame(node: self.waveformBackgroundNode, frame: waveformBackgroundFrame)
transition.updateFrame(node: self.waveformButton, frame: CGRect(origin: CGPoint(x: leftInset + 45.0, y: 0.0), size: CGSize(width: width - leftInset - rightInset - 90.0, height: panelHeight))) transition.updateFrame(node: self.waveformButton, frame: CGRect(origin: CGPoint(x: leftInset + 45.0, y: 0.0), size: CGSize(width: width - leftInset - rightInset - 90.0, height: panelHeight)))
@ -260,9 +253,6 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode {
self.playButton.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3, delay: 0.1) self.playButton.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3, delay: 0.1)
self.playButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.1) self.playButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.1)
self.pauseButton.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3, delay: 0.1)
self.pauseButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.1)
self.durationLabel.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) self.durationLabel.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.waveformScubberNode.layer.animateScaleY(from: 0.1, to: 1.0, duration: 0.3, delay: 0.1) self.waveformScubberNode.layer.animateScaleY(from: 0.1, to: 1.0, duration: 0.3, delay: 0.1)
@ -312,3 +302,52 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode {
} }
} }
private enum PlayPauseIconNodeState: Equatable {
case play
case pause
}
private final class PlayPauseIconNode: ManagedAnimationNode {
private let duration: Double = 0.4
private var iconState: PlayPauseIconNodeState = .pause
init() {
super.init(size: CGSize(width: 28.0, height: 28.0))
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 41), duration: 0.01))
}
func enqueueState(_ state: PlayPauseIconNodeState, animated: Bool) {
guard self.iconState != state else {
return
}
let previousState = self.iconState
self.iconState = state
switch previousState {
case .pause:
switch state {
case .play:
if animated {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 83), duration: self.duration))
} else {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01))
}
case .pause:
break
}
case .play:
switch state {
case .pause:
if animated {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 41), duration: self.duration))
} else {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 41), duration: 0.01))
}
case .play:
break
}
}
}
}

View File

@ -1415,7 +1415,6 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
animatePosition(for: prevPreviewInputPanelNode.waveformScubberNode) animatePosition(for: prevPreviewInputPanelNode.waveformScubberNode)
animatePosition(for: prevPreviewInputPanelNode.durationLabel) animatePosition(for: prevPreviewInputPanelNode.durationLabel)
animatePosition(for: prevPreviewInputPanelNode.playButton) animatePosition(for: prevPreviewInputPanelNode.playButton)
animatePosition(for: prevPreviewInputPanelNode.pauseButton)
} }
func animateAlpha(for previewSubnode: ASDisplayNode) { func animateAlpha(for previewSubnode: ASDisplayNode) {
@ -1430,7 +1429,6 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
animateAlpha(for: prevPreviewInputPanelNode.waveformScubberNode) animateAlpha(for: prevPreviewInputPanelNode.waveformScubberNode)
animateAlpha(for: prevPreviewInputPanelNode.durationLabel) animateAlpha(for: prevPreviewInputPanelNode.durationLabel)
animateAlpha(for: prevPreviewInputPanelNode.playButton) animateAlpha(for: prevPreviewInputPanelNode.playButton)
animateAlpha(for: prevPreviewInputPanelNode.pauseButton)
let binNode = prevPreviewInputPanelNode.binNode let binNode = prevPreviewInputPanelNode.binNode
self.animatingBinNode = binNode self.animatingBinNode = binNode

View File

@ -8,6 +8,7 @@ import SwiftSignalKit
import TelegramUIPreferences import TelegramUIPreferences
import AccountContext import AccountContext
import ShareController import ShareController
import UndoUI
final class OverlayAudioPlayerControllerImpl: ViewController, OverlayAudioPlayerController { final class OverlayAudioPlayerControllerImpl: ViewController, OverlayAudioPlayerController {
private let context: AccountContext private let context: AccountContext
@ -68,6 +69,46 @@ final class OverlayAudioPlayerControllerImpl: ViewController, OverlayAudioPlayer
strongSelf.dismiss() strongSelf.dismiss()
} }
}, externalShare: true) }, externalShare: true)
shareController.completed = { [weak self] peerIds in
if let strongSelf = self {
let _ = (strongSelf.context.account.postbox.transaction { transaction -> [Peer] in
var peers: [Peer] = []
for peerId in peerIds {
if let peer = transaction.getPeer(peerId) {
peers.append(peer)
}
}
return peers
} |> deliverOnMainQueue).start(next: { [weak self] peers in
if let strongSelf = self {
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
let text: String
var savedMessages = false
if peerIds.count == 1, let peerId = peerIds.first, peerId == strongSelf.context.account.peerId {
text = presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One
savedMessages = true
} else {
if peers.count == 1, let peer = peers.first {
let peerName = peer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
text = presentationData.strings.Conversation_ForwardTooltip_Chat_One(peerName).0
} else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last {
let firstPeerName = firstPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
let secondPeerName = secondPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
text = presentationData.strings.Conversation_ForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).0
} else if let peer = peers.first {
let peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
text = presentationData.strings.Conversation_ForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").0
} else {
text = ""
}
}
strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current)
}
})
}
}
strongSelf.controllerNode.view.endEditing(true) strongSelf.controllerNode.view.endEditing(true)
strongSelf.present(shareController, in: .window(.root)) strongSelf.present(shareController, in: .window(.root))
} }

View File

@ -12,6 +12,7 @@ import TelegramUIPreferences
import AccountContext import AccountContext
import PhotoResources import PhotoResources
import AppBundle import AppBundle
import ManagedAnimationNode
private func generateBackground(theme: PresentationTheme) -> UIImage? { private func generateBackground(theme: PresentationTheme) -> UIImage? {
return generateImage(CGSize(width: 20.0, height: 10.0 + 8.0), rotatedContext: { size, context in return generateImage(CGSize(width: 20.0, height: 10.0 + 8.0), rotatedContext: { size, context in
@ -114,6 +115,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
private var currentIsPaused: Bool? private var currentIsPaused: Bool?
private let playPauseButton: IconButtonNode private let playPauseButton: IconButtonNode
private let playPauseIconNode: PlayPauseIconNode
private var currentOrder: MusicPlaybackSettingsOrder? private var currentOrder: MusicPlaybackSettingsOrder?
private let orderButton: IconButtonNode private let orderButton: IconButtonNode
@ -211,6 +213,8 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
self.playPauseButton = IconButtonNode() self.playPauseButton = IconButtonNode()
self.playPauseButton.displaysAsynchronously = false self.playPauseButton.displaysAsynchronously = false
self.playPauseIconNode = PlayPauseIconNode()
self.backwardButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Previous"), color: presentationData.theme.list.itemPrimaryTextColor) self.backwardButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Previous"), color: presentationData.theme.list.itemPrimaryTextColor)
self.forwardButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Next"), color: presentationData.theme.list.itemPrimaryTextColor) self.forwardButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Next"), color: presentationData.theme.list.itemPrimaryTextColor)
@ -240,6 +244,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
self.addSubnode(self.backwardButton) self.addSubnode(self.backwardButton)
self.addSubnode(self.forwardButton) self.addSubnode(self.forwardButton)
self.addSubnode(self.playPauseButton) self.addSubnode(self.playPauseButton)
self.playPauseButton.addSubnode(self.playPauseIconNode)
self.addSubnode(self.separatorNode) self.addSubnode(self.separatorNode)
@ -323,10 +328,12 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
if strongSelf.wasPlaying { if strongSelf.wasPlaying {
isPaused = false isPaused = false
} }
let isFirstTime = strongSelf.currentIsPaused == nil
if strongSelf.currentIsPaused != isPaused { if strongSelf.currentIsPaused != isPaused {
strongSelf.currentIsPaused = isPaused strongSelf.currentIsPaused = isPaused
strongSelf.updatePlayPauseButton(paused: isPaused) strongSelf.updatePlayPauseButton(paused: isPaused, animated: !isFirstTime)
} }
strongSelf.playPauseButton.isEnabled = true strongSelf.playPauseButton.isEnabled = true
@ -548,7 +555,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
self.backwardButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Previous"), color: presentationData.theme.list.itemPrimaryTextColor) self.backwardButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Previous"), color: presentationData.theme.list.itemPrimaryTextColor)
self.forwardButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Next"), color: presentationData.theme.list.itemPrimaryTextColor) self.forwardButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Next"), color: presentationData.theme.list.itemPrimaryTextColor)
if let isPaused = self.currentIsPaused { if let isPaused = self.currentIsPaused {
self.updatePlayPauseButton(paused: isPaused) self.updatePlayPauseButton(paused: isPaused, animated: false)
} }
if let order = self.currentOrder { if let order = self.currentOrder {
self.updateOrderButton(order) self.updateOrderButton(order)
@ -611,11 +618,12 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
} }
} }
private func updatePlayPauseButton(paused: Bool) { private func updatePlayPauseButton(paused: Bool, animated: Bool) {
self.playPauseIconNode.customColor = self.presentationData.theme.list.itemPrimaryTextColor
if paused { if paused {
self.playPauseButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Play"), color: self.presentationData.theme.list.itemPrimaryTextColor) self.playPauseIconNode.enqueueState(.play, animated: animated)
} else { } else {
self.playPauseButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Pause"), color: self.presentationData.theme.list.itemPrimaryTextColor) self.playPauseIconNode.enqueueState(.pause, animated: animated)
} }
} }
@ -779,7 +787,10 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
transition.updateFrame(node: self.backwardButton, frame: CGRect(origin: buttonsRect.origin, size: buttonSize)) transition.updateFrame(node: self.backwardButton, frame: CGRect(origin: buttonsRect.origin, size: buttonSize))
transition.updateFrame(node: self.forwardButton, frame: CGRect(origin: CGPoint(x: buttonsRect.maxX - buttonSize.width, y: buttonsRect.minY), size: buttonSize)) transition.updateFrame(node: self.forwardButton, frame: CGRect(origin: CGPoint(x: buttonsRect.maxX - buttonSize.width, y: buttonsRect.minY), size: buttonSize))
transition.updateFrame(node: self.playPauseButton, frame: CGRect(origin: CGPoint(x: buttonsRect.minX + floor((buttonsRect.width - buttonSize.width) / 2.0), y: buttonsRect.minY), size: buttonSize))
let playPauseFrame = CGRect(origin: CGPoint(x: buttonsRect.minX + floor((buttonsRect.width - buttonSize.width) / 2.0), y: buttonsRect.minY), size: buttonSize)
transition.updateFrame(node: self.playPauseButton, frame: playPauseFrame)
transition.updateFrame(node: self.playPauseIconNode, frame: CGRect(origin: CGPoint(x: -6.0, y: -6.0), size: CGSize(width: 76.0, height: 76.0)))
return panelHeight return panelHeight
} }
@ -892,3 +903,53 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
return result return result
} }
} }
private enum PlayPauseIconNodeState: Equatable {
case play
case pause
}
private final class PlayPauseIconNode: ManagedAnimationNode {
private let duration: Double = 0.4
private var iconState: PlayPauseIconNodeState = .pause
init() {
super.init(size: CGSize(width: 76.0, height: 76.0))
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 41), duration: 0.01))
}
func enqueueState(_ state: PlayPauseIconNodeState, animated: Bool) {
guard self.iconState != state else {
return
}
let previousState = self.iconState
self.iconState = state
switch previousState {
case .pause:
switch state {
case .play:
if animated {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 83), duration: self.duration))
} else {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01))
}
case .pause:
break
}
case .play:
switch state {
case .pause:
if animated {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 41), duration: self.duration))
} else {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 41), duration: 0.01))
}
case .play:
break
}
}
}
}

View File

@ -2774,13 +2774,19 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
} else { } else {
screenData = peerInfoScreenData(context: context, peerId: peerId, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, isSettings: self.isSettings, ignoreGroupInCommon: ignoreGroupInCommon) screenData = peerInfoScreenData(context: context, peerId: peerId, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, isSettings: self.isSettings, ignoreGroupInCommon: ignoreGroupInCommon)
var currentIsVideo = false
let item = self.headerNode.avatarListNode.listContainerNode.currentItemNode?.item
if let item = item, case let .image(image) = item {
currentIsVideo = !image.2.isEmpty
}
self.headerNode.displayAvatarContextMenu = { [weak self] node, gesture in self.headerNode.displayAvatarContextMenu = { [weak self] node, gesture in
guard let strongSelf = self, let peer = strongSelf.data?.peer else { guard let strongSelf = self, let peer = strongSelf.data?.peer else {
return return
} }
let items: [ContextMenuItem] = [ let items: [ContextMenuItem] = [
.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.PeerInfo_ReportProfilePhoto, icon: { theme in .action(ContextMenuActionItem(text: currentIsVideo ? strongSelf.presentationData.strings.PeerInfo_ReportProfileVideo : strongSelf.presentationData.strings.PeerInfo_ReportProfilePhoto, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Report"), color: theme.actionSheet.primaryTextColor) return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Report"), color: theme.actionSheet.primaryTextColor)
}, action: { [weak self] c, f in }, action: { [weak self] c, f in
if let strongSelf = self, let parent = strongSelf.controller { if let strongSelf = self, let parent = strongSelf.controller {
@ -5434,12 +5440,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|> take(1) |> take(1)
}) })
strongSelf.activeActionDisposable.set((combineLatest(signals) strongSelf.activeActionDisposable.set((combineLatest(signals)
|> deliverOnMainQueue).start(completed: { |> deliverOnMainQueue).start())
guard let strongSelf = self else {
return
}
strongSelf.controller?.present(OverlayStatusController(theme: strongSelf.presentationData.theme, type: .success), in: .window(.root))
}))
} }
}) })
if let peerSelectionController = peerSelectionController { if let peerSelectionController = peerSelectionController {

View File

@ -37,6 +37,7 @@ public enum UndoOverlayContent {
case voiceChatCanSpeak(text: String) case voiceChatCanSpeak(text: String)
case sticker(account: Account, file: TelegramMediaFile, text: String) case sticker(account: Account, file: TelegramMediaFile, text: String)
case copy(text: String) case copy(text: String)
case mediaSaved(text: String)
} }
public enum UndoOverlayAction { public enum UndoOverlayAction {

View File

@ -687,6 +687,27 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
self.textNode.attributedText = attributedText self.textNode.attributedText = attributedText
self.textNode.maximumNumberOfLines = 2 self.textNode.maximumNumberOfLines = 2
displayUndo = false
self.originalRemainingSeconds = 3
case let .mediaSaved(text):
self.avatarNode = nil
self.iconNode = nil
self.iconCheckNode = nil
self.animationNode = nil
let animatedStickerNode = AnimatedStickerNode()
self.animatedStickerNode = animatedStickerNode
if let path = getAppBundle().path(forResource: "anim_savemedia", ofType: "tgs") {
animatedStickerNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 80, height: 80, playbackMode: .once, mode: .direct(cachePathPrefix: nil))
animatedStickerNode.visibility = true
}
let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white)
let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white)
let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .natural)
self.textNode.attributedText = attributedText
self.textNode.maximumNumberOfLines = 2
displayUndo = false displayUndo = false
self.originalRemainingSeconds = 3 self.originalRemainingSeconds = 3
} }
@ -717,7 +738,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
switch content { switch content {
case .removedChat: case .removedChat:
self.panelWrapperNode.addSubnode(self.timerTextNode) self.panelWrapperNode.addSubnode(self.timerTextNode)
case .archivedChat, .hidArchive, .revealedArchive, .autoDelete, .succeed, .emoji, .swipeToReply, .actionSucceeded, .stickersModified, .chatAddedToFolder, .chatRemovedFromFolder, .messagesUnpinned, .setProximityAlert, .invitedToVoiceChat, .linkCopied, .banned, .importedMessage, .audioRate, .forward, .gigagroupConversion, .linkRevoked, .voiceChatRecording, .voiceChatFlag, .voiceChatCanSpeak, .sticker, .copy: case .archivedChat, .hidArchive, .revealedArchive, .autoDelete, .succeed, .emoji, .swipeToReply, .actionSucceeded, .stickersModified, .chatAddedToFolder, .chatRemovedFromFolder, .messagesUnpinned, .setProximityAlert, .invitedToVoiceChat, .linkCopied, .banned, .importedMessage, .audioRate, .forward, .gigagroupConversion, .linkRevoked, .voiceChatRecording, .voiceChatFlag, .voiceChatCanSpeak, .sticker, .copy, .mediaSaved:
break break
case .dice: case .dice:
self.panelWrapperNode.clipsToBounds = true self.panelWrapperNode.clipsToBounds = true