From 0f9d40016f9cc797a6b68b630dbc8d1599c3ba1a Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Wed, 17 Mar 2021 09:19:57 +0400 Subject: [PATCH] Various Improvements --- .../Sources/ChatListController.swift | 14 ++- submodules/GalleryUI/BUILD | 1 + .../ChatItemGalleryFooterContentNode.swift | 77 ++++++++++++++- .../Items/UniversalVideoGalleryItem.swift | 16 ++-- submodules/ItemListVenueItem/BUILD | 1 + .../Sources/ItemListVenueItem.swift | 70 ++++++++++++-- .../LocationPickerControllerNode.swift | 38 ++++---- .../Sources/ManagedAnimationNode.swift | 14 ++- .../Sources/ChannelMembersController.swift | 8 +- .../SearchBarNode/Sources/SearchBarNode.swift | 2 +- .../LocalizationListControllerNode.swift | 55 +++++------ .../LocalizationListItem.swift | 55 ++++++++++- .../Sources/SharePeersContainerNode.swift | 10 +- submodules/TelegramBaseController/BUILD | 1 + .../MediaNavigationAccessoryHeaderNode.swift | 82 +++++++++++----- .../Sources/VoiceChatController.swift | 12 ++- .../Sources/PresentationData.swift | 2 +- .../Resources/Animations/anim_playpause.tgs | Bin 0 -> 1785 bytes .../Resources/Animations/anim_savemedia.tgs | Bin 0 -> 3982 bytes .../ChatChannelSubscriberInputPanelNode.swift | 53 +++++++++-- .../ChatRecordingPreviewInputPanelNode.swift | 89 +++++++++++++----- .../Sources/ChatTextInputPanelNode.swift | 2 - .../OverlayAudioPlayerController.swift | 41 ++++++++ .../Sources/OverlayPlayerControlsNode.swift | 73 ++++++++++++-- .../Sources/PeerInfo/PeerInfoScreen.swift | 15 +-- .../Sources/UndoOverlayController.swift | 1 + .../Sources/UndoOverlayControllerNode.swift | 23 ++++- 27 files changed, 594 insertions(+), 161 deletions(-) create mode 100644 submodules/TelegramUI/Resources/Animations/anim_playpause.tgs create mode 100644 submodules/TelegramUI/Resources/Animations/anim_savemedia.tgs diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index ac1af87bd4..328609b702 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -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)) diff --git a/submodules/GalleryUI/BUILD b/submodules/GalleryUI/BUILD index 06170f2d03..6a06062b21 100644 --- a/submodules/GalleryUI/BUILD +++ b/submodules/GalleryUI/BUILD @@ -28,6 +28,7 @@ swift_library( "//submodules/OverlayStatusController:OverlayStatusController", "//submodules/PresentationDataUtils:PresentationDataUtils", "//submodules/UrlEscaping:UrlEscaping", + "//submodules/ManagedAnimationNode:ManagedAnimationNode" ], visibility = [ "//visibility:public", diff --git a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift index 3eca056b51..666c44c163 100644 --- a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift +++ b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift @@ -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 + } + } + } +} diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index fe7bd8b2cf..7445bf9b4d 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -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 diff --git a/submodules/ItemListVenueItem/BUILD b/submodules/ItemListVenueItem/BUILD index f80b0d9376..7ca5b05b58 100644 --- a/submodules/ItemListVenueItem/BUILD +++ b/submodules/ItemListVenueItem/BUILD @@ -18,6 +18,7 @@ swift_library( "//submodules/TelegramUIPreferences:TelegramUIPreferences", "//submodules/PresentationDataUtils:PresentationDataUtils", "//submodules/LocationResources:LocationResources", + "//submodules/ShimmerEffect:ShimmerEffect", ], visibility = [ "//visibility:public", diff --git a/submodules/ItemListVenueItem/Sources/ItemListVenueItem.swift b/submodules/ItemListVenueItem/Sources/ItemListVenueItem.swift index e4f4921b56..c4e3932999 100644 --- a/submodules/ItemListVenueItem/Sources/ItemListVenueItem.swift +++ b/submodules/ItemListVenueItem/Sources/ItemListVenueItem.swift @@ -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() + } } }) } diff --git a/submodules/LocationUI/Sources/LocationPickerControllerNode.swift b/submodules/LocationUI/Sources/LocationPickerControllerNode.swift index e1b9e6cbd9..c882fb209d 100644 --- a/submodules/LocationUI/Sources/LocationPickerControllerNode.swift +++ b/submodules/LocationUI/Sources/LocationPickerControllerNode.swift @@ -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 { diff --git a/submodules/ManagedAnimationNode/Sources/ManagedAnimationNode.swift b/submodules/ManagedAnimationNode/Sources/ManagedAnimationNode.swift index 50f8a1402b..d42228667f 100644 --- a/submodules/ManagedAnimationNode/Sources/ManagedAnimationNode.swift +++ b/submodules/ManagedAnimationNode/Sources/ManagedAnimationNode.swift @@ -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 { diff --git a/submodules/PeerInfoUI/Sources/ChannelMembersController.swift b/submodules/PeerInfoUI/Sources/ChannelMembersController.swift index 9e1de8dd89..7400f4e348 100644 --- a/submodules/PeerInfoUI/Sources/ChannelMembersController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelMembersController.swift @@ -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)) + } + } } diff --git a/submodules/SearchBarNode/Sources/SearchBarNode.swift b/submodules/SearchBarNode/Sources/SearchBarNode.swift index 94ffecf976..82424af16f 100644 --- a/submodules/SearchBarNode/Sources/SearchBarNode.swift +++ b/submodules/SearchBarNode/Sources/SearchBarNode.swift @@ -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 diff --git a/submodules/SettingsUI/Sources/Language Selection/LocalizationListControllerNode.swift b/submodules/SettingsUI/Sources/Language Selection/LocalizationListControllerNode.swift index aa17f128bf..f7393cf892 100644 --- a/submodules/SettingsUI/Sources/Language Selection/LocalizationListControllerNode.swift +++ b/submodules/SettingsUI/Sources/Language Selection/LocalizationListControllerNode.swift @@ -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() @@ -365,19 +366,22 @@ final class LocalizationListControllerNode: ViewControllerTracingNode { } let preferencesKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.localizationListState])) + let previousState = Atomic(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() - 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() - } } }) } diff --git a/submodules/SettingsUI/Sources/Language Selection/LocalizationListItem.swift b/submodules/SettingsUI/Sources/Language Selection/LocalizationListItem.swift index c00aef3a25..ba852bc7ef 100644 --- a/submodules/SettingsUI/Sources/Language Selection/LocalizationListItem.swift +++ b/submodules/SettingsUI/Sources/Language Selection/LocalizationListItem.swift @@ -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() + } } }) } diff --git a/submodules/ShareController/Sources/SharePeersContainerNode.swift b/submodules/ShareController/Sources/SharePeersContainerNode.swift index 8b05d91369..e323faa404 100644 --- a/submodules/ShareController/Sources/SharePeersContainerNode.swift +++ b/submodules/ShareController/Sources/SharePeersContainerNode.swift @@ -133,14 +133,16 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { var index: Int32 = 0 var existingPeerIds: Set = 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 { diff --git a/submodules/TelegramBaseController/BUILD b/submodules/TelegramBaseController/BUILD index dfa86f0840..7d1c7d3d24 100644 --- a/submodules/TelegramBaseController/BUILD +++ b/submodules/TelegramBaseController/BUILD @@ -21,6 +21,7 @@ swift_library( "//submodules/PresentationDataUtils:PresentationDataUtils", "//submodules/Markdown:Markdown", "//submodules/TelegramCallsUI:TelegramCallsUI", + "//submodules/ManagedAnimationNode:ManagedAnimationNode", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift b/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift index 31aab50b23..4f8ffc4958 100644 --- a/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift +++ b/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift @@ -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 + } + } + } +} diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift index f5fe1cbff4..c5a6569624 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift @@ -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) { diff --git a/submodules/TelegramPresentationData/Sources/PresentationData.swift b/submodules/TelegramPresentationData/Sources/PresentationData.swift index f3ec3dfcb2..1850dd0668 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationData.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationData.swift @@ -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 = "." diff --git a/submodules/TelegramUI/Resources/Animations/anim_playpause.tgs b/submodules/TelegramUI/Resources/Animations/anim_playpause.tgs new file mode 100644 index 0000000000000000000000000000000000000000..d6e7b0953d9d25ba76c86874e439d0a425cee422 GIT binary patch literal 1785 zcmVbiwFpG1WjN717U7yZC`L~VR>+2b#rAdYIARH0PR~#ZyPrd{wv0wcU2DG z)U8F^Tnn`5MbJTYZYHBpfxB2tw5&Qh~u=wkn=f%6t5<9+q`<8nw zHk;*kQ%z^{y2AS9nw~#&)pt+$>FR3xS2eY*p2ha|$^=xh!O@|v?y=8@YDL9@d)?z} zMZN#T2Y9Kgb*=-hL;ZL30&8b#612v#QU46)*mSk4arwQX_gkDm#38f1r^5R%qC!W1 zpu`7J(xi`2WBlPS>;5>lHSC-t){1JepQh3EI^vx765hvEMM(*aYX> zHX%=LPI%H&+g;h(Qsdu&J*;(Cwzk#y*ZMjeV-UxN3Iz1XUjTJi?zBvjtzn}4(Wn-5 z-Ilvi8$`+4I+yM6UWV5Rho@p-_-IVsD^Hkf$&*}|t{wLArf%e;9oD&QwI-|eZdh74 zamXVX8vjD@tr)t^?c#YUVMJk5{l3Ofx!Vd_PZC#LvAL}>ux$(t0V^ibocXC|nII@N z__QA+tYAi%&g)sDCmQ%k<&xiY@=-O{&UvWWnXT)=u~5f*($N6mqHieHXHDQoPffZf z+f~yOKTsvtaR+%~HCT&!udC^H{cicPw=O=?Th0!9D?p&Np@8QLsESsok~yDt2X-^y z(I?8A&V?8c5vX5E3EQ9%hL)pJk)^14DXN~z#kGbwDK=RV&=A zW@e(%xJFwHM_}3eEIX3J#4F(T+GZJQl)waOjgrd}`X6JOL}yzNsGh?9Vo10VBvMAg zip;x*GUwso9pPYmFyM~c7*t}|?;!{Zf=TdNP;e(u@XBQrf+G-;1WF467|l+#HoMUn zc6vypH1Kl-dIsDmb>POGYyvqo1+vwPXtUTvnmdaoV-UembE_^F4*Q!&V{uX0I^pbT zNLo^nJ!E7VkX7tII13IRBOVMfrXC*Fc>=^3CxJj`iGsxaz0p1G!*C=gaG)k&XuUQa z9%vcP7s4bMierb6p{xx?q2?v8ycap9i~ubG_NmD)a6d(vL&}R}MQQ*;(lSEI$~NKX zGE+lDHSS`Nq(ewNzDl@(1o42?fqfPQ5)ZtjIb8l3k`MzyVs#G^Ua|-Y8-=3Y?SzO) zkev#`DNsVj#HB>4T=2UX!GoUO0vf_j_7fWtqY$kXRFoCjbs#uK#k6taI8GFATwX+* z#RhOZ19BpgjbOvAy#d=Zfb6Z7cZ{Rur1Fm8p=oBg3%ZL|Rw` z6+RUi7CsXc_xB>&EE3)}zW~|5@KA@uu-JG63%OYDyV&7v9?-=At&8zMGkJJ&M&ujb z;AVt_dpSqE?mIdqTK{2h{l~zL)>h64FiH0LD6VETL5+L5^|Gve8{*E%=)j1FPhd?+ zqyNx+NaEi}2a2X>(XBrtJGk>NPE5D4?M1Zh$%vbemjgoO?J4pL_V2+d-J={kMhAEq zeSAXc+wZ^o;p*4r^>($s`kC&sSIyO%_3GU-Hn_#>>&@a}vt*3u5_|JdvFbgBQ_C$} zhOuod9g{;R+z{`)zddp#Pv^(GhlkF8`~0f4 zB;WI;f$33MX`tHc|EP(yUu^D&I+0&Ci=i6V0`tpy@wB;Ftsg7GtiuW|d_og3KCaoQ z&V89o4EXI3zPaQKY%giBp_ofq7x{B93E+p40Innf{troj=Hs&fk^neA3ven4(0sZq zKyxw;(45KxG$j#G5&&}V$He|8RG_?vZ;_;yGi?XY_6oAuXG`LSOT*eDa)4Q%YlF9}9}*9HSUphE)Qt@ z>96Mh6_0OD68Yor#xKa^PZ9a!UjvN)QlU`!LgfpUFI2uz`9kFjl`mAjQ29dT3zaWa b{=ZJ;vHo$fzL)>jpTGPKh0eMd6hHs~enNY` literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Resources/Animations/anim_savemedia.tgs b/submodules/TelegramUI/Resources/Animations/anim_savemedia.tgs new file mode 100644 index 0000000000000000000000000000000000000000..83f2104743193a03479a5c95c2ac25e9c556c20c GIT binary patch literal 3982 zcmV;94{`7xiwFoN*Ee7Q17U7yZC`U?c4cj4WNBe8YIARH0PS2&a~ru4{VOh>hf{-Z zz_;DnWG_xCl}&PS>A=|XTCpstDDGEn`M>9NH!uSXhoofDo6Xwg5+z_TK%@Ji(XRpe z*VT`!=PTETUah*-o9*hkAG+1m-Re1h-Qt&b-Rh^+bEkB-dYgaWyvIA=-2QZPeY<&$ z*IvJV&Fwe$_m{i-)$^AZ-RgSt%jK5-{&jqOb%WokTkU>XJvX_Rzy9;Zi)UZGc!6H- zxBT=SUix))OCKA4?VY^*GoGps-DQZ2p>~1Ul?5@TFO>OEnEOZ;{Zj~?nYV+uR@w@3+`BIO+q<?>Dq0XzFO#4Pu&ktD7JVa6M zI0P%`*L&%WK>{&0r|Z(omH8TW4=aM4?ESl?mq?E}ynZ10rC)3w)qh_0-{FZliZ@T-sFSO1>ytCa7xs`2Y> zufL%8Ge)mDieB?5^zPoTPK22yyqt%YC8W&Zr1oRrSfV3aqN*_>A|+p*V=%KfnDO($ z^qyUY$&&9`0Jg-a?yL9DXvy3!1MtsR*SpIt@J9_BU0ysc9JXyZ{MB}Q`xASl5)bPG z@$izyq8W9=f46!A$r_*1-z_tm3{|VMnChdWZF@4>b~2oeeF_PlOojfe6wRh~fRN7H zznk^12G{EcalzTllL&T%Fsu=4YehQJc@7WZhPKN;AIS}ig=XZi==Bsid>TD@3Z(MJ zsz6NQxRuf;^q91(Dsr{zT}l)bV)?{G&Ad`$13dN$EWZyvjg_cF=c-qR3);0K%{TLP zZqn<)%*J19Z0%$x9lTWb(VKc86=?9FsyZDB4tOO?orRuvjZTT9NsnVN;h{>vHQ{x~ zviV-ckgK%es@R)I9CZl2(JEJ2;(vHz_%#cv>pa)!$Zt7}X9>}+A423d5ksQ_xaHYl zvH`kWduYy4FS_?3=aK=Z*1?sN?(2eV+xzP72?7Ik6pVqtp{Py`w?_MNfkhmQAp}VNHO-P(+W}q)}~piWT$WpmL8e7sZq zLK>1(s3BquMQWl7+sC1j0#YSzP?c2_|8BK^3Z9&w<0Cd?s7To0EiXH%K!&CYFMbazeXv^m7SvK#B@62crYK*Pq()?MkSail5!kXW z`z>n;XJA<@A7&gW*=2cUFsgzJQs(3r-bk15a*O%v9uOs2-4yu4uA0d#!O2-UTO{HD?#v6FdNvIZ2SjPgJ5Wly*}GRB)K z`Mr44%)@t%Hw_1Sn0b&)M2r);#9J0S6S`A13$>ovi4nG|lt1ce*SQHIzrFkkB1g|u zILDc)*EJ17%*2D&!LBl%$%%;v#h&P{P-Ah<5^>TYOsFGH+nZ=h#94AG#wdw6W2kEo z*hS{v#3qI)5od@JgQ91ON6x&kq&1keDDdTa*8H%o7hsRa%*4D&#rtAic+0CO1cU2SvX zFqG}y=P;V}@EFFspXC@CuT=G^PL>db!O(S8rZ7hGU?YdAWo|hPcUe)G#KeY<`E=nh zCf7)&X013$Zb7pfN|R#mOv`j*Z-cD~3|ME1wk9xyh7c+O<064zA~0O8_zPZ0K2H24 zl>AEJFGY-zzf={$2az4z`7oHGF$-`SFN@_vA3XzB<8_uFl%i)e(#{vlZ)b1E%!M> z%E1#6E7m0~L^(OS_9<1I6WjFO>jWsz5Dm`svE!V6*#Bv#wUfkeB7)QNz^*qBi=#5$!$D8cAKU(8Sf zm(<9-lw->np&s`zG7OoEj1WPMimt%}nJ3gW5&H$FkP1Xl5hRI25TWxnr)!7=j~8?c z5r)i!kbx6d86sc^YN6r*t~Orb5f6 zzP|nW0cZ7RBT*+$7Rt70iKMvm9(0#`vDCMHT;Ja5rQX%j6>aJs%l0wdJ9_tF7qzK( zGd!U8`ec7n=CRwn-Q$ksDa@%pFAg8hQ|MEDUL3r>-5kYt&1jwuo%6?$p$x|@^06?E z&RR?nW9Uyf&Um*#cgZe|^Pmgw#|JU7nvD5$lbJ3`sE)__MB}(vqd?bpHOf_!-~6t| zgB@0rZC#t*B|6MSSW{DY7u}3PB~qI4Rd8nMYDX1rSu8G9hhn{R3>??o>`<l%P%ljUHeE$k}mXG$$b_0ZMnsR(Vn8^vM(|M<*DWA8bm z=D|6l>NLTdIul44CZQ&{^AQB=?l)pWa=+; zP99!=5)ZF{jKsOuP~US|1AJA5M~rAHTfi#}d})a@26TxGm?6_N@DGYFCe3cffcX=# zb|V^;+-_n?DxVavSZ(i{TL&yh-CIwR$AZj&q`_9Puq-{J zP7t3)5NnhLn9jpJxsIG5uVA_J4=<-!Rk;YK$gaa_PW&cMw-p&jbU&{udy)%b74TZV z0b>VrK&-v2(*cgzxpLdepi8(EI+~Oeqk;)ALlASH?*PJ;`i_Xb-%rFmd7si`a5ia^ z(OAQc`x-2it**fW^-=PKvrpuPvHY5}2^mr@vALnG^^))#v7%{}ayHsb4ISr=={`}N z!g1}TQ9XKNNZ@57u(pB4z#^#Vt4d>NKPt^eq`;IBLx!ASF_wHUl|B0m`Z5zt=~G@- za5@QFA+X{2ieV{xNkFuS1>pPU2zsqeo$A=oVSAqXfHB_)& zQFbobeA=-l1OWPxl6+i&Xk(!LpAOu(toU-MBmX05XgIzuxN;t6d1(af;%PCvyBsfk z*;zi5onYc{8lSoU-u-cdqa;HhBmP?u0^ZO^ZAClw0aM{! zm(4|IEB>^(_J^y?HAy>J3q*{s%NOf#*;m#IbSt&87JYdF5U%W%Yl_7XLU=QoX;z%I z-s_IWGiR|}nk}>7Y_rTlI 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() { diff --git a/submodules/TelegramUI/Sources/ChatRecordingPreviewInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatRecordingPreviewInputPanelNode.swift index b00eec3f7c..87a1007e4f 100644 --- a/submodules/TelegramUI/Sources/ChatRecordingPreviewInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatRecordingPreviewInputPanelNode.swift @@ -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 + } + } + } +} diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index b764d05659..00dfc33dcb 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -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 diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerController.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerController.swift index 55908db048..c0050e3893 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerController.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerController.swift @@ -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)) } diff --git a/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift b/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift index 6caa842a03..ff2256c9d4 100644 --- a/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift +++ b/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift @@ -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 + } + } + } +} diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 44d6c182a4..623eff3cfe 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -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 { diff --git a/submodules/UndoUI/Sources/UndoOverlayController.swift b/submodules/UndoUI/Sources/UndoOverlayController.swift index ab34091394..bf84710a72 100644 --- a/submodules/UndoUI/Sources/UndoOverlayController.swift +++ b/submodules/UndoUI/Sources/UndoOverlayController.swift @@ -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 { diff --git a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift index 3c9459503a..453956d61c 100644 --- a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift +++ b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift @@ -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