diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index f1ae79757f..e02d55ccbb 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -9414,6 +9414,7 @@ Sorry for the inconvenience."; "ChatList.ContextSelectChats" = "Select Chats"; "StoryFeed.TooltipPremiumPosting" = "Posting stories is currently available only\nto subscribers of [Telegram Premium]()."; +"StoryFeed.TooltipPremiumPostingLimited" = "This month, posting stories is only available to subscribers of [Telegram Premium]()."; "StoryFeed.TooltipStoryLimitValue_1" = "1 story"; "StoryFeed.TooltipStoryLimitValue_any" = "%d stories"; "StoryFeed.TooltipStoryLimit" = "You can't post more than **%@** stories in **24 hours**."; @@ -9623,7 +9624,7 @@ Sorry for the inconvenience."; "Story.Editor.ExpirationValue_1" = "1 Hour"; "Story.Editor.ExpirationValue_any" = "%d Hours"; -"Story.Editor.TooltipPremiumExpiration" = "Subscribe to **Telegram Premium** to make your stories disappear after 6, 12 or 48 hours."; +"Story.Editor.TooltipPremiumExpiration" = "Subscribe to [Telegram Premium]() to make your stories disappear after 6, 12 or 48 hours."; "Story.Editor.InputPlaceholderAddCaption" = "Add a caption..."; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index bdd233c464..89ae9dd065 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -894,7 +894,7 @@ public protocol SharedAccountContext: AnyObject { func makeChatQrCodeScreen(context: AccountContext, peer: Peer, threadId: Int64?) -> ViewController - func makePremiumIntroController(context: AccountContext, source: PremiumIntroSource, forceDark: Bool) -> ViewController + func makePremiumIntroController(context: AccountContext, source: PremiumIntroSource, forceDark: Bool, dismissed: (() -> Void)?) -> ViewController func makePremiumDemoController(context: AccountContext, subject: PremiumDemoSubject, action: @escaping () -> Void) -> ViewController func makePremiumLimitController(context: AccountContext, subject: PremiumLimitSubject, count: Int32, action: @escaping () -> Void) -> ViewController diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 2e0a40829d..f2ac8c6fbd 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -2612,7 +2612,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController let text: String if premiumNeeded { - text = self.presentationData.strings.StoryFeed_TooltipPremiumPosting + text = self.presentationData.strings.StoryFeed_TooltipPremiumPostingLimited } else if reachedCountLimit { let valueText = self.presentationData.strings.StoryFeed_TooltipStoryLimitValue(Int32(storiesCountLimit)) text = self.presentationData.strings.StoryFeed_TooltipStoryLimit(valueText).string @@ -2634,7 +2634,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController location: .point(location, .top), shouldDismissOnTouch: { [weak self] point, containerFrame in if containerFrame.contains(point), premiumNeeded { - let controller = context.sharedContext.makePremiumIntroController(context: context, source: .stories, forceDark: false) + let controller = context.sharedContext.makePremiumIntroController(context: context, source: .stories, forceDark: false, dismissed: nil) self?.push(controller) return .dismiss(consume: true) } else { diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index c9fd03b977..bc55fda784 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -1562,7 +1562,7 @@ public final class ChatListNode: ListView { let _ = dismissServerProvidedSuggestion(account: self.context.account, suggestion: .restorePremium).start() } } - let controller = self.context.sharedContext.makePremiumIntroController(context: self.context, source: .ads, forceDark: false) + let controller = self.context.sharedContext.makePremiumIntroController(context: self.context, source: .ads, forceDark: false, dismissed: nil) self.push?(controller) }, openChatFolderUpdates: { [weak self] in guard let self else { diff --git a/submodules/DrawingUI/Sources/StickerPickerScreen.swift b/submodules/DrawingUI/Sources/StickerPickerScreen.swift index c86c9d53c1..6410f5d443 100644 --- a/submodules/DrawingUI/Sources/StickerPickerScreen.swift +++ b/submodules/DrawingUI/Sources/StickerPickerScreen.swift @@ -163,10 +163,22 @@ private final class StickerSelectionComponent: Component { }, sendEmoji: { _, _, _ in }, - sendGif: { _, _, _, _, _ in + sendGif: { [weak self] file, _, _, _, _ in + if let self, let controller = self.component?.getController() { + controller.completion(.video(file.media)) + controller.dismiss(animated: true) + } return false }, - sendBotContextResultAsGif: { _, _, _, _, _, _ in + sendBotContextResultAsGif: { [weak self] collection, result, _, _, _, _ in + if let self, let controller = self.component?.getController() { + if case let .internalReference(reference) = result { + if let file = reference.file { + controller.completion(.video(file)) + controller.dismiss(animated: true) + } + } + } return false }, updateChoosingSticker: { _ in }, @@ -279,8 +291,8 @@ private final class StickerSelectionComponent: Component { topPanelScrollingOffset: { [weak self] offset, transition in if let self { if self.ignoreNextZeroScrollingOffset && offset == 0.0 { - self.ignoreNextZeroScrollingOffset = false } else { + self.ignoreNextZeroScrollingOffset = false self.topPanelScrollingOffset = offset } } @@ -320,11 +332,17 @@ private final class StickerSelectionComponent: Component { inputNodeInteraction: inputNodeInteraction, mode: mappedMode, stickerActionTitle: presentationData.strings.StickerPack_AddSticker, - trendingGifsPromise: Promise(nil), + trendingGifsPromise: self.component?.getController()?.node.trendingGifsPromise ?? Promise(nil), cancel: { }, peekBehavior: stickerPeekBehavior ) + searchContainerNode.openGifContextMenu = { [weak self] item, sourceNode, sourceRect, gesture, isSaved in + guard let self, let node = self.component?.getController()?.node else { + return + } + node.openGifContextMenu(file: item.file, contextResult: item.contextResult, sourceView: sourceNode.view, sourceRect: sourceRect, gesture: gesture, isSaved: isSaved) + } return searchContainerNode }, contentIdUpdated: { [weak self] id in @@ -416,7 +434,7 @@ public class StickerPickerScreen: ViewController { private var content: StickerPickerInputData? private let contentDisposable = MetaDisposable() private var hasRecentGifsDisposable: Disposable? - private let trendingGifsPromise = Promise(nil) + fileprivate let trendingGifsPromise = Promise(nil) private var scheduledEmojiContentAnimationHint: EmojiPagerContentComponent.ContentAnimation? private(set) var isExpanded = false @@ -510,10 +528,105 @@ public class StickerPickerScreen: ViewController { self?.controller?.presentLocationPicker() } + let gifItems: Signal + if controller.hasGifs { + let hasRecentGifs = context.engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: Namespaces.OrderedItemList.CloudRecentGifs)) + |> map { savedGifs -> Bool in + return !savedGifs.isEmpty + } + + self.hasRecentGifsDisposable = (hasRecentGifs + |> deliverOnMainQueue).start(next: { [weak self] hasRecentGifs in + guard let strongSelf = self else { + return + } + + if let gifMode = strongSelf.gifMode { + if !hasRecentGifs, case .recent = gifMode { + strongSelf.gifMode = .trending + } + } else { + strongSelf.gifMode = hasRecentGifs ? .recent : .trending + } + }) + + self.trendingGifsPromise.set(.single(nil)) + self.trendingGifsPromise.set(paneGifSearchForQuery(context: context, query: "", offset: nil, incompleteResults: true, delayRequest: false, updateActivity: nil) + |> map { items -> ChatMediaInputGifPaneTrendingState? in + if let items = items { + return ChatMediaInputGifPaneTrendingState(files: items.files, nextOffset: items.nextOffset) + } else { + return nil + } + }) + + let gifInputInteraction = GifPagerContentComponent.InputInteraction( + performItemAction: { [weak self] item, view, rect in + guard let self else { + return + } + self.controller?.completion(.video(item.file.media)) + self.controller?.dismiss(animated: true) + }, + openGifContextMenu: { [weak self] item, sourceView, sourceRect, gesture, isSaved in + guard let self else { + return + } + self.openGifContextMenu(file: item.file, contextResult: item.contextResult, sourceView: sourceView, sourceRect: sourceRect, gesture: gesture, isSaved: isSaved) + }, + loadMore: { [weak self] token in + guard let strongSelf = self, let gifContext = strongSelf.gifContext else { + return + } + gifContext.loadMore(token: token) + }, + openSearch: { [weak self] in + if let self, let componentView = self.hostView.componentView as? StickerSelectionComponent.View { + if let pagerView = componentView.keyboardView.view as? EntityKeyboardComponent.View { + pagerView.openSearch() + } + self.update(isExpanded: true, transition: .animated(duration: 0.4, curve: .spring)) + } + }, + updateSearchQuery: { [weak self] query in + guard let self else { + return + } + if let query { + self.gifMode = .emojiSearch(query) + } else { + self.gifMode = .recent + } + }, + hideBackground: true, + hasSearch: true + ) + self.gifInputInteraction = gifInputInteraction + + gifItems = .single(EntityKeyboardGifContent( + hasRecentGifs: true, + component: GifPagerContentComponent( + context: context, + inputInteraction: gifInputInteraction, + subject: .recent, + items: [], + isLoading: false, + loadMoreToken: nil, + displaySearchWithPlaceholder: nil, + searchCategories: nil, + searchInitiallyHidden: true, + searchState: .empty(hasResults: false), + hideBackground: true + ) + )) + } else { + gifItems = .single(nil) + } + let data = combineLatest( queue: Queue.mainQueue(), controller.inputData, - .single(nil) |> then(self.gifComponent.get() |> map(Optional.init)), + gifItems |> then(self.gifComponent.get() |> map(Optional.init)), self.stickerSearchState.get(), self.emojiSearchState.get() ) @@ -561,70 +674,6 @@ public class StickerPickerScreen: ViewController { strongSelf.updateContent(inputData) } })) - - if controller.hasGifs { - let hasRecentGifs = context.engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: Namespaces.OrderedItemList.CloudRecentGifs)) - |> map { savedGifs -> Bool in - return !savedGifs.isEmpty - } - - self.hasRecentGifsDisposable = (hasRecentGifs - |> deliverOnMainQueue).start(next: { [weak self] hasRecentGifs in - guard let strongSelf = self else { - return - } - - if let gifMode = strongSelf.gifMode { - if !hasRecentGifs, case .recent = gifMode { - strongSelf.gifMode = .trending - } - } else { - strongSelf.gifMode = hasRecentGifs ? .recent : .trending - } - }) - } - - self.gifInputInteraction = GifPagerContentComponent.InputInteraction( - performItemAction: { [weak self] item, view, rect in - guard let self else { - return - } - self.controller?.completion(.video(item.file.media)) - self.controller?.dismiss(animated: true) - }, - openGifContextMenu: { [weak self] item, sourceView, sourceRect, gesture, isSaved in - guard let self else { - return - } - self.openGifContextMenu(file: item.file, contextResult: item.contextResult, sourceView: sourceView, sourceRect: sourceRect, gesture: gesture, isSaved: isSaved) - }, - loadMore: { [weak self] token in - guard let strongSelf = self, let gifContext = strongSelf.gifContext else { - return - } - gifContext.loadMore(token: token) - }, - openSearch: { [weak self] in - if let self, let componentView = self.hostView.componentView as? StickerSelectionComponent.View { - if let pagerView = componentView.keyboardView.view as? EntityKeyboardComponent.View { - pagerView.openSearch() - } - self.update(isExpanded: true, transition: .animated(duration: 0.4, curve: .spring)) - } - }, - updateSearchQuery: { [weak self] query in - guard let self else { - return - } - if let query { - self.gifMode = .emojiSearch(query) - } else { - self.gifMode = .recent - } - }, - hideBackground: true, - hasSearch: true - ) } deinit { @@ -640,7 +689,7 @@ public class StickerPickerScreen: ViewController { } } - private func openGifContextMenu(file: FileMediaReference, contextResult: (ChatContextResultCollection, ChatContextResult)?, sourceView: UIView, sourceRect: CGRect, gesture: ContextGesture, isSaved: Bool) { + fileprivate func openGifContextMenu(file: FileMediaReference, contextResult: (ChatContextResultCollection, ChatContextResult)?, sourceView: UIView, sourceRect: CGRect, gesture: ContextGesture, isSaved: Bool) { guard let controller = self.controller else { return } @@ -676,14 +725,14 @@ public class StickerPickerScreen: ViewController { if isSaved { self.controller?.completion(.video(file.media)) self.controller?.dismiss(animated: true) - } else { - + } else if let (_, result) = contextResult { + if case let .internalReference(reference) = result { + if let file = reference.file { + self.controller?.completion(.video(file)) + self.controller?.dismiss(animated: true) + } + } } -// if isSaved { -// let _ = self.interaction?.sendGif(file, sourceView, sourceRect, false, false) -// } else if let (collection, result) = contextResult { -// let _ = self.interaction?.sendBotContextResultAsGif(collection, result, sourceView, sourceRect, false, false) -// } } }))) @@ -724,7 +773,7 @@ public class StickerPickerScreen: ViewController { } controller.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: presentationData.strings.Premium_MaxSavedGifsTitle("\(limit)").string, text: text, customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { [weak controller] action in if case .info = action { - let premiumController = context.sharedContext.makePremiumIntroController(context: context, source: .savedGifs, forceDark: true) + let premiumController = context.sharedContext.makePremiumIntroController(context: context, source: .savedGifs, forceDark: true, dismissed: nil) controller?.push(premiumController) return true } diff --git a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift index 6b0deaf947..ab0adbfdff 100644 --- a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift +++ b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift @@ -1353,7 +1353,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll } controllerInteraction?.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: presentationData.strings.Premium_MaxSavedGifsTitle("\(limit)").string, text: text, customUndoText: nil, timeout: nil), elevatedLayout: true, animateInAsReplacement: false, action: { action in if case .info = action { - let controller = context.sharedContext.makePremiumIntroController(context: context, source: .savedGifs, forceDark: false) + let controller = context.sharedContext.makePremiumIntroController(context: context, source: .savedGifs, forceDark: false, dismissed: nil) controllerInteraction?.pushController(controller) return true } @@ -1566,7 +1566,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll } controllerInteraction?.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: presentationData.strings.Premium_MaxSavedGifsTitle("\(limit)").string, text: text, customUndoText: nil, timeout: nil), elevatedLayout: true, animateInAsReplacement: false, action: { action in if case .info = action { - let controller = context.sharedContext.makePremiumIntroController(context: context, source: .savedGifs, forceDark: false) + let controller = context.sharedContext.makePremiumIntroController(context: context, source: .savedGifs, forceDark: false, dismissed: nil) controllerInteraction?.pushController(controller) return true } diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index 173f34e1ea..461bb52e9d 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -274,7 +274,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { super.init() if case .assets(nil, .default) = controller.subject { - } else if case .assets(nil, .story) = controller.subject { + } else if case .assets(nil, .story) = controller.subject, (savedStoriesContentOffset ?? 0.0).isZero { } else { self.preloadPromise.set(false) } diff --git a/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift b/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift index df15d41c55..9ace2cac26 100644 --- a/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift +++ b/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift @@ -100,7 +100,7 @@ public final class TwoFactorDataInputScreen: ViewController { super.viewWillAppear(animated) switch self.mode { - case .rememberPassword, .password: + case .rememberPassword, .password, .passwordHint, .emailAddress: (self.displayNode as? TwoFactorDataInputScreenNode)?.focus() default: break @@ -210,6 +210,7 @@ public final class TwoFactorDataInputScreen: ViewController { navigationController.replaceController(strongSelf, with: TwoFactorDataInputScreen(sharedContext: strongSelf.sharedContext, engine: strongSelf.engine, mode: .passwordHint(recovery: recovery, password: values[0], doneText: doneText), stateUpdated: strongSelf.stateUpdated, presentation: strongSelf.navigationPresentation), animated: true) case let .emailAddress(password, hint, doneText): guard let text = (strongSelf.displayNode as! TwoFactorDataInputScreenNode).inputText.first, !text.isEmpty else { + (strongSelf.displayNode as? TwoFactorDataInputScreenNode)?.onAction(success: false) return } let statusController = OverlayStatusController(theme: strongSelf.presentationData.theme, type: .loading(cancelled: nil)) @@ -1303,6 +1304,8 @@ private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, UIS } } + private var skipIsHiddenUntilError = false + init(sharedContext: SharedAccountContext, presentationData: PresentationData, mode: TwoFactorDataInputMode, action: @escaping () -> Void, skipAction: @escaping () -> Void, changeEmailAction: @escaping () -> Void, resendCodeAction: @escaping () -> Void) { self.presentationData = presentationData self.mode = mode @@ -1413,6 +1416,7 @@ private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, UIS text = NSAttributedString(string: presentationData.strings.TwoFactorSetup_Email_Text, font: Font.regular(16.0), textColor: presentationData.theme.list.itemPrimaryTextColor) buttonText = presentationData.strings.TwoFactorSetup_Email_Action skipActionText = presentationData.strings.TwoFactorSetup_Email_SkipAction + self.skipIsHiddenUntilError = true changeEmailActionText = "" resendCodeActionText = "" inputNodes = [ @@ -1538,8 +1542,8 @@ private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, UIS self.skipActionTitleNode.displaysAsynchronously = false self.skipActionTitleNode.attributedText = NSAttributedString(string: skipActionText, font: Font.regular(16.0), textColor: self.presentationData.theme.list.itemAccentColor) self.skipActionButtonNode = HighlightTrackingButtonNode() - self.skipActionTitleNode.isHidden = skipActionText.isEmpty - self.skipActionButtonNode.isHidden = skipActionText.isEmpty + self.skipActionTitleNode.isHidden = skipActionText.isEmpty || self.skipIsHiddenUntilError + self.skipActionButtonNode.isHidden = skipActionText.isEmpty || self.skipIsHiddenUntilError self.changeEmailActionTitleNode = ImmediateTextNode() self.changeEmailActionTitleNode.isUserInteractionEnabled = false @@ -1707,8 +1711,8 @@ private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, UIS case .emailAddress, .updateEmailAddress, .passwordRecovery: let hasText = strongSelf.inputNodes.contains(where: { !$0.text.isEmpty }) strongSelf.buttonNode.isHidden = !hasText - strongSelf.skipActionTitleNode.isHidden = hasText - strongSelf.skipActionButtonNode.isHidden = hasText + strongSelf.skipActionTitleNode.isHidden = hasText || strongSelf.skipIsHiddenUntilError + strongSelf.skipActionButtonNode.isHidden = hasText || strongSelf.skipIsHiddenUntilError case let .emailConfirmation(_, _, codeLength, _): let text = strongSelf.inputNodes[0].text let hasText = !text.isEmpty @@ -1822,35 +1826,48 @@ private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, UIS func onAction(success: Bool) { switch self.mode { - case .rememberPassword: - if !success { + case .emailAddress: + if !success { + self.inputNodes.first?.layer.addShakeAnimation() + HapticFeedback().error() + + if self.skipIsHiddenUntilError { + self.skipIsHiddenUntilError = false + self.skipActionTitleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.skipActionTitleNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) self.skipActionTitleNode.isHidden = false self.skipActionButtonNode.isHidden = false - - let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) - transition.updateAlpha(node: self.buttonNode, alpha: 0.0) - transition.updateAlpha(node: self.skipActionTitleNode, alpha: 1.0) - - if let snapshotView = self.textNode.view.snapshotContentTree() { - snapshotView.frame = self.textNode.view.frame - self.textNode.view.superview?.addSubview(snapshotView) - - snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in - snapshotView?.removeFromSuperview() - }) - - self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - } - - self.textNode.attributedText = NSAttributedString(string: self.presentationData.strings.TwoFactorRemember_WrongPassword, font: Font.regular(16.0), textColor: self.presentationData.theme.list.itemDestructiveColor) - self.inputNodes.first?.isFailed = true - - if let (layout, navigationHeight) = self.validLayout { - self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate) - } } - default: - break + } + case .rememberPassword: + if !success { + self.skipActionTitleNode.isHidden = false + self.skipActionButtonNode.isHidden = false + + let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) + transition.updateAlpha(node: self.buttonNode, alpha: 0.0) + transition.updateAlpha(node: self.skipActionTitleNode, alpha: 1.0) + + if let snapshotView = self.textNode.view.snapshotContentTree() { + snapshotView.frame = self.textNode.view.frame + self.textNode.view.superview?.addSubview(snapshotView) + + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + + self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + self.textNode.attributedText = NSAttributedString(string: self.presentationData.strings.TwoFactorRemember_WrongPassword, font: Font.regular(16.0), textColor: self.presentationData.theme.list.itemDestructiveColor) + self.inputNodes.first?.isFailed = true + + if let (layout, navigationHeight) = self.validLayout { + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate) + } + } + default: + break } } diff --git a/submodules/SettingsUI/Sources/ArchiveSettingsController.swift b/submodules/SettingsUI/Sources/ArchiveSettingsController.swift index c675aaba98..33f75c86cc 100644 --- a/submodules/SettingsUI/Sources/ArchiveSettingsController.swift +++ b/submodules/SettingsUI/Sources/ArchiveSettingsController.swift @@ -208,7 +208,7 @@ public func archiveSettingsController(context: AccountContext) -> ViewController guard let controller else { return } - let premiumController = context.sharedContext.makePremiumIntroController(context: context, source: .settings, forceDark: false) + let premiumController = context.sharedContext.makePremiumIntroController(context: context, source: .settings, forceDark: false, dismissed: nil) controller.push(premiumController) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Privacy/BlockedPeersContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/BlockedPeersContext.swift index c7d2f7f683..87922c03ca 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Privacy/BlockedPeersContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/BlockedPeersContext.swift @@ -204,7 +204,12 @@ public final class BlockedPeersContext { return peers } |> castError(BlockedPeersContextAddError.self) - |> mapToSignal { peers -> Signal in + |> mapToSignal { [weak self] peers -> Signal in + Queue.mainQueue().async { + if let strongSelf = self { + strongSelf._state = BlockedPeersContextState(isLoadingMore: strongSelf._state.isLoadingMore, canLoadMore: strongSelf._state.canLoadMore, totalCount: peers.count, peers: peers.map(RenderedPeer.init)) + } + } let inputPeers = peers.compactMap { apiInputPeer($0) } return network.request(Api.functions.contacts.setBlocked(flags: flags, id: inputPeers, limit: Int32(peers.count))) |> mapError { _ -> BlockedPeersContextAddError in diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index b058831581..5aeed5c837 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -404,26 +404,29 @@ final class MediaEditorScreenComponent: Component { private var nextTransitionUserData: Any? @objc private func deactivateInput() { - guard let view = self.inputPanel.view as? MessageInputPanelComponent.View else { + guard let _ = self.inputPanel.view as? MessageInputPanelComponent.View else { return } - if view.canDeactivateInput() { - self.currentInputMode = .text - if hasFirstResponder(self) { - if let view = self.inputPanel.view as? MessageInputPanelComponent.View { - self.nextTransitionUserData = TextFieldComponent.AnimationHint(kind: .textFocusChanged) - if view.isActive { - view.deactivateInput() - } else { - self.endEditing(true) - } +// if view.canDeactivateInput() { + self.currentInputMode = .text + if hasFirstResponder(self) { + if let view = self.inputPanel.view as? MessageInputPanelComponent.View { + self.nextTransitionUserData = TextFieldComponent.AnimationHint(kind: .textFocusChanged) + if view.isActive { + view.deactivateInput() + } else { + self.endEditing(true) } - } else { - self.state?.updated(transition: .spring(duration: 0.4).withUserData(TextFieldComponent.AnimationHint(kind: .textFocusChanged))) } } else { - view.animateError() + self.state?.updated(transition: .spring(duration: 0.4).withUserData(TextFieldComponent.AnimationHint(kind: .textFocusChanged))) } +// } else { +// if let controller = self.environment?.controller() as? MediaEditorScreen { +// controller.presentCaptionLimitPremiumSuggestion(isPremium: self.sta) +// } +// view.animateError() +// } } private var animatingButtons = false @@ -1168,7 +1171,7 @@ final class MediaEditorScreenComponent: Component { guard let self, let controller = self.environment?.controller() as? MediaEditorScreen else { return } - controller.presentCaptionLimitPremiumSuggestion() + controller.presentCaptionLimitPremiumSuggestion(isPremium: self.state?.isPremium ?? false) }, presentTextFormattingTooltip: { [weak self] in guard let self, let controller = self.environment?.controller() as? MediaEditorScreen else { @@ -1193,6 +1196,11 @@ final class MediaEditorScreenComponent: Component { controller.node.interaction?.insertEntity(entity, scale: 2.5) self.deactivateInput() } + case .text: + let text = self.getInputText() + if text.length > component.context.userLimits.maxStoryCaptionLength { + controller.presentCaptionLimitPremiumSuggestion(isPremium: self.state?.isPremium ?? false) + } default: break } @@ -2978,7 +2986,15 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate if let self { if let content { let stickerEntity = DrawingStickerEntity(content: content) - self.interaction?.insertEntity(stickerEntity, scale: 1.33) + let scale: CGFloat + if case .image = content { + scale = 2.5 + } else if case .video = content { + scale = 2.5 + } else { + scale = 1.33 + } + self.interaction?.insertEntity(stickerEntity, scale: scale) self.hasAnyChanges = true self.controller?.isSavingAvailable = true @@ -3642,7 +3658,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate let controller = UndoOverlayController(presentationData: presentationData, content: .autoDelete(isOn: true, title: nil, text: text, customUndoText: nil), elevatedLayout: false, position: .top, animateInAsReplacement: false, action: { [weak self] action in if case .info = action, let self { - let controller = context.sharedContext.makePremiumIntroController(context: context, source: .stories, forceDark: true) + let controller = context.sharedContext.makePremiumIntroController(context: context, source: .stories, forceDark: true, dismissed: nil) self.push(controller) } return false } @@ -3650,7 +3666,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.present(controller, in: .current) } - fileprivate func presentCaptionLimitPremiumSuggestion() { + fileprivate func presentCaptionLimitPremiumSuggestion(isPremium: Bool) { self.dismissAllTooltips() let context = self.context @@ -3661,7 +3677,9 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate let controller = UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_read", scale: 0.25, colors: [:], title: title, text: text, customUndoText: nil, timeout: nil), elevatedLayout: false, position: .top, animateInAsReplacement: false, action: { [weak self] action in if case .info = action, let self { - let controller = context.sharedContext.makePremiumIntroController(context: context, source: .stories, forceDark: true) + let controller = context.sharedContext.makePremiumIntroController(context: context, source: .stories, forceDark: true, dismissed: { + + }) self.push(controller) } return false } @@ -3679,7 +3697,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate let controller = UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: text), elevatedLayout: false, position: .top, animateInAsReplacement: false, action: { [weak self] action in if case .info = action, let self { - let controller = context.sharedContext.makePremiumIntroController(context: context, source: .stories, forceDark: true) + let controller = context.sharedContext.makePremiumIntroController(context: context, source: .stories, forceDark: true, dismissed: nil) self.push(controller) } return false } @@ -3961,6 +3979,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } if mediaEditor.resultIsVideo { + self.saveDraft(id: randomId) + var firstFrame: Signal<(UIImage?, UIImage?), NoError> let firstFrameTime = CMTime(seconds: mediaEditor.values.videoTrimRange?.lowerBound ?? 0.0, preferredTimescale: CMTimeScale(60)) diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift index fa1695dacb..56681691f0 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift @@ -771,7 +771,7 @@ public final class MessageInputPanelComponent: Component { } } - if let maxLength = component.maxLength, maxLength - self.textFieldExternalState.textLength < 5 { + if let maxLength = component.maxLength, maxLength - self.textFieldExternalState.textLength < 5 && isEditing { let remainingLength = max(-999, maxLength - self.textFieldExternalState.textLength) let counterSize = self.counter.update( transition: .immediate, diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/StickersResultPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/StickersResultPanelComponent.swift index 6f2351860a..18a6803502 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/StickersResultPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/StickersResultPanelComponent.swift @@ -277,7 +277,7 @@ final class StickersResultPanelComponent: Component { guard let self, let component = self.component else { return } - let controller = component.context.sharedContext.makePremiumIntroController(context: component.context, source: .stickers, forceDark: false) + let controller = component.context.sharedContext.makePremiumIntroController(context: component.context, source: .stickers, forceDark: false, dismissed: nil) component.present(controller) })) } else { diff --git a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift index 29362b20e6..4ef92c455c 100644 --- a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift +++ b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift @@ -402,7 +402,8 @@ public final class PeerListItemComponent: Component { let labelData: (String, Bool) if let presence = component.presence { let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 - labelData = stringAndActivityForUserPresence(strings: component.strings, dateTimeFormat: PresentationDateTimeFormat(), presence: presence, relativeTo: Int32(timestamp)) + let dateTimeFormat = component.context.sharedContext.currentPresentationData.with { $0 }.dateTimeFormat + labelData = stringAndActivityForUserPresence(strings: component.strings, dateTimeFormat: dateTimeFormat, presence: presence, relativeTo: Int32(timestamp)) } else if let subtitle = component.subtitle { labelData = (subtitle, false) } else { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift index e33421d5e5..20099333b7 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift @@ -453,6 +453,8 @@ final class StoryItemContentComponent: Component { } func update(component: StoryItemContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let previousItem = self.component?.item + self.component = component self.state = state let environment = environment[StoryContentItem.Environment.self].value @@ -730,7 +732,7 @@ final class StoryItemContentComponent: Component { } if self.contentLoaded { - if reloadMedia { + if let previousItem, previousItem.mediaAreas != component.item.mediaAreas { if let mediaAreasEffectView = self.mediaAreasEffectView { self.mediaAreasEffectView = nil mediaAreasEffectView.removeFromSuperview() diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 10a97b1f71..82d0015f39 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -2515,6 +2515,8 @@ public final class StoryItemSetContainerComponent: Component { self.sendMessageContext.enqueueGifData(view: self, data: data) case let .sticker(image, isMemoji): self.sendMessageContext.enqueueStickerImage(view: self, image: image, isMemoji: isMemoji) + case .text: + break } }, audioRecorder: self.sendMessageContext.audioRecorderValue, @@ -4192,8 +4194,8 @@ public final class StoryItemSetContainerComponent: Component { initialPrivacy: privacy, stateContext: stateContext, completion: { [weak self] result, _, _, peers in - if blockedPeers { -// let _ = self.storiesBlockedPeers.updatePeerIds(result.additionallyIncludePeers).start() + if blockedPeers, let blockedPeers = self?.component?.blockedPeers { + let _ = blockedPeers.updatePeerIds(result.additionallyIncludePeers).start() completion(privacy) } else if case .closeFriends = privacy.base { let _ = context.engine.privacy.updateCloseFriends(peerIds: result.additionallyIncludePeers).start() @@ -4437,6 +4439,8 @@ public final class StoryItemSetContainerComponent: Component { self.updateIsProgressPaused() self.state?.updated(transition: .easeInOut(duration: 0.2)) + HapticFeedback().success() + commit({}) } } @@ -4485,6 +4489,8 @@ public final class StoryItemSetContainerComponent: Component { self.updateIsProgressPaused() self.state?.updated(transition: .easeInOut(duration: 0.2)) + HapticFeedback().success() + commit({}) } } @@ -4502,6 +4508,8 @@ public final class StoryItemSetContainerComponent: Component { self.rewindCurrentItem() self.updateIsProgressPaused() self.state?.updated(transition: .easeInOut(duration: 0.2)) + + HapticFeedback().success() } commit({}) } @@ -4515,6 +4523,8 @@ public final class StoryItemSetContainerComponent: Component { self.updateIsProgressPaused() self.state?.updated(transition: .easeInOut(duration: 0.2)) + HapticFeedback().success() + commit({}) } diff --git a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift index 0adb2fef2f..f2e3adc8af 100644 --- a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift +++ b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift @@ -60,6 +60,7 @@ public final class TextFieldComponent: Component { case images([UIImage]) case video(Data) case gif(Data) + case text } @@ -348,6 +349,7 @@ public final class TextFieldComponent: Component { } } + component.paste(.text) return true } diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift index aac98745ab..1eb00baaf2 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift @@ -368,7 +368,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { if case .undo = action { var replaceImpl: ((ViewController) -> Void)? let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .voiceToText, action: { - let controller = context.sharedContext.makePremiumIntroController(context: context, source: .settings, forceDark: false) + let controller = context.sharedContext.makePremiumIntroController(context: context, source: .settings, forceDark: false, dismissed: nil) replaceImpl?(controller) }) replaceImpl = { [weak controller] c in diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift index 35f63b5bfa..a731df238b 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift @@ -1548,7 +1548,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { let context = item.context var replaceImpl: ((ViewController) -> Void)? let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .voiceToText, action: { - let controller = context.sharedContext.makePremiumIntroController(context: context, source: .settings, forceDark: false) + let controller = context.sharedContext.makePremiumIntroController(context: context, source: .settings, forceDark: false, dismissed: nil) replaceImpl?(controller) }) replaceImpl = { [weak controller] c in diff --git a/submodules/TelegramUI/Sources/PeerSelectionController.swift b/submodules/TelegramUI/Sources/PeerSelectionController.swift index f910318e17..bf3b7b6439 100644 --- a/submodules/TelegramUI/Sources/PeerSelectionController.swift +++ b/submodules/TelegramUI/Sources/PeerSelectionController.swift @@ -201,7 +201,7 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon let context = strongSelf.context var replaceImpl: ((ViewController) -> Void)? let controller = context.sharedContext.makePremiumLimitController(context: context, subject: .folders, count: strongSelf.tabContainerNode?.filtersCount ?? 0, action: { - let controller = context.sharedContext.makePremiumIntroController(context: context, source: .folders, forceDark: false) + let controller = context.sharedContext.makePremiumIntroController(context: context, source: .folders, forceDark: false, dismissed: nil) replaceImpl?(controller) }) replaceImpl = { [weak controller] c in diff --git a/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift index 6329e07c74..bc000f8e27 100644 --- a/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift @@ -293,7 +293,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { } var replaceImpl: ((ViewController) -> Void)? let controller = context.sharedContext.makePremiumLimitController(context: context, subject: .folders, count: strongSelf.controller?.tabContainerNode?.filtersCount ?? 0, action: { - let controller = context.sharedContext.makePremiumIntroController(context: context, source: .folders, forceDark: false) + let controller = context.sharedContext.makePremiumIntroController(context: context, source: .folders, forceDark: false, dismissed: nil) replaceImpl?(controller) }) replaceImpl = { [weak controller] c in diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index bfbb525595..ac26e9c94a 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1732,7 +1732,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { return archiveSettingsController(context: context) } - public func makePremiumIntroController(context: AccountContext, source: PremiumIntroSource, forceDark: Bool) -> ViewController { + public func makePremiumIntroController(context: AccountContext, source: PremiumIntroSource, forceDark: Bool, dismissed: (() -> Void)?) -> ViewController { let mappedSource: PremiumSource switch source { case .settings: diff --git a/submodules/WebSearchUI/BUILD b/submodules/WebSearchUI/BUILD index 77b829ca44..e8b8681c49 100644 --- a/submodules/WebSearchUI/BUILD +++ b/submodules/WebSearchUI/BUILD @@ -32,6 +32,7 @@ swift_library( "//submodules/AppBundle:AppBundle", "//submodules/PresentationDataUtils:PresentationDataUtils", "//submodules/AttachmentUI:AttachmentUI", + "//submodules/RadialStatusNode:RadialStatusNode", ], visibility = [ "//visibility:public", diff --git a/submodules/WebSearchUI/Sources/WebSearchItem.swift b/submodules/WebSearchUI/Sources/WebSearchItem.swift index 98df5e9c31..cc3b7f9587 100644 --- a/submodules/WebSearchUI/Sources/WebSearchItem.swift +++ b/submodules/WebSearchUI/Sources/WebSearchItem.swift @@ -7,6 +7,7 @@ import SwiftSignalKit import TelegramPresentationData import CheckNode import PhotoResources +import RadialStatusNode final class WebSearchItem: GridItem { var section: GridSection? @@ -44,6 +45,7 @@ final class WebSearchItemNode: GridItemNode { private let imageNodeBackground: ASDisplayNode private let imageNode: TransformImageNode private var checkNode: CheckNode? + private var statusNode: RadialStatusNode? private(set) var item: WebSearchItem? private var currentDimensions: CGSize? @@ -81,6 +83,32 @@ final class WebSearchItemNode: GridItemNode { self.imageNode.view.addGestureRecognizer(recognizer) } + func updateProgress(_ value: Float?, animated: Bool) { + if let value { + let statusNode: RadialStatusNode + if let current = self.statusNode { + statusNode = current + } else { + statusNode = RadialStatusNode(backgroundNodeColor: UIColor(rgb: 0x000000, alpha: 0.6)) + statusNode.isUserInteractionEnabled = false + self.addSubnode(statusNode) + self.statusNode = statusNode + } + let adjustedProgress = max(0.027, CGFloat(value)) + let state: RadialStatusNodeState = .progress(color: .white, lineWidth: nil, value: adjustedProgress, cancelEnabled: true, animateRotation: true) + statusNode.transitionToState(state) + } else if let statusNode = self.statusNode { + self.statusNode = nil + if animated { + statusNode.transitionToState(.none, animated: true, completion: { [weak statusNode] in + statusNode?.removeFromSupernode() + }) + } else { + statusNode.removeFromSupernode() + } + } + } + func setup(item: WebSearchItem, synchronousLoad: Bool) { if self.item !== item { var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?