diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index fce72fef5b..8b25b9de20 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -13801,3 +13801,10 @@ Sorry for the inconvenience."; "Notification.StarsGift.TransferToChannel" = "%@ transferred a unique collectible to %@"; "Notification.StarsGift.TransferToChannelYou" = "You transferred a unique collectible to %@"; + +"Gift.Convert.Success.ChannelText" = "**%1$@** were sent to channel's balance."; +"Gift.Convert.Success.ChannelText.Stars_1" = "%@ Star"; +"Gift.Convert.Success.ChannelText.Stars_any" = "%@ Stars"; + +"Stars.Transfer.Terms" = "By purchasing you agree to the [Terms of Service]()."; +"Stars.Transfer.Terms_URL" = "https://telegram.org/tos/stars"; diff --git a/submodules/LegacyComponents/Sources/TGMediaEditingContext.m b/submodules/LegacyComponents/Sources/TGMediaEditingContext.m index e1f9cfab72..35e9581560 100644 --- a/submodules/LegacyComponents/Sources/TGMediaEditingContext.m +++ b/submodules/LegacyComponents/Sources/TGMediaEditingContext.m @@ -962,7 +962,11 @@ [_coverImageCache setImage:image forKey:itemId attributes:NULL]; _coverImagePipe.sink([TGMediaImageUpdate imageUpdateWithItem:item representation:image]); - [_coverPositions setObject:position forKey:itemId]; + if (position != nil) { + [_coverPositions setObject:position forKey:itemId]; + } else { + [_coverPositions removeObjectForKey:itemId]; + } } - (void)setFullSizeImage:(UIImage *)image forItem:(id)item diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/StickerPaneSearchContentNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/StickerPaneSearchContentNode.swift index 23230d48ba..ef37c0f7d8 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/StickerPaneSearchContentNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/StickerPaneSearchContentNode.swift @@ -355,13 +355,13 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode { let signal: Signal<([(String?, FoundStickerItem)], FoundStickerSets, Bool, FoundStickerSets?)?, NoError> if !text.isEmpty { let context = self.context - let stickers: Signal<[(String?, FoundStickerItem)], NoError> = Signal { subscriber in - var signals: Signal<[Signal<(String?, [FoundStickerItem]), NoError>], NoError> = .single([]) + let stickers: Signal<([(String?, FoundStickerItem)], Bool), NoError> = Signal { subscriber in + var signals: Signal<[Signal<(String?, [FoundStickerItem], Bool), NoError>], NoError> = .single([]) let query = text.trimmingCharacters(in: .whitespacesAndNewlines) if query.isSingleEmoji { signals = .single([context.engine.stickers.searchStickers(query: nil, emoticon: [text.basicEmoji.0]) - |> map { (nil, $0.items) }]) + |> map { (nil, $0.items, $0.isFinalResult) }]) } else if query.count > 1, let languageCode = languageCode, !languageCode.isEmpty && languageCode != "emoji" { var signal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query.lowercased(), completeMatch: query.count < 3) if !languageCode.lowercased().hasPrefix("en") { @@ -377,10 +377,10 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode { } } signals = signal - |> map { keywords -> [Signal<(String?, [FoundStickerItem]), NoError>] in + |> map { keywords -> [Signal<(String?, [FoundStickerItem], Bool), NoError>] in let emoticon = keywords.flatMap { $0.emoticons }.map { $0.basicEmoji.0 } return [context.engine.stickers.searchStickers(query: query, emoticon: emoticon, inputLanguageCode: languageCode) - |> map { (nil, $0.items) }] + |> map { (nil, $0.items, $0.isFinalResult) }] } } @@ -389,12 +389,16 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode { return combineLatest(signals) }).start(next: { results in var result: [(String?, FoundStickerItem)] = [] - for (emoji, stickers) in results { + var allAreFinal = true + for (emoji, stickers, isFinal) in results { for sticker in stickers { result.append((emoji, sticker)) } + if !isFinal { + allAreFinal = false + } } - subscriber.putNext(result) + subscriber.putNext((result, allAreFinal)) }, completed: { // subscriber.putCompletion() }) @@ -456,7 +460,7 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode { signal = combineLatest(stickers, packs) |> map { stickers, packs -> ([(String?, FoundStickerItem)], FoundStickerSets, Bool, FoundStickerSets?)? in - return (stickers, packs.0, packs.1, packs.2) + return (stickers.0, packs.0, packs.1 && stickers.1, packs.2) } self.updateActivity?(true) } else { diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift index 92732d9e47..41e7f07cd2 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift @@ -344,98 +344,93 @@ final class GiftSetupScreenComponent: Component { let entities = generateChatInputTextEntities(self.textInputState.text) let source: BotPaymentInvoiceSource = .starGift(hideName: self.hideName, includeUpgrade: self.includeUpgrade, peerId: peerId, giftId: starGift.id, text: self.textInputState.text.string, entities: entities) - let inputData = BotCheckoutController.InputData.fetch(context: component.context, source: source) - |> map(Optional.init) - |> `catch` { _ -> Signal in - return .single(nil) - } let completion = component.completion - let _ = (inputData - |> deliverOnMainQueue).startStandalone(next: { [weak self] inputData in - guard let inputData else { + let signal = BotCheckoutController.InputData.fetch(context: component.context, source: source) + |> `catch` { _ -> Signal in + return .fail(.generic) + } + |> mapToSignal { inputData -> Signal in + return component.context.engine.payments.sendStarsPaymentForm(formId: inputData.form.id, source: source) + } + |> deliverOnMainQueue + + let _ = signal.start(next: { [weak self] result in + guard let self, let controller = self.environment?.controller(), let navigationController = controller.navigationController as? NavigationController else { return } - let _ = (component.context.engine.payments.sendStarsPaymentForm(formId: inputData.form.id, source: source) - |> deliverOnMainQueue).start(next: { [weak self] result in - if let self, peerId.namespace == Namespaces.Peer.CloudChannel, let controller = self.environment?.controller(), let navigationController = controller.navigationController as? NavigationController { - var controllers = navigationController.viewControllers - controllers = controllers.filter { !($0 is GiftSetupScreen) && !($0 is GiftOptionsScreenProtocol) } - navigationController.setViewControllers(controllers, animated: true) - - let tooltipController = UndoOverlayController( - presentationData: presentationData, - content: .sticker( - context: context, - file: starGift.file, - loop: true, - title: nil, - text: presentationData.strings.Gift_Send_Success(self.peerMap[peerId]?.compactDisplayTitle ?? "", presentationData.strings.Gift_Send_Success_Stars(Int32(starGift.price))).string, - undoText: nil, - customAction: nil - ), - action: { _ in return true } - ) - (navigationController.viewControllers.last as? ViewController)?.present(tooltipController, in: .current) - - navigationController.view.addSubview(ConfettiView(frame: navigationController.view.bounds)) - } + + if peerId.namespace == Namespaces.Peer.CloudChannel { + var controllers = navigationController.viewControllers + controllers = controllers.filter { !($0 is GiftSetupScreen) && !($0 is GiftOptionsScreenProtocol) } + navigationController.setViewControllers(controllers, animated: true) - if let completion { - completion() - - if let self, let controller = self.environment?.controller() { - controller.dismiss() - } - } else { - guard let self, let controller = self.environment?.controller(), let navigationController = controller.navigationController as? NavigationController else { - return - } - - if peerId.namespace != Namespaces.Peer.CloudChannel { - var controllers = navigationController.viewControllers - controllers = controllers.filter { !($0 is GiftSetupScreen) && !($0 is GiftOptionsScreenProtocol) && !($0 is PeerInfoScreen) && !($0 is ContactSelectionController) } - var foundController = false - for controller in controllers.reversed() { - if let chatController = controller as? ChatController, case .peer(id: component.peerId) = chatController.chatLocation { - chatController.hintPlayNextOutgoingGift() - foundController = true - break - } - } - if !foundController { - let chatController = component.context.sharedContext.makeChatController(context: component.context, chatLocation: .peer(id: component.peerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil) - chatController.hintPlayNextOutgoingGift() - controllers.append(chatController) - } - navigationController.setViewControllers(controllers, animated: true) + let tooltipController = UndoOverlayController( + presentationData: presentationData, + content: .sticker( + context: context, + file: starGift.file, + loop: true, + title: nil, + text: presentationData.strings.Gift_Send_Success(self.peerMap[peerId]?.compactDisplayTitle ?? "", presentationData.strings.Gift_Send_Success_Stars(Int32(starGift.price))).string, + undoText: nil, + customAction: nil + ), + action: { _ in return true } + ) + (navigationController.viewControllers.last as? ViewController)?.present(tooltipController, in: .current) + + navigationController.view.addSubview(ConfettiView(frame: navigationController.view.bounds)) + } else if peerId.namespace == Namespaces.Peer.CloudUser { + var controllers = navigationController.viewControllers + controllers = controllers.filter { !($0 is GiftSetupScreen) && !($0 is GiftOptionsScreenProtocol) && !($0 is PeerInfoScreen) && !($0 is ContactSelectionController) } + var foundController = false + for controller in controllers.reversed() { + if let chatController = controller as? ChatController, case .peer(id: component.peerId) = chatController.chatLocation { + chatController.hintPlayNextOutgoingGift() + foundController = true + break } } - - starsContext.load(force: true) - }, error: { [weak self] error in - guard let self, let controller = self.environment?.controller() else { - return + if !foundController { + let chatController = component.context.sharedContext.makeChatController(context: component.context, chatLocation: .peer(id: component.peerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil) + chatController.hintPlayNextOutgoingGift() + controllers.append(chatController) } + navigationController.setViewControllers(controllers, animated: true) + } + + if let completion { + completion() - self.inProgress = false - self.state?.updated() - - let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - var errorText: String? - switch error { - case .starGiftOutOfStock: - errorText = presentationData.strings.Gift_Send_ErrorOutOfStock - default: - errorText = presentationData.strings.Gift_Send_ErrorUnknown + if let controller = self.environment?.controller() { + controller.dismiss() } - - if let errorText = errorText { - let alertController = textAlertController(context: component.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) - controller.present(alertController, in: .window(.root)) - } - }) + } + + starsContext.load(force: true) + }, error: { [weak self] error in + guard let self, let controller = self.environment?.controller() else { + return + } + + self.inProgress = false + self.state?.updated() + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + var errorText: String? + switch error { + case .starGiftOutOfStock: + errorText = presentationData.strings.Gift_Send_ErrorOutOfStock + default: + errorText = presentationData.strings.Gift_Send_ErrorUnknown + } + + if let errorText = errorText { + let alertController = textAlertController(context: component.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) + controller.present(alertController, in: .window(.root)) + } }) } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index ed70d21472..62f71b1247 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -3112,10 +3112,13 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID } self.entitiesView.canInteract = { [weak self] in if let self, let controller = self.controller { - return !controller.node.recording.isActive - } else { - return true + if controller.node.recording.isActive { + return false + } else if case .avatarEditor = controller.mode, self.drawingScreen == nil { + return false + } } + return true } self.availableReactionsDisposable = (allowedStoryReactions(context: controller.context) @@ -3252,11 +3255,12 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID if let mediaEntityView = self.entitiesView.add(mediaEntity, announce: false) as? DrawingMediaEntityView { self.entitiesView.sendSubviewToBack(mediaEntityView) mediaEntityView.updated = { [weak self, weak mediaEntity] in - if let self, let mediaEntity { + if let self, let mediaEditor = self.mediaEditor, let mediaEntity { let rotation = mediaEntity.rotation - initialRotation let position = CGPoint(x: mediaEntity.position.x - initialPosition.x, y: mediaEntity.position.y - initialPosition.y) let scale = mediaEntity.scale / initialScale - self.mediaEditor?.setCrop(offset: position, scale: scale, rotation: rotation, mirroring: false) + let mirroring = mediaEditor.values.cropMirroring + mediaEditor.setCrop(offset: position, scale: scale, rotation: rotation, mirroring: mirroring) self.updateMaskDrawingView(position: position, scale: scale, rotation: rotation) } @@ -3465,6 +3469,13 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID } return false }) as? DrawingStickerEntityView { + #if DEBUG + if let data = result.dayImage.pngData() { + let path = NSTemporaryDirectory() + "\(Int(Date().timeIntervalSince1970)).png" + try? data.write(to: URL(fileURLWithPath: path)) + } + #endif + existingEntityView.isNightTheme = isNightTheme let messageEntity = existingEntityView.entity as! DrawingStickerEntity messageEntity.renderImage = result.dayImage @@ -5613,6 +5624,8 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID self.previousDrawingData = self.drawingView.drawingData self.previousDrawingEntities = self.entitiesView.entities + self.cropScrollView?.isUserInteractionEnabled = false + self.interaction?.deactivate() let controller = DrawingScreen( context: self.context, @@ -5668,6 +5681,8 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID self.previousDrawingData = nil self.previousDrawingEntities = nil + + self.cropScrollView?.isUserInteractionEnabled = true } controller.requestApply = { [weak controller, weak self] in guard let self else { @@ -5690,6 +5705,8 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID self.interaction?.activate() self.entitiesView.selectEntity(nil) + + self.cropScrollView?.isUserInteractionEnabled = true } self.controller?.present(controller, in: .current) self.animateOutToTool(tool: mode) @@ -7594,6 +7611,13 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID let values = mediaEditor.values.withUpdatedCoverDimensions(dimensions) makeEditorImageComposition(context: self.node.ciContext, postbox: self.context.account.postbox, inputImage: image, dimensions: storyDimensions, outputDimensions: dimensions.aspectFitted(CGSize(width: 1080, height: 1080)), values: values, time: .zero, textScale: 2.0, completion: { [weak self] resultImage in if let self, let resultImage { + #if DEBUG + if let data = resultImage.jpegData(compressionQuality: 0.7) { + let path = NSTemporaryDirectory() + "\(Int(Date().timeIntervalSince1970)).jpg" + try? data.write(to: URL(fileURLWithPath: path)) + } + #endif + self.completion(MediaEditorScreenImpl.Result(media: .image(image: resultImage, dimensions: PixelDimensions(resultImage.size))), { [weak self] finished in self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in self?.dismiss() diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift index 88df209521..60232b1e43 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift @@ -141,11 +141,11 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr } public func scrollViewDidScroll(_ scrollView: UIScrollView) { - self.updateScrolling(transition: .immediate) + self.updateScrolling(interactive: true, transition: .immediate) } private var notify = false - func updateScrolling(transition: ComponentTransition) { + func updateScrolling(interactive: Bool = false, transition: ComponentTransition) { if let starsProducts = self.starsProducts, let params = self.currentParams { let optionSpacing: CGFloat = 10.0 let itemsSideInset = params.sideInset + 16.0 @@ -644,7 +644,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr } let bottomContentOffset = max(0.0, self.scrollNode.view.contentSize.height - self.scrollNode.view.contentOffset.y - self.scrollNode.view.frame.height) - if bottomContentOffset < 200.0 { + if interactive, bottomContentOffset < 200.0 { self.profileGifts.loadMore() } } diff --git a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift index 3602685b47..49157706af 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift @@ -293,9 +293,11 @@ private final class SheetContent: CombinedComponent { .position(CGPoint(x: context.availableSize.width / 2.0, y: background.size.height / 2.0)) ) + var isExtendedMedia = false let subject: StarsImageComponent.Subject if !component.extendedMedia.isEmpty { subject = .extendedMedia(component.extendedMedia) + isExtendedMedia = true } else if let peer = state.botPeer { if let photo = component.invoice.photo { subject = .photo(photo) @@ -381,7 +383,7 @@ private final class SheetContent: CombinedComponent { contentSize.height += title.size.height contentSize.height += 13.0 - if isBot, let peer = state.botPeer { + if isBot && !isExtendedMedia, let peer = state.botPeer { contentSize.height -= 3.0 let peerShortcut = peerShortcut.update( component: PremiumPeerShortcutComponent( @@ -650,48 +652,49 @@ private final class SheetContent: CombinedComponent { .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + button.size.height / 2.0)) ) contentSize.height += button.size.height - if isSubscription { - contentSize.height += 14.0 - - let termsTextFont = Font.regular(13.0) - let termsTextColor = theme.actionSheet.secondaryTextColor - let termsLinkColor = theme.actionSheet.controlAccentColor - let termsMarkdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: termsTextFont, textColor: termsTextColor), bold: MarkdownAttributeSet(font: termsTextFont, textColor: termsTextColor), link: MarkdownAttributeSet(font: termsTextFont, textColor: termsLinkColor), linkAttribute: { contents in - return (TelegramTextAttributes.URL, contents) - }) - let info = info.update( - component: BalancedTextComponent( - text: .markdown( - text: strings.Stars_Subscription_Terms, - attributes: termsMarkdownAttributes - ), - horizontalAlignment: .center, - maximumNumberOfLines: 0, - lineSpacing: 0.2, - highlightColor: linkColor.withAlphaComponent(0.2), - highlightAction: { attributes in - if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { - return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) - } else { - return nil - } - }, - tapAction: { [weak controller] attributes, _ in - if let controller, let navigationController = controller.navigationController as? NavigationController { - let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: strings.Stars_Subscription_Terms_URL, forceExternal: false, presentationData: presentationData, navigationController: navigationController, dismissInput: {}) - } - } + + let termsText = isSubscription ? strings.Stars_Subscription_Terms : strings.Stars_Transfer_Terms + let termsURL = isSubscription ? strings.Stars_Subscription_Terms_URL : strings.Stars_Transfer_Terms_URL + + contentSize.height += 14.0 + + let termsTextFont = Font.regular(13.0) + let termsTextColor = theme.actionSheet.secondaryTextColor + let termsLinkColor = theme.actionSheet.controlAccentColor + let termsMarkdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: termsTextFont, textColor: termsTextColor), bold: MarkdownAttributeSet(font: termsTextFont, textColor: termsTextColor), link: MarkdownAttributeSet(font: termsTextFont, textColor: termsLinkColor), linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + }) + let info = info.update( + component: BalancedTextComponent( + text: .markdown( + text: termsText, + attributes: termsMarkdownAttributes ), - availableSize: CGSize(width: constrainedTitleWidth, height: context.availableSize.height), - transition: .immediate - ) - context.add(info - .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + info.size.height / 2.0)) - ) - contentSize.height += info.size.height - - } + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.2, + highlightColor: linkColor.withAlphaComponent(0.2), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + } else { + return nil + } + }, + tapAction: { [weak controller] attributes, _ in + if let controller, let navigationController = controller.navigationController as? NavigationController { + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: termsURL, forceExternal: false, presentationData: presentationData, navigationController: navigationController, dismissInput: {}) + } + } + ), + availableSize: CGSize(width: constrainedTitleWidth, height: context.availableSize.height), + transition: .immediate + ) + context.add(info + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + info.size.height / 2.0)) + ) + contentSize.height += info.size.height contentSize.height += 48.0