Various Improvements

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

View File

@ -154,6 +154,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
private let tabContainerNode: ChatListFilterTabContainerNode
private 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))

View File

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

View File

@ -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
}
}
}
}

View File

@ -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

View File

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

View File

@ -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()
}
}
})
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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))
}
}
}

View File

@ -928,7 +928,7 @@ public class SearchBarNode: ASDisplayNode, UITextFieldDelegate {
let textBackgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minX + padding, y: verticalOffset + textBackgroundHeight), size: CGSize(width: contentFrame.width - padding * 2.0 - (self.hasCancelButton ? cancelButtonSize.width + 11.0 : 0.0), height: textBackgroundHeight))
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

View File

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

View File

@ -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()
}
}
})
}

View File

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

View File

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

View File

@ -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
}
}
}
}

View File

@ -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) {

View File

@ -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 = "."

View File

@ -38,7 +38,7 @@ private func titleAndColorForAction(_ action: SubscriberAction, theme: Presentat
}
}
private func actionForPeer(peer: Peer, interfaceState: ChatPresentationInterfaceState, isMuted: Bool) -> SubscriberAction? {
private func actionForPeer(peer: Peer, interfaceState: ChatPresentationInterfaceState, isJoining: Bool, isMuted: Bool) -> SubscriberAction? {
if case .pinnedMessages = interfaceState.subject {
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() {

View File

@ -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
}
}
}
}

View File

@ -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

View File

@ -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))
}

View File

@ -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
}
}
}
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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