diff --git a/Telegram/Telegram-iOS/Resources/ClearDownloadList.tgs b/Telegram/Telegram-iOS/Resources/ClearDownloadList.tgs new file mode 100644 index 0000000000..28b75e25d6 Binary files /dev/null and b/Telegram/Telegram-iOS/Resources/ClearDownloadList.tgs differ diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 216168d964..fa0fcff809 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -7328,3 +7328,29 @@ Sorry for the inconvenience."; "Attachment.DeselectedItems_0" = "%@ items deselected"; "PrivacyPhoneNumberSettings.CustomPublicLink" = "Users who have your number saved in their contacts will also see it on Telegram.\n\nThis public link opens a chat with you:\n[https://t.me/%@]()"; + +"DownloadList.DownloadingHeader" = "Downloading"; +"DownloadList.DownloadedHeader" = "Recently Downloaded"; +"DownloadList.PauseAll" = "Pause All"; +"DownloadList.ResumeAll" = "Resume All"; +"DownloadList.Clear" = "Clear"; +"DownloadList.OptionManageDeviceStorage" = "Manage Device Storage"; +"DownloadList.ClearDownloadList" = "Clear Download List"; +"DownloadList.DeleteFromCache" = "Delete from Cache"; +"DownloadList.RaisePriority" = "Raise Priority"; +"DownloadList.CancelDownloading" = "Cancel Downloading"; + +"DownloadList.RemoveFileAlertTitle_1" = "Remove Document?"; +"DownloadList.RemoveFileAlertTitle_any" = "Remove %@ Documents?"; +"DownloadList.RemoveFileAlertText_1" = "Are you sure you want to remove this\ndocument from Downloads?\nIt will be deleted from your disk, but\nwill remain accessible in the cloud."; +"DownloadList.RemoveFileAlertText_any" = "Are you sure you want to remove these\n%@ documents from Downloads?\nThey will be deleted from your disk, but\nwill remain accessible in the cloud."; +"DownloadList.RemoveFileAlertRemove" = "Remove"; + +"DownloadList.ClearAlertTitle" = "Downloaded Files"; +"DownloadList.ClearAlertText" = "Telegram allows to store all received and sent\ndocuments in the cloud and save storage\nspace on your device."; + +"ChatList.Search.FilterDownloads" = "Downloads"; + +"LiveStream.NoViewers" = "No viewers"; +"LiveStream.ViewerCount_1" = "1 viewer"; +"LiveStream.ViewerCount_any" = "%@ viewers"; diff --git a/submodules/ChatListSearchItemHeader/Sources/ChatListSearchItemHeader.swift b/submodules/ChatListSearchItemHeader/Sources/ChatListSearchItemHeader.swift index b9dd2785a4..5ffb291a72 100644 --- a/submodules/ChatListSearchItemHeader/Sources/ChatListSearchItemHeader.swift +++ b/submodules/ChatListSearchItemHeader/Sources/ChatListSearchItemHeader.swift @@ -77,11 +77,9 @@ public enum ChatListSearchItemHeaderType { case .subscribers: return strings.Channel_ChannelSubscribersHeader case .downloading: - //TODO:localize - return "Downloading" + return strings.DownloadList_DownloadingHeader case .recentDownloads: - //TODO:localize - return "Recently Downloaded" + return strings.DownloadList_DownloadedHeader } } @@ -261,6 +259,10 @@ public final class ChatListSearchItemHeaderNode: ListViewItemHeaderNode { self.sectionHeaderNode.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset) } + override public func animateAdded(duration: Double) { + self.layer.animateAlpha(from: 0.0, to: self.alpha, duration: 0.2) + } + override public func animateRemoved(duration: Double) { self.alpha = 0.0 self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: true) diff --git a/submodules/ChatListUI/BUILD b/submodules/ChatListUI/BUILD index 52115e69e3..fae5b54a5f 100644 --- a/submodules/ChatListUI/BUILD +++ b/submodules/ChatListUI/BUILD @@ -66,6 +66,7 @@ swift_library( "//submodules/ComponentFlow:ComponentFlow", "//submodules/Components/LottieAnimationComponent:LottieAnimationComponent", "//submodules/Components/ProgressIndicatorComponent:ProgressIndicatorComponent", + "//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode", ], visibility = [ "//visibility:public", diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 22f9f2d944..d8499ba996 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -155,6 +155,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController private let tabContainerNode: ChatListFilterTabContainerNode private var tabContainerData: ([ChatListFilterTabEntry], Bool)? + private var hasDownloads: Bool = false private var activeDownloadsDisposable: Disposable? private var clearUnseenDownloadsTimer: SwiftSignalKit.Timer? @@ -476,8 +477,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController self.navigationBar?.setContentNode(self.searchContentNode, animated: false) enum State: Equatable { - case empty - case downloading(Double) + case empty(hasDownloads: Bool) + case downloading(progress: Double) case hasUnseen } @@ -626,14 +627,14 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } else { totalProgress = totalProgressInBytes / totalBytes } - return .downloading(totalProgress) + return .downloading(progress: totalProgress) } else { for item in recentDownloadItems { if !item.isSeen { return .hasUnseen } } - return .empty + return .empty(hasDownloads: !recentDownloadItems.isEmpty) } } |> mapToSignal { value -> Signal in @@ -642,11 +643,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController |> distinctUntilChanged |> deliverOnMainQueue) - /*if !"".isEmpty { - stateSignal = Signal.single(.downloading) - |> then(Signal.single(.hasUnseen) |> delay(3.0, queue: .mainQueue())) - }*/ - self.activeDownloadsDisposable = stateSignal.start(next: { [weak self] state in guard let strongSelf = self else { return @@ -655,6 +651,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController let progressValue: Double? switch state { case let .downloading(progress): + strongSelf.hasDownloads = true + animation = LottieAnimationComponent.Animation( name: "anim_search_downloading", colors: [ @@ -669,6 +667,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController strongSelf.clearUnseenDownloadsTimer?.invalidate() strongSelf.clearUnseenDownloadsTimer = nil case .hasUnseen: + strongSelf.hasDownloads = true + animation = LottieAnimationComponent.Animation( name: "anim_search_downloaded", colors: [ @@ -701,7 +701,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController }, queue: .mainQueue()) strongSelf.clearUnseenDownloadsTimer?.start() } - case .empty: + case let .empty(hasDownloadsValue): + strongSelf.hasDownloads = hasDownloadsValue + animation = nil progressValue = nil @@ -2084,7 +2086,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } if let searchContentNode = strongSelf.searchContentNode { - if let filterContainerNodeAndActivate = strongSelf.chatListDisplayNode.activateSearch(placeholderNode: searchContentNode.placeholderNode, displaySearchFilters: displaySearchFilters, initialFilter: filter, navigationController: strongSelf.navigationController as? NavigationController) { + if let filterContainerNodeAndActivate = strongSelf.chatListDisplayNode.activateSearch(placeholderNode: searchContentNode.placeholderNode, displaySearchFilters: displaySearchFilters, hasDownloads: strongSelf.hasDownloads, initialFilter: filter, navigationController: strongSelf.navigationController as? NavigationController) { let (filterContainerNode, activate) = filterContainerNodeAndActivate if displaySearchFilters { strongSelf.navigationBar?.setSecondaryContentNode(filterContainerNode, animated: false) @@ -2102,6 +2104,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController if !tabsIsEmpty { Queue.mainQueue().after(0.01) { filterContainerNode.layer.animatePosition(from: CGPoint(x: 0.0, y: 38.0), to: CGPoint(), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + filterContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) strongSelf.tabContainerNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -64.0), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true) } diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index dd9cb8f4d5..88705ad474 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -1193,14 +1193,14 @@ final class ChatListControllerNode: ASDisplayNode { } } - func activateSearch(placeholderNode: SearchBarPlaceholderNode, displaySearchFilters: Bool, initialFilter: ChatListSearchFilter, navigationController: NavigationController?) -> (ASDisplayNode, (Bool) -> Void)? { + func activateSearch(placeholderNode: SearchBarPlaceholderNode, displaySearchFilters: Bool, hasDownloads: Bool, initialFilter: ChatListSearchFilter, navigationController: NavigationController?) -> (ASDisplayNode, (Bool) -> Void)? { guard let (containerLayout, _, _, cleanNavigationBarHeight) = self.containerLayout, let navigationBar = self.navigationBar, self.searchDisplayController == nil else { return nil } let filter: ChatListNodePeersFilter = [] - let contentNode = ChatListSearchContainerNode(context: self.context, filter: filter, groupId: self.groupId, displaySearchFilters: displaySearchFilters, initialFilter: initialFilter, openPeer: { [weak self] peer, _, dismissSearch in + let contentNode = ChatListSearchContainerNode(context: self.context, filter: filter, groupId: self.groupId, displaySearchFilters: displaySearchFilters, hasDownloads: hasDownloads, initialFilter: initialFilter, openPeer: { [weak self] peer, _, dismissSearch in self?.requestOpenPeerFromSearch?(peer, dismissSearch) }, openDisabledPeer: { _ in }, openRecentPeerOptions: { [weak self] peer in diff --git a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift index 9247c46a5a..a9bc667784 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift @@ -30,6 +30,7 @@ import ShareController import UndoUI import TextFormat import Postbox +import TelegramAnimatedStickerNode private enum ChatListTokenId: Int32 { case archive @@ -83,6 +84,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo private let peersFilter: ChatListNodePeersFilter private let groupId: EngineChatList.Group private let displaySearchFilters: Bool + private let hasDownloads: Bool private var interaction: ChatListSearchInteraction? private let openMessage: (EnginePeer, EngineMessage.Id, Bool) -> Void private let navigationController: NavigationController? @@ -127,11 +129,12 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo private var validLayout: (ContainerViewLayout, CGFloat)? - public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, filter: ChatListNodePeersFilter, groupId: EngineChatList.Group, displaySearchFilters: Bool, initialFilter: ChatListSearchFilter = .chats, openPeer originalOpenPeer: @escaping (EnginePeer, EnginePeer?, Bool) -> Void, openDisabledPeer: @escaping (EnginePeer) -> Void, openRecentPeerOptions: @escaping (EnginePeer) -> Void, openMessage originalOpenMessage: @escaping (EnginePeer, EngineMessage.Id, Bool) -> Void, addContact: ((String) -> Void)?, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)?, present: @escaping (ViewController, Any?) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, navigationController: NavigationController?) { + public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, filter: ChatListNodePeersFilter, groupId: EngineChatList.Group, displaySearchFilters: Bool, hasDownloads: Bool, initialFilter: ChatListSearchFilter = .chats, openPeer originalOpenPeer: @escaping (EnginePeer, EnginePeer?, Bool) -> Void, openDisabledPeer: @escaping (EnginePeer) -> Void, openRecentPeerOptions: @escaping (EnginePeer) -> Void, openMessage originalOpenMessage: @escaping (EnginePeer, EngineMessage.Id, Bool) -> Void, addContact: ((String) -> Void)?, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)?, present: @escaping (ViewController, Any?) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, navigationController: NavigationController?) { self.context = context self.peersFilter = filter self.groupId = groupId self.displaySearchFilters = displaySearchFilters + self.hasDownloads = hasDownloads self.navigationController = navigationController self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } @@ -265,7 +268,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo if let suggestedFilters = strongSelf.suggestedFilters, !suggestedFilters.isEmpty { filters = suggestedFilters } else { - filters = [.chats, .media, .downloads, .links, .files, .music, .voice] + filters = defaultAvailableSearchPanes(hasDownloads: strongSelf.hasDownloads).map(\.filter) } strongSelf.filterContainerNode.update(size: CGSize(width: layout.size.width - 40.0, height: 38.0), sideInset: layout.safeInsets.left - 20.0, filters: filters.map { .filter($0) }, selectedFilter: strongSelf.selectedFilter?.id, transitionFraction: strongSelf.transitionFraction, presentationData: strongSelf.presentationData, transition: transition) } @@ -736,7 +739,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo let availablePanes: [ChatListSearchPaneKey] if self.displaySearchFilters { - availablePanes = defaultAvailableSearchPanes + availablePanes = defaultAvailableSearchPanes(hasDownloads: self.hasDownloads) } else { availablePanes = [.chats] } @@ -817,9 +820,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo var items: [ContextMenuItem] = [] if isCachedValue { - //TODO:localize - items.append(.action(ContextMenuActionItem(text: "Delete from Cache", textColor: .primary, icon: { _ in - return nil + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.DownloadList_DeleteFromCache, textColor: .primary, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.primaryColor) }, action: { _, f in guard let strongSelf = self, let downloadResource = downloadResource else { f(.default) @@ -832,9 +834,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo }))) } else { if let downloadResource = downloadResource, !downloadResource.isFirstInList { - //TODO:localize - items.append(.action(ContextMenuActionItem(text: "Raise Priority", textColor: .primary, icon: { _ in - return nil + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.DownloadList_RaisePriority, textColor: .primary, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Raise"), color: theme.contextMenu.primaryColor) }, action: { _, f in guard let strongSelf = self else { f(.default) @@ -849,9 +850,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo }))) } - //TODO:localize - items.append(.action(ContextMenuActionItem(text: "Cancel Downloading", textColor: .primary, icon: { _ in - return nil + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.DownloadList_CancelDownloading, textColor: .primary, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.contextMenu.primaryColor) }, action: { _, f in guard let strongSelf = self, let downloadResource = downloadResource else { f(.default) @@ -1097,20 +1097,13 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo let title: String let text: String - //TODO:localize - if messageIds.count == 1 { - title = "Remove Document?" - text = "Are you sure you want to remove this\ndocument from Downloads?\nIt will be deleted from your disk, but\nwill remain accessible in the cloud."; - } else { - title = "Remove \(messages.count) Documents?" - text = "Do you want to remove these\n\(messages.count) documents from Downloads?\nThey will be deleted from your disk,\nbut will remain accessible\nin the cloud." - } + title = strongSelf.presentationData.strings.DownloadList_RemoveFileAlertTitle(Int32(messages.count)) + text = strongSelf.presentationData.strings.DownloadList_RemoveFileAlertText(Int32(messages.count)) strongSelf.present?(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: title, text: text, actions: [ TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { }), - //TODO:localize - TextAlertAction(type: .defaultAction, title: "Remove", action: { + TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.DownloadList_RemoveFileAlertRemove, action: { guard let strongSelf = self else { return } @@ -1449,3 +1442,117 @@ private final class ContextControllerContentSourceImpl: ContextControllerContent self.controller.didAppearInContextPreview() } } + +final class ActionSheetAnimationAndTextItem: ActionSheetItem { + public let title: String + public let text: String + + public init(title: String, text: String) { + self.title = title + self.text = text + } + + public func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode { + let node = ActionSheetAnimationAndTextItemNode(theme: theme) + node.setItem(self) + return node + } + + public func updateNode(_ node: ActionSheetItemNode) { + guard let node = node as? ActionSheetAnimationAndTextItemNode else { + assertionFailure() + return + } + + node.setItem(self) + node.requestLayoutUpdate() + } +} + +final class ActionSheetAnimationAndTextItemNode: ActionSheetItemNode { + private let defaultFont: UIFont + + private let theme: ActionSheetControllerTheme + + private var item: ActionSheetAnimationAndTextItem? + + private let animationNode: AnimatedStickerNode + private let textLabel: ImmediateTextNode + private let titleLabel: ImmediateTextNode + + private let accessibilityArea: AccessibilityAreaNode + + override public init(theme: ActionSheetControllerTheme) { + self.theme = theme + self.defaultFont = Font.regular(floor(theme.baseFontSize * 13.0 / 17.0)) + + self.animationNode = AnimatedStickerNode() + self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: "ClearDownloadList"), width: 256, height: 256, playbackMode: .loop, mode: .direct(cachePathPrefix: nil)) + self.animationNode.visibility = true + + self.titleLabel = ImmediateTextNode() + self.titleLabel.isUserInteractionEnabled = false + self.titleLabel.maximumNumberOfLines = 0 + self.titleLabel.displaysAsynchronously = false + self.titleLabel.truncationType = .end + self.titleLabel.isAccessibilityElement = false + + self.textLabel = ImmediateTextNode() + self.textLabel.isUserInteractionEnabled = false + self.textLabel.maximumNumberOfLines = 0 + self.textLabel.displaysAsynchronously = false + self.textLabel.truncationType = .end + self.textLabel.isAccessibilityElement = false + + self.accessibilityArea = AccessibilityAreaNode() + self.accessibilityArea.accessibilityTraits = .staticText + + super.init(theme: theme) + + self.addSubnode(self.animationNode) + + self.titleLabel.isUserInteractionEnabled = false + self.textLabel.isUserInteractionEnabled = false + + self.addSubnode(self.titleLabel) + self.addSubnode(self.textLabel) + + self.addSubnode(self.accessibilityArea) + } + + func setItem(_ item: ActionSheetAnimationAndTextItem) { + self.item = item + + let defaultTitleFont = Font.semibold(floor(theme.baseFontSize * 17.0 / 17.0)) + let defaultFont = Font.regular(floor(theme.baseFontSize * 16.0 / 17.0)) + + self.titleLabel.attributedText = NSAttributedString(string: item.title, font: defaultTitleFont, textColor: self.theme.primaryTextColor, paragraphAlignment: .center) + self.textLabel.attributedText = NSAttributedString(string: item.text, font: defaultFont, textColor: self.theme.secondaryTextColor, paragraphAlignment: .center) + self.accessibilityArea.accessibilityLabel = item.title + } + + public override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { + let topInset: CGFloat = 20.0 + let textSpacing: CGFloat = 10.0 + let bottomInset: CGFloat = 16.0 + let imageInset: CGFloat = 6.0 + + let titleSize = self.titleLabel.updateLayout(CGSize(width: max(1.0, constrainedSize.width - 20.0), height: constrainedSize.height)) + let textSize = self.textLabel.updateLayout(CGSize(width: max(1.0, constrainedSize.width - 20.0), height: constrainedSize.height)) + var size = CGSize(width: constrainedSize.width, height: max(57.0, titleSize.height + textSpacing + textSize.height + bottomInset)) + + let imageSize = CGSize(width: 140.0, height: 140.0) + size.height += topInset + 160.0 + imageInset + + self.animationNode.frame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: topInset), size: imageSize) + self.animationNode.updateLayout(size: imageSize) + + self.titleLabel.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: size.height - titleSize.height - textSize.height - textSpacing - bottomInset), size: titleSize) + self.textLabel.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: size.height - textSize.height - bottomInset), size: textSize) + + self.accessibilityArea.frame = CGRect(origin: CGPoint(), size: size) + + self.updateInternalLayout(size, constrainedSize: constrainedSize) + return size + } +} diff --git a/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift index 5a9e169af5..3e77c92363 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift @@ -85,8 +85,7 @@ private final class ItemNode: ASDisplayNode { title = presentationData.strings.ChatList_Search_FilterMedia icon = nil case .downloads: - //TODO:localize - title = "Downloads" + title = presentationData.strings.ChatList_Search_FilterDownloads icon = nil case .links: title = presentationData.strings.ChatList_Search_FilterLinks diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index a4626c788d..4c569e6e53 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -552,19 +552,17 @@ public enum ChatListSearchEntry: Comparable, Identifiable { let header: ChatListSearchItemHeader switch orderingKey { case .downloading: - //TODO:localize if allPaused { - header = ChatListSearchItemHeader(type: .downloading, theme: presentationData.theme, strings: presentationData.strings, actionTitle: "Resume All", action: { + header = ChatListSearchItemHeader(type: .downloading, theme: presentationData.theme, strings: presentationData.strings, actionTitle: presentationData.strings.DownloadList_ResumeAll, action: { toggleAllPaused() }) } else { - header = ChatListSearchItemHeader(type: .downloading, theme: presentationData.theme, strings: presentationData.strings, actionTitle: "Pause All", action: { + header = ChatListSearchItemHeader(type: .downloading, theme: presentationData.theme, strings: presentationData.strings, actionTitle: presentationData.strings.DownloadList_PauseAll, action: { toggleAllPaused() }) } case .downloaded: - //TODO:localize - header = ChatListSearchItemHeader(type: .recentDownloads, theme: presentationData.theme, strings: presentationData.strings, actionTitle: "Clear", action: { + header = ChatListSearchItemHeader(type: .recentDownloads, theme: presentationData.theme, strings: presentationData.strings, actionTitle: presentationData.strings.DownloadList_Clear, action: { openClearRecentlyDownloaded() }) case .index: @@ -949,6 +947,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { |> map { items -> (inProgressItems: [DownloadItem], doneItems: [RenderedRecentDownloadItem]) in return (items.compactMap { $0 }, recentDownloadItems) } + |> delay(0.1, queue: .mainQueue()) } } else { downloadItems = .single(([], [])) @@ -1076,7 +1075,6 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { } } |> map { notificationSettings, unreadCounts, peers -> (peers: [EngineRenderedPeer], unread: [EnginePeer.Id: (Int32, Bool)]) in - var unread: [EnginePeer.Id: (Int32, Bool)] = [:] for peer in peers { var isMuted: Bool = false @@ -1626,7 +1624,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { let animated = (previousSelectedMessageIds == nil) != (strongSelf.selectedMessages == nil) let firstTime = previousEntries == nil - let transition = chatListSearchContainerPreparedTransition(from: previousEntries ?? [], to: newEntries, displayingResults: entriesAndFlags?.0 != nil, isEmpty: !isSearching && (entriesAndFlags?.0.isEmpty ?? false), isLoading: isSearching, animated: animated, context: context, presentationData: strongSelf.presentationData, enableHeaders: true, filter: peersFilter, key: strongSelf.key, tagMask: tagMask, interaction: chatListInteraction, listInteraction: listInteraction, peerContextAction: { message, node, rect, gesture in + var transition = chatListSearchContainerPreparedTransition(from: previousEntries ?? [], to: newEntries, displayingResults: entriesAndFlags?.0 != nil, isEmpty: !isSearching && (entriesAndFlags?.0.isEmpty ?? false), isLoading: isSearching, animated: animated, context: context, presentationData: strongSelf.presentationData, enableHeaders: true, filter: peersFilter, key: strongSelf.key, tagMask: tagMask, interaction: chatListInteraction, listInteraction: listInteraction, peerContextAction: { message, node, rect, gesture in interaction.peerContextAction?(message, node, rect, gesture) }, toggleExpandLocalResults: { [weak self] in guard let strongSelf = self else { @@ -1658,10 +1656,9 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { var items: [ActionSheetItem] = [] //TODO:localize - items.append(ActionSheetTextItem(title: "Telegram allows to store all received and sent\ndocuments in the cloud and save storage\nspace on your device.")) + items.append(ActionSheetAnimationAndTextItem(title: strongSelf.presentationData.strings.DownloadList_ClearAlertTitle, text: strongSelf.presentationData.strings.DownloadList_ClearAlertText)) - //TODO:localize - items.append(ActionSheetButtonItem(title: "Manage Device Storage", color: .accent, action: { [weak actionSheet] in + items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.DownloadList_OptionManageDeviceStorage, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() guard let strongSelf = self else { return @@ -1670,8 +1667,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { strongSelf.context.sharedContext.openStorageUsage(context: strongSelf.context) })) - //TODO:localize - items.append(ActionSheetButtonItem(title: "Clear Downloads List", color: .destructive, action: { [weak actionSheet] in + items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.DownloadList_ClearDownloadList, color: .destructive, action: { [weak actionSheet] in actionSheet?.dismissAnimated() guard let strongSelf = self else { return @@ -1712,9 +1708,9 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { }) }) strongSelf.currentEntries = newEntries - /*if key == .downloads, !firstTime { + if key == .downloads, !firstTime, !"".isEmpty { transition.animated = true - }*/ + } strongSelf.enqueueTransition(transition, firstTime: firstTime) var messages: [EngineMessage] = [] diff --git a/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift index 440cfefc55..a201c7d6e2 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift @@ -55,7 +55,36 @@ public enum ChatListSearchPaneKey { case voice } -let defaultAvailableSearchPanes: [ChatListSearchPaneKey] = [.chats, .media, .downloads, .links, .files, .music, .voice] +extension ChatListSearchPaneKey { + var filter: ChatListSearchFilter { + switch self { + case .chats: + return .chats + case .media: + return .media + case .downloads: + return .downloads + case .links: + return .links + case .files: + return .files + case .music: + return .music + case .voice: + return .voice + } + } +} + +func defaultAvailableSearchPanes(hasDownloads: Bool) -> [ChatListSearchPaneKey] { + var result: [ChatListSearchPaneKey] = [.chats, .media, .downloads, .links, .files, .music, .voice] + + if !hasDownloads { + result.removeAll(where: { $0 == .downloads }) + } + + return result +} struct ChatListSearchPaneSpecifier: Equatable { var key: ChatListSearchPaneKey diff --git a/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift b/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift index 1a272feb94..0dfda3ffd7 100644 --- a/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift +++ b/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift @@ -522,7 +522,10 @@ public final class ListMessageFileItemNode: ListMessageNode { descriptionText = NSAttributedString(string: descriptionString, font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor) iconImage = .roundVideo(file) } else if !isAudio { - let fileName: String = file.fileName ?? "File" + var fileName: String = file.fileName ?? "File" + if file.isVideo { + fileName = item.presentationData.strings.Message_Video + } titleText = NSAttributedString(string: fileName, font: titleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor) var fileExtension: String? @@ -569,8 +572,7 @@ public final class ListMessageFileItemNode: ListMessageNode { } else if let image = media as? TelegramMediaImage { selectedMedia = image - //TODO:localize - let fileName: String = "Photo" + let fileName: String = item.presentationData.strings.Message_Photo titleText = NSAttributedString(string: fileName, font: titleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor) if let representation = smallestImageRepresentation(image.representations) { @@ -656,7 +658,7 @@ public final class ListMessageFileItemNode: ListMessageNode { if let file = selectedMedia as? TelegramMediaFile { updatedStatusSignal = messageFileMediaResourceStatus(context: item.context, file: file, message: message, isRecentActions: false, isSharedMedia: true, isGlobalSearch: item.isGlobalSearchResult, isDownloadList: item.isDownloadList) |> mapToSignal { value -> Signal in - if case .Fetching = value.fetchStatus { + if case .Fetching = value.fetchStatus, !item.isDownloadList { return .single(value) |> delay(0.1, queue: Queue.concurrentDefaultQueue()) } else { return .single(value) @@ -686,7 +688,7 @@ public final class ListMessageFileItemNode: ListMessageNode { } else if let image = selectedMedia as? TelegramMediaImage { updatedStatusSignal = messageImageMediaResourceStatus(context: item.context, image: image, message: message, isRecentActions: false, isSharedMedia: true, isGlobalSearch: item.isGlobalSearchResult || item.isDownloadList) |> mapToSignal { value -> Signal in - if case .Fetching = value.fetchStatus { + if case .Fetching = value.fetchStatus, !item.isDownloadList { return .single(value) |> delay(0.1, queue: Queue.concurrentDefaultQueue()) } else { return .single(value) @@ -1301,7 +1303,12 @@ public final class ListMessageFileItemNode: ListMessageNode { transition.updateFrame(node: self.descriptionNode, frame: descriptionFrame) } - let alphaTransition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .easeInOut) + let alphaTransition: ContainedViewLayoutTransition + if item.isDownloadList { + alphaTransition = .immediate + } else { + alphaTransition = .animated(duration: 0.3, curve: .easeInOut) + } if downloadingString != nil { alphaTransition.updateAlpha(node: self.descriptionProgressNode, alpha: 1.0) alphaTransition.updateAlpha(node: self.descriptionNode, alpha: 0.0) diff --git a/submodules/SearchBarNode/Sources/SearchBarNode.swift b/submodules/SearchBarNode/Sources/SearchBarNode.swift index 96b50ab5b8..94ad0b03ae 100644 --- a/submodules/SearchBarNode/Sources/SearchBarNode.swift +++ b/submodules/SearchBarNode/Sources/SearchBarNode.swift @@ -1052,14 +1052,28 @@ public class SearchBarNode: ASDisplayNode, UITextFieldDelegate { }) self.textBackgroundNode.isHidden = true + + if let accessoryComponentView = node.accessoryComponentView { + let tempContainer = UIView() + + let accessorySize = accessoryComponentView.bounds.size + tempContainer.frame = CGRect(origin: CGPoint(x: self.textBackgroundNode.frame.maxX - accessorySize.width - 4.0, y: floor((self.textBackgroundNode.frame.minY + self.textBackgroundNode.frame.height - accessorySize.height) / 2.0)), size: accessorySize) + + let targetTempContainerFrame = CGRect(origin: CGPoint(x: targetTextBackgroundFrame.maxX - accessorySize.width - 4.0, y: floor((targetTextBackgroundFrame.minY + 8.0 + targetTextBackgroundFrame.height - accessorySize.height) / 2.0)), size: accessorySize) + + tempContainer.layer.animateFrame(from: tempContainer.frame, to: targetTempContainerFrame, duration: duration, timingFunction: timingFunction, removeOnCompletion: false) + + accessoryComponentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + tempContainer.addSubview(accessoryComponentView) + self.view.addSubview(tempContainer) + } self.textBackgroundNode.layer.animateFrame(from: self.textBackgroundNode.frame, to: targetTextBackgroundFrame, duration: duration, timingFunction: timingFunction, removeOnCompletion: false, completion: { [weak node] _ in textBackgroundCompleted = true intermediateCompletion() - if let node = node, let accessoryComponentView = node.accessoryComponentView { - accessoryComponentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - accessoryComponentView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.35, timingFunction: kCAMediaTimingFunctionSpring) + if let node = node, let accessoryComponentContainer = node.accessoryComponentContainer, let accessoryComponentView = node.accessoryComponentView { + accessoryComponentContainer.addSubview(accessoryComponentView) } }) diff --git a/submodules/SearchBarNode/Sources/SearchBarPlaceholderNode.swift b/submodules/SearchBarNode/Sources/SearchBarPlaceholderNode.swift index d4b6c22355..154cbadcca 100644 --- a/submodules/SearchBarNode/Sources/SearchBarPlaceholderNode.swift +++ b/submodules/SearchBarNode/Sources/SearchBarPlaceholderNode.swift @@ -36,7 +36,8 @@ public class SearchBarPlaceholderNode: ASDisplayNode { public private(set) var placeholderString: NSAttributedString? - var accessoryComponentView: ComponentHostView? + private(set) var accessoryComponentContainer: UIView? + private(set) var accessoryComponentView: ComponentHostView? convenience public override init() { self.init(fieldStyle: .legacy) @@ -110,13 +111,22 @@ public class SearchBarPlaceholderNode: ASDisplayNode { public func setAccessoryComponent(component: AnyComponent?) { if let component = component { + let accessoryComponentContainer: UIView + if let current = self.accessoryComponentContainer { + accessoryComponentContainer = current + } else { + accessoryComponentContainer = UIView() + self.accessoryComponentContainer = accessoryComponentContainer + self.view.addSubview(accessoryComponentContainer) + } + let accessoryComponentView: ComponentHostView if let current = self.accessoryComponentView { accessoryComponentView = current } else { accessoryComponentView = ComponentHostView() self.accessoryComponentView = accessoryComponentView - self.view.addSubview(accessoryComponentView) + accessoryComponentContainer.addSubview(accessoryComponentView) } let accessorySize = accessoryComponentView.update( transition: .immediate, @@ -124,7 +134,8 @@ public class SearchBarPlaceholderNode: ASDisplayNode { environment: {}, containerSize: CGSize(width: 32.0, height: 32.0) ) - accessoryComponentView.frame = CGRect(origin: CGPoint(x: self.bounds.width - accessorySize.width - 4.0, y: floor((self.bounds.height - accessorySize.height) / 2.0)), size: accessorySize) + accessoryComponentContainer.frame = CGRect(origin: CGPoint(x: self.bounds.width - accessorySize.width - 4.0, y: floor((self.bounds.height - accessorySize.height) / 2.0)), size: accessorySize) + accessoryComponentView.frame = CGRect(origin: CGPoint(), size: accessorySize) } else if let accessoryComponentView = self.accessoryComponentView { self.accessoryComponentView = nil accessoryComponentView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) @@ -220,8 +231,8 @@ public class SearchBarPlaceholderNode: ASDisplayNode { transition.updateAlpha(node: strongSelf.backgroundNode, alpha: outerAlpha) transition.updateFrame(node: strongSelf.backgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: constrainedSize.width, height: height))) - if let accessoryComponentView = strongSelf.accessoryComponentView { - accessoryComponentView.frame = CGRect(origin: CGPoint(x: constrainedSize.width - accessoryComponentView.bounds.width - 4.0, y: floor((constrainedSize.height - accessoryComponentView.bounds.height) / 2.0)), size: accessoryComponentView.bounds.size) + if let accessoryComponentContainer = strongSelf.accessoryComponentContainer { + accessoryComponentContainer.frame = CGRect(origin: CGPoint(x: constrainedSize.width - accessoryComponentContainer.bounds.width - 4.0, y: floor((constrainedSize.height - accessoryComponentContainer.bounds.height) / 2.0)), size: accessoryComponentContainer.bounds.size) } } }) diff --git a/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift b/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift index bcf45903eb..b47b6e199c 100644 --- a/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift +++ b/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift @@ -658,7 +658,6 @@ public final class MediaStreamComponent: CombinedComponent { } ).minSize(CGSize(width: 44.0, height: 44.0)))))*/ - //TODO:localize let navigationBar = navigationBar.update( component: NavigationBarComponent( topInset: environment.statusBarHeight, @@ -670,7 +669,7 @@ public final class MediaStreamComponent: CombinedComponent { }) ), rightItems: navigationRightItems, - centerItem: AnyComponent(Text(text: "Live Stream", font: Font.semibold(17.0), color: .white)) + centerItem: AnyComponent(Text(text: environment.strings.VoiceChatChannel_Title, font: Font.semibold(17.0), color: .white)) ), availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height), transition: context.transition @@ -681,10 +680,10 @@ public final class MediaStreamComponent: CombinedComponent { var infoItem: AnyComponent? if let originInfo = context.state.originInfo { let memberCountString: String - if originInfo.memberCount == 1 { - memberCountString = "1 viewer" + if originInfo.memberCount == 0 { + memberCountString = environment.strings.LiveStream_NoViewers } else { - memberCountString = "\(originInfo.memberCount) viewers" + memberCountString = environment.strings.LiveStream_ViewerCount(Int32(originInfo.memberCount)) } infoItem = AnyComponent(OriginInfoComponent( title: originInfo.title, diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Raise.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Raise.imageset/Contents.json new file mode 100644 index 0000000000..f4752e4481 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Raise.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "promote.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Raise.imageset/promote.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Raise.imageset/promote.pdf new file mode 100644 index 0000000000..f799af499e --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Raise.imageset/promote.pdf @@ -0,0 +1,98 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 5.335022 3.205078 cm +0.000000 0.000000 0.000000 scn +6.194798 17.265186 m +6.454496 17.524885 6.875551 17.524885 7.135249 17.265186 c +13.135250 11.265186 l +13.394949 11.005488 13.394949 10.584434 13.135250 10.324736 c +12.875551 10.065037 12.454496 10.065037 12.194798 10.324736 c +6.665023 15.854509 l +1.135250 10.324736 l +0.875551 10.065037 0.454496 10.065037 0.194797 10.324736 c +-0.064902 10.584434 -0.064902 11.005488 0.194797 11.265186 c +6.194798 17.265186 l +h +7.135226 12.265148 m +6.875528 12.524847 6.454473 12.524847 6.194775 12.265148 c +0.194774 6.265148 l +-0.064925 6.005449 -0.064925 5.584394 0.194774 5.324696 c +0.454473 5.064997 0.875528 5.064997 1.135227 5.324696 c +6.665000 10.854470 l +12.194775 5.324696 l +12.454473 5.064997 12.875528 5.064997 13.135227 5.324696 c +13.394926 5.584394 13.394926 6.005449 13.135227 6.265148 c +7.135226 12.265148 l +h +6.194775 7.265148 m +0.194774 1.265148 l +-0.064925 1.005449 -0.064925 0.584394 0.194774 0.324696 c +0.454473 0.064997 0.875528 0.064997 1.135227 0.324696 c +6.665000 5.854470 l +12.194775 0.324696 l +12.454473 0.064997 12.875528 0.064997 13.135227 0.324696 c +13.394926 0.584394 13.394926 1.005449 13.135227 1.265148 c +7.135226 7.265148 l +6.875528 7.524847 6.454473 7.524847 6.194775 7.265148 c +h +f* +n +Q + +endstream +endobj + +3 0 obj + 1325 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000001415 00000 n +0000001438 00000 n +0000001611 00000 n +0000001685 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1744 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift index 25bef393a7..26941aefaa 100644 --- a/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift @@ -577,6 +577,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { filter: self.filter, groupId: EngineChatList.Group(.root), displaySearchFilters: false, + hasDownloads: false, openPeer: { [weak self] peer, chatPeer, _ in guard let strongSelf = self else { return