diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 9d693b6bb7..aa26391efb 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -14293,3 +14293,6 @@ Sorry for the inconvenience."; "Gift.Resale.Unavailable.Title" = "Resell Gift"; "Gift.Resale.Unavailable.Text" = "Sorry, you can't list this gift yet.\n\Reselling will be available on %@."; + +"Gift.Transfer.Unavailable.Title" = "Transfer Gift"; +"Gift.Transfer.Unavailable.Text" = "Sorry, you can't transfer this gift yet.\n\Transferring will be available on %@."; diff --git a/submodules/AccountContext/Sources/Premium.swift b/submodules/AccountContext/Sources/Premium.swift index 2dea082727..fffbc9fa04 100644 --- a/submodules/AccountContext/Sources/Premium.swift +++ b/submodules/AccountContext/Sources/Premium.swift @@ -101,6 +101,7 @@ public enum PremiumLimitSubject { case membershipInSharedFolders case channels case expiringStories + case multiStories case storiesWeekly case storiesMonthly case storiesChannelBoost(peer: EnginePeer, isCurrent: Bool, level: Int32, currentLevelBoosts: Int32, nextLevelBoosts: Int32?, link: String?, myBoostCount: Int32, canBoostAgain: Bool) diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index 3b9bcc6b5e..f8a7049e91 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -388,10 +388,10 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att self.itemsDisposable = (updatedState |> deliverOnMainQueue).start(next: { [weak self] state in - guard let strongSelf = self else { + guard let self else { return } - strongSelf.updateState(state) + self.updateState(state) }) self.gridNode.scrollingInitiated = { [weak self] in @@ -404,15 +404,16 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att self.hiddenMediaDisposable = (self.hiddenMediaId.get() |> deliverOnMainQueue).start(next: { [weak self] id in - if let strongSelf = self { - strongSelf.controller?.interaction?.hiddenMediaId = id - strongSelf.gridNode.forEachItemNode { itemNode in - if let itemNode = itemNode as? MediaPickerGridItemNode { - itemNode.updateHiddenMedia() - } - } - strongSelf.selectionNode?.updateHiddenMedia() + guard let self else { + return } + self.controller?.interaction?.hiddenMediaId = id + self.gridNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? MediaPickerGridItemNode { + itemNode.updateHiddenMedia() + } + } + self.selectionNode?.updateHiddenMedia() }) if let selectionState = self.controller?.interaction?.selectionState { @@ -431,8 +432,8 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att self.selectionChangedDisposable = (selectionChangedSignal(selectionState: selectionState) |> deliverOnMainQueue).start(next: { [weak self] animated in - if let strongSelf = self { - strongSelf.updateSelectionState(animated: animated) + if let self { + self.updateSelectionState(animated: animated) } }) } @@ -451,8 +452,8 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att self.itemsDimensionsUpdatedDisposable = (itemsDimensionsUpdatedSignal(editingState: editingState) |> deliverOnMainQueue).start(next: { [weak self] _ in - if let strongSelf = self { - strongSelf.updateSelectionState() + if let self { + self.updateSelectionState() } }) } @@ -536,8 +537,8 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att self?.gridNode.scrollView.isScrollEnabled = isEnabled } selectionGesture.itemAt = { [weak self] point in - if let strongSelf = self, let itemNode = strongSelf.gridNode.itemNodeAtPoint(point) as? MediaPickerGridItemNode, let selectableItem = itemNode.selectableItem { - return (selectableItem, strongSelf.controller?.interaction?.selectionState?.isIdentifierSelected(selectableItem.uniqueIdentifier) ?? false) + if let self, let itemNode = self.gridNode.itemNodeAtPoint(point) as? MediaPickerGridItemNode, let selectableItem = itemNode.selectableItem { + return (selectableItem, self.controller?.interaction?.selectionState?.isIdentifierSelected(selectableItem.uniqueIdentifier) ?? false) } else { return nil } @@ -2004,8 +2005,11 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att var hasSelect = false if forCollage { hasSelect = true - } else if case .story = mode, selectionContext.selectionLimit > 1 { - hasSelect = true + } else if case .story = mode { + if selectionContext.selectionLimit == 1 && context.isPremium { + } else { + hasSelect = true + } } if hasSelect { @@ -2584,6 +2588,34 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att } @objc private func selectPressed() { + let context = self.context + if let selectionState = self.interaction?.selectionState, selectionState.selectionLimit == 1, !context.isPremium { + var replaceImpl: ((ViewController) -> Void)? + let controller = context.sharedContext.makePremiumLimitController( + context: self.context, + subject: .multiStories, + count: 1, + forceDark: true, + cancel: {}, + action: { + let controller = context.sharedContext.makePremiumIntroController( + context: context, + source: .stories, + forceDark: true, + dismissed: nil + ) + replaceImpl?(controller) + return true + }) + replaceImpl = { [weak controller] c in + controller?.replace(with: c) + } + self.requestDismiss { + self.parentController()?.push(controller) + } + return + } + self.navigationItem.setRightBarButton(nil, animated: true) self.explicitMultipleSelection = true @@ -2724,11 +2756,10 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att loop: true ), action: { [weak self] _, f in f(.default) - guard let strongSelf = self else { + guard let self else { return } - - if let selectionContext = strongSelf.interaction?.selectionState, let editingContext = strongSelf.interaction?.editingState { + if let selectionContext = self.interaction?.selectionState, let editingContext = self.interaction?.editingState { for case let item as TGMediaEditableItem in selectionContext.selectedItems() { editingContext.setSpoiler(hasGeneric, for: item) } @@ -2754,10 +2785,10 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att } let controller = self.context.sharedContext.makeStarsAmountScreen(context: self.context, initialValue: price, completion: { [weak self] amount in - guard let strongSelf = self else { + guard let self else { return } - if let selectionContext = strongSelf.interaction?.selectionState, let editingContext = strongSelf.interaction?.editingState { + if let selectionContext = self.interaction?.selectionState, let editingContext = self.interaction?.editingState { selectionContext.selectionLimit = 10 for case let item as TGMediaEditableItem in selectionContext.selectedItems() { editingContext.setPrice(NSNumber(value: amount), for: item) diff --git a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift index a313b56cbc..be2c2e03cc 100644 --- a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift @@ -1101,6 +1101,30 @@ private final class LimitSheetContent: CombinedComponent { badgePosition = max(0.32, CGFloat(component.count) / CGFloat(premiumLimit)) badgeGraphPosition = badgePosition + if isPremiumDisabled { + badgeText = "\(limit)" + let numberString = strings.Premium_MaxExpiringStoriesNoPremiumTextNumberFormat(Int32(limit)) + string = strings.Premium_MaxExpiringStoriesNoPremiumTextFormat(numberString).string + } + buttonAnimationName = nil + case .multiStories: + let limit = state.limits.maxExpiringStoriesCount + let premiumLimit = state.premiumLimits.maxExpiringStoriesCount + iconName = "Premium/Stories" + badgeText = "\(limit)" + if component.count >= premiumLimit { + let limitNumberString = strings.Premium_MaxExpiringStoriesFinalTextNumberFormat(Int32(premiumLimit)) + string = strings.Premium_MaxExpiringStoriesFinalTextFormat(limitNumberString).string + } else { + let limitNumberString = strings.Premium_MaxExpiringStoriesTextNumberFormat(Int32(limit)) + let premiumLimitNumberString = strings.Premium_MaxExpiringStoriesTextPremiumNumberFormat(Int32(premiumLimit)) + string = strings.Premium_MaxExpiringStoriesTextFormat(limitNumberString, premiumLimitNumberString).string + } + defaultValue = "" + premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)" + badgePosition = max(0.32, CGFloat(component.count) / CGFloat(premiumLimit)) + badgeGraphPosition = badgePosition + if isPremiumDisabled { badgeText = "\(limit)" let numberString = strings.Premium_MaxExpiringStoriesNoPremiumTextNumberFormat(Int32(limit)) @@ -1210,7 +1234,6 @@ private final class LimitSheetContent: CombinedComponent { remaining = nextLevelBoosts - component.count } - if let _ = link { if let remaining { let storiesString = strings.ChannelBoost_StoriesPerDay(level + 1) @@ -1813,6 +1836,7 @@ public class PremiumLimitScreen: ViewControllerComponentContainer { case membershipInSharedFolders case channels case expiringStories + case multiStories case storiesWeekly case storiesMonthly diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift index 32aa9cdc41..2eb5ec91de 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift @@ -484,6 +484,13 @@ public enum StarGift: Equatable, Codable, PostboxCoding { case peerId(EnginePeer.Id) case name(String) case address(String) + + public var peerId: EnginePeer.Id? { + if case let .peerId(peerId) = self { + return peerId + } + return nil + } } public enum DecodingError: Error { diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index b0ced0d654..dc9af27814 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -3253,6 +3253,24 @@ public class GiftViewScreen: ViewControllerComponentContainer { guard let self, let arguments = self.subject.arguments, let navigationController = self.navigationController as? NavigationController, case let .unique(gift) = arguments.gift, let reference = arguments.reference, let transferStars = arguments.transferStars else { return } + + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + if let canTransferDate = arguments.canTransferDate, currentTime < canTransferDate { + let dateString = stringForFullDate(timestamp: canTransferDate, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat) + let controller = textAlertController( + context: self.context, + title: presentationData.strings.Gift_Transfer_Unavailable_Title, + text: presentationData.strings.Gift_Transfer_Unavailable_Text(dateString).string, + actions: [ + TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {}) + ], + parseMarkdown: true + ) + self.present(controller, in: .window(.root)) + return + } + + let _ = (context.account.stateManager.contactBirthdays |> take(1) |> deliverOnMainQueue).start(next: { birthdays in @@ -3477,8 +3495,33 @@ public class GiftViewScreen: ViewControllerComponentContainer { } let _ = ((updateResellStars?(price) ?? context.engine.payments.updateStarGiftResalePrice(reference: reference, price: price)) - |> deliverOnMainQueue).startStandalone(error: { error in + |> deliverOnMainQueue).startStandalone(error: { [weak self] error in + guard let self else { + return + } + let title: String? + let text: String + switch error { + case .generic: + title = nil + text = presentationData.strings.Gift_Send_ErrorUnknown + case let .starGiftResellTooEarly(canResaleDate): + let dateString = stringForFullDate(timestamp: canResaleDate, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat) + title = presentationData.strings.Gift_Resale_Unavailable_Title + text = presentationData.strings.Gift_Resale_Unavailable_Text(dateString).string + } + + let controller = textAlertController( + context: self.context, + title: title, + text: text, + actions: [ + TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {}) + ], + parseMarkdown: true + ) + self.present(controller, in: .window(.root)) }, completed: { [weak self] in guard let self else { return @@ -3597,13 +3640,15 @@ public class GiftViewScreen: ViewControllerComponentContainer { } if case let .unique(gift) = arguments.gift, let resellStars = gift.resellStars, resellStars > 0 { - items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_View_Context_ChangePrice, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/PriceTag"), color: theme.contextMenu.primaryColor) - }, action: { c, _ in - c?.dismiss(completion: nil) - - resellGiftImpl?(true) - }))) + if arguments.reference != nil || gift.owner.peerId == context.account.peerId { + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_View_Context_ChangePrice, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/PriceTag"), color: theme.contextMenu.primaryColor) + }, action: { c, _ in + c?.dismiss(completion: nil) + + resellGiftImpl?(true) + }))) + } } items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_View_Context_CopyLink, icon: { theme in diff --git a/submodules/TelegramUI/Components/MarqueeComponent/BUILD b/submodules/TelegramUI/Components/MarqueeComponent/BUILD index 543348ff1a..a82427e16c 100644 --- a/submodules/TelegramUI/Components/MarqueeComponent/BUILD +++ b/submodules/TelegramUI/Components/MarqueeComponent/BUILD @@ -12,6 +12,7 @@ swift_library( deps = [ "//submodules/Display", "//submodules/ComponentFlow", + "//submodules/SSignalKit/SwiftSignalKit", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/MarqueeComponent/Sources/MarqueeComponent.swift b/submodules/TelegramUI/Components/MarqueeComponent/Sources/MarqueeComponent.swift index 6b1dac5eeb..29f0d81dcc 100644 --- a/submodules/TelegramUI/Components/MarqueeComponent/Sources/MarqueeComponent.swift +++ b/submodules/TelegramUI/Components/MarqueeComponent/Sources/MarqueeComponent.swift @@ -2,9 +2,10 @@ import Foundation import UIKit import Display import ComponentFlow +import SwiftSignalKit -private let animationDuration: TimeInterval = 12.0 -private let animationDelay: TimeInterval = 2.0 +private let animationSpeed: TimeInterval = 50.0 +private let animationDelay: TimeInterval = 2.5 private let spacing: CGFloat = 20.0 public final class MarqueeComponent: Component { @@ -43,6 +44,8 @@ public final class MarqueeComponent: Component { private let gradientMaskLayer = SimpleGradientLayer() private var isAnimating = false private var isOverflowing = false + + private var component: MarqueeComponent? override init(frame: CGRect) { super.init(frame: frame) @@ -60,6 +63,9 @@ public final class MarqueeComponent: Component { } public func update(component: MarqueeComponent, availableSize: CGSize) -> CGSize { + let previousComponent = self.component + self.component = component + let attributedText = component.attributedText if let measureState = self.measureState { if measureState.attributedText.isEqual(to: attributedText) && measureState.availableSize == availableSize { @@ -88,7 +94,7 @@ public final class MarqueeComponent: Component { if isOverflowing { self.setupMarqueeTextLayers(textImage: image.cgImage!, textWidth: boundingRect.width, containerWidth: availableSize.width) self.setupGradientMask(size: CGSize(width: availableSize.width, height: boundingRect.height)) - self.startAnimation() + self.startAnimation(force: previousComponent?.attributedText != attributedText) } else { self.stopAnimation() self.textLayer.frame = CGRect(origin: CGPoint(x: innerPadding, y: 0.0), size: boundingRect.size) @@ -137,17 +143,26 @@ public final class MarqueeComponent: Component { self.layer.mask = self.gradientMaskLayer } - private func startAnimation() { - guard !self.isAnimating else { + private func startAnimation(force: Bool = false) { + guard !self.isAnimating || force else { return } self.isAnimating = true self.containerLayer.removeAllAnimations() - self.containerLayer.animateBoundsOriginXAdditive(from: 0.0, to: self.textLayer.frame.width + spacing, duration: animationDuration, delay: animationDelay, timingFunction: CAMediaTimingFunctionName.linear.rawValue, completion: { _ in - self.isAnimating = false - self.startAnimation() + let distance = self.textLayer.frame.width + spacing + let duration = distance / animationSpeed + Queue.mainQueue().after(animationDelay, { + guard self.isAnimating else { + return + } + self.containerLayer.animateBoundsOriginXAdditive(from: 0.0, to: distance, duration: duration, delay: 0.0, timingFunction: CAMediaTimingFunctionName.linear.rawValue, completion: { finished in + if finished { + self.isAnimating = false + self.startAnimation() + } + }) }) } diff --git a/submodules/TelegramUI/Components/Stars/StarsBalanceOverlayComponent/Sources/StarsBalanceOverlayComponent.swift b/submodules/TelegramUI/Components/Stars/StarsBalanceOverlayComponent/Sources/StarsBalanceOverlayComponent.swift index c304e8ca32..8542e65081 100644 --- a/submodules/TelegramUI/Components/Stars/StarsBalanceOverlayComponent/Sources/StarsBalanceOverlayComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsBalanceOverlayComponent/Sources/StarsBalanceOverlayComponent.swift @@ -62,8 +62,10 @@ public final class StarsBalanceOverlayComponent: Component { self.balanceDisposable?.dispose() } + private var didTap = false @objc private func tapped() { - if let component = self.component { + if let component = self.component, !self.didTap { + self.didTap = true component.action() } } diff --git a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift index 7e126d9e4f..2c5c8dd5cf 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift @@ -120,8 +120,6 @@ func chatHistoryEntriesForView( } } - //var existingGroupStableIds: [UInt32] = [] - //var groupBucket: [(Message, Bool, ChatHistoryMessageSelection, ChatMessageEntryAttributes, MessageHistoryEntryLocation?)] = [] var count = 0 loop: for entry in view.entries { var message = entry.message @@ -198,7 +196,7 @@ func chatHistoryEntriesForView( } if groupMessages || reverseGroupedMessages { - if let messageGroupingKey = message.groupingKey, (groupMessages || reverseGroupedMessages) { + if let messageGroupingKey = message.groupingKey { let selection: ChatHistoryMessageSelection if let selectedMessages = selectedMessages { selection = .selectable(selected: selectedMessages.contains(message.id)) @@ -271,6 +269,22 @@ func chatHistoryEntriesForView( } } + if !groupMessages && reverseGroupedMessages { + var flatEntries: [ChatHistoryEntry] = [] + + for entry in entries { + switch entry { + case let .MessageGroupEntry(_, messages, presentationData): + for (message, isRead, selection, attributes, location) in messages { + flatEntries.append(.MessageEntry(message, presentationData, isRead, location, selection, attributes)) + } + default: + flatEntries.append(entry) + } + } + entries = flatEntries + } + let insertPendingProcessingMessage: ([Message], Int) -> Void = { messages, index in let serviceMessage = Message( stableId: UInt32.max - messages[0].stableId, diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 814506b77f..365fca7ed3 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -2803,6 +2803,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { mappedSubject = .channels case .expiringStories: mappedSubject = .expiringStories + case .multiStories: + mappedSubject = .multiStories case .storiesWeekly: mappedSubject = .storiesWeekly case .storiesMonthly: diff --git a/submodules/TranslateUI/Sources/ChatTranslation.swift b/submodules/TranslateUI/Sources/ChatTranslation.swift index 9111cbf26e..74ea83d24c 100644 --- a/submodules/TranslateUI/Sources/ChatTranslation.swift +++ b/submodules/TranslateUI/Sources/ChatTranslation.swift @@ -179,9 +179,7 @@ public func chatTranslationState(context: AccountContext, peerId: EnginePeer.Id) if baseLang.hasSuffix(rawSuffix) { baseLang = String(baseLang.dropLast(rawSuffix.count)) } - - - + return combineLatest( context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.translationSettings]) |> map { sharedData -> TranslationSettings in