Merge commit 'c5081979f04f4c165d9572a7e50eb769af6dc5c8'

This commit is contained in:
Isaac 2025-04-29 00:33:05 +02:00
commit 313833b7d9
12 changed files with 190 additions and 47 deletions

View File

@ -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 %@.";

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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 {

View File

@ -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)
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)
})))
resellGiftImpl?(true)
})))
}
}
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_View_Context_CopyLink, icon: { theme in

View File

@ -12,6 +12,7 @@ swift_library(
deps = [
"//submodules/Display",
"//submodules/ComponentFlow",
"//submodules/SSignalKit/SwiftSignalKit",
],
visibility = [
"//visibility:public",

View File

@ -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 {
@ -44,6 +45,8 @@ public final class MarqueeComponent: Component {
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()
}
})
})
}

View File

@ -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()
}
}

View File

@ -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,

View File

@ -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:

View File

@ -180,8 +180,6 @@ public func chatTranslationState(context: AccountContext, peerId: EnginePeer.Id)
baseLang = String(baseLang.dropLast(rawSuffix.count))
}
return combineLatest(
context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.translationSettings])
|> map { sharedData -> TranslationSettings in