Various improvements

This commit is contained in:
Ilya Laktyushin 2024-02-27 10:38:24 +04:00
parent a603c138e9
commit 35e9887343
48 changed files with 590 additions and 202 deletions

View File

@ -11218,6 +11218,7 @@ Sorry for the inconvenience.";
"GroupBoost.AdditionalFeatures" = "Additional Features";
"GroupBoost.AdditionalFeaturesText" = "By gaining **boosts**, your group reaches higher levels and unlocks more features.";
"ChannelBoost.AdditionalFeaturesText" = "By gaining **boosts**, your channel reaches higher levels and unlocks more features.";
"Stats.Boosts.Group.NoBoostersYet" = "No users currently boost your group";
"Stats.Boosts.Group.BoostersInfo" = "Your group is currently boosted by these members.";

View File

@ -935,7 +935,7 @@ public protocol SharedAccountContext: AnyObject {
func makeHashtagSearchController(context: AccountContext, peer: EnginePeer?, query: String, all: Bool) -> ViewController
func makeMyStoriesController(context: AccountContext, isArchive: Bool) -> ViewController
func makeArchiveSettingsController(context: AccountContext) -> ViewController
func makeFilterSettingsController(context: AccountContext, modal: Bool, dismissed: (() -> Void)?) -> ViewController
func makeFilterSettingsController(context: AccountContext, modal: Bool, scrollToTags: Bool, dismissed: (() -> Void)?) -> ViewController
func makeBusinessSetupScreen(context: AccountContext) -> ViewController
func makeChatbotSetupScreen(context: AccountContext) -> ViewController
func makeBusinessLocationSetupScreen(context: AccountContext, initialValue: TelegramBusinessLocation?, completion: @escaping (TelegramBusinessLocation?) -> Void) -> ViewController

View File

@ -70,6 +70,14 @@ public enum PremiumDemoSubject {
case messageTags
case lastSeen
case messagePrivacy
case folderTags
case businessLocation
case businessHours
case businessGreetingMessage
case businessQuickReplies
case businessAwayMessage
case businessChatBots
}
public enum PremiumLimitSubject {

View File

@ -5617,7 +5617,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
private func openFilterSettings() {
self.chatListDisplayNode.mainContainerNode.updateEnableAdjacentFilterLoading(false)
if let navigationController = self.context.sharedContext.mainWindow?.viewController as? NavigationController {
let controller = self.context.sharedContext.makeFilterSettingsController(context: self.context, modal: true, dismissed: { [weak self] in
let controller = self.context.sharedContext.makeFilterSettingsController(context: self.context, modal: true, scrollToTags: false, dismissed: { [weak self] in
self?.chatListDisplayNode.mainContainerNode.updateEnableAdjacentFilterLoading(true)
})
navigationController.pushViewController(controller)

View File

@ -41,6 +41,19 @@ private enum ChatListFilterPresetListSection: Int32 {
case tags
}
public enum ChatListFilterPresetListEntryTag: ItemListItemTag {
case displayTags
public func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? ChatListFilterPresetListEntryTag, self == other {
return true
} else {
return false
}
}
}
private func stringForUserCount(_ peers: [EnginePeer.Id: SelectivePrivacyPeer], strings: PresentationStrings) -> String {
if peers.isEmpty {
return strings.PrivacyLastSeenSettings_EmpryUsersPlaceholder
@ -192,7 +205,7 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry {
//TODO:localize
return ItemListSwitchItem(presentationData: presentationData, title: "Show Folder Tags", value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.updateDisplayTags(value)
})
}, tag: ChatListFilterPresetListEntryTag.displayTags)
case .displayTagsFooter:
//TODO:localize
return ItemListTextItem(presentationData: presentationData, text: .plain("Display folder names for each chat in the chat list."), sectionId: self.section)
@ -285,7 +298,7 @@ public enum ChatListFilterPresetListControllerMode {
case modal
}
public func chatListFilterPresetListController(context: AccountContext, mode: ChatListFilterPresetListControllerMode, dismissed: (() -> Void)? = nil) -> ViewController {
public func chatListFilterPresetListController(context: AccountContext, mode: ChatListFilterPresetListControllerMode, scrollToTags: Bool = false, dismissed: (() -> Void)? = nil) -> ViewController {
let initialState = ChatListFilterPresetListControllerState()
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
let stateValue = Atomic(value: initialState)
@ -627,7 +640,8 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.ChatListFolderSettings_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: chatListFilterPresetListControllerEntries(presentationData: presentationData, state: state, filters: filtersWithCountsValue, updatedFilterOrder: updatedFilterOrderValue, suggestedFilters: suggestedFilters, displayTags: displayTags, isPremium: isPremium, limits: limits, premiumLimits: premiumLimits), style: .blocks, animateChanges: true)
let entries = chatListFilterPresetListControllerEntries(presentationData: presentationData, state: state, filters: filtersWithCountsValue, updatedFilterOrder: updatedFilterOrderValue, suggestedFilters: suggestedFilters, displayTags: displayTags, isPremium: isPremium, limits: limits, premiumLimits: premiumLimits)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, initialScrollToItem: scrollToTags ? ListViewScrollToItem(index: entries.count - 1, position: .center(.bottom), animated: true, curve: .Spring(duration: 0.4), directionHint: .Down) : nil, animateChanges: true)
return (controllerState, (listState, arguments))
}

View File

@ -889,11 +889,15 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM
}
})
if case let .share(_, selfPeer, _) = self.mode {
switch self.mode {
case let .share(_, selfPeer, _):
if let selfPeer {
self.headerNode.mapNode.userLocationAnnotation = LocationPinAnnotation(context: context, theme: self.presentationData.theme, peer: selfPeer)
}
self.headerNode.mapNode.hasPickerAnnotation = true
case .pick:
self.headerNode.mapNode.userLocationAnnotation = LocationPinAnnotation(context: context, theme: self.presentationData.theme, location: TelegramMediaMap(coordinate: CLLocationCoordinate2DMake(0, 0)), queryId: nil, resultId: nil, forcedSelection: true)
self.headerNode.mapNode.hasPickerAnnotation = true
}
self.listNode.updateFloatingHeaderOffset = { [weak self] offset, listTransition in

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,61 @@
import Foundation
import UIKit
import SceneKit
import Display
import AppBundle
private let sceneVersion: Int = 1
final class BadgeBusinessView: UIView, PhoneDemoDecorationView {
private let sceneView: SCNView
private var leftParticles: SCNNode?
private var rightParticles: SCNNode?
override init(frame: CGRect) {
self.sceneView = SCNView(frame: CGRect(origin: .zero, size: frame.size))
self.sceneView.backgroundColor = .clear
if let scene = loadCompressedScene(name: "business", version: sceneVersion) {
self.sceneView.scene = scene
}
self.sceneView.isUserInteractionEnabled = false
self.sceneView.preferredFramesPerSecond = 60
super.init(frame: frame)
self.alpha = 0.0
self.addSubview(self.sceneView)
self.leftParticles = self.sceneView.scene?.rootNode.childNode(withName: "leftParticles", recursively: false)
self.rightParticles = self.sceneView.scene?.rootNode.childNode(withName: "rightParticles", recursively: false)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setVisible(_ visible: Bool) {
if visible, let leftParticles = self.leftParticles, let rightParticles = self.rightParticles, leftParticles.parent == nil {
self.sceneView.scene?.rootNode.addChildNode(leftParticles)
self.sceneView.scene?.rootNode.addChildNode(rightParticles)
}
let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear)
transition.updateAlpha(layer: self.layer, alpha: visible ? 0.5 : 0.0, completion: { [weak self] finished in
if let strongSelf = self, finished && !visible && strongSelf.leftParticles?.parent != nil {
strongSelf.leftParticles?.removeFromParentNode()
strongSelf.rightParticles?.removeFromParentNode()
}
})
}
func resetAnimation() {
}
override func layoutSubviews() {
super.layoutSubviews()
self.sceneView.frame = CGRect(origin: .zero, size: frame.size)
}
}

View File

@ -13,8 +13,8 @@ final class BadgeStarsView: UIView, PhoneDemoDecorationView {
override init(frame: CGRect) {
self.sceneView = SCNView(frame: CGRect(origin: .zero, size: frame.size))
self.sceneView.backgroundColor = .clear
if let url = getAppBundle().url(forResource: "badge", withExtension: "scn") {
self.sceneView.scene = try? SCNScene(url: url, options: nil)
if let scene = loadCompressedScene(name: "badge", version: 1) {
self.sceneView.scene = scene
}
self.sceneView.isUserInteractionEnabled = false
self.sceneView.preferredFramesPerSecond = 60
@ -67,8 +67,8 @@ final class EmojiStarsView: UIView, PhoneDemoDecorationView {
override init(frame: CGRect) {
self.sceneView = SCNView(frame: CGRect(origin: .zero, size: frame.size))
self.sceneView.backgroundColor = .clear
if let url = getAppBundle().url(forResource: "emoji", withExtension: "scn") {
self.sceneView.scene = try? SCNScene(url: url, options: nil)
if let scene = loadCompressedScene(name: "emoji", version: 1) {
self.sceneView.scene = scene
}
self.sceneView.isUserInteractionEnabled = false
self.sceneView.preferredFramesPerSecond = 60
@ -121,8 +121,8 @@ final class TagStarsView: UIView, PhoneDemoDecorationView {
override init(frame: CGRect) {
self.sceneView = SCNView(frame: CGRect(origin: .zero, size: frame.size))
self.sceneView.backgroundColor = .clear
if let url = getAppBundle().url(forResource: "tag", withExtension: "scn") {
self.sceneView.scene = try? SCNScene(url: url, options: nil)
if let scene = loadCompressedScene(name: "tag", version: 1) {
self.sceneView.scene = scene
}
self.sceneView.isUserInteractionEnabled = false
self.sceneView.preferredFramesPerSecond = 60

View File

@ -12,7 +12,7 @@ import TelegramCore
import MultilineTextComponent
import TelegramPresentationData
private let sceneVersion: Int = 3
private let sceneVersion: Int = 1
public final class BoostHeaderBackgroundComponent: Component {
let isVisible: Bool
@ -58,7 +58,7 @@ public final class BoostHeaderBackgroundComponent: Component {
private func setup() {
guard let url = getAppBundle().url(forResource: "boost", withExtension: "scn"), let scene = try? SCNScene(url: url, options: nil) else {
guard let scene = loadCompressedScene(name: "boost", version: sceneVersion) else {
return
}

View File

@ -13,8 +13,6 @@ import AnimationCache
import MultiAnimationRenderer
import EmojiStatusComponent
private let sceneVersion: Int = 3
class EmojiHeaderComponent: Component {
let context: AccountContext
let animationCache: AnimationCache

View File

@ -5,6 +5,8 @@ import Display
import AppBundle
import LegacyComponents
private let sceneVersion: Int = 1
final class FasterStarsView: UIView, PhoneDemoDecorationView {
private let sceneView: SCNView
@ -13,8 +15,8 @@ final class FasterStarsView: UIView, PhoneDemoDecorationView {
override init(frame: CGRect) {
self.sceneView = SCNView(frame: CGRect(origin: .zero, size: frame.size))
self.sceneView.backgroundColor = .clear
if let url = getAppBundle().url(forResource: "lightspeed", withExtension: "scn") {
self.sceneView.scene = try? SCNScene(url: url, options: nil)
if let scene = loadCompressedScene(name: "lightspeed", version: sceneVersion) {
self.sceneView.scene = scene
}
self.sceneView.isUserInteractionEnabled = false
self.sceneView.preferredFramesPerSecond = 60

View File

@ -14,7 +14,7 @@ import MergedAvatarsNode
import MultilineTextComponent
import TelegramPresentationData
private let sceneVersion: Int = 3
private let sceneVersion: Int = 1
final class GiftAvatarComponent: Component {
let context: AccountContext
@ -106,7 +106,7 @@ final class GiftAvatarComponent: Component {
}
private func setup() {
guard let url = getAppBundle().url(forResource: "gift", withExtension: "scn"), let scene = try? SCNScene(url: url, options: nil) else {
guard let scene = loadCompressedScene(name: "gift", version: sceneVersion) else {
return
}

View File

@ -371,6 +371,7 @@ final class PhoneDemoComponent: Component {
case emoji
case hello
case tag
case business
}
enum Model {
@ -547,6 +548,13 @@ final class PhoneDemoComponent: Component {
self.decorationView = starsView
self.decorationContainerView.addSubview(starsView)
}
case .business:
if let _ = self.decorationView as? BadgeBusinessView {
} else {
let starsView = BadgeBusinessView(frame: self.decorationContainerView.bounds)
self.decorationView = starsView
self.decorationContainerView.addSubview(starsView)
}
}
self.phoneView.setup(context: component.context, videoFile: component.videoFile, position: component.position)

View File

@ -698,7 +698,7 @@ private final class SheetContent: CombinedComponent {
isCurrent = mode == .current
}
case .features:
textString = strings.GroupBoost_AdditionalFeaturesText
textString = isGroup ? strings.GroupBoost_AdditionalFeaturesText : strings.ChannelBoost_AdditionalFeaturesText
}
let defaultTitle = strings.ChannelBoost_Level("\(level)").string

View File

@ -8,7 +8,7 @@ import GZip
import AppBundle
import LegacyComponents
private let sceneVersion: Int = 1
private let sceneVersion: Int = 2
private func deg2rad(_ number: Float) -> Float {
return number * .pi / 180
@ -223,24 +223,7 @@ class PremiumCoinComponent: Component {
}
private func setup() {
let resourceUrl: URL
if let url = getAppBundle().url(forResource: "coin", withExtension: "scn") {
resourceUrl = url
} else {
let fileName = "coin_\(sceneVersion).scn"
let tmpUrl = URL(fileURLWithPath: NSTemporaryDirectory() + fileName)
if !FileManager.default.fileExists(atPath: tmpUrl.path) {
guard let url = getAppBundle().url(forResource: "coin", withExtension: ""),
let compressedData = try? Data(contentsOf: url),
let decompressedData = TGGUnzipData(compressedData, 8 * 1024 * 1024) else {
return
}
try? decompressedData.write(to: tmpUrl)
}
resourceUrl = tmpUrl
}
guard let scene = try? SCNScene(url: resourceUrl, options: nil) else {
guard let scene = loadCompressedScene(name: "coin", version: sceneVersion) else {
return
}
@ -316,8 +299,8 @@ class PremiumCoinComponent: Component {
return
}
let fromScale: Float = 0.85
let toScale: Float = 0.9
let fromScale: Float = 0.9
let toScale: Float = 1.0
let animation = CABasicAnimation(keyPath: "scale")
animation.duration = 2.0

View File

@ -1177,7 +1177,7 @@ private final class DemoSheetContent: CombinedComponent {
text = strings.Premium_LastSeenInfo
case .messagePrivacy:
text = strings.Premium_MessagePrivacyInfo
case .doubleLimits, .stories, .business:
default:
text = ""
}
@ -1392,6 +1392,14 @@ public class PremiumDemoScreen: ViewControllerComponentContainer {
case lastSeen
case messagePrivacy
case business
case folderTags
case businessLocation
case businessHours
case businessGreetingMessage
case businessQuickReplies
case businessAwayMessage
case businessChatBots
}
public enum Source: Equatable {

View File

@ -533,6 +533,8 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent {
demoSubject = .messagePrivacy
case .business:
demoSubject = .business
default:
demoSubject = .doubleLimits
}
let buttonText: String

View File

@ -439,6 +439,15 @@ public enum PremiumPerk: CaseIterable {
case lastSeen
case messagePrivacy
case business
case folderTags
case businessLocation
case businessHours
case businessGreetingMessage
case businessQuickReplies
case businessAwayMessage
case businessChatBots
public static var allCases: [PremiumPerk] {
return [
@ -520,6 +529,8 @@ public enum PremiumPerk: CaseIterable {
return "message_privacy"
case .business:
return "business"
default:
return ""
}
}
@ -567,6 +578,8 @@ public enum PremiumPerk: CaseIterable {
return strings.Premium_MessagePrivacy
case .business:
return strings.Premium_Business
default:
return ""
}
}
@ -614,6 +627,8 @@ public enum PremiumPerk: CaseIterable {
return strings.Premium_MessagePrivacyInfo
case .business:
return strings.Premium_BusinessInfo
default:
return ""
}
}
@ -661,6 +676,8 @@ public enum PremiumPerk: CaseIterable {
return "Premium/Perk/MessagePrivacy"
case .business:
return "Premium/Perk/Business"
default:
return ""
}
}
}
@ -1948,75 +1965,77 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
))),
action: { [weak state] _ in
var demoSubject: PremiumDemoScreen.Subject
switch perk {
case .doubleLimits:
demoSubject = .doubleLimits
case .moreUpload:
demoSubject = .moreUpload
case .fasterDownload:
demoSubject = .fasterDownload
case .voiceToText:
demoSubject = .voiceToText
case .noAds:
demoSubject = .noAds
case .uniqueReactions:
demoSubject = .uniqueReactions
case .premiumStickers:
demoSubject = .premiumStickers
case .advancedChatManagement:
demoSubject = .advancedChatManagement
case .profileBadge:
demoSubject = .profileBadge
case .animatedUserpics:
demoSubject = .animatedUserpics
case .appIcons:
demoSubject = .appIcons
case .animatedEmoji:
demoSubject = .animatedEmoji
case .emojiStatus:
demoSubject = .emojiStatus
case .translation:
demoSubject = .translation
case .stories:
demoSubject = .stories
case .colors:
demoSubject = .colors
let _ = ApplicationSpecificNotice.setDismissedPremiumColorsBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()
case .wallpapers:
demoSubject = .wallpapers
let _ = ApplicationSpecificNotice.setDismissedPremiumWallpapersBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()
case .messageTags:
demoSubject = .messageTags
let _ = ApplicationSpecificNotice.setDismissedMessageTagsBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()
case .lastSeen:
demoSubject = .lastSeen
let _ = ApplicationSpecificNotice.setDismissedLastSeenBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()
case .messagePrivacy:
demoSubject = .messagePrivacy
let _ = ApplicationSpecificNotice.setDismissedMessagePrivacyBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()
case .business:
demoSubject = .business
switch perk {
case .doubleLimits:
demoSubject = .doubleLimits
case .moreUpload:
demoSubject = .moreUpload
case .fasterDownload:
demoSubject = .fasterDownload
case .voiceToText:
demoSubject = .voiceToText
case .noAds:
demoSubject = .noAds
case .uniqueReactions:
demoSubject = .uniqueReactions
case .premiumStickers:
demoSubject = .premiumStickers
case .advancedChatManagement:
demoSubject = .advancedChatManagement
case .profileBadge:
demoSubject = .profileBadge
case .animatedUserpics:
demoSubject = .animatedUserpics
case .appIcons:
demoSubject = .appIcons
case .animatedEmoji:
demoSubject = .animatedEmoji
case .emojiStatus:
demoSubject = .emojiStatus
case .translation:
demoSubject = .translation
case .stories:
demoSubject = .stories
case .colors:
demoSubject = .colors
let _ = ApplicationSpecificNotice.setDismissedPremiumColorsBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()
case .wallpapers:
demoSubject = .wallpapers
let _ = ApplicationSpecificNotice.setDismissedPremiumWallpapersBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()
case .messageTags:
demoSubject = .messageTags
let _ = ApplicationSpecificNotice.setDismissedMessageTagsBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()
case .lastSeen:
demoSubject = .lastSeen
let _ = ApplicationSpecificNotice.setDismissedLastSeenBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()
case .messagePrivacy:
demoSubject = .messagePrivacy
let _ = ApplicationSpecificNotice.setDismissedMessagePrivacyBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()
case .business:
demoSubject = .business
default:
demoSubject = .doubleLimits
}
let isPremium = state?.isPremium == true
var dismissImpl: (() -> Void)?
let controller = PremiumLimitsListScreen(context: accountContext, subject: demoSubject, source: .intro(state?.price), order: state?.configuration.perks, buttonText: isPremium ? strings.Common_OK : (state?.isAnnual == true ? strings.Premium_SubscribeForAnnual(state?.price ?? "").string : strings.Premium_SubscribeFor(state?.price ?? "").string), isPremium: isPremium, forceDark: forceDark)
controller.action = { [weak state] in
dismissImpl?()
if state?.isPremium == false {
buy()
}
let isPremium = state?.isPremium == true
var dismissImpl: (() -> Void)?
let controller = PremiumLimitsListScreen(context: accountContext, subject: demoSubject, source: .intro(state?.price), order: state?.configuration.perks, buttonText: isPremium ? strings.Common_OK : (state?.isAnnual == true ? strings.Premium_SubscribeForAnnual(state?.price ?? "").string : strings.Premium_SubscribeFor(state?.price ?? "").string), isPremium: isPremium, forceDark: forceDark)
controller.action = { [weak state] in
dismissImpl?()
if state?.isPremium == false {
buy()
}
}
controller.disposed = {
updateIsFocused(false)
}
present(controller)
dismissImpl = { [weak controller] in
controller?.dismiss(animated: true, completion: nil)
}
updateIsFocused(true)
addAppLogEvent(postbox: accountContext.account.postbox, type: "premium.promo_screen_tap", data: ["item": perk.identifier])
}
controller.disposed = {
updateIsFocused(false)
}
present(controller)
dismissImpl = { [weak controller] in
controller?.dismiss(animated: true, completion: nil)
}
updateIsFocused(true)
addAppLogEvent(postbox: accountContext.account.postbox, type: "premium.promo_screen_tap", data: ["item": perk.identifier])
}
))))
i += 1
@ -2100,43 +2119,78 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
foregroundColor: .white,
iconName: perk.iconName
))),
action: { _ in
switch perk {
case .location:
let _ = (accountContext.engine.data.get(
TelegramEngine.EngineData.Item.Peer.BusinessLocation(id: accountContext.account.peerId)
)
|> deliverOnMainQueue).start(next: { [weak accountContext] businessLocation in
guard let accountContext else {
return
action: { [weak state] _ in
let isPremium = state?.isPremium == true
if isPremium {
switch perk {
case .location:
let _ = (accountContext.engine.data.get(
TelegramEngine.EngineData.Item.Peer.BusinessLocation(id: accountContext.account.peerId)
)
|> deliverOnMainQueue).start(next: { [weak accountContext] businessLocation in
guard let accountContext else {
return
}
push(accountContext.sharedContext.makeBusinessLocationSetupScreen(context: accountContext, initialValue: businessLocation, completion: { _ in }))
})
case .hours:
let _ = (accountContext.engine.data.get(
TelegramEngine.EngineData.Item.Peer.BusinessHours(id: accountContext.account.peerId)
)
|> deliverOnMainQueue).start(next: { [weak accountContext] businessHours in
guard let accountContext else {
return
}
push(accountContext.sharedContext.makeBusinessHoursSetupScreen(context: accountContext, initialValue: businessHours, completion: { _ in }))
})
case .quickReplies:
let _ = (accountContext.sharedContext.makeQuickReplySetupScreenInitialData(context: accountContext)
|> take(1)
|> deliverOnMainQueue).start(next: { [weak accountContext] initialData in
guard let accountContext else {
return
}
push(accountContext.sharedContext.makeQuickReplySetupScreen(context: accountContext, initialData: initialData))
})
case .greetings:
push(accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreen(context: accountContext, isAwayMode: false))
case .awayMessages:
push(accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreen(context: accountContext, isAwayMode: true))
case .chatbots:
push(accountContext.sharedContext.makeChatbotSetupScreen(context: accountContext))
}
} else {
var demoSubject: PremiumDemoScreen.Subject
switch perk {
case .location:
demoSubject = .businessLocation
case .hours:
demoSubject = .businessHours
case .quickReplies:
demoSubject = .businessQuickReplies
case .greetings:
demoSubject = .businessGreetingMessage
case .awayMessages:
demoSubject = .businessAwayMessage
case .chatbots:
demoSubject = .businessChatBots
}
var dismissImpl: (() -> Void)?
let controller = PremiumLimitsListScreen(context: accountContext, subject: demoSubject, source: .intro(state?.price), order: [.businessLocation, .businessHours, .businessQuickReplies, .businessGreetingMessage, .businessAwayMessage, .businessChatBots], buttonText: isPremium ? strings.Common_OK : (state?.isAnnual == true ? strings.Premium_SubscribeForAnnual(state?.price ?? "").string : strings.Premium_SubscribeFor(state?.price ?? "").string), isPremium: isPremium, forceDark: forceDark)
controller.action = { [weak state] in
dismissImpl?()
if state?.isPremium == false {
buy()
}
push(accountContext.sharedContext.makeBusinessLocationSetupScreen(context: accountContext, initialValue: businessLocation, completion: { _ in }))
})
case .hours:
let _ = (accountContext.engine.data.get(
TelegramEngine.EngineData.Item.Peer.BusinessHours(id: accountContext.account.peerId)
)
|> deliverOnMainQueue).start(next: { [weak accountContext] businessHours in
guard let accountContext else {
return
}
push(accountContext.sharedContext.makeBusinessHoursSetupScreen(context: accountContext, initialValue: businessHours, completion: { _ in }))
})
case .quickReplies:
let _ = (accountContext.sharedContext.makeQuickReplySetupScreenInitialData(context: accountContext)
|> take(1)
|> deliverOnMainQueue).start(next: { [weak accountContext] initialData in
guard let accountContext else {
return
}
push(accountContext.sharedContext.makeQuickReplySetupScreen(context: accountContext, initialData: initialData))
})
case .greetings:
push(accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreen(context: accountContext, isAwayMode: false))
case .awayMessages:
push(accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreen(context: accountContext, isAwayMode: true))
case .chatbots:
push(accountContext.sharedContext.makeChatbotSetupScreen(context: accountContext))
}
controller.disposed = {
updateIsFocused(false)
}
present(controller)
dismissImpl = { [weak controller] in
controller?.dismiss(animated: true, completion: nil)
}
updateIsFocused(true)
}
}
))))
@ -2238,7 +2292,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
iconName: "Premium/BusinessPerk/Tag"
))),
action: { _ in
push(accountContext.sharedContext.makeFilterSettingsController(context: accountContext, modal: false, dismissed: nil))
push(accountContext.sharedContext.makeFilterSettingsController(context: accountContext, modal: false, scrollToTags: true, dismissed: nil))
}
))))

View File

@ -833,6 +833,129 @@ public class PremiumLimitsListScreen: ViewController {
)
)
)
availableItems[.businessLocation] = DemoPagerComponent.Item(
AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.businessLocation,
component: AnyComponent(
PageComponent(
content: AnyComponent(PhoneDemoComponent(
context: context,
position: .top,
model: .island,
videoFile: configuration.videos["business_location"],
decoration: .business
)),
title: strings.Business_Location,
text: strings.Business_LocationInfo,
textColor: textColor
)
)
)
)
availableItems[.businessHours] = DemoPagerComponent.Item(
AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.businessHours,
component: AnyComponent(
PageComponent(
content: AnyComponent(PhoneDemoComponent(
context: context,
position: .top,
model: .island,
videoFile: configuration.videos["business_hours"],
decoration: .business
)),
title: strings.Business_OpeningHours,
text: strings.Business_OpeningHoursInfo,
textColor: textColor
)
)
)
)
availableItems[.businessQuickReplies] = DemoPagerComponent.Item(
AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.businessQuickReplies,
component: AnyComponent(
PageComponent(
content: AnyComponent(PhoneDemoComponent(
context: context,
position: .top,
model: .island,
videoFile: configuration.videos["greeting_message"],
decoration: .business
)),
title: strings.Business_QuickReplies,
text: strings.Business_QuickRepliesInfo,
textColor: textColor
)
)
)
)
availableItems[.businessGreetingMessage] = DemoPagerComponent.Item(
AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.businessGreetingMessage,
component: AnyComponent(
PageComponent(
content: AnyComponent(PhoneDemoComponent(
context: context,
position: .top,
model: .island,
videoFile: configuration.videos["greeting_message"],
decoration: .business
)),
title: strings.Business_GreetingMessages,
text: strings.Business_GreetingMessagesInfo,
textColor: textColor
)
)
)
)
availableItems[.businessAwayMessage] = DemoPagerComponent.Item(
AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.businessAwayMessage,
component: AnyComponent(
PageComponent(
content: AnyComponent(PhoneDemoComponent(
context: context,
position: .top,
model: .island,
videoFile: configuration.videos["away_message"],
decoration: .business
)),
title: strings.Business_AwayMessages,
text: strings.Business_AwayMessagesInfo,
textColor: textColor
)
)
)
)
availableItems[.businessChatBots] = DemoPagerComponent.Item(
AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.businessChatBots,
component: AnyComponent(
PageComponent(
content: AnyComponent(PhoneDemoComponent(
context: context,
position: .top,
model: .island,
videoFile: configuration.videos["business_bots"],
decoration: .business
)),
title: strings.Business_Chatbots,
text: strings.Business_ChatbotsInfo,
textColor: textColor
)
)
)
)
if let order = controller.order {
var items: [DemoPagerComponent.Item] = order.compactMap { availableItems[$0] }

View File

@ -8,7 +8,7 @@ import GZip
import AppBundle
import LegacyComponents
private let sceneVersion: Int = 6
private let sceneVersion: Int = 7
private func deg2rad(_ number: Float) -> Float {
return number * .pi / 180
@ -45,7 +45,31 @@ private func generateDiffuseTexture() -> UIImage {
})!
}
class PremiumStarComponent: Component {
func loadCompressedScene(name: String, version: Int) -> SCNScene? {
let resourceUrl: URL
if let url = getAppBundle().url(forResource: name, withExtension: "scn") {
resourceUrl = url
} else {
let fileName = "\(name)_\(version).scn"
let tmpUrl = URL(fileURLWithPath: NSTemporaryDirectory() + fileName)
if !FileManager.default.fileExists(atPath: tmpUrl.path) {
guard let url = getAppBundle().url(forResource: name, withExtension: ""),
let compressedData = try? Data(contentsOf: url),
let decompressedData = TGGUnzipData(compressedData, 8 * 1024 * 1024) else {
return nil
}
try? decompressedData.write(to: tmpUrl)
}
resourceUrl = tmpUrl
}
guard let scene = try? SCNScene(url: resourceUrl, options: nil) else {
return nil
}
return scene
}
final class PremiumStarComponent: Component {
let isIntro: Bool
let isVisible: Bool
let hasIdleAnimations: Bool
@ -251,24 +275,7 @@ class PremiumStarComponent: Component {
}
private func setup() {
let resourceUrl: URL
if let url = getAppBundle().url(forResource: "star", withExtension: "scn") {
resourceUrl = url
} else {
let fileName = "star_\(sceneVersion).scn"
let tmpUrl = URL(fileURLWithPath: NSTemporaryDirectory() + fileName)
if !FileManager.default.fileExists(atPath: tmpUrl.path) {
guard let url = getAppBundle().url(forResource: "star", withExtension: ""),
let compressedData = try? Data(contentsOf: url),
let decompressedData = TGGUnzipData(compressedData, 8 * 1024 * 1024) else {
return
}
try? decompressedData.write(to: tmpUrl)
}
resourceUrl = tmpUrl
}
guard let scene = try? SCNScene(url: resourceUrl, options: nil) else {
guard let scene = loadCompressedScene(name: "star", version: sceneVersion) else {
return
}

View File

@ -5,6 +5,8 @@ import Display
import AppBundle
import SwiftSignalKit
private let sceneVersion: Int = 1
final class SwirlStarsView: UIView, PhoneDemoDecorationView {
private let sceneView: SCNView
@ -13,8 +15,8 @@ final class SwirlStarsView: UIView, PhoneDemoDecorationView {
override init(frame: CGRect) {
self.sceneView = SCNView(frame: CGRect(origin: .zero, size: frame.size))
self.sceneView.backgroundColor = .clear
if let url = getAppBundle().url(forResource: "swirl", withExtension: "scn") {
self.sceneView.scene = try? SCNScene(url: url, options: nil)
if let scene = loadCompressedScene(name: "swirl", version: sceneVersion) {
self.sceneView.scene = scene
}
self.sceneView.isUserInteractionEnabled = false
self.sceneView.preferredFramesPerSecond = 60

View File

@ -16,6 +16,8 @@ public final class EmptyStateIndicatorComponent: Component {
public let text: String
public let actionTitle: String?
public let action: () -> Void
public let additionalActionTitle: String?
public let additionalAction: () -> Void
public init(
context: AccountContext,
@ -24,7 +26,9 @@ public final class EmptyStateIndicatorComponent: Component {
title: String,
text: String,
actionTitle: String?,
action: @escaping () -> Void
action: @escaping () -> Void,
additionalActionTitle: String?,
additionalAction: @escaping () -> Void
) {
self.context = context
self.theme = theme
@ -33,6 +37,8 @@ public final class EmptyStateIndicatorComponent: Component {
self.text = text
self.actionTitle = actionTitle
self.action = action
self.additionalActionTitle = additionalActionTitle
self.additionalAction = additionalAction
}
public static func ==(lhs: EmptyStateIndicatorComponent, rhs: EmptyStateIndicatorComponent) -> Bool {
@ -54,6 +60,9 @@ public final class EmptyStateIndicatorComponent: Component {
if lhs.actionTitle != rhs.actionTitle {
return false
}
if lhs.additionalActionTitle != rhs.additionalActionTitle {
return false
}
return true
}
@ -65,6 +74,7 @@ public final class EmptyStateIndicatorComponent: Component {
private let title = ComponentView<Empty>()
private let text = ComponentView<Empty>()
private var button: ComponentView<Empty>?
private var additionalButton: ComponentView<Empty>?
override public init(frame: CGRect) {
super.init(frame: frame)
@ -139,7 +149,7 @@ public final class EmptyStateIndicatorComponent: Component {
}
)),
environment: {},
containerSize: CGSize(width: 240.0, height: 50.0)
containerSize: CGSize(width: 260.0, height: 50.0)
)
} else {
if let button = self.button {
@ -148,14 +158,52 @@ public final class EmptyStateIndicatorComponent: Component {
}
}
var additionalButtonSize: CGSize?
if let additionalActionTitle = component.additionalActionTitle {
let additionalButton: ComponentView<Empty>
if let current = self.additionalButton {
additionalButton = current
} else {
additionalButton = ComponentView()
self.additionalButton = additionalButton
}
additionalButtonSize = additionalButton.update(
transition: transition,
component: AnyComponent(Button(
content: AnyComponent(Text(
text: additionalActionTitle, font:
Font.regular(17.0),
color: component.theme.list.itemAccentColor)
),
action: { [weak self] in
guard let self, let component = self.component else {
return
}
component.additionalAction()
}
)),
environment: {},
containerSize: CGSize(width: 262.0, height: 50.0)
)
} else {
if let additionalButton = self.additionalButton {
self.additionalButton = nil
additionalButton.view?.removeFromSuperview()
}
}
let animationSpacing: CGFloat = 11.0
let titleSpacing: CGFloat = 17.0
let buttonSpacing: CGFloat = 17.0
let buttonSpacing: CGFloat = 21.0
var totalHeight: CGFloat = animationSize.height + animationSpacing + titleSize.height + titleSpacing + textSize.height
if let buttonSize {
totalHeight += buttonSpacing + buttonSize.height
}
if let additionalButtonSize {
totalHeight += buttonSpacing + additionalButtonSize.height
}
var contentY = floor((availableSize.height - totalHeight) * 0.5)
@ -185,7 +233,14 @@ public final class EmptyStateIndicatorComponent: Component {
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
contentY += buttonSize.height + buttonSpacing
}
if let additionalButtonSize, let additionalButtonView = self.additionalButton?.view {
if additionalButtonView.superview == nil {
self.addSubview(additionalButtonView)
}
transition.setFrame(view: additionalButtonView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - additionalButtonSize.width) * 0.5), y: contentY), size: additionalButtonSize))
contentY += additionalButtonSize.height
}
return availableSize

View File

@ -178,7 +178,11 @@ public final class ListActionItemComponent: Component {
let contentRightInset: CGFloat
switch component.accessory {
case .none:
contentRightInset = 16.0
if let _ = component.icon {
contentRightInset = 42.0
} else {
contentRightInset = 16.0
}
case .arrow:
contentRightInset = 30.0
case .toggle:
@ -189,7 +193,7 @@ public final class ListActionItemComponent: Component {
contentHeight += verticalInset
if component.leftIcon != nil {
contentLeftInset += 46.0
contentLeftInset += 52.0
}
let titleSize = self.title.update(
@ -239,7 +243,7 @@ public final class ListActionItemComponent: Component {
var iconOffset: CGFloat = 0.0
if case .none = component.accessory {
iconOffset = 6.0
iconOffset = 26.0
}
let iconFrame = CGRect(origin: CGPoint(x: availableSize.width - contentRightInset - iconSize.width + iconOffset, y: floor((contentHeight - iconSize.height) * 0.5)), size: iconSize)

View File

@ -8795,7 +8795,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}
})
case .chatFolders:
let controller = self.context.sharedContext.makeFilterSettingsController(context: self.context, modal: false, dismissed: nil)
let controller = self.context.sharedContext.makeFilterSettingsController(context: self.context, modal: false, scrollToTags: false, dismissed: nil)
push(controller)
case .notificationsAndSounds:
if let settings = self.data?.globalSettings {

View File

@ -295,7 +295,23 @@ final class PeerInfoStoryGridScreenComponent: Component {
let _ = paneNode.scrollToTop()
}
func openCreateStory() {
guard let component = self.component else {
return
}
if let rootController = component.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface {
let coordinator = rootController.openStoryCamera(customTarget: nil, transitionIn: nil, transitionedIn: {}, transitionOut: { _, _ in return nil })
coordinator?.animateIn()
}
}
private var isUpdating = false
func update(component: PeerInfoStoryGridScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
self.component = component
self.state = state
@ -313,7 +329,7 @@ final class PeerInfoStoryGridScreenComponent: Component {
var bottomInset: CGFloat = environment.safeInsets.bottom
if self.selectedCount != 0 {
if self.selectedCount != 0 || (component.scope == .saved && self.paneNode?.isEmpty == false) {
let selectionPanel: ComponentView<Empty>
var selectionPanelTransition = transition
if let current = self.selectionPanel {
@ -327,7 +343,7 @@ final class PeerInfoStoryGridScreenComponent: Component {
let buttonText: String
switch component.scope {
case .saved:
buttonText = environment.strings.ChatList_Context_Archive
buttonText = self.selectedCount > 0 ? environment.strings.ChatList_Context_Archive : environment.strings.StoryList_SavedAddAction
case .archive:
buttonText = environment.strings.StoryList_SaveToProfile
}
@ -344,7 +360,7 @@ final class PeerInfoStoryGridScreenComponent: Component {
guard let self, let component = self.component, let environment = self.environment else {
return
}
guard let paneNode = self.paneNode, !paneNode.selectedIds.isEmpty else {
guard let paneNode = self.paneNode else {
return
}
@ -361,21 +377,25 @@ final class PeerInfoStoryGridScreenComponent: Component {
switch component.scope {
case .saved:
let selectedCount = paneNode.selectedItems.count
let _ = component.context.engine.messages.updateStoriesArePinned(peerId: component.peerId, ids: paneNode.selectedItems, isPinned: false).start()
paneNode.setIsSelectionModeActive(false)
(self.environment?.controller() as? PeerInfoStoryGridScreen)?.updateTitle()
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme)
let title: String = presentationData.strings.StoryList_TooltipStoriesSavedToProfile(Int32(selectedCount))
environment.controller()?.present(UndoOverlayController(
presentationData: presentationData,
content: .info(title: nil, text: title, timeout: nil, customUndoText: nil),
elevatedLayout: false,
animateInAsReplacement: false,
action: { _ in return false }
), in: .current)
if selectedCount == 0 {
self.openCreateStory()
} else {
let _ = component.context.engine.messages.updateStoriesArePinned(peerId: component.peerId, ids: paneNode.selectedItems, isPinned: false).start()
paneNode.setIsSelectionModeActive(false)
(self.environment?.controller() as? PeerInfoStoryGridScreen)?.updateTitle()
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme)
let title: String = presentationData.strings.StoryList_TooltipStoriesSavedToProfile(Int32(selectedCount))
environment.controller()?.present(UndoOverlayController(
presentationData: presentationData,
content: .info(title: nil, text: title, timeout: nil, customUndoText: nil),
elevatedLayout: false,
animateInAsReplacement: false,
action: { _ in return false }
), in: .current)
}
case .archive:
let _ = component.context.engine.messages.updateStoriesArePinned(peerId: component.peerId, ids: paneNode.selectedItems, isPinned: true).start()
@ -449,10 +469,28 @@ final class PeerInfoStoryGridScreenComponent: Component {
},
listContext: nil
)
paneNode.isEmptyUpdated = { [weak self] _ in
guard let self else {
return
}
if !self.isUpdating {
self.state?.updated(transition: .immediate)
}
}
self.paneNode = paneNode
self.addSubview(paneNode.view)
if let selectionPanelView = self.selectionPanel?.view {
self.insertSubview(paneNode.view, belowSubview: selectionPanelView)
} else {
self.addSubview(paneNode.view)
}
paneNode.emptyAction = { [weak self] in
guard let self else {
return
}
self.openCreateStory()
}
paneNode.additionalEmptyAction = { [weak self] in
guard let self, let component = self.component else {
return
}

View File

@ -948,6 +948,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
}
}
public var isEmptyUpdated: (Bool) -> Void = { _ in }
public private(set) var isSelectionModeActive: Bool
private var currentParams: (size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, navigationHeight: CGFloat, presentationData: PresentationData)?
@ -985,6 +987,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
public var openCurrentDate: (() -> Void)?
public var paneDidScroll: (() -> Void)?
public var emptyAction: (() -> Void)?
public var additionalEmptyAction: (() -> Void)?
public var ensureRectVisible: ((UIView, CGRect) -> Void)?
@ -1729,6 +1732,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
private func updateHistory(items: SparseItemGrid.Items, synchronous: Bool, reloadAtTop: Bool) {
self.items = items
self.isEmptyUpdated(self.isEmpty)
if let (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, navigationHeight, presentationData) = self.currentParams {
var gridSnapshot: UIView?
@ -2027,14 +2031,21 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
context: self.context,
theme: presentationData.theme,
animationName: "StoryListEmpty",
title: self.isArchive ? presentationData.strings.StoryList_ArchivedEmptyState_Title : presentationData.strings.StoryList_SavedEmptyState_Title,
text: self.isArchive ? presentationData.strings.StoryList_ArchivedEmptyState_Text : presentationData.strings.StoryList_SavedEmptyState_Text,
actionTitle: self.isArchive ? nil : presentationData.strings.StoryList_SavedEmptyAction,
title: self.isArchive ? presentationData.strings.StoryList_ArchivedEmptyState_Title : presentationData.strings.StoryList_SavedEmptyPosts_Title,
text: self.isArchive ? presentationData.strings.StoryList_ArchivedEmptyState_Text : presentationData.strings.StoryList_SavedEmptyPosts_Text,
actionTitle: self.isArchive ? nil : presentationData.strings.StoryList_SavedAddAction,
action: { [weak self] in
guard let self else {
return
}
self.emptyAction?()
},
additionalActionTitle: self.isArchive ? nil : presentationData.strings.StoryList_SavedEmptyAction,
additionalAction: { [weak self] in
guard let self else {
return
}
self.additionalEmptyAction?()
}
)),
environment: {},

View File

@ -134,6 +134,9 @@ final class StoryAuthorInfoComponent: Component {
if timeString.count < 6 {
combinedString.append(NSAttributedString(string: "\(timeString)", font: Font.regular(11.0), textColor: subtitleColor))
}
if component.isEdited {
combinedString.append(NSAttributedString(string: "\(component.strings.Story_HeaderEdited)", font: Font.regular(11.0), textColor: subtitleColor))
}
subtitle = combinedString
subtitleTruncationType = .middle
} else {

View File

@ -1882,8 +1882,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return archiveSettingsController(context: context)
}
public func makeFilterSettingsController(context: AccountContext, modal: Bool, dismissed: (() -> Void)?) -> ViewController {
return chatListFilterPresetListController(context: context, mode: modal ? .modal : .default, dismissed: dismissed)
public func makeFilterSettingsController(context: AccountContext, modal: Bool, scrollToTags: Bool, dismissed: (() -> Void)?) -> ViewController {
return chatListFilterPresetListController(context: context, mode: modal ? .modal : .default, scrollToTags: scrollToTags, dismissed: dismissed)
}
public func makeBusinessSetupScreen(context: AccountContext) -> ViewController {
@ -2040,6 +2040,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
mappedSubject = .lastSeen
case .messagePrivacy:
mappedSubject = .messagePrivacy
default:
mappedSubject = .doubleLimits
}
return PremiumDemoScreen(context: context, subject: mappedSubject, action: action)
}