Add story stealth mode shortcut in chat list

This commit is contained in:
Ilya Laktyushin 2024-04-23 02:51:25 +04:00
parent 28b48c1758
commit 891fed3189
24 changed files with 366 additions and 66 deletions

View File

@ -12134,3 +12134,6 @@ Sorry for the inconvenience.";
"Channel.AdminLogFilter.Section.Messages" = "Messages"; "Channel.AdminLogFilter.Section.Messages" = "Messages";
"Premium.Gift.ContactSelection.AddBirthday" = "Add Your Birthday"; "Premium.Gift.ContactSelection.AddBirthday" = "Add Your Birthday";
"Story.StealthMode.EnableAndOpenAction" = "Enable and Open the Story";

View File

@ -1012,7 +1012,7 @@ public protocol SharedAccountContext: AnyObject {
func makeChatQrCodeScreen(context: AccountContext, peer: Peer, threadId: Int64?, temporary: Bool) -> ViewController func makeChatQrCodeScreen(context: AccountContext, peer: Peer, threadId: Int64?, temporary: Bool) -> ViewController
func makePremiumIntroController(context: AccountContext, source: PremiumIntroSource, forceDark: Bool, dismissed: (() -> Void)?) -> ViewController func makePremiumIntroController(context: AccountContext, source: PremiumIntroSource, forceDark: Bool, dismissed: (() -> Void)?) -> ViewController
func makePremiumDemoController(context: AccountContext, subject: PremiumDemoSubject, action: @escaping () -> Void) -> ViewController func makePremiumDemoController(context: AccountContext, subject: PremiumDemoSubject, forceDark: Bool, action: @escaping () -> Void, dismissed: (() -> Void)?) -> ViewController
func makePremiumLimitController(context: AccountContext, subject: PremiumLimitSubject, count: Int32, forceDark: Bool, cancel: @escaping () -> Void, action: @escaping () -> Bool) -> ViewController func makePremiumLimitController(context: AccountContext, subject: PremiumLimitSubject, count: Int32, forceDark: Bool, cancel: @escaping () -> Void, action: @escaping () -> Bool) -> ViewController
func makePremiumGiftController(context: AccountContext, source: PremiumGiftSource, completion: (() -> Void)?) -> ViewController func makePremiumGiftController(context: AccountContext, source: PremiumGiftSource, completion: (() -> Void)?) -> ViewController
func makePremiumPrivacyControllerController(context: AccountContext, subject: PremiumPrivacySubject, peerId: EnginePeer.Id) -> ViewController func makePremiumPrivacyControllerController(context: AccountContext, subject: PremiumPrivacySubject, peerId: EnginePeer.Id) -> ViewController

View File

@ -72,6 +72,7 @@ public enum PremiumDemoSubject {
case lastSeen case lastSeen
case messagePrivacy case messagePrivacy
case folderTags case folderTags
case business
case businessLocation case businessLocation
case businessHours case businessHours

View File

@ -103,6 +103,7 @@ swift_library(
"//submodules/TelegramUI/Components/Settings/PeerNameColorItem", "//submodules/TelegramUI/Components/Settings/PeerNameColorItem",
"//submodules/TelegramUI/Components/Settings/BirthdayPickerScreen", "//submodules/TelegramUI/Components/Settings/BirthdayPickerScreen",
"//submodules/Components/MultilineTextComponent", "//submodules/Components/MultilineTextComponent",
"//submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -210,6 +210,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
private var sharedOpenStoryProgressDisposable = MetaDisposable() private var sharedOpenStoryProgressDisposable = MetaDisposable()
var currentTooltipUpdateTimer: Foundation.Timer?
private var fullScreenEffectView: RippleEffectView? private var fullScreenEffectView: RippleEffectView?
public override func updateNavigationCustomData(_ data: Any?, progress: CGFloat, transition: ContainedViewLayoutTransition) { public override func updateNavigationCustomData(_ data: Any?, progress: CGFloat, transition: ContainedViewLayoutTransition) {
@ -3082,20 +3084,29 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
} }
}))) })))
// items.append(.action(ContextMenuActionItem(text: presentationData.strings.StoryFeed_ViewAnonymously, icon: { theme in items.append(.action(ContextMenuActionItem(text: presentationData.strings.StoryFeed_ViewAnonymously, icon: { theme in
// return generateTintedImage(image: UIImage(bundleImageName: self.context.isPremium ? "Chat/Context Menu/Eye" : "Chat/Context Menu/EyeLocked"), color: theme.contextMenu.primaryColor) return generateTintedImage(image: UIImage(bundleImageName: self.context.isPremium ? "Chat/Context Menu/Eye" : "Chat/Context Menu/EyeLocked"), color: theme.contextMenu.primaryColor)
// }, action: { [weak self] _, a in }, action: { [weak self] _, a in
// a(.default) a(.default)
//
// guard let self else { guard let self else {
// return return
// } }
// if self.context.isPremium { if self.context.isPremium {
//// self.sendMessageContext.requestStealthMode(view: self) self.requestStealthMode(openStory: { [weak self] presentTooltip in
// } else { guard let self else {
//// self.presentStealthModeUpgradeScreen() return
// } }
// }))) self.openStories(peerId: peer.id, completion: { storyController in
presentTooltip(storyController)
})
})
} else {
self.presentStealthModeUpgrade(action: { [weak self] in
self?.presentUpgradeStoriesScreen()
})
}
})))
let hideText: String let hideText: String
if self.location == .chatList(groupId: .archive) { if self.location == .chatList(groupId: .archive) {
@ -3985,6 +3996,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
} }
public func openStories(peerId: EnginePeer.Id) { public func openStories(peerId: EnginePeer.Id) {
self.openStories(peerId: peerId, completion: { _ in })
}
public func openStories(peerId: EnginePeer.Id, completion: @escaping (StoryContainerScreen) -> Void = { _ in }) {
if let navigationBarView = self.chatListDisplayNode.navigationBarView.view as? ChatListNavigationBar.View { if let navigationBarView = self.chatListDisplayNode.navigationBarView.view as? ChatListNavigationBar.View {
if navigationBarView.storiesUnlocked { if navigationBarView.storiesUnlocked {
self.shouldFixStorySubscriptionOrder = true self.shouldFixStorySubscriptionOrder = true
@ -4087,7 +4102,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
self.sharedOpenStoryProgressDisposable.set(nil) self.sharedOpenStoryProgressDisposable.set(nil)
componentView.storyPeerListView()?.setLoadingItem(peerId: peerId, signal: signal) componentView.storyPeerListView()?.setLoadingItem(peerId: peerId, signal: signal)
} }
} },
completion: completion
) )
return return

