mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-04 21:41:45 +00:00
Various Improvements
This commit is contained in:
parent
86e27f0706
commit
0f9d40016f
@ -154,6 +154,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
private let tabContainerNode: ChatListFilterTabContainerNode
|
||||
private var tabContainerData: ([ChatListFilterTabEntry], Bool)?
|
||||
|
||||
private var didSetupTabs = false
|
||||
|
||||
public override func updateNavigationCustomData(_ data: Any?, progress: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
if self.isNodeLoaded {
|
||||
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 animated = strongSelf.didSetupTabs
|
||||
strongSelf.didSetupTabs = true
|
||||
|
||||
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 {
|
||||
parentController.navigationBar?.setSecondaryContentNode(isEmpty ? nil : strongSelf.tabContainerNode)
|
||||
parentController.navigationBar?.setSecondaryContentNode(isEmpty ? nil : strongSelf.tabContainerNode, animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
if let layout = strongSelf.validLayout {
|
||||
if wasEmpty != isEmpty {
|
||||
strongSelf.containerLayoutUpdated(layout, transition: .immediate)
|
||||
(strongSelf.parent as? TabBarController)?.updateLayout()
|
||||
let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate
|
||||
strongSelf.containerLayoutUpdated(layout, transition: transition)
|
||||
(strongSelf.parent as? TabBarController)?.updateLayout(transition: transition)
|
||||
} 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.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))
|
||||
|
||||
@ -28,6 +28,7 @@ swift_library(
|
||||
"//submodules/OverlayStatusController:OverlayStatusController",
|
||||
"//submodules/PresentationDataUtils:PresentationDataUtils",
|
||||
"//submodules/UrlEscaping:UrlEscaping",
|
||||
"//submodules/ManagedAnimationNode:ManagedAnimationNode"
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@ -20,6 +20,7 @@ import LocalizedPeerData
|
||||
import TextSelectionNode
|
||||
import UrlEscaping
|
||||
import UndoUI
|
||||
import ManagedAnimationNode
|
||||
|
||||
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)
|
||||
@ -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 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)
|
||||
|
||||
@ -130,6 +129,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
|
||||
private let backwardButton: HighlightableButtonNode
|
||||
private let forwardButton: HighlightableButtonNode
|
||||
private let playbackControlButton: HighlightableButtonNode
|
||||
private let playPauseIconNode: PlayPauseIconNode
|
||||
|
||||
private let statusButtonNode: HighlightTrackingButtonNode
|
||||
private let statusNode: RadialStatusNode
|
||||
@ -179,7 +179,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
|
||||
self.forwardButton.isHidden = !seekable
|
||||
if status == .Local {
|
||||
self.playbackControlButton.isHidden = false
|
||||
self.playbackControlButton.setImage(playImage, for: [])
|
||||
self.playPauseIconNode.enqueueState(.play, animated: true)
|
||||
} else {
|
||||
self.playbackControlButton.isHidden = true
|
||||
}
|
||||
@ -207,7 +207,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
|
||||
self.backwardButton.isHidden = !seekable
|
||||
self.forwardButton.isHidden = !seekable
|
||||
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.statusNode.isHidden = true
|
||||
}
|
||||
@ -314,6 +314,8 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
|
||||
self.playbackControlButton = HighlightableButtonNode()
|
||||
self.playbackControlButton.isHidden = true
|
||||
|
||||
self.playPauseIconNode = PlayPauseIconNode()
|
||||
|
||||
self.statusButtonNode = HighlightTrackingButtonNode()
|
||||
self.statusNode = RadialStatusNode(backgroundNodeColor: .clear)
|
||||
self.statusNode.isUserInteractionEnabled = false
|
||||
@ -361,6 +363,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
|
||||
self.contentNode.addSubnode(self.backwardButton)
|
||||
self.contentNode.addSubnode(self.forwardButton)
|
||||
self.contentNode.addSubnode(self.playbackControlButton)
|
||||
self.playbackControlButton.addSubnode(self.playPauseIconNode)
|
||||
|
||||
self.contentNode.addSubnode(self.statusNode)
|
||||
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.playPauseIconNode.frame = self.playbackControlButton.bounds.offsetBy(dx: 2.0, dy: 2.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))
|
||||
@ -1079,6 +1083,11 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
|
||||
}
|
||||
}
|
||||
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
|
||||
if let strongSelf = self {
|
||||
let _ = (strongSelf.context.account.postbox.transaction { transaction -> [Peer] in
|
||||
@ -1140,6 +1149,11 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
|
||||
let shareAction: ([Message]) -> Void = { messages in
|
||||
if let strongSelf = self {
|
||||
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
|
||||
if let strongSelf = self {
|
||||
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)
|
||||
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
|
||||
if let strongSelf = self {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -484,7 +484,13 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
private var previousPlaying: Bool?
|
||||
|
||||
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) {
|
||||
@ -713,13 +719,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
|
||||
if strongSelf.isCentral && playing && strongSelf.previousPlaying != true && !disablePlayerControls {
|
||||
strongSelf.controlsTimer?.invalidate()
|
||||
|
||||
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
|
||||
strongSelf.setupControlsTimer()
|
||||
} else if !playing {
|
||||
strongSelf.controlsTimer?.invalidate()
|
||||
strongSelf.controlsTimer = nil
|
||||
|
||||
@ -18,6 +18,7 @@ swift_library(
|
||||
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
|
||||
"//submodules/PresentationDataUtils:PresentationDataUtils",
|
||||
"//submodules/LocationResources:LocationResources",
|
||||
"//submodules/ShimmerEffect:ShimmerEffect",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@ -9,11 +9,12 @@ import SyncCore
|
||||
import TelegramPresentationData
|
||||
import ItemListUI
|
||||
import LocationResources
|
||||
import ShimmerEffect
|
||||
|
||||
public final class ItemListVenueItem: ListViewItem, ItemListItem {
|
||||
let presentationData: ItemListPresentationData
|
||||
let account: Account
|
||||
let venue: TelegramMediaMap
|
||||
let venue: TelegramMediaMap?
|
||||
let title: String?
|
||||
let subtitle: String?
|
||||
let style: ItemListStyle
|
||||
@ -23,7 +24,7 @@ public final class ItemListVenueItem: ListViewItem, ItemListItem {
|
||||
public let sectionId: ItemListSectionId
|
||||
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.account = account
|
||||
self.venue = venue
|
||||
@ -117,6 +118,9 @@ public class ItemListVenueItemNode: ListViewItemNode, ItemListItemNode {
|
||||
private let addressNode: TextNode
|
||||
private let infoButton: HighlightableButtonNode
|
||||
|
||||
private var placeholderNode: ShimmerEffectNode?
|
||||
private var absoluteLocation: (CGRect, CGSize)?
|
||||
|
||||
private var item: ItemListVenueItem?
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
||||
let makeAddressLayout = TextNode.asyncLayout(self.addressNode)
|
||||
@ -188,27 +201,27 @@ public class ItemListVenueItemNode: ListViewItemNode, ItemListItemNode {
|
||||
updatedTheme = item.presentationData.theme
|
||||
}
|
||||
|
||||
let venueType = item.venue.venue?.type ?? ""
|
||||
if currentItem?.venue.venue?.type != venueType {
|
||||
let venueType = item.venue?.venue?.type ?? ""
|
||||
if currentItem?.venue?.venue?.type != venueType {
|
||||
updatedVenueType = venueType
|
||||
}
|
||||
|
||||
let title: String
|
||||
if let venueTitle = item.venue.venue?.title {
|
||||
if let venueTitle = item.venue?.venue?.title {
|
||||
title = venueTitle
|
||||
} else if let customTitle = item.title {
|
||||
title = customTitle
|
||||
} else {
|
||||
title = ""
|
||||
title = " "
|
||||
}
|
||||
|
||||
let subtitle: String
|
||||
if let address = item.venue.venue?.address {
|
||||
if let address = item.venue?.venue?.address {
|
||||
subtitle = address
|
||||
} else if let customSubtitle = item.subtitle {
|
||||
subtitle = customSubtitle
|
||||
} else {
|
||||
subtitle = ""
|
||||
subtitle = " "
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
if strongSelf.bottomStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0)
|
||||
strongSelf.addSubnode(strongSelf.bottomStripeNode)
|
||||
}
|
||||
if strongSelf.maskNode.supernode != nil {
|
||||
strongSelf.maskNode.removeFromSupernode()
|
||||
@ -350,6 +363,45 @@ public class ItemListVenueItemNode: ListViewItemNode, ItemListItemNode {
|
||||
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))
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -10,7 +10,6 @@ import SwiftSignalKit
|
||||
import MergeLists
|
||||
import ItemListUI
|
||||
import ItemListVenueItem
|
||||
import ActivityIndicator
|
||||
import TelegramPresentationData
|
||||
import TelegramStringFormatting
|
||||
import AccountContext
|
||||
@ -41,7 +40,7 @@ private enum LocationPickerEntry: Comparable, Identifiable {
|
||||
case location(PresentationTheme, String, String, TelegramMediaMap?, CLLocationCoordinate2D?)
|
||||
case liveLocation(PresentationTheme, String, String, CLLocationCoordinate2D?)
|
||||
case header(PresentationTheme, String)
|
||||
case venue(PresentationTheme, TelegramMediaMap, Int)
|
||||
case venue(PresentationTheme, TelegramMediaMap?, Int)
|
||||
case attribution(PresentationTheme, LocationAttribution)
|
||||
|
||||
var stableId: LocationPickerEntryId {
|
||||
@ -52,8 +51,8 @@ private enum LocationPickerEntry: Comparable, Identifiable {
|
||||
return .liveLocation
|
||||
case .header:
|
||||
return .header
|
||||
case let .venue(_, venue, _):
|
||||
return .venue(venue.venue?.id ?? "")
|
||||
case let .venue(_, venue, index):
|
||||
return .venue(venue?.venue?.id ?? "\(index)")
|
||||
case .attribution:
|
||||
return .attribution
|
||||
}
|
||||
@ -80,7 +79,7 @@ private enum LocationPickerEntry: Comparable, Identifiable {
|
||||
return false
|
||||
}
|
||||
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
|
||||
} else {
|
||||
return false
|
||||
@ -158,9 +157,9 @@ private enum LocationPickerEntry: Comparable, Identifiable {
|
||||
case let .header(_, title):
|
||||
return LocationSectionHeaderItem(presentationData: ItemListPresentationData(presentationData), title: title)
|
||||
case let .venue(_, venue, _):
|
||||
let venueType = venue.venue?.type ?? ""
|
||||
return ItemListVenueItem(presentationData: ItemListPresentationData(presentationData), account: account, venue: venue, style: .plain, action: {
|
||||
interaction?.sendVenue(venue)
|
||||
let venueType = venue?.venue?.type ?? ""
|
||||
return ItemListVenueItem(presentationData: ItemListPresentationData(presentationData), account: account, venue: venue, style: .plain, action: venue.flatMap { venue in
|
||||
return { interaction?.sendVenue(venue) }
|
||||
}, infoAction: ["home", "work"].contains(venueType) ? {
|
||||
interaction?.openHomeWorkInfo()
|
||||
} : nil)
|
||||
@ -253,7 +252,6 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM
|
||||
private let listNode: ListView
|
||||
private let emptyResultsTextNode: ImmediateTextNode
|
||||
private let headerNode: LocationMapHeaderNode
|
||||
private let activityIndicator: ActivityIndicator
|
||||
private let shadeNode: ASDisplayNode
|
||||
private let innerShadeNode: ASDisplayNode
|
||||
|
||||
@ -301,8 +299,6 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM
|
||||
|
||||
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.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
|
||||
self.shadeNode.alpha = 0.0
|
||||
@ -316,7 +312,6 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM
|
||||
self.addSubnode(self.listNode)
|
||||
self.addSubnode(self.headerNode)
|
||||
self.addSubnode(self.optionsNode)
|
||||
self.listNode.addSubnode(self.activityIndicator)
|
||||
self.listNode.addSubnode(self.emptyResultsTextNode)
|
||||
self.shadeNode.addSubnode(self.innerShadeNode)
|
||||
self.addSubnode(self.shadeNode)
|
||||
@ -504,8 +499,8 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM
|
||||
entries.append(.header(presentationData.theme, presentationData.strings.Map_ChooseAPlace.uppercased()))
|
||||
|
||||
let displayedVenues = foundVenues != nil || state.searchingVenuesAround ? foundVenues : venues
|
||||
var index: Int = 0
|
||||
if let venues = displayedVenues {
|
||||
var index: Int = 0
|
||||
var attribution: LocationAttribution?
|
||||
for venue in venues {
|
||||
if venue.venue?.provider == "foursquare" {
|
||||
@ -519,6 +514,11 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM
|
||||
if let attribution = 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 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)))
|
||||
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.layoutActivityIndicator(transition: listTransition)
|
||||
strongSelf.layoutEmptyResultsPlaceholder(transition: listTransition)
|
||||
}
|
||||
|
||||
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
|
||||
if let strongSelf = self {
|
||||
strongSelf.activityIndicator.isHidden = !transition.isLoading
|
||||
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.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 {
|
||||
return
|
||||
}
|
||||
@ -812,10 +811,7 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM
|
||||
headerHeight = topInset
|
||||
}
|
||||
|
||||
let indicatorSize = self.activityIndicator.measure(CGSize(width: 100.0, height: 100.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 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))
|
||||
@ -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.alpha = layout.intrinsicInsets.bottom > 0.0 ? 1.0 : 0.0
|
||||
|
||||
self.layoutActivityIndicator(transition: transition)
|
||||
self.layoutEmptyResultsPlaceholder(transition: transition)
|
||||
|
||||
if isFirstLayout {
|
||||
while !self.enqueuedTransitions.isEmpty {
|
||||
|
||||
@ -137,6 +137,14 @@ open class ManagedAnimationNode: ASDisplayNode {
|
||||
public var trackStack: [ManagedAnimationItem] = []
|
||||
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) {
|
||||
self.intrinsicSize = size
|
||||
|
||||
@ -242,7 +250,11 @@ open class ManagedAnimationNode: ASDisplayNode {
|
||||
if state.frameIndex != frameIndex {
|
||||
state.frameIndex = frameIndex
|
||||
if let image = state.draw() {
|
||||
self.imageNode.image = image
|
||||
if let customColor = self.customColor {
|
||||
self.imageNode.image = generateTintedImage(image: image, color: customColor)
|
||||
} else {
|
||||
self.imageNode.image = image
|
||||
}
|
||||
}
|
||||
|
||||
for (callbackFrame, callback) in state.item.callbacks {
|
||||
|
||||
@ -281,7 +281,13 @@ private func channelMembersControllerEntries(context: AccountContext, presentati
|
||||
if let peer = view.peers[view.peerId] as? TelegramChannel, peer.addressName == nil {
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -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))
|
||||
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 {
|
||||
let iconSize = iconImage.size
|
||||
|
||||
@ -14,7 +14,6 @@ import AccountContext
|
||||
import ShareController
|
||||
import SearchBarNode
|
||||
import SearchUI
|
||||
import ActivityIndicator
|
||||
import UndoUI
|
||||
|
||||
private enum LanguageListSection: ItemListSectionId {
|
||||
@ -33,12 +32,12 @@ private enum LanguageListEntryType {
|
||||
}
|
||||
|
||||
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 {
|
||||
switch self {
|
||||
case let .localization(_, info, _, _, _, _, _):
|
||||
return .localization(info.languageCode)
|
||||
case let .localization(index, info, _, _, _, _, _):
|
||||
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 {
|
||||
switch self {
|
||||
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: {
|
||||
selectLocalization(info)
|
||||
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)
|
||||
}
|
||||
}, setItemWithRevealedOptions: setItemWithRevealedOptions, removeItem: removeItem)
|
||||
}
|
||||
}
|
||||
@ -259,16 +260,17 @@ private struct LanguageListNodeTransition {
|
||||
let firstTime: Bool
|
||||
let isLoading: 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 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 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 {
|
||||
@ -285,7 +287,6 @@ final class LocalizationListControllerNode: ViewControllerTracingNode {
|
||||
private var containerLayout: (ContainerViewLayout, CGFloat)?
|
||||
let listNode: ListView
|
||||
private var queuedTransitions: [LanguageListNodeTransition] = []
|
||||
private var activityIndicator: ActivityIndicator?
|
||||
private var searchDisplayController: SearchDisplayController?
|
||||
|
||||
private let presentationDataValue = Promise<PresentationData>()
|
||||
@ -365,19 +366,22 @@ final class LocalizationListControllerNode: ViewControllerTracingNode {
|
||||
}
|
||||
|
||||
let preferencesKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.localizationListState]))
|
||||
let previousState = Atomic<LocalizationListState?>(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
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
var entries: [LanguageListEntry] = []
|
||||
var activeLanguageCode: String?
|
||||
if let localizationSettings = sharedData.entries[SharedDataKeys.localizationSettings] as? LocalizationSettings {
|
||||
activeLanguageCode = localizationSettings.primaryComponent.languageCode
|
||||
}
|
||||
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
|
||||
|
||||
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)
|
||||
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 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)
|
||||
})
|
||||
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 })
|
||||
|
||||
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 {
|
||||
self.dequeueTransitions()
|
||||
}
|
||||
@ -463,7 +469,7 @@ final class LocalizationListControllerNode: ViewControllerTracingNode {
|
||||
}
|
||||
|
||||
private func dequeueTransitions() {
|
||||
guard let (layout, navigationBarHeight) = self.containerLayout else {
|
||||
guard let _ = self.containerLayout else {
|
||||
return
|
||||
}
|
||||
while !self.queuedTransitions.isEmpty {
|
||||
@ -473,6 +479,8 @@ final class LocalizationListControllerNode: ViewControllerTracingNode {
|
||||
if transition.firstTime {
|
||||
options.insert(.Synchronous)
|
||||
options.insert(.LowLatency)
|
||||
} else if transition.crossfade {
|
||||
options.insert(.AnimateCrossfade)
|
||||
} else if transition.animated {
|
||||
options.insert(.AnimateInsertion)
|
||||
}
|
||||
@ -482,17 +490,6 @@ final class LocalizationListControllerNode: ViewControllerTracingNode {
|
||||
strongSelf.didSetReady = 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()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ import ItemListUI
|
||||
import PresentationDataUtils
|
||||
import ActivityIndicator
|
||||
import ChatListSearchItemNode
|
||||
import ShimmerEffect
|
||||
|
||||
struct LocalizationListItemEditing: Equatable {
|
||||
let editable: Bool
|
||||
@ -23,6 +24,7 @@ class LocalizationListItem: ListViewItem, ItemListItem {
|
||||
let subtitle: String
|
||||
let checked: Bool
|
||||
let activity: Bool
|
||||
let loading: Bool
|
||||
let editing: LocalizationListItemEditing
|
||||
let sectionId: ItemListSectionId
|
||||
let alwaysPlain: Bool
|
||||
@ -30,13 +32,14 @@ class LocalizationListItem: ListViewItem, ItemListItem {
|
||||
let setItemWithRevealedOptions: (String?, 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.id = id
|
||||
self.title = title
|
||||
self.subtitle = subtitle
|
||||
self.checked = checked
|
||||
self.activity = activity
|
||||
self.loading = loading
|
||||
self.editing = editing
|
||||
self.sectionId = sectionId
|
||||
self.alwaysPlain = alwaysPlain
|
||||
@ -108,6 +111,9 @@ class LocalizationListItemNode: ItemListRevealOptionsItemNode {
|
||||
private var item: LocalizationListItem?
|
||||
private var layoutParams: (ListViewItemLayoutParams, ItemListNeighbors)?
|
||||
|
||||
private var placeholderNode: ShimmerEffectNode?
|
||||
private var absoluteLocation: (CGRect, CGSize)?
|
||||
|
||||
private var editableControlNode: ItemListEditableControlNode?
|
||||
private var reorderControlNode: ItemListEditableReorderControlNode?
|
||||
|
||||
@ -117,7 +123,7 @@ class LocalizationListItemNode: ItemListRevealOptionsItemNode {
|
||||
if self.editableControlNode != nil {
|
||||
return false
|
||||
}
|
||||
if let _ = self.layoutParams?.0 {
|
||||
if let _ = self.layoutParams?.0, let item = self.item, !item.loading {
|
||||
return super.canBeSelected
|
||||
} else {
|
||||
return false
|
||||
@ -167,6 +173,15 @@ class LocalizationListItemNode: ItemListRevealOptionsItemNode {
|
||||
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) {
|
||||
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
||||
let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode)
|
||||
@ -322,6 +337,42 @@ class LocalizationListItemNode: ItemListRevealOptionsItemNode {
|
||||
strongSelf.setRevealOptions((left: [], right: []))
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -133,14 +133,16 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
|
||||
var index: Int32 = 0
|
||||
|
||||
var existingPeerIds: Set<PeerId> = Set()
|
||||
|
||||
entries.append(SharePeerEntry(index: index, peer: RenderedPeer(peer: accountPeer), presence: nil, theme: theme, strings: strings))
|
||||
existingPeerIds.insert(accountPeer.id)
|
||||
index += 1
|
||||
|
||||
for peer in foundPeers.reversed() {
|
||||
entries.append(SharePeerEntry(index: index, peer: peer, presence: nil, theme: theme, strings: strings))
|
||||
existingPeerIds.insert(peer.peerId)
|
||||
index += 1
|
||||
if !existingPeerIds.contains(peer.peerId) {
|
||||
entries.append(SharePeerEntry(index: index, peer: peer, presence: nil, theme: theme, strings: strings))
|
||||
existingPeerIds.insert(peer.peerId)
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
|
||||
for (peer, presence) in initialPeers {
|
||||
|
||||
@ -21,6 +21,7 @@ swift_library(
|
||||
"//submodules/PresentationDataUtils:PresentationDataUtils",
|
||||
"//submodules/Markdown:Markdown",
|
||||
"//submodules/TelegramCallsUI:TelegramCallsUI",
|
||||
"//submodules/ManagedAnimationNode:ManagedAnimationNode",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@ -10,6 +10,7 @@ import TelegramUIPreferences
|
||||
import UniversalMediaPlayer
|
||||
import AccountContext
|
||||
import TelegramStringFormatting
|
||||
import ManagedAnimationNode
|
||||
|
||||
private let titleFont = Font.regular(12.0)
|
||||
private let subtitleFont = Font.regular(10.0)
|
||||
@ -147,8 +148,7 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollVi
|
||||
|
||||
private let closeButton: HighlightableButtonNode
|
||||
private let actionButton: HighlightTrackingButtonNode
|
||||
private let actionPauseNode: ASImageNode
|
||||
private let actionPlayNode: ASImageNode
|
||||
private let playPauseIconNode: PlayPauseIconNode
|
||||
private let rateButton: HighlightableButtonNode
|
||||
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.displaysAsynchronously = false
|
||||
|
||||
self.actionPauseNode = ASImageNode()
|
||||
self.actionPauseNode.contentMode = .center
|
||||
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.playPauseIconNode = PlayPauseIconNode()
|
||||
self.playPauseIconNode.customColor = self.theme.rootController.navigationBar.accentTextColor
|
||||
|
||||
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.accessibilityAreaNode)
|
||||
|
||||
self.actionButton.addSubnode(self.actionPauseNode)
|
||||
self.actionButton.addSubnode(self.actionPlayNode)
|
||||
self.actionButton.addSubnode(self.playPauseIconNode)
|
||||
self.addSubnode(self.actionButton)
|
||||
|
||||
self.closeButton.addTarget(self, action: #selector(self.closeButtonPressed), forControlEvents: .touchUpInside)
|
||||
@ -341,8 +328,7 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollVi
|
||||
} else {
|
||||
paused = true
|
||||
}
|
||||
strongSelf.actionPlayNode.isHidden = !paused
|
||||
strongSelf.actionPauseNode.isHidden = paused
|
||||
strongSelf.playPauseIconNode.enqueueState(paused ? .play : .pause, animated: true)
|
||||
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.closeButton.setImage(PresentationResourcesRootController.navigationPlayerCloseButton(self.theme), for: [])
|
||||
self.actionPlayNode.image = PresentationResourcesRootController.navigationPlayerPlayIcon(self.theme)
|
||||
self.actionPauseNode.image = PresentationResourcesRootController.navigationPlayerPauseIcon(self.theme)
|
||||
self.playPauseIconNode.customColor = self.theme.rootController.navigationBar.accentTextColor
|
||||
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: []))
|
||||
|
||||
@ -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)))
|
||||
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.actionPlayNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: 40.0, height: 37.0)))
|
||||
transition.updateFrame(node: self.actionPauseNode, 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.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)))
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -3139,7 +3139,7 @@ public final class VoiceChatController: ViewController {
|
||||
muteState: memberMuteState,
|
||||
canManageCall: self.callState?.canManageCall ?? false,
|
||||
volume: member.volume,
|
||||
raisedHand: member.raiseHandRating != nil,
|
||||
raisedHand: member.hasRaiseHand,
|
||||
displayRaisedHandStatus: self.displayedRaisedHands.contains(member.peer.id)
|
||||
)))
|
||||
index += 1
|
||||
@ -3416,6 +3416,12 @@ public final class VoiceChatController: ViewController {
|
||||
}
|
||||
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
|
||||
@ -3474,6 +3480,10 @@ public final class VoiceChatController: ViewController {
|
||||
return true
|
||||
}
|
||||
|> filter { $0 })
|
||||
|
||||
self.scrollToTop = { [weak self] in
|
||||
self?.controllerNode.scrollToTop()
|
||||
}
|
||||
}
|
||||
|
||||
required init(coder aDecoder: NSCoder) {
|
||||
|
||||
@ -158,7 +158,7 @@ private func currentDateTimeFormat() -> PresentationDateTimeFormat {
|
||||
var dateSeparator = "/"
|
||||
var dateSuffix = ""
|
||||
if let dateString = DateFormatter.dateFormat(fromTemplate: "MdY", options: 0, locale: locale) {
|
||||
for separator in [". ", ".", "/", "-", "/"] {
|
||||
for separator in [".", "/", "-", "/"] {
|
||||
if dateString.contains(separator) {
|
||||
if separator == ". " {
|
||||
dateSuffix = "."
|
||||
|
||||
BIN
submodules/TelegramUI/Resources/Animations/anim_playpause.tgs
Normal file
BIN
submodules/TelegramUI/Resources/Animations/anim_playpause.tgs
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/Animations/anim_savemedia.tgs
Normal file
BIN
submodules/TelegramUI/Resources/Animations/anim_savemedia.tgs
Normal file
Binary file not shown.
@ -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 {
|
||||
var canManagePin = false
|
||||
if let channel = peer as? TelegramChannel {
|
||||
@ -64,6 +64,13 @@ private func actionForPeer(peer: Peer, interfaceState: ChatPresentationInterface
|
||||
}
|
||||
} else {
|
||||
if let channel = peer as? TelegramChannel {
|
||||
if case .broadcast = channel.info, isJoining {
|
||||
if isMuted {
|
||||
return .unmuteNotifications
|
||||
} else {
|
||||
return .muteNotifications
|
||||
}
|
||||
}
|
||||
switch channel.participationStatus {
|
||||
case .kicked:
|
||||
return .kicked
|
||||
@ -102,10 +109,11 @@ final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode {
|
||||
|
||||
private let actionDisposable = MetaDisposable()
|
||||
private let badgeDisposable = MetaDisposable()
|
||||
private var isJoining: Bool = false
|
||||
|
||||
private var presentationInterfaceState: ChatPresentationInterfaceState?
|
||||
|
||||
private var layoutData: (CGFloat, CGFloat, CGFloat)?
|
||||
private var layoutData: (CGFloat, CGFloat, CGFloat, UIEdgeInsets, CGFloat, Bool, LayoutMetrics)?
|
||||
|
||||
override init() {
|
||||
self.button = HighlightableButtonNode()
|
||||
@ -168,14 +176,34 @@ final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode {
|
||||
|
||||
switch action {
|
||||
case .join:
|
||||
self.activityIndicator.isHidden = false
|
||||
self.activityIndicator.startAnimating()
|
||||
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.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)
|
||||
|> afterDisposed { [weak self] in
|
||||
Queue.mainQueue().async {
|
||||
if let strongSelf = self {
|
||||
strongSelf.activityIndicator.isHidden = true
|
||||
strongSelf.activityIndicator.stopAnimating()
|
||||
strongSelf.isJoining = false
|
||||
}
|
||||
}
|
||||
}).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 {
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
if self.presentationInterfaceState != interfaceState || force {
|
||||
let previousState = self.presentationInterfaceState
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
self.action = action
|
||||
let (title, color) = titleAndColorForAction(action, theme: interfaceState.theme, strings: interfaceState.strings)
|
||||
|
||||
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
|
||||
}
|
||||
if let snapshotView = self.button.view.snapshotContentTree() {
|
||||
|
||||
@ -11,14 +11,7 @@ import UniversalMediaPlayer
|
||||
import AppBundle
|
||||
import ContextUI
|
||||
import AnimationUI
|
||||
|
||||
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)
|
||||
}
|
||||
import ManagedAnimationNode
|
||||
|
||||
extension AudioWaveformNode: CustomMediaPlayerScrubbingForegroundNode {
|
||||
|
||||
@ -30,7 +23,7 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode {
|
||||
let sendButton: HighlightTrackingButtonNode
|
||||
private var sendButtonRadialStatusNode: ChatSendButtonRadialStatusNode?
|
||||
let playButton: HighlightableButtonNode
|
||||
let pauseButton: HighlightableButtonNode
|
||||
private let playPauseIconNode: PlayPauseIconNode
|
||||
private let waveformButton: ASButtonNode
|
||||
let waveformBackgroundNode: ASImageNode
|
||||
|
||||
@ -76,13 +69,10 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode {
|
||||
|
||||
self.playButton = HighlightableButtonNode()
|
||||
self.playButton.displaysAsynchronously = false
|
||||
self.playButton.setImage(generatePlayIcon(theme), for: [])
|
||||
self.playButton.isUserInteractionEnabled = false
|
||||
self.pauseButton = HighlightableButtonNode()
|
||||
self.pauseButton.displaysAsynchronously = false
|
||||
self.pauseButton.setImage(generatePauseIcon(theme), for: [])
|
||||
self.pauseButton.isHidden = true
|
||||
self.pauseButton.isUserInteractionEnabled = false
|
||||
|
||||
self.playPauseIconNode = PlayPauseIconNode()
|
||||
self.playPauseIconNode.enqueueState(.play, animated: false)
|
||||
self.playPauseIconNode.customColor = theme.chat.inputPanel.actionControlForegroundColor
|
||||
|
||||
self.waveformButton = ASButtonNode()
|
||||
self.waveformButton.accessibilityTraits.insert(.startsMediaSession)
|
||||
@ -106,9 +96,9 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode {
|
||||
self.addSubnode(self.sendButton)
|
||||
self.addSubnode(self.waveformScubberNode)
|
||||
self.addSubnode(self.playButton)
|
||||
self.addSubnode(self.pauseButton)
|
||||
self.addSubnode(self.durationLabel)
|
||||
self.addSubnode(self.waveformButton)
|
||||
self.playButton.addSubnode(self.playPauseIconNode)
|
||||
|
||||
self.sendButton.highligthedChanged = { [weak self] highlighted in
|
||||
if let strongSelf = self {
|
||||
@ -168,6 +158,9 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode {
|
||||
if let context = self.context {
|
||||
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)
|
||||
mediaPlayer.actionAtEnd = .action{ [weak mediaPlayer] in
|
||||
mediaPlayer?.seek(timestamp: 0.0)
|
||||
}
|
||||
self.mediaPlayer = mediaPlayer
|
||||
self.durationLabel.defaultDuration = Double(recordedMediaPreview.duration)
|
||||
self.durationLabel.status = mediaPlayer.status
|
||||
@ -177,11 +170,10 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode {
|
||||
if let strongSelf = self {
|
||||
switch status.status {
|
||||
case .playing, .buffering(_, true, _, _):
|
||||
strongSelf.playButton.isHidden = true
|
||||
strongSelf.playPauseIconNode.enqueueState(.pause, animated: true)
|
||||
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.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))
|
||||
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)))
|
||||
@ -259,10 +252,7 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode {
|
||||
|
||||
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.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.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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1415,7 +1415,6 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
animatePosition(for: prevPreviewInputPanelNode.waveformScubberNode)
|
||||
animatePosition(for: prevPreviewInputPanelNode.durationLabel)
|
||||
animatePosition(for: prevPreviewInputPanelNode.playButton)
|
||||
animatePosition(for: prevPreviewInputPanelNode.pauseButton)
|
||||
}
|
||||
|
||||
func animateAlpha(for previewSubnode: ASDisplayNode) {
|
||||
@ -1430,7 +1429,6 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
animateAlpha(for: prevPreviewInputPanelNode.waveformScubberNode)
|
||||
animateAlpha(for: prevPreviewInputPanelNode.durationLabel)
|
||||
animateAlpha(for: prevPreviewInputPanelNode.playButton)
|
||||
animateAlpha(for: prevPreviewInputPanelNode.pauseButton)
|
||||
|
||||
let binNode = prevPreviewInputPanelNode.binNode
|
||||
self.animatingBinNode = binNode
|
||||
|
||||
@ -8,6 +8,7 @@ import SwiftSignalKit
|
||||
import TelegramUIPreferences
|
||||
import AccountContext
|
||||
import ShareController
|
||||
import UndoUI
|
||||
|
||||
final class OverlayAudioPlayerControllerImpl: ViewController, OverlayAudioPlayerController {
|
||||
private let context: AccountContext
|
||||
@ -68,6 +69,46 @@ final class OverlayAudioPlayerControllerImpl: ViewController, OverlayAudioPlayer
|
||||
strongSelf.dismiss()
|
||||
}
|
||||
}, 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.present(shareController, in: .window(.root))
|
||||
}
|
||||
|
||||
@ -12,6 +12,7 @@ import TelegramUIPreferences
|
||||
import AccountContext
|
||||
import PhotoResources
|
||||
import AppBundle
|
||||
import ManagedAnimationNode
|
||||
|
||||
private func generateBackground(theme: PresentationTheme) -> UIImage? {
|
||||
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 let playPauseButton: IconButtonNode
|
||||
private let playPauseIconNode: PlayPauseIconNode
|
||||
|
||||
private var currentOrder: MusicPlaybackSettingsOrder?
|
||||
private let orderButton: IconButtonNode
|
||||
@ -211,6 +213,8 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
|
||||
self.playPauseButton = IconButtonNode()
|
||||
self.playPauseButton.displaysAsynchronously = false
|
||||
|
||||
self.playPauseIconNode = PlayPauseIconNode()
|
||||
|
||||
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)
|
||||
|
||||
@ -240,6 +244,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
|
||||
self.addSubnode(self.backwardButton)
|
||||
self.addSubnode(self.forwardButton)
|
||||
self.addSubnode(self.playPauseButton)
|
||||
self.playPauseButton.addSubnode(self.playPauseIconNode)
|
||||
|
||||
self.addSubnode(self.separatorNode)
|
||||
|
||||
@ -323,10 +328,12 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
|
||||
if strongSelf.wasPlaying {
|
||||
isPaused = false
|
||||
}
|
||||
|
||||
let isFirstTime = strongSelf.currentIsPaused == nil
|
||||
if strongSelf.currentIsPaused != isPaused {
|
||||
strongSelf.currentIsPaused = isPaused
|
||||
|
||||
strongSelf.updatePlayPauseButton(paused: isPaused)
|
||||
strongSelf.updatePlayPauseButton(paused: isPaused, animated: !isFirstTime)
|
||||
}
|
||||
|
||||
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.forwardButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Next"), color: presentationData.theme.list.itemPrimaryTextColor)
|
||||
if let isPaused = self.currentIsPaused {
|
||||
self.updatePlayPauseButton(paused: isPaused)
|
||||
self.updatePlayPauseButton(paused: isPaused, animated: false)
|
||||
}
|
||||
if let order = self.currentOrder {
|
||||
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 {
|
||||
self.playPauseButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Play"), color: self.presentationData.theme.list.itemPrimaryTextColor)
|
||||
self.playPauseIconNode.enqueueState(.play, animated: animated)
|
||||
} 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.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
|
||||
}
|
||||
@ -892,3 +903,53 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2774,13 +2774,19 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
||||
} else {
|
||||
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
|
||||
guard let strongSelf = self, let peer = strongSelf.data?.peer else {
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
}, action: { [weak self] c, f in
|
||||
if let strongSelf = self, let parent = strongSelf.controller {
|
||||
@ -5434,12 +5440,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
||||
|> take(1)
|
||||
})
|
||||
strongSelf.activeActionDisposable.set((combineLatest(signals)
|
||||
|> deliverOnMainQueue).start(completed: {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.controller?.present(OverlayStatusController(theme: strongSelf.presentationData.theme, type: .success), in: .window(.root))
|
||||
}))
|
||||
|> deliverOnMainQueue).start())
|
||||
}
|
||||
})
|
||||
if let peerSelectionController = peerSelectionController {
|
||||
|
||||
@ -37,6 +37,7 @@ public enum UndoOverlayContent {
|
||||
case voiceChatCanSpeak(text: String)
|
||||
case sticker(account: Account, file: TelegramMediaFile, text: String)
|
||||
case copy(text: String)
|
||||
case mediaSaved(text: String)
|
||||
}
|
||||
|
||||
public enum UndoOverlayAction {
|
||||
|
||||
@ -687,6 +687,27 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
self.textNode.attributedText = attributedText
|
||||
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
|
||||
self.originalRemainingSeconds = 3
|
||||
}
|
||||
@ -717,7 +738,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
switch content {
|
||||
case .removedChat:
|
||||
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
|
||||
case .dice:
|
||||
self.panelWrapperNode.clipsToBounds = true
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user