diff --git a/submodules/TelegramUI/Components/EmptyStateIndicatorComponent/BUILD b/submodules/TelegramUI/Components/EmptyStateIndicatorComponent/BUILD new file mode 100644 index 0000000000..dc8ed1d04c --- /dev/null +++ b/submodules/TelegramUI/Components/EmptyStateIndicatorComponent/BUILD @@ -0,0 +1,24 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "EmptyStateIndicatorComponent", + module_name = "EmptyStateIndicatorComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/ComponentFlow", + "//submodules/Components/AnimatedStickerComponent", + "//submodules/Components/MultilineTextComponent", + "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/EmptyStateIndicatorComponent/Sources/EmptyStateIndicatorComponent.swift b/submodules/TelegramUI/Components/EmptyStateIndicatorComponent/Sources/EmptyStateIndicatorComponent.swift new file mode 100644 index 0000000000..6b4afe2853 --- /dev/null +++ b/submodules/TelegramUI/Components/EmptyStateIndicatorComponent/Sources/EmptyStateIndicatorComponent.swift @@ -0,0 +1,183 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import AnimatedStickerComponent +import ButtonComponent +import TelegramPresentationData +import AccountContext +import MultilineTextComponent + +public final class EmptyStateIndicatorComponent: Component { + public let context: AccountContext + public let theme: PresentationTheme + public let animationName: String + public let title: String + public let text: String + public let actionTitle: String + public let action: () -> Void + + public init( + context: AccountContext, + theme: PresentationTheme, + animationName: String, + title: String, + text: String, + actionTitle: String, + action: @escaping () -> Void + ) { + self.context = context + self.theme = theme + self.animationName = animationName + self.title = title + self.text = text + self.actionTitle = actionTitle + self.action = action + } + + public static func ==(lhs: EmptyStateIndicatorComponent, rhs: EmptyStateIndicatorComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.animationName != rhs.animationName { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.text != rhs.text { + return false + } + if lhs.actionTitle != rhs.actionTitle { + return false + } + return true + } + + public final class View: UIView { + private var component: EmptyStateIndicatorComponent? + private weak var componentState: EmptyComponentState? + + private let animation = ComponentView() + private let title = ComponentView() + private let text = ComponentView() + private let button = ComponentView() + + override public init(frame: CGRect) { + super.init(frame: frame) + } + + required public init(coder: NSCoder) { + preconditionFailure() + } + + public func update(component: EmptyStateIndicatorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + self.componentState = state + + let animationSize = self.animation.update( + transition: transition, + component: AnyComponent(AnimatedStickerComponent( + account: component.context.account, + animation: AnimatedStickerComponent.Animation(source: .bundle(name: component.animationName), loop: true), + size: CGSize(width: 120.0, height: 120.0) + )), + environment: {}, + containerSize: CGSize(width: 120.0, height: 120.0) + ) + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.title, font: Font.semibold(17.0), textColor: component.theme.list.itemPrimaryTextColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 0 + )), + environment: {}, + containerSize: CGSize(width: min(300.0, availableSize.width - 16.0 * 2.0), height: 1000.0) + ) + let textSize = self.text.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.text, font: Font.regular(15.0), textColor: component.theme.list.itemSecondaryTextColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 0 + )), + environment: {}, + containerSize: CGSize(width: min(300.0, availableSize.width - 16.0 * 2.0), height: 1000.0) + ) + let buttonSize = self.button.update( + transition: transition, + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + color: component.theme.list.itemCheckColors.fillColor, + foreground: component.theme.list.itemCheckColors.foregroundColor, + pressedColor: component.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) + ), + content: AnyComponentWithIdentity(id: 0, component: AnyComponent( + Text(text: component.actionTitle, font: Font.semibold(17.0), color: component.theme.list.itemCheckColors.foregroundColor) + )), + isEnabled: true, + displaysProgress: false, + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.action() + } + )), + environment: {}, + containerSize: CGSize(width: 240.0, height: 50.0) + ) + + let animationSpacing: CGFloat = 11.0 + let titleSpacing: CGFloat = 17.0 + let buttonSpacing: CGFloat = 17.0 + + let totalHeight: CGFloat = animationSize.height + animationSpacing + titleSize.height + titleSpacing + textSize.height + buttonSpacing + buttonSize.height + + var contentY = floor((availableSize.height - totalHeight) * 0.5) + + if let animationView = self.animation.view { + if animationView.superview == nil { + self.addSubview(animationView) + } + transition.setFrame(view: animationView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - animationSize.width) * 0.5), y: contentY), size: animationSize)) + contentY += animationSize.height + animationSpacing + } + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: contentY), size: titleSize)) + contentY += titleSize.height + titleSpacing + } + if let textView = self.text.view { + if textView.superview == nil { + self.addSubview(textView) + } + transition.setFrame(view: textView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - textSize.width) * 0.5), y: contentY), size: textSize)) + contentY += textSize.height + buttonSpacing + } + if let buttonView = self.button.view { + if buttonView.superview == nil { + self.addSubview(buttonView) + } + transition.setFrame(view: buttonView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - buttonSize.width) * 0.5), y: contentY), size: buttonSize)) + contentY += buttonSize.height + } + + return availableSize + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift index f6d37aa4e8..be12de4009 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift @@ -131,31 +131,33 @@ final class PeerInfoStoryGridScreenComponent: Component { paneNode.clearSelection() }))) - } else { - var recurseGenerateAction: ((Bool) -> ContextMenuActionItem)? - let generateAction: (Bool) -> ContextMenuActionItem = { [weak pane] isZoomIn in - let nextZoomLevel = isZoomIn ? pane?.availableZoomLevels().increment : pane?.availableZoomLevels().decrement - let canZoom: Bool = nextZoomLevel != nil + } else if let paneNode = self.paneNode { + if !paneNode.isEmpty { + var recurseGenerateAction: ((Bool) -> ContextMenuActionItem)? + let generateAction: (Bool) -> ContextMenuActionItem = { [weak pane] isZoomIn in + let nextZoomLevel = isZoomIn ? pane?.availableZoomLevels().increment : pane?.availableZoomLevels().decrement + let canZoom: Bool = nextZoomLevel != nil + + return ContextMenuActionItem(id: isZoomIn ? 0 : 1, text: isZoomIn ? strings.SharedMedia_ZoomIn : strings.SharedMedia_ZoomOut, textColor: canZoom ? .primary : .disabled, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: isZoomIn ? "Chat/Context Menu/ZoomIn" : "Chat/Context Menu/ZoomOut"), color: canZoom ? theme.contextMenu.primaryColor : theme.contextMenu.primaryColor.withMultipliedAlpha(0.4)) + }, action: canZoom ? { action in + guard let pane = pane, let zoomLevel = isZoomIn ? pane.availableZoomLevels().increment : pane.availableZoomLevels().decrement else { + return + } + pane.updateZoomLevel(level: zoomLevel) + if let recurseGenerateAction = recurseGenerateAction { + action.updateAction(0, recurseGenerateAction(true)) + action.updateAction(1, recurseGenerateAction(false)) + } + } : nil) + } + recurseGenerateAction = { isZoomIn in + return generateAction(isZoomIn) + } - return ContextMenuActionItem(id: isZoomIn ? 0 : 1, text: isZoomIn ? strings.SharedMedia_ZoomIn : strings.SharedMedia_ZoomOut, textColor: canZoom ? .primary : .disabled, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: isZoomIn ? "Chat/Context Menu/ZoomIn" : "Chat/Context Menu/ZoomOut"), color: canZoom ? theme.contextMenu.primaryColor : theme.contextMenu.primaryColor.withMultipliedAlpha(0.4)) - }, action: canZoom ? { action in - guard let pane = pane, let zoomLevel = isZoomIn ? pane.availableZoomLevels().increment : pane.availableZoomLevels().decrement else { - return - } - pane.updateZoomLevel(level: zoomLevel) - if let recurseGenerateAction = recurseGenerateAction { - action.updateAction(0, recurseGenerateAction(true)) - action.updateAction(1, recurseGenerateAction(false)) - } - } : nil) + items.append(.action(generateAction(true))) + items.append(.action(generateAction(false))) } - recurseGenerateAction = { isZoomIn in - return generateAction(isZoomIn) - } - - items.append(.action(generateAction(true))) - items.append(.action(generateAction(false))) if component.peerId == component.context.account.peerId, case .saved = component.scope { var ignoreNextActions = false @@ -392,6 +394,13 @@ final class PeerInfoStoryGridScreenComponent: Component { self.paneNode = paneNode self.addSubview(paneNode.view) + paneNode.emptyAction = { [weak self] in + guard let self, let component = self.component else { + return + } + self.environment?.controller()?.push(PeerInfoStoryGridScreen(context: component.context, peerId: component.peerId, scope: .archive)) + } + self.paneStatusDisposable = (paneNode.status |> deliverOnMainQueue).start(next: { [weak self] status in guard let self else { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/BUILD index 712da58eaa..b861093abe 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/BUILD @@ -36,6 +36,7 @@ swift_library( "//submodules/InvisibleInkDustNode", "//submodules/MediaPickerUI", "//submodules/TelegramUI/Components/Stories/StoryContainerScreen", + "//submodules/TelegramUI/Components/EmptyStateIndicatorComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift index 77f5e9b15a..73c8f528d2 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift @@ -26,6 +26,7 @@ import AppBundle import InvisibleInkDustNode import MediaPickerUI import StoryContainerScreen +import EmptyStateIndicatorComponent private let mediaBadgeBackgroundColor = UIColor(white: 0.0, alpha: 0.6) private let mediaBadgeTextColor = UIColor.white @@ -849,6 +850,14 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr return result } + public var isEmpty: Bool { + if let items = self.items, items.items.count != 0 { + return false + } else { + return true + } + } + private var currentParams: (size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData)? private let ready = Promise() @@ -883,6 +892,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr public var openCurrentDate: (() -> Void)? public var paneDidScroll: (() -> Void)? + public var emptyAction: (() -> Void)? private weak var currentGestureItem: SparseItemGridDisplayItem? @@ -892,6 +902,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr private weak var pendingOpenListContext: PeerStoryListContentContextImpl? private var preloadArchiveListContext: PeerStoryListContext? + + private var emptyStateView: ComponentView? public init(context: AccountContext, peerId: PeerId, chatLocation: ChatLocation, contentType: ContentType, captureProtected: Bool, isSaved: Bool, isArchive: Bool, navigationController: @escaping () -> NavigationController?, listContext: PeerStoryListContext?) { self.context = context @@ -1863,6 +1875,67 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr self.currentParams = (size, topInset, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) transition.updateFrame(node: self.contextGestureContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))) + + if let items = self.items, items.items.isEmpty, items.count == 0, !self.isArchive { + let emptyStateView: ComponentView + var emptyStateTransition = Transition(transition) + if let current = self.emptyStateView { + emptyStateView = current + } else { + emptyStateTransition = .immediate + emptyStateView = ComponentView() + self.emptyStateView = emptyStateView + } + //TODO:localize + let emptyStateSize = emptyStateView.update( + transition: emptyStateTransition, + component: AnyComponent(EmptyStateIndicatorComponent( + context: self.context, + theme: presentationData.theme, + animationName: "StoryListEmpty", + title: "No saved stories", + text: "Open the Archive to select stories you\nwant to be displayed in your profile.", + actionTitle: "Open Archive", + action: { [weak self] in + guard let self else { + return + } + self.emptyAction?() + } + )), + environment: {}, + containerSize: CGSize(width: size.width, height: size.height - topInset - bottomInset) + ) + if let emptyStateComponentView = emptyStateView.view { + if emptyStateComponentView.superview == nil { + self.view.addSubview(emptyStateComponentView) + if self.didUpdateItemsOnce { + emptyStateComponentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } + emptyStateTransition.setFrame(view: emptyStateComponentView, frame: CGRect(origin: CGPoint(x: floor((size.width - emptyStateSize.width) * 0.5), y: topInset), size: emptyStateSize)) + } + if self.didUpdateItemsOnce { + Transition(animation: .curve(duration: 0.2, curve: .easeInOut)).setBackgroundColor(view: self.view, color: presentationData.theme.list.blocksBackgroundColor) + } else { + self.view.backgroundColor = presentationData.theme.list.blocksBackgroundColor + } + } else { + if let emptyStateView = self.emptyStateView { + let subTransition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) + self.emptyStateView = nil + + if let emptyStateComponentView = emptyStateView.view { + subTransition.setAlpha(view: emptyStateComponentView, alpha: 0.0, completion: { [weak emptyStateComponentView] _ in + emptyStateComponentView?.removeFromSuperview() + }) + } + + subTransition.setBackgroundColor(view: self.view, color: presentationData.theme.list.blocksBackgroundColor) + } else { + self.view.backgroundColor = .clear + } + } transition.updateFrame(node: self.itemGrid, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))) if let items = self.items { diff --git a/submodules/TelegramUI/Resources/Animations/StoryListEmpty.tgs b/submodules/TelegramUI/Resources/Animations/StoryListEmpty.tgs new file mode 100644 index 0000000000..0fb36135ee Binary files /dev/null and b/submodules/TelegramUI/Resources/Animations/StoryListEmpty.tgs differ