View File

@ -0,0 +1,162 @@
import Foundation
import Display
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import AccountContext
import StoryContainerScreen
import StoryStealthModeSheetScreen
import UndoUI
extension ChatListControllerImpl {
func requestStealthMode(openStory: @escaping (@escaping (StoryContainerScreen) -> Void) -> Void) {
let context = self.context
let _ = (context.engine.data.get(
TelegramEngine.EngineData.Item.Configuration.StoryConfigurationState(),
TelegramEngine.EngineData.Item.Configuration.App()
)
|> deliverOnMainQueue).start(next: { [weak self] config, appConfig in
guard let self else {
return
}
let timestamp = Int32(Date().timeIntervalSince1970)
if let activeUntilTimestamp = config.stealthModeState.actualizedNow().activeUntilTimestamp, activeUntilTimestamp > timestamp {
let remainingActiveSeconds = activeUntilTimestamp - timestamp
let presentationData = context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme)
let text = presentationData.strings.Story_ToastStealthModeActiveText(timeIntervalString(strings: presentationData.strings, value: remainingActiveSeconds)).string
let tooltipScreen = UndoOverlayController(
presentationData: presentationData,
content: .actionSucceeded(title: presentationData.strings.Story_ToastStealthModeActiveTitle, text: text, cancel: "", destructive: false),
elevatedLayout: false,
animateInAsReplacement: false,
action: { _ in
return false
}
)
tooltipScreen.tag = "no_auto_dismiss"
weak var tooltipScreenValue: UndoOverlayController? = tooltipScreen
self.currentTooltipUpdateTimer?.invalidate()
self.currentTooltipUpdateTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { [weak self] _ in
guard let self else {
return
}
guard let tooltipScreenValue else {
self.currentTooltipUpdateTimer?.invalidate()
self.currentTooltipUpdateTimer = nil
return
}
let timestamp = Int32(Date().timeIntervalSince1970)
let remainingActiveSeconds = max(1, activeUntilTimestamp - timestamp)
let presentationData = context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme)
let text = presentationData.strings.Story_ToastStealthModeActiveText(timeIntervalString(strings: presentationData.strings, value: remainingActiveSeconds)).string
tooltipScreenValue.content = .actionSucceeded(title: presentationData.strings.Story_ToastStealthModeActiveTitle, text: text, cancel: "", destructive: false)
})
openStory({ storyController in
storyController.presentExternalTooltip(tooltipScreen)
})
return
}
let pastPeriod: Int32
let futurePeriod: Int32
if let data = appConfig.data, let futurePeriodF = data["stories_stealth_future_period"] as? Double, let pastPeriodF = data["stories_stealth_past_period"] as? Double {
futurePeriod = Int32(futurePeriodF)
pastPeriod = Int32(pastPeriodF)
} else {
pastPeriod = 5 * 60
futurePeriod = 25 * 60
}
let sheet = StoryStealthModeSheetScreen(
context: context,
mode: .control(external: true, cooldownUntilTimestamp: config.stealthModeState.actualizedNow().cooldownUntilTimestamp),
forceDark: false,
backwardDuration: pastPeriod,
forwardDuration: futurePeriod,
buttonAction: {
let _ = (context.engine.messages.enableStoryStealthMode()
|> deliverOnMainQueue).start(completed: {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme)
let text = presentationData.strings.Story_ToastStealthModeActivatedText(timeIntervalString(strings: presentationData.strings, value: pastPeriod), timeIntervalString(strings: presentationData.strings, value: futurePeriod)).string
let tooltipScreen = UndoOverlayController(
presentationData: presentationData,
content: .actionSucceeded(title: presentationData.strings.Story_ToastStealthModeActivatedTitle, text: text, cancel: "", destructive: false),
elevatedLayout: false,
animateInAsReplacement: false,
action: { _ in
return false
}
)
openStory({ storyController in
storyController.presentExternalTooltip(tooltipScreen)
})
HapticFeedback().success()
})
}
)
self.push(sheet)
})
}
func presentStealthModeUpgrade(action: @escaping () -> Void) {
let context = self.context
let _ = (context.engine.data.get(
TelegramEngine.EngineData.Item.Configuration.StoryConfigurationState(),
TelegramEngine.EngineData.Item.Configuration.App()
)
|> deliverOnMainQueue).start(next: { [weak self] config, appConfig in
guard let self else {
return
}
let pastPeriod: Int32
let futurePeriod: Int32
if let data = appConfig.data, let futurePeriodF = data["stories_stealth_future_period"] as? Double, let pastPeriodF = data["stories_stealth_past_period"] as? Double {
futurePeriod = Int32(futurePeriodF)
pastPeriod = Int32(pastPeriodF)
} else {
pastPeriod = 5 * 60
futurePeriod = 25 * 60
}
let sheet = StoryStealthModeSheetScreen(
context: context,
mode: .upgrade,
forceDark: false,
backwardDuration: pastPeriod,
forwardDuration: futurePeriod,
buttonAction: {
action()
}
)
self.push(sheet)
})
}
func presentUpgradeStoriesScreen() {
let context = self.context
var replaceImpl: ((ViewController) -> Void)?
let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .stories, forceDark: false, action: {
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .storiesStealthMode, forceDark: false, dismissed: nil)
replaceImpl?(controller)
}, dismissed: nil)
replaceImpl = { [weak self, weak controller] c in
controller?.dismiss(animated: true, completion: {
guard let self else {
return
}
self.push(c)
})
}
self.push(controller)
}
}

View File

@ -1579,10 +1579,10 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi
}, },
openTagColorPremium: { openTagColorPremium: {
var replaceImpl: ((ViewController) -> Void)? var replaceImpl: ((ViewController) -> Void)?
let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .folderTags, action: { let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .folderTags, forceDark: false, action: {
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .folderTags, forceDark: false, dismissed: nil) let controller = context.sharedContext.makePremiumIntroController(context: context, source: .folderTags, forceDark: false, dismissed: nil)
replaceImpl?(controller) replaceImpl?(controller)
}) }, dismissed: nil)
replaceImpl = { [weak controller] c in replaceImpl = { [weak controller] c in
controller?.replace(with: c) controller?.replace(with: c)
} }

View File

@ -559,10 +559,10 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
context.engine.peers.updateChatListFiltersDisplayTags(isEnabled: value) context.engine.peers.updateChatListFiltersDisplayTags(isEnabled: value)
}, updateDisplayTagsLocked: { }, updateDisplayTagsLocked: {
var replaceImpl: ((ViewController) -> Void)? var replaceImpl: ((ViewController) -> Void)?
let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .folderTags, action: { let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .folderTags, forceDark: false, action: {
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .folderTags, forceDark: false, dismissed: nil) let controller = context.sharedContext.makePremiumIntroController(context: context, source: .folderTags, forceDark: false, dismissed: nil)
replaceImpl?(controller) replaceImpl?(controller)
}) }, dismissed: nil)
replaceImpl = { [weak controller] c in replaceImpl = { [weak controller] c in
controller?.replace(with: c) controller?.replace(with: c)
} }

View File

@ -1425,6 +1425,71 @@ public class PremiumDemoScreen: ViewControllerComponentContainer {
case businessChatBots case businessChatBots
case businessIntro case businessIntro
case businessLinks case businessLinks
public var perk: PremiumPerk {
switch self {
case .doubleLimits:
return .doubleLimits
case .moreUpload:
return .moreUpload
case .fasterDownload:
return .fasterDownload
case .voiceToText:
return .voiceToText
case .noAds:
return .noAds
case .uniqueReactions:
return .uniqueReactions
case .premiumStickers:
return .premiumStickers
case .advancedChatManagement:
return .advancedChatManagement
case .profileBadge:
return .profileBadge
case .animatedUserpics:
return .animatedUserpics
case .appIcons:
return .appIcons
case .animatedEmoji:
return .animatedEmoji
case .emojiStatus:
return .emojiStatus
case .translation:
return .translation
case .stories:
return .stories
case .colors:
return .colors
case .wallpapers:
return .wallpapers
case .messageTags:
return .messageTags
case .lastSeen:
return .lastSeen
case .messagePrivacy:
return .messagePrivacy
case .business:
return .business
case .folderTags:
return .folderTags
case .businessLocation:
return .businessLocation
case .businessHours:
return .businessHours
case .businessGreetingMessage:
return .businessGreetingMessage
case .businessQuickReplies:
return .businessQuickReplies
case .businessAwayMessage:
return .businessAwayMessage
case .businessChatBots:
return .businessChatBots
case .businessIntro:
return .businessIntro
case .businessLinks:
return .businessLinks
}
}
} }
public enum Source: Equatable { public enum Source: Equatable {

View File

@ -369,7 +369,7 @@ private final class RecentActionsSettingsSheetComponent: Component {
) )
} }
private func updateScrolling(transition: Transition) { private func updateScrolling(isFirstTime: Bool = false, transition: Transition) {
guard let environment = self.environment, let controller = environment.controller(), let itemLayout = self.itemLayout else { guard let environment = self.environment, let controller = environment.controller(), let itemLayout = self.itemLayout else {
return return
} }
@ -389,16 +389,23 @@ private final class RecentActionsSettingsSheetComponent: Component {
var topOffsetFraction = topOffset / topOffsetDistance var topOffsetFraction = topOffset / topOffsetDistance
topOffsetFraction = max(0.0, min(1.0, topOffsetFraction)) topOffsetFraction = max(0.0, min(1.0, topOffsetFraction))
let modalStyleOverlayTransition: ContainedViewLayoutTransition
if isFirstTime {
modalStyleOverlayTransition = .animated(duration: 0.4, curve: .spring)
} else {
modalStyleOverlayTransition = transition.containedViewLayoutTransition
}
let transitionFactor: CGFloat = 1.0 - topOffsetFraction let transitionFactor: CGFloat = 1.0 - topOffsetFraction
if self.isUpdating { if self.isUpdating {
DispatchQueue.main.async { [weak controller] in DispatchQueue.main.async { [weak controller] in
guard let controller else { guard let controller else {
return return
} }
controller.updateModalStyleOverlayTransitionFactor(transitionFactor, transition: transition.containedViewLayoutTransition) controller.updateModalStyleOverlayTransitionFactor(transitionFactor, transition: modalStyleOverlayTransition)
} }
} else { } else {
controller.updateModalStyleOverlayTransitionFactor(transitionFactor, transition: transition.containedViewLayoutTransition) controller.updateModalStyleOverlayTransitionFactor(transitionFactor, transition: modalStyleOverlayTransition)
} }
} }
@ -444,7 +451,9 @@ private final class RecentActionsSettingsSheetComponent: Component {
let sideInset: CGFloat = 16.0 + environment.safeInsets.left let sideInset: CGFloat = 16.0 + environment.safeInsets.left
var isFirstTime = false
if self.component == nil { if self.component == nil {
isFirstTime = true
self.selectedMembersActions = Set(MembersActionType.actionTypesFromFlags(component.initialValue.events)) self.selectedMembersActions = Set(MembersActionType.actionTypesFromFlags(component.initialValue.events))
self.selectedSettingsActions = Set(SettingsActionType.actionTypesFromFlags(component.initialValue.events)) self.selectedSettingsActions = Set(SettingsActionType.actionTypesFromFlags(component.initialValue.events))
self.selectedMessagesActions = Set(MessagesActionType.actionTypesFromFlags(component.initialValue.events)) self.selectedMessagesActions = Set(MessagesActionType.actionTypesFromFlags(component.initialValue.events))
@ -924,7 +933,7 @@ private final class RecentActionsSettingsSheetComponent: Component {
} }
} }
self.ignoreScrolling = false self.ignoreScrolling = false
self.updateScrolling(transition: transition) self.updateScrolling(isFirstTime: isFirstTime, transition: transition)
return availableSize return availableSize
} }

View File

@ -625,10 +625,10 @@ public final class AdsReportScreen: ViewControllerComponentContainer {
}) })
case .premiumRequired: case .premiumRequired:
var replaceImpl: ((ViewController) -> Void)? var replaceImpl: ((ViewController) -> Void)?
let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .noAds, action: { let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .noAds, forceDark: false, action: {
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .ads, forceDark: false, dismissed: nil) let controller = context.sharedContext.makePremiumIntroController(context: context, source: .ads, forceDark: false, dismissed: nil)
replaceImpl?(controller) replaceImpl?(controller)
}) }, dismissed: nil)
replaceImpl = { [weak controller] c in replaceImpl = { [weak controller] c in
controller?.replace(with: c) controller?.replace(with: c)
} }

View File

@ -370,10 +370,10 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
let tipController = UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_voiceToText", scale: 0.065, colors: [:], title: nil, text: presentationData.strings.Message_AudioTranscription_SubscribeToPremium, customUndoText: presentationData.strings.Message_AudioTranscription_SubscribeToPremiumAction, timeout: nil), elevatedLayout: false, position: .top, animateInAsReplacement: false, action: { action in let tipController = UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_voiceToText", scale: 0.065, colors: [:], title: nil, text: presentationData.strings.Message_AudioTranscription_SubscribeToPremium, customUndoText: presentationData.strings.Message_AudioTranscription_SubscribeToPremiumAction, timeout: nil), elevatedLayout: false, position: .top, animateInAsReplacement: false, action: { action in
if case .undo = action { if case .undo = action {
var replaceImpl: ((ViewController) -> Void)? var replaceImpl: ((ViewController) -> Void)?
let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .voiceToText, action: { let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .voiceToText, forceDark: false, action: {
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .settings, forceDark: false, dismissed: nil) let controller = context.sharedContext.makePremiumIntroController(context: context, source: .settings, forceDark: false, dismissed: nil)
replaceImpl?(controller) replaceImpl?(controller)
}) }, dismissed: nil)
replaceImpl = { [weak controller] c in replaceImpl = { [weak controller] c in
controller?.replace(with: c) controller?.replace(with: c)
} }
@ -537,10 +537,10 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
let tipController = UndoOverlayController(presentationData: presentationData, content: .universal(animation: "Transcribe", scale: 0.06, colors: [:], title: nil, text: text, customUndoText: nil, timeout: timeout), elevatedLayout: false, position: .top, animateInAsReplacement: false, action: { action in let tipController = UndoOverlayController(presentationData: presentationData, content: .universal(animation: "Transcribe", scale: 0.06, colors: [:], title: nil, text: text, customUndoText: nil, timeout: timeout), elevatedLayout: false, position: .top, animateInAsReplacement: false, action: { action in
if case .info = action { if case .info = action {
var replaceImpl: ((ViewController) -> Void)? var replaceImpl: ((ViewController) -> Void)?
let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .voiceToText, action: { let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .voiceToText, forceDark: false, action: {
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .settings, forceDark: false, dismissed: nil) let controller = context.sharedContext.makePremiumIntroController(context: context, source: .settings, forceDark: false, dismissed: nil)
replaceImpl?(controller) replaceImpl?(controller)
}) }, dismissed: nil)
replaceImpl = { [weak controller] c in replaceImpl = { [weak controller] c in
controller?.replace(with: c) controller?.replace(with: c)
} }

View File

@ -1829,10 +1829,10 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
if case .undo = action { if case .undo = action {
let context = item.context let context = item.context
var replaceImpl: ((ViewController) -> Void)? var replaceImpl: ((ViewController) -> Void)?
let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .voiceToText, action: { let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .voiceToText, forceDark: false, action: {
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .settings, forceDark: false, dismissed: nil) let controller = context.sharedContext.makePremiumIntroController(context: context, source: .settings, forceDark: false, dismissed: nil)
replaceImpl?(controller) replaceImpl?(controller)
}) }, dismissed: nil)
replaceImpl = { [weak controller] c in replaceImpl = { [weak controller] c in
controller?.replace(with: c) controller?.replace(with: c)
} }
@ -1941,10 +1941,10 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
let tipController = UndoOverlayController(presentationData: presentationData, content: .universal(animation: "Transcribe", scale: 0.06, colors: [:], title: nil, text: text, customUndoText: nil, timeout: timeout), elevatedLayout: false, position: .top, animateInAsReplacement: false, action: { action in let tipController = UndoOverlayController(presentationData: presentationData, content: .universal(animation: "Transcribe", scale: 0.06, colors: [:], title: nil, text: text, customUndoText: nil, timeout: timeout), elevatedLayout: false, position: .top, animateInAsReplacement: false, action: { action in
if case .info = action { if case .info = action {
var replaceImpl: ((ViewController) -> Void)? var replaceImpl: ((ViewController) -> Void)?
let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .voiceToText, action: { let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .voiceToText, forceDark: false, action: {
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .settings, forceDark: false, dismissed: nil) let controller = context.sharedContext.makePremiumIntroController(context: context, source: .settings, forceDark: false, dismissed: nil)
replaceImpl?(controller) replaceImpl?(controller)
}) }, dismissed: nil)
replaceImpl = { [weak controller] c in replaceImpl = { [weak controller] c in
controller?.replace(with: c) controller?.replace(with: c)
} }

View File

@ -2244,7 +2244,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable {
} }
} }
private let deletedMessagesDisplayedLimit = 5 private let deletedMessagesDisplayedLimit = 4
func chatRecentActionsEntries(entries: [ChannelAdminEventLogEntry], presentationData: ChatPresentationData, expandedDeletedMessages: Set<EngineMessage.Id>) -> [ChatRecentActionsEntry] { func chatRecentActionsEntries(entries: [ChannelAdminEventLogEntry], presentationData: ChatPresentationData, expandedDeletedMessages: Set<EngineMessage.Id>) -> [ChatRecentActionsEntry] {
var result: [ChatRecentActionsEntry] = [] var result: [ChatRecentActionsEntry] = []
@ -2252,7 +2252,7 @@ func chatRecentActionsEntries(entries: [ChannelAdminEventLogEntry], presentation
func appendCurrentDeleteEntries() { func appendCurrentDeleteEntries() {
if !deleteMessageEntries.isEmpty, let lastEntry = deleteMessageEntries.last, let lastMessageId = lastEntry.event.action.messageId { if !deleteMessageEntries.isEmpty, let lastEntry = deleteMessageEntries.last, let lastMessageId = lastEntry.event.action.messageId {
let isExpandable = deleteMessageEntries.count > deletedMessagesDisplayedLimit let isExpandable = deleteMessageEntries.count >= deletedMessagesDisplayedLimit
let isExpanded = expandedDeletedMessages.contains(lastMessageId) || !isExpandable let isExpanded = expandedDeletedMessages.contains(lastMessageId) || !isExpandable
let isGroup = deleteMessageEntries.count > 1 let isGroup = deleteMessageEntries.count > 1

View File

@ -726,10 +726,10 @@ public func PeerNameColorScreen(
action: { action in action: { action in
if case .info = action { if case .info = action {
var replaceImpl: ((ViewController) -> Void)? var replaceImpl: ((ViewController) -> Void)?
let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .colors, action: { let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .colors, forceDark: false, action: {
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .settings, forceDark: false, dismissed: nil) let controller = context.sharedContext.makePremiumIntroController(context: context, source: .settings, forceDark: false, dismissed: nil)
replaceImpl?(controller) replaceImpl?(controller)
}) }, dismissed: nil)
replaceImpl = { [weak controller] c in replaceImpl = { [weak controller] c in
controller?.replace(with: c) controller?.replace(with: c)
} }

View File

@ -527,10 +527,10 @@ public class WallpaperGalleryController: ViewController {
if forBoth && !strongSelf.context.isPremium { if forBoth && !strongSelf.context.isPremium {
let context = strongSelf.context let context = strongSelf.context
var replaceImpl: ((ViewController) -> Void)? var replaceImpl: ((ViewController) -> Void)?
let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .wallpapers, action: { let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .wallpapers, forceDark: false, action: {
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .wallpapers, forceDark: false, dismissed: nil) let controller = context.sharedContext.makePremiumIntroController(context: context, source: .wallpapers, forceDark: false, dismissed: nil)
replaceImpl?(controller) replaceImpl?(controller)
}) }, dismissed: nil)
replaceImpl = { [weak controller] c in replaceImpl = { [weak controller] c in
controller?.replace(with: c) controller?.replace(with: c)
} }

View File

@ -170,7 +170,8 @@ public extension StoryContainerScreen {
transitionIn: @escaping () -> StoryContainerScreen.TransitionIn?, transitionIn: @escaping () -> StoryContainerScreen.TransitionIn?,
transitionOut: @escaping (EnginePeer.Id) -> StoryContainerScreen.TransitionOut?, transitionOut: @escaping (EnginePeer.Id) -> StoryContainerScreen.TransitionOut?,
setFocusedItem: @escaping (Signal<StoryId?, NoError>) -> Void, setFocusedItem: @escaping (Signal<StoryId?, NoError>) -> Void,
setProgress: @escaping (Signal<Never, NoError>) -> Void setProgress: @escaping (Signal<Never, NoError>) -> Void,
completion: @escaping (StoryContainerScreen) -> Void = { _ in }
) { ) {
let storyContent = StoryContentContextImpl(context: context, isHidden: isHidden, focusedPeerId: peerId, singlePeer: singlePeer, fixedOrder: initialOrder) let storyContent = StoryContentContextImpl(context: context, isHidden: isHidden, focusedPeerId: peerId, singlePeer: singlePeer, fixedOrder: initialOrder)
let signal = storyContent.state let signal = storyContent.state
@ -211,6 +212,7 @@ public extension StoryContainerScreen {
) )
setFocusedItem(storyContainerScreen.focusedItem) setFocusedItem(storyContainerScreen.focusedItem)
parentController?.push(storyContainerScreen) parentController?.push(storyContainerScreen)
completion(storyContainerScreen)
} }
|> ignoreValues |> ignoreValues

View File

@ -1223,6 +1223,16 @@ private final class StoryContainerScreenComponent: Component {
} }
} }
func presentExternalTooltip(_ tooltipScreen: UndoOverlayController) {
guard let stateValue = self.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id], let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View else {
return
}
itemSetComponentView.sendMessageContext.tooltipScreen = tooltipScreen
itemSetComponentView.updateIsProgressPaused()
self.environment?.controller()?.present(tooltipScreen, in: .current)
}
func update(component: StoryContainerScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: Transition) -> CGSize { func update(component: StoryContainerScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: Transition) -> CGSize {
if self.didAnimateOut { if self.didAnimateOut {
return availableSize return availableSize
@ -2057,6 +2067,12 @@ public class StoryContainerScreen: ViewControllerComponentContainer {
} }
} }
public func presentExternalTooltip(_ tooltipScreen: UndoOverlayController) {
if let componentView = self.node.hostView.componentView as? StoryContainerScreenComponent.View {
componentView.presentExternalTooltip(tooltipScreen)
}
}
func dismissWithoutTransitionOut() { func dismissWithoutTransitionOut() {
self.focusedItemPromise.set(.single(nil)) self.focusedItemPromise.set(.single(nil))

View File

@ -5757,22 +5757,27 @@ public final class StoryItemSetContainerComponent: Component {
}) })
} }
private func presentStoriesUpgradeScreen(source: PremiumSource) { private func presentStoriesUpgradeScreen(source: PremiumIntroSource) {
guard let component = self.component else { guard let component = self.component else {
return return
} }
let context = component.context let context = component.context
var replaceImpl: ((ViewController) -> Void)? var replaceImpl: ((ViewController) -> Void)?
let controller = PremiumLimitsListScreen(context: context, subject: .stories, source: .other, order: [.stories], buttonText: component.strings.Story_PremiumUpgradeStoriesButton, isPremium: false, forceDark: true) var dismissedImpl: (() -> Void)?
controller.action = { [weak self] in let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .stories, forceDark: true, action: { [weak self] in
guard let self else { guard let self else {
return return
} }
let controller = PremiumIntroScreen(context: context, source: source, forceDark: true) var dismissedImpl: (() -> Void)?
let controller = context.sharedContext.makePremiumIntroController(context: context, source: source, forceDark: true, dismissed: {
dismissedImpl?()
})
self.sendMessageContext.actionSheet = controller self.sendMessageContext.actionSheet = controller
controller.wasDismissed = { [weak self, weak controller]in replaceImpl?(controller)
dismissedImpl = { [weak self, weak controller] in
guard let self else { guard let self else {
return return
} }
@ -5782,10 +5787,10 @@ public final class StoryItemSetContainerComponent: Component {
} }
self.updateIsProgressPaused() self.updateIsProgressPaused()
} }
}, dismissed: {
replaceImpl?(controller) dismissedImpl?()
} })
controller.disposed = { [weak self, weak controller] in dismissedImpl = { [weak self, weak controller] in
guard let self else { guard let self else {
return return
} }

View File

@ -3186,7 +3186,8 @@ final class StoryItemSetContainerSendMessage {
let sheet = StoryStealthModeSheetScreen( let sheet = StoryStealthModeSheetScreen(
context: component.context, context: component.context,
mode: .control(cooldownUntilTimestamp: config.stealthModeState.actualizedNow().cooldownUntilTimestamp), mode: .control(external: false, cooldownUntilTimestamp: config.stealthModeState.actualizedNow().cooldownUntilTimestamp),
forceDark: true,
backwardDuration: pastPeriod, backwardDuration: pastPeriod,
forwardDuration: futurePeriod, forwardDuration: futurePeriod,
buttonAction: { [weak self, weak view] in buttonAction: { [weak self, weak view] in
@ -3261,6 +3262,7 @@ final class StoryItemSetContainerSendMessage {
let sheet = StoryStealthModeSheetScreen( let sheet = StoryStealthModeSheetScreen(
context: component.context, context: component.context,
mode: .upgrade, mode: .upgrade,
forceDark: true,
backwardDuration: pastPeriod, backwardDuration: pastPeriod,
forwardDuration: futurePeriod, forwardDuration: futurePeriod,
buttonAction: { buttonAction: {

View File

@ -100,7 +100,7 @@ private final class StoryStealthModeSheetContentComponent: Component {
self.state = state self.state = state
var remainingCooldownSeconds: Int32 = 0 var remainingCooldownSeconds: Int32 = 0
if case let .control(cooldownUntilTimestamp) = component.mode { if case let .control(_, cooldownUntilTimestamp) = component.mode {
if let cooldownUntilTimestamp { if let cooldownUntilTimestamp {
remainingCooldownSeconds = cooldownUntilTimestamp - Int32(Date().timeIntervalSince1970) remainingCooldownSeconds = cooldownUntilTimestamp - Int32(Date().timeIntervalSince1970)
remainingCooldownSeconds = max(0, remainingCooldownSeconds) remainingCooldownSeconds = max(0, remainingCooldownSeconds)
@ -243,9 +243,9 @@ private final class StoryStealthModeSheetContentComponent: Component {
let buttonText: String let buttonText: String
let content: AnyComponentWithIdentity<Empty> let content: AnyComponentWithIdentity<Empty>
switch component.mode { switch component.mode {
case .control: case let .control(external, _):
if remainingCooldownSeconds <= 0 { if remainingCooldownSeconds <= 0 {
buttonText = environment.strings.Story_StealthMode_EnableAction buttonText = external ? environment.strings.Story_StealthMode_EnableAndOpenAction : environment.strings.Story_StealthMode_EnableAction
} else { } else {
buttonText = environment.strings.Story_StealthMode_CooldownAction(stringForDuration(remainingCooldownSeconds)).string buttonText = environment.strings.Story_StealthMode_CooldownAction(stringForDuration(remainingCooldownSeconds)).string
} }
@ -283,7 +283,7 @@ private final class StoryStealthModeSheetContentComponent: Component {
} }
switch component.mode { switch component.mode {
case let .control(cooldownUntilTimestamp): case let .control(_, cooldownUntilTimestamp):
var remainingCooldownSeconds: Int32 = 0 var remainingCooldownSeconds: Int32 = 0
if let cooldownUntilTimestamp { if let cooldownUntilTimestamp {
remainingCooldownSeconds = cooldownUntilTimestamp - Int32(Date().timeIntervalSince1970) remainingCooldownSeconds = cooldownUntilTimestamp - Int32(Date().timeIntervalSince1970)
@ -468,13 +468,14 @@ private final class StoryStealthModeSheetScreenComponent: Component {
public class StoryStealthModeSheetScreen: ViewControllerComponentContainer { public class StoryStealthModeSheetScreen: ViewControllerComponentContainer {
public enum Mode: Equatable { public enum Mode: Equatable {
case control(cooldownUntilTimestamp: Int32?) case control(external: Bool, cooldownUntilTimestamp: Int32?)
case upgrade case upgrade
} }
public init( public init(
context: AccountContext, context: AccountContext,
mode: Mode, mode: Mode,
forceDark: Bool,
backwardDuration: Int32, backwardDuration: Int32,
forwardDuration: Int32, forwardDuration: Int32,
buttonAction: (() -> Void)? = nil buttonAction: (() -> Void)? = nil
@ -485,7 +486,7 @@ public class StoryStealthModeSheetScreen: ViewControllerComponentContainer {
backwardDuration: backwardDuration, backwardDuration: backwardDuration,
forwardDuration: forwardDuration, forwardDuration: forwardDuration,
buttonAction: buttonAction buttonAction: buttonAction
), navigationBarAppearance: .none, theme: .dark) ), navigationBarAppearance: .none, theme: forceDark ? .dark : .default)
self.statusBar.statusBarStyle = .Ignore self.statusBar.statusBarStyle = .Ignore
self.navigationPresentation = .flatModal self.navigationPresentation = .flatModal

View File

@ -11930,10 +11930,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
case .info: case .info:
let context = self.context let context = self.context
var replaceImpl: ((ViewController) -> Void)? var replaceImpl: ((ViewController) -> Void)?
let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .fasterDownload, action: { let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .fasterDownload, forceDark: false, action: {
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .fasterDownload, forceDark: false, dismissed: nil) let controller = context.sharedContext.makePremiumIntroController(context: context, source: .fasterDownload, forceDark: false, dismissed: nil)
replaceImpl?(controller) replaceImpl?(controller)
}) }, dismissed: nil)
replaceImpl = { [weak controller] c in replaceImpl = { [weak controller] c in
controller?.replace(with: c) controller?.replace(with: c)
} }

View File

@ -591,10 +591,10 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
}, iconSource: nil, action: { c, _ in }, iconSource: nil, action: { c, _ in
c.dismiss(completion: { c.dismiss(completion: {
var replaceImpl: ((ViewController) -> Void)? var replaceImpl: ((ViewController) -> Void)?
let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .noAds, action: { let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .noAds, forceDark: false, action: {
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .ads, forceDark: false, dismissed: nil) let controller = context.sharedContext.makePremiumIntroController(context: context, source: .ads, forceDark: false, dismissed: nil)
replaceImpl?(controller) replaceImpl?(controller)
}) }, dismissed: nil)
replaceImpl = { [weak controller] c in replaceImpl = { [weak controller] c in
controller?.replace(with: c) controller?.replace(with: c)
} }
@ -1088,10 +1088,10 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
}, action: { _, f in }, action: { _, f in
let context = context let context = context
var replaceImpl: ((ViewController) -> Void)? var replaceImpl: ((ViewController) -> Void)?
let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .fasterDownload, action: { let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .fasterDownload, forceDark: false, action: {
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .fasterDownload, forceDark: false, dismissed: nil) let controller = context.sharedContext.makePremiumIntroController(context: context, source: .fasterDownload, forceDark: false, dismissed: nil)
replaceImpl?(controller) replaceImpl?(controller)
}) }, dismissed: nil)
replaceImpl = { [weak controller] c in replaceImpl = { [weak controller] c in
controller?.replace(with: c) controller?.replace(with: c)
} }

View File

@ -2062,7 +2062,9 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return controller return controller
} }
public func makePremiumDemoController(context: AccountContext, subject: PremiumDemoSubject, action: @escaping () -> Void) -> ViewController { public func makePremiumDemoController(context: AccountContext, subject: PremiumDemoSubject, forceDark: Bool, action: @escaping () -> Void, dismissed: (() -> Void)?) -> ViewController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var buttonText: String = presentationData.strings.Common_OK
let mappedSubject: PremiumDemoScreen.Subject let mappedSubject: PremiumDemoScreen.Subject
switch subject { switch subject {
case .doubleLimits: case .doubleLimits:
@ -2095,6 +2097,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
mappedSubject = .translation mappedSubject = .translation
case .stories: case .stories:
mappedSubject = .stories mappedSubject = .stories
buttonText = presentationData.strings.Story_PremiumUpgradeStoriesButton
case .colors: case .colors:
mappedSubject = .colors mappedSubject = .colors
case .wallpapers: case .wallpapers:
@ -2107,11 +2110,25 @@ public final class SharedAccountContextImpl: SharedAccountContext {
mappedSubject = .messagePrivacy mappedSubject = .messagePrivacy
case .folderTags: case .folderTags:
mappedSubject = .folderTags mappedSubject = .folderTags
case .business:
mappedSubject = .business
buttonText = presentationData.strings.Chat_EmptyStateIntroFooterPremiumActionButton
default: default:
mappedSubject = .doubleLimits mappedSubject = .doubleLimits
} }
switch mappedSubject {
case .stories, .business, .doubleLimits:
let controller = PremiumLimitsListScreen(context: context, subject: mappedSubject, source: .other, order: [mappedSubject.perk], buttonText: buttonText, isPremium: false, forceDark: forceDark)
controller.action = action
if let dismissed {
controller.disposed = dismissed
}
return controller
default:
return PremiumDemoScreen(context: context, subject: mappedSubject, action: action) return PremiumDemoScreen(context: context, subject: mappedSubject, action: action)
} }
}
public func makePremiumLimitController(context: AccountContext, subject: PremiumLimitSubject, count: Int32, forceDark: Bool, cancel: @escaping () -> Void, action: @escaping () -> Bool) -> ViewController { public func makePremiumLimitController(context: AccountContext, subject: PremiumLimitSubject, count: Int32, forceDark: Bool, cancel: @escaping () -> Void, action: @escaping () -> Bool) -> ViewController {
let mappedSubject: PremiumLimitScreen.Subject let mappedSubject: PremiumLimitScreen.Subject