Merge commit '9e0600edfa45ce017d315be9101615dc480717d8' into post-suggestion

# Conflicts:
#	submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift
This commit is contained in:
Isaac 2025-04-28 23:37:49 +02:00
commit b92293efe9
67 changed files with 2697 additions and 1384 deletions

View File

@ -14202,3 +14202,94 @@ Sorry for the inconvenience.";
"Story.Privacy.KeepOnMyPageManyInfo" = "Keep these stories on your profile even after they expire in %@. Privacy settings will apply.";
"Story.Privacy.KeepOnChannelPageManyInfo" = "Keep these stories on the channel profile even after they expire in %@.";
"Story.Privacy.KeepOnGroupPageManyInfo" = "Keep these stories on the group page even after they expire in %@.";
"Gift.Options.Gift.Filter.Resale" = "Resale";
"Gift.Options.Gift.Resale" = "resale";
"Stars.Intro.Transaction.GiftPurchase" = "Gift Purchase";
"Stars.Intro.Transaction.GiftSale" = "Gift Sale";
"Stars.Transaction.GiftPurchase" = "Gift Purchase";
"Stars.Transaction.GiftSale" = "Gift Sale";
"Channel.Info.AutoTranslate" = "Auto-Translate Messages";
"ChannelBoost.Table.AutoTranslate" = "Autotranslation of Messages";
"ChannelBoost.AutoTranslate" = "Autotranslation of Messages";
"ChannelBoost.AutoTranslateLevelText" = "Your channel needs **Level %1$@** to enable autotranslation of messages.";
"Channel.AdminLog.MessageToggleAutoTranslateOn" = "%@ enabled autotranslation of messages";
"Channel.AdminLog.MessageToggleAutoTranslateOff" = "%@ disabled autotranslation of messages";
"Notification.StarsGift.Bought" = "%1$@ gifted you %2$@ for %3$@";
"Notification.StarsGift.Bought.Stars_1" = "%@ Star";
"Notification.StarsGift.Bought.Stars_any" = "%@ Stars";
"Notification.StarsGift.BoughtYou" = "You gifted %1$@ for %2$@";
"Notification.StarsGift.BoughtForYouself" = "You bought this gift for %1$@";
"Gift.View.Context.ChangePrice" = "Change Price";
"Gift.View.Context.ViewInProfile" = "View in Profile";
"Gift.View.Sell" = "sell";
"Gift.View.Unlist" = "unlist";
"Gift.View.BuyFor" = "Buy for";
"Gift.View.SellingGiftInfo" = "%@ is selling this gift and you can buy it.";
"Gift.View.Resale.Success.Title" = "Gift Sent";
"Gift.View.Resale.Success.Text" = "%@ has been notified about your gift.";
"Gift.View.Resale.SuccessYou.Title" = "Gift Acquired";
"Gift.View.Resale.SuccessYou.Text" = "%@ is now yours.";
"Gift.View.Resale.Unlist.Title" = "Unlist This Item?";
"Gift.View.Resale.Unlist.Text" = "It will no longer be for sale.";
"Gift.View.Resale.Unlist.Unlist" = "Unlist";
"Gift.View.Resale.Unlist.Success" = "%@ is removed from sale.";
"Gift.View.Resale.List.Success" = "%@ is now for sale!";
"Gift.View.Resale.Relist.Success" = "%1$@ is relisted for %2$@.";
"Gift.View.Resale.Relist.Success.Stars_1" = "%@ Star";
"Gift.View.Resale.Relist.Success.Stars_any" = "%@ Stars";
"Stars.SellGift.Title" = "Sell Gift";
"Stars.SellGift.EditTitle" = "Edit Price";
"Stars.SellGift.AmountTitle" = "PRICE IN STARS";
"Stars.SellGift.AmountPlaceholder" = "Enter Price";
"Stars.SellGift.AmountInfo" = "You will receive **%@**.";
"Stars.SellGift.AmountInfo.Stars_1" = "%@ Star";
"Stars.SellGift.AmountInfo.Stars_any" = "%@ Stars";
"Stars.SellGift.Sell" = "Sell";
"Stars.SellGift.SellFor" = "Sell for";
"PeerInfo.Gifts.Sale" = "sale";
"Gift.Store.ForResale_1" = "%@ for resale";
"Gift.Store.ForResale_any" = "%@ for resale";
"Gift.Store.Sort.Price" = "Price";
"Gift.Store.Sort.Date" = "Date";
"Gift.Store.Sort.Number" = "Number";
"Gift.Store.SortByPrice" = "Sort By Price";
"Gift.Store.SortByDate" = "Sort By Date";
"Gift.Store.SortByNumber" = "Sort By Number";
"Gift.Store.Filter.Model" = "Model";
"Gift.Store.Filter.Backdrop" = "Backdrop";
"Gift.Store.Filter.Symbol" = "Symbol";
"Gift.Store.Filter.Selected.Model_1" = "%@ Model";
"Gift.Store.Filter.Selected.Model_any" = "%@ Models";
"Gift.Store.Filter.Selected.Backdrop_1" = "%@ Backdrop";
"Gift.Store.Filter.Selected.Backdrop_any" = "%@ Backdrops";
"Gift.Store.Filter.Selected.Symbol_1" = "%@ Symbol";
"Gift.Store.Filter.Selected.Symbol_any" = "%@ Symbols";
"Gift.Store.Search" = "Search";
"Gift.Store.SelectAll" = "Select All";
"Gift.Store.NoResults" = "No Results";
"Gift.Store.EmptyResults" = "No Matching Gifts";
"Gift.Store.ClearFilters" = "Clear Filters";
"Gift.Send.AvailableForResale" = "Available for Resale";
"MediaPicker.CreateStory_1" = "Create %@ Story";
"MediaPicker.CreateStory_any" = "Create %@ Stories";
"MediaPicker.CombineIntoCollage" = "Combine into Collage";
"Gift.Resale.Unavailable.Title" = "Resell Gift";
"Gift.Resale.Unavailable.Text" = "Sorry, you can't list this gift yet.\n\Reselling will be available on %@.";

View File

@ -125,6 +125,7 @@ public enum BoostSubject: Equatable {
case emojiPack
case noAds
case wearGift
case autoTranslate
}
public enum StarsPurchasePurpose: Equatable {
@ -164,6 +165,7 @@ public struct PremiumConfiguration {
minChannelCustomWallpaperLevel: 10,
minChannelRestrictAdsLevel: 50,
minChannelWearGiftLevel: 8,
minChannelAutoTranslateLevel: 3,
minGroupProfileIconLevel: 7,
minGroupEmojiStatusLevel: 8,
minGroupWallpaperLevel: 9,
@ -193,6 +195,7 @@ public struct PremiumConfiguration {
public let minChannelCustomWallpaperLevel: Int32
public let minChannelRestrictAdsLevel: Int32
public let minChannelWearGiftLevel: Int32
public let minChannelAutoTranslateLevel: Int32
public let minGroupProfileIconLevel: Int32
public let minGroupEmojiStatusLevel: Int32
public let minGroupWallpaperLevel: Int32
@ -221,6 +224,7 @@ public struct PremiumConfiguration {
minChannelCustomWallpaperLevel: Int32,
minChannelRestrictAdsLevel: Int32,
minChannelWearGiftLevel: Int32,
minChannelAutoTranslateLevel: Int32,
minGroupProfileIconLevel: Int32,
minGroupEmojiStatusLevel: Int32,
minGroupWallpaperLevel: Int32,
@ -248,6 +252,7 @@ public struct PremiumConfiguration {
self.minChannelCustomWallpaperLevel = minChannelCustomWallpaperLevel
self.minChannelRestrictAdsLevel = minChannelRestrictAdsLevel
self.minChannelWearGiftLevel = minChannelWearGiftLevel
self.minChannelAutoTranslateLevel = minChannelAutoTranslateLevel
self.minGroupProfileIconLevel = minGroupProfileIconLevel
self.minGroupEmojiStatusLevel = minGroupEmojiStatusLevel
self.minGroupWallpaperLevel = minGroupWallpaperLevel
@ -283,6 +288,7 @@ public struct PremiumConfiguration {
minChannelCustomWallpaperLevel: get(data["channel_custom_wallpaper_level_min"]) ?? defaultValue.minChannelCustomWallpaperLevel,
minChannelRestrictAdsLevel: get(data["channel_restrict_sponsored_level_min"]) ?? defaultValue.minChannelRestrictAdsLevel,
minChannelWearGiftLevel: get(data["channel_emoji_status_level_min"]) ?? defaultValue.minChannelWearGiftLevel,
minChannelAutoTranslateLevel: get(data["channel_autotranslation_level_min"]) ?? defaultValue.minChannelAutoTranslateLevel,
minGroupProfileIconLevel: get(data["group_profile_bg_icon_level_min"]) ?? defaultValue.minGroupProfileIconLevel,
minGroupEmojiStatusLevel: get(data["group_emoji_status_level_min"]) ?? defaultValue.minGroupEmojiStatusLevel,
minGroupWallpaperLevel: get(data["group_wallpaper_level_min"]) ?? defaultValue.minGroupWallpaperLevel,

View File

@ -173,8 +173,11 @@ private final class CameraContext {
self.positionValue = configuration.position
self._positionPromise = ValuePromise<Camera.Position>(configuration.position)
#if targetEnvironment(simulator)
#else
self.setDualCameraEnabled(configuration.isDualEnabled, change: false)
#endif
NotificationCenter.default.addObserver(
self,
selector: #selector(self.sessionRuntimeError),

View File

@ -67,6 +67,7 @@ public final class SheetComponent<ChildEnvironmentType: Equatable>: Component {
public let externalState: ExternalState?
public let animateOut: ActionSlot<Action<()>>
public let onPan: () -> Void
public let willDismiss: () -> Void
public init(
content: AnyComponent<ChildEnvironmentType>,
@ -76,7 +77,8 @@ public final class SheetComponent<ChildEnvironmentType: Equatable>: Component {
isScrollEnabled: Bool = true,
externalState: ExternalState? = nil,
animateOut: ActionSlot<Action<()>>,
onPan: @escaping () -> Void = {}
onPan: @escaping () -> Void = {},
willDismiss: @escaping () -> Void = {}
) {
self.content = content
self.backgroundColor = backgroundColor
@ -86,6 +88,7 @@ public final class SheetComponent<ChildEnvironmentType: Equatable>: Component {
self.externalState = externalState
self.animateOut = animateOut
self.onPan = onPan
self.willDismiss = willDismiss
}
public static func ==(lhs: SheetComponent, rhs: SheetComponent) -> Bool {
@ -222,6 +225,7 @@ public final class SheetComponent<ChildEnvironmentType: Equatable>: Component {
let currentContentOffset = scrollView.contentOffset
targetContentOffset.pointee = currentContentOffset
if velocity.y > 300.0 {
self.component?.willDismiss()
self.animateOut(initialVelocity: initialVelocity, completion: {
self.dismiss?(false)
})
@ -233,6 +237,7 @@ public final class SheetComponent<ChildEnvironmentType: Equatable>: Component {
scrollView.setContentOffset(CGPoint(x: 0.0, y: scrollView.contentSize.height - scrollView.contentInset.top), animated: true)
}
} else {
self.component?.willDismiss()
self.animateOut(initialVelocity: initialVelocity, completion: {
self.dismiss?(false)
})

View File

@ -283,7 +283,7 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes
let transition: ContainedViewLayoutTransition
let dismissProgress: CGFloat
if (velocity.y < -0.5 || progress >= 0.5) && self.checkInteractiveDismissWithControllers() {
if let controller = self.container.controllers.last as? MinimizableController {
if let controller = self.container.controllers.last as? MinimizableController, controller.isMinimizable {
dismissProgress = 0.0
targetOffset = 0.0
transition = .immediate

View File

@ -370,7 +370,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode {
}
let packRevealOptions: [ItemListRevealOption]
if item.editing.editable && item.enabled {
if item.editing.editable && item.enabled && !item.editing.editing {
packRevealOptions = [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)]
} else {
packRevealOptions = []
@ -564,7 +564,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode {
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
}
let revealOffset = strongSelf.revealOffset
let revealOffset = !packRevealOptions.isEmpty ? strongSelf.revealOffset : 0.0
let transition: ContainedViewLayoutTransition
if animated {

View File

@ -2004,7 +2004,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
var hasSelect = false
if forCollage {
hasSelect = true
} else if case .story = mode {
} else if case .story = mode, selectionContext.selectionLimit > 1 {
hasSelect = true
}
@ -2395,15 +2395,11 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
transition.updateTransformScale(node: self.moreButtonNode.iconNode, scale: moreIsVisible ? 1.0 : 0.1)
if case .assets(_, .story) = self.subject, self.selectionCount > 0 {
//TODO:localize
var text = "Create 1 Story"
if self.selectionCount > 1 {
text = "Create \(self.selectionCount) Stories"
}
let text = self.presentationData.strings.MediaPicker_CreateStory(self.selectionCount)
self.mainButtonStatePromise.set(.single(AttachmentMainButtonState(text: text, badge: nil, font: .bold, background: .color(self.presentationData.theme.actionSheet.controlAccentColor), textColor: self.presentationData.theme.list.itemCheckColors.foregroundColor, isVisible: true, progress: .none, isEnabled: true, hasShimmer: false, position: .top)))
if self.selectionCount > 1 && self.selectionCount <= 6 {
self.secondaryButtonStatePromise.set(.single(AttachmentMainButtonState(text: "Combine into Collage", badge: nil, font: .regular, background: .color(.clear), textColor: self.presentationData.theme.actionSheet.controlAccentColor, isVisible: true, progress: .none, isEnabled: true, hasShimmer: false, iconName: "Media Editor/Collage", smallSpacing: true, position: .bottom)))
self.secondaryButtonStatePromise.set(.single(AttachmentMainButtonState(text: self.presentationData.strings.MediaPicker_CombineIntoCollage, badge: nil, font: .regular, background: .color(.clear), textColor: self.presentationData.theme.actionSheet.controlAccentColor, isVisible: true, progress: .none, isEnabled: true, hasShimmer: false, iconName: "Media Editor/Collage", smallSpacing: true, position: .bottom)))
} else {
self.secondaryButtonStatePromise.set(.single(nil))
}
@ -3406,7 +3402,7 @@ public func stickerMediaPickerController(
destinationCornerRadius: 0.0
)
},
completion: { result, _, commit in
completion: { result, _, _, commit in
completion(result, nil, .zero, nil, true, { _ in return nil }, {
returnToCameraImpl?()
})
@ -3524,7 +3520,7 @@ public func avatarMediaPickerController(
destinationCornerRadius: 0.0
)
},
completion: { result, _, commit in
completion: { result, _, _, commit in
completion(result, nil, .zero, nil, true, { _ in return nil }, {
returnToCameraImpl?()
})

View File

@ -542,6 +542,8 @@ NSString *suffix = @"";
return @"iPhone 16 Pro";
if ([platform isEqualToString:@"iPhone17,2"])
return @"iPhone 16 Pro Max";
if ([platform isEqualToString:@"iPhone17,5"])
return @"iPhone 16e";
if ([platform hasPrefix:@"iPod1"])
return @"iPod touch 1G";

View File

@ -61,6 +61,8 @@ func requiredBoostSubjectLevel(subject: BoostSubject, group: Bool, context: Acco
return configuration.minChannelRestrictAdsLevel
case .wearGift:
return configuration.minChannelWearGiftLevel
case .autoTranslate:
return configuration.minChannelAutoTranslateLevel
}
}
@ -243,6 +245,7 @@ private final class LevelSectionComponent: CombinedComponent {
case emojiPack
case noAds
case wearGift
case autoTranslate
func title(strings: PresentationStrings, isGroup: Bool) -> String {
switch self {
@ -274,6 +277,8 @@ private final class LevelSectionComponent: CombinedComponent {
return strings.ChannelBoost_Table_NoAds
case .wearGift:
return strings.ChannelBoost_Table_WearGift
case .autoTranslate:
return strings.ChannelBoost_Table_AutoTranslate
}
}
@ -307,6 +312,8 @@ private final class LevelSectionComponent: CombinedComponent {
return "Premium/BoostPerk/NoAds"
case .wearGift:
return "Premium/BoostPerk/NoAds"
case .autoTranslate:
return "Chat/Title Panels/Translate"
}
}
}
@ -647,6 +654,8 @@ private final class SheetContent: CombinedComponent {
textString = strings.ChannelBoost_EnableNoAdsLevelText("\(requiredLevel)").string
case .wearGift:
textString = strings.ChannelBoost_WearGiftLevelText("\(requiredLevel)").string
case .autoTranslate:
textString = strings.ChannelBoost_AutoTranslateLevelText("\(requiredLevel)").string
}
} else {
let boostsString = strings.ChannelBoost_MoreBoostsNeeded_Boosts(Int32(remaining))
@ -1162,6 +1171,9 @@ private final class SheetContent: CombinedComponent {
if !isGroup && level >= requiredBoostSubjectLevel(subject: .noAds, group: isGroup, context: component.context, configuration: premiumConfiguration) {
perks.append(.noAds)
}
if !isGroup && level >= requiredBoostSubjectLevel(subject: .autoTranslate, group: isGroup, context: component.context, configuration: premiumConfiguration) {
perks.append(.autoTranslate)
}
// if !isGroup && level >= requiredBoostSubjectLevel(subject: .wearGift, group: isGroup, context: component.context, configuration: premiumConfiguration) {
// perks.append(.wearGift)
// }
@ -1466,6 +1478,8 @@ private final class BoostLevelsContainerComponent: CombinedComponent {
titleString = strings.ChannelBoost_NoAds
case .wearGift:
titleString = strings.ChannelBoost_WearGift
case .autoTranslate:
titleString = strings.ChannelBoost_AutoTranslate
}
} else {
titleString = isGroup == true ? strings.GroupBoost_Title_Current : strings.ChannelBoost_Title_Current

View File

@ -1062,7 +1062,7 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta
} else {
rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Edit), style: .regular, enabled: true, action: {
updateState {
$0.withUpdatedEditing(true).withUpdatedSelectedPackIds(Set())
$0.withUpdatedEditing(true).withUpdatedPackIdWithRevealedOptions(nil).withUpdatedSelectedPackIds(Set())
}
})
}

View File

@ -131,6 +131,9 @@ func parseTelegramGroupOrChannel(chat: Api.Chat) -> Peer? {
if (flags & Int32(1 << 30)) != 0 {
channelFlags.insert(.isForum)
}
if (flags2 & Int32(1 << 15)) != 0 {
channelFlags.insert(.autoTranslateEnabled)
}
var storiesHidden: Bool?
if flags2 & (1 << 2) == 0 { // stories_hidden_min

View File

@ -192,11 +192,11 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe
return nil
}
return TelegramMediaAction(action: .starGift(gift: gift, convertStars: convertStars, text: text, entities: entities, nameHidden: (flags & (1 << 0)) != 0, savedToProfile: (flags & (1 << 2)) != 0, converted: (flags & (1 << 3)) != 0, upgraded: (flags & (1 << 5)) != 0, canUpgrade: (flags & (1 << 10)) != 0, upgradeStars: upgradeStars, isRefunded: (flags & (1 << 9)) != 0, upgradeMessageId: upgradeMessageId, peerId: peer?.peerId, senderId: fromId?.peerId, savedId: savedId))
case let .messageActionStarGiftUnique(flags, apiGift, canExportAt, transferStars, fromId, peer, savedId, resaleStars):
case let .messageActionStarGiftUnique(flags, apiGift, canExportAt, transferStars, fromId, peer, savedId, resaleStars, canTransferDate, canResaleDate):
guard let gift = StarGift(apiStarGift: apiGift) else {
return nil
}
return TelegramMediaAction(action: .starGiftUnique(gift: gift, isUpgrade: (flags & (1 << 0)) != 0, isTransferred: (flags & (1 << 1)) != 0, savedToProfile: (flags & (1 << 2)) != 0, canExportDate: canExportAt, transferStars: transferStars, isRefunded: (flags & (1 << 5)) != 0, peerId: peer?.peerId, senderId: fromId?.peerId, savedId: savedId, resaleStars: resaleStars))
return TelegramMediaAction(action: .starGiftUnique(gift: gift, isUpgrade: (flags & (1 << 0)) != 0, isTransferred: (flags & (1 << 1)) != 0, savedToProfile: (flags & (1 << 2)) != 0, canExportDate: canExportAt, transferStars: transferStars, isRefunded: (flags & (1 << 5)) != 0, peerId: peer?.peerId, senderId: fromId?.peerId, savedId: savedId, resaleStars: resaleStars, canTransferDate: canTransferDate, canResaleDate: canResaleDate))
case let .messageActionPaidMessagesRefunded(count, stars):
return TelegramMediaAction(action: .paidMessagesRefunded(count: count, stars: stars))
case let .messageActionPaidMessagesPrice(stars):

View File

@ -181,6 +181,7 @@ public struct TelegramChannelFlags: OptionSet {
public static let joinToSend = TelegramChannelFlags(rawValue: 1 << 9)
public static let requestToJoin = TelegramChannelFlags(rawValue: 1 << 10)
public static let isForum = TelegramChannelFlags(rawValue: 1 << 11)
public static let autoTranslateEnabled = TelegramChannelFlags(rawValue: 1 << 12)
}
public final class TelegramChannel: Peer, Equatable {

View File

@ -156,7 +156,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
case giftStars(currency: String, amount: Int64, count: Int64, cryptoCurrency: String?, cryptoAmount: Int64?, transactionId: String?)
case prizeStars(amount: Int64, isUnclaimed: Bool, boostPeerId: PeerId?, transactionId: String?, giveawayMessageId: MessageId?)
case starGift(gift: StarGift, convertStars: Int64?, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool, upgraded: Bool, canUpgrade: Bool, upgradeStars: Int64?, isRefunded: Bool, upgradeMessageId: Int32?, peerId: EnginePeer.Id?, senderId: EnginePeer.Id?, savedId: Int64?)
case starGiftUnique(gift: StarGift, isUpgrade: Bool, isTransferred: Bool, savedToProfile: Bool, canExportDate: Int32?, transferStars: Int64?, isRefunded: Bool, peerId: EnginePeer.Id?, senderId: EnginePeer.Id?, savedId: Int64?, resaleStars: Int64?)
case starGiftUnique(gift: StarGift, isUpgrade: Bool, isTransferred: Bool, savedToProfile: Bool, canExportDate: Int32?, transferStars: Int64?, isRefunded: Bool, peerId: EnginePeer.Id?, senderId: EnginePeer.Id?, savedId: Int64?, resaleStars: Int64?, canTransferDate: Int32?, canResaleDate: Int32?)
case paidMessagesRefunded(count: Int32, stars: Int64)
case paidMessagesPriceEdited(stars: Int64)
case conferenceCall(ConferenceCall)
@ -283,7 +283,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
case 44:
self = .starGift(gift: decoder.decodeObjectForKey("gift", decoder: { StarGift(decoder: $0) }) as! StarGift, convertStars: decoder.decodeOptionalInt64ForKey("convertStars"), text: decoder.decodeOptionalStringForKey("text"), entities: decoder.decodeOptionalObjectArrayWithDecoderForKey("entities"), nameHidden: decoder.decodeBoolForKey("nameHidden", orElse: false), savedToProfile: decoder.decodeBoolForKey("savedToProfile", orElse: false), converted: decoder.decodeBoolForKey("converted", orElse: false), upgraded: decoder.decodeBoolForKey("upgraded", orElse: false), canUpgrade: decoder.decodeBoolForKey("canUpgrade", orElse: false), upgradeStars: decoder.decodeOptionalInt64ForKey("upgradeStars"), isRefunded: decoder.decodeBoolForKey("isRefunded", orElse: false), upgradeMessageId: decoder.decodeOptionalInt32ForKey("upgradeMessageId"), peerId: decoder.decodeOptionalInt64ForKey("peerId").flatMap { EnginePeer.Id($0) }, senderId: decoder.decodeOptionalInt64ForKey("senderId").flatMap { EnginePeer.Id($0) }, savedId: decoder.decodeOptionalInt64ForKey("savedId"))
case 45:
self = .starGiftUnique(gift: decoder.decodeObjectForKey("gift", decoder: { StarGift(decoder: $0) }) as! StarGift, isUpgrade: decoder.decodeBoolForKey("isUpgrade", orElse: false), isTransferred: decoder.decodeBoolForKey("isTransferred", orElse: false), savedToProfile: decoder.decodeBoolForKey("savedToProfile", orElse: false), canExportDate: decoder.decodeOptionalInt32ForKey("canExportDate"), transferStars: decoder.decodeOptionalInt64ForKey("transferStars"), isRefunded: decoder.decodeBoolForKey("isRefunded", orElse: false), peerId: decoder.decodeOptionalInt64ForKey("peerId").flatMap { EnginePeer.Id($0) }, senderId: decoder.decodeOptionalInt64ForKey("senderId").flatMap { EnginePeer.Id($0) }, savedId: decoder.decodeOptionalInt64ForKey("savedId"), resaleStars: decoder.decodeOptionalInt64ForKey("resaleStars"))
self = .starGiftUnique(gift: decoder.decodeObjectForKey("gift", decoder: { StarGift(decoder: $0) }) as! StarGift, isUpgrade: decoder.decodeBoolForKey("isUpgrade", orElse: false), isTransferred: decoder.decodeBoolForKey("isTransferred", orElse: false), savedToProfile: decoder.decodeBoolForKey("savedToProfile", orElse: false), canExportDate: decoder.decodeOptionalInt32ForKey("canExportDate"), transferStars: decoder.decodeOptionalInt64ForKey("transferStars"), isRefunded: decoder.decodeBoolForKey("isRefunded", orElse: false), peerId: decoder.decodeOptionalInt64ForKey("peerId").flatMap { EnginePeer.Id($0) }, senderId: decoder.decodeOptionalInt64ForKey("senderId").flatMap { EnginePeer.Id($0) }, savedId: decoder.decodeOptionalInt64ForKey("savedId"), resaleStars: decoder.decodeOptionalInt64ForKey("resaleStars"), canTransferDate: decoder.decodeOptionalInt32ForKey("canTransferDate"), canResaleDate: decoder.decodeOptionalInt32ForKey("canResaleDate"))
case 46:
self = .paidMessagesRefunded(count: decoder.decodeInt32ForKey("count", orElse: 0), stars: decoder.decodeInt64ForKey("stars", orElse: 0))
case 47:
@ -633,7 +633,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
} else {
encoder.encodeNil(forKey: "savedId")
}
case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, isRefunded, peerId, senderId, savedId, resaleStars):
case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, isRefunded, peerId, senderId, savedId, resaleStars, canTransferDate, canResaleDate):
encoder.encodeInt32(45, forKey: "_rawValue")
encoder.encodeObject(gift, forKey: "gift")
encoder.encodeBool(isUpgrade, forKey: "isUpgrade")
@ -670,6 +670,16 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
} else {
encoder.encodeNil(forKey: "resaleStars")
}
if let canTransferDate {
encoder.encodeInt32(canTransferDate, forKey: "canTransferDate")
} else {
encoder.encodeNil(forKey: "canTransferDate")
}
if let canResaleDate {
encoder.encodeInt32(canResaleDate, forKey: "canResaleDate")
} else {
encoder.encodeNil(forKey: "canResaleDate")
}
case let .paidMessagesRefunded(count, stars):
encoder.encodeInt32(46, forKey: "_rawValue")
encoder.encodeInt32(count, forKey: "count")
@ -723,7 +733,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
peerIds.append(senderId)
}
return peerIds
case let .starGiftUnique(_, _, _, _, _, _, _, peerId, senderId, _, _):
case let .starGiftUnique(_, _, _, _, _, _, _, peerId, senderId, _, _, _, _):
var peerIds: [PeerId] = []
if let peerId {
peerIds.append(peerId)

View File

@ -2467,5 +2467,32 @@ public extension TelegramEngine.EngineData.Item {
}
}
}
public struct AutoTranslateEnabled: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem {
public typealias Result = Bool
fileprivate var id: EnginePeer.Id
public var mapKey: EnginePeer.Id {
return self.id
}
public init(id: EnginePeer.Id) {
self.id = id
}
var key: PostboxViewKey {
return .peer(peerId: self.id, components: [])
}
func extract(view: PostboxView) -> Result {
guard let view = view as? PeerView else {
preconditionFailure()
}
if let channel = peerViewMainPeer(view) as? TelegramChannel {
return channel.flags.contains(.autoTranslateEnabled)
}
return false
}
}
}
}

View File

@ -1702,7 +1702,7 @@ func _internal_editStoryPrivacy(account: Account, id: Int32, privacy: EngineStor
}
public enum StoriesUploadAvailability {
case available
case available(remainingCount: Int32)
case weeklyLimit
case monthlyLimit
case expiringLimit
@ -1729,10 +1729,9 @@ func _internal_checkStoriesUploadAvailability(account: Account, target: Stories.
return account.network.request(Api.functions.stories.canSendStory(peer: inputPeer))
|> map { result -> StoriesUploadAvailability in
if result == .boolTrue {
return .available
} else {
return .unknownLimit
switch result {
case let .canSendStoryCount(countRemains):
return .available(remainingCount: countRemains)
}
}
|> `catch` { error -> Signal<StoriesUploadAvailability, NoError> in

View File

@ -849,6 +849,11 @@ public enum BuyStarGiftError {
case generic
}
public enum UpdateStarGiftPriceError {
case generic
case starGiftResellTooEarly(Int32)
}
public enum UpgradeStarGiftError {
case generic
}
@ -963,7 +968,7 @@ func _internal_upgradeStarGift(account: Account, formId: Int64?, reference: Star
case let .updateNewMessage(message, _, _):
if let message = StoreMessage(apiMessage: message, accountPeerId: account.peerId, peerIsForum: false) {
for media in message.media {
if let action = media as? TelegramMediaAction, case let .starGiftUnique(gift, _, _, savedToProfile, canExportDate, transferStars, _, peerId, _, savedId, _) = action.action, case let .Id(messageId) = message.id {
if let action = media as? TelegramMediaAction, case let .starGiftUnique(gift, _, _, savedToProfile, canExportDate, transferStars, _, peerId, _, savedId, _, canTransferDate, canResaleDate) = action.action, case let .Id(messageId) = message.id {
let reference: StarGiftReference
if let peerId, let savedId {
reference = .peer(peerId: peerId, id: savedId)
@ -984,7 +989,9 @@ func _internal_upgradeStarGift(account: Account, formId: Int64?, reference: Star
canUpgrade: false,
canExportDate: canExportDate,
upgradeStars: nil,
transferStars: transferStars
transferStars: transferStars,
canTransferDate: canTransferDate,
canResaleDate: canResaleDate
))
}
}
@ -1475,11 +1482,18 @@ private final class ProfileGiftsContextImpl {
}
return false
})
self.pushState()
return _internal_buyStarGift(account: self.account, slug: slug, peerId: peerId)
}
func removeStarGift(gift: TelegramCore.StarGift) {
self.gifts.removeAll(where: { $0.gift == gift })
self.filteredGifts.removeAll(where: { $0.gift == gift })
self.pushState()
}
func upgradeStarGift(formId: Int64?, reference: StarGiftReference, keepOriginalInfo: Bool) -> Signal<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError> {
return Signal { [weak self] subscriber in
guard let self else {
@ -1487,7 +1501,13 @@ private final class ProfileGiftsContextImpl {
}
let disposable = MetaDisposable()
disposable.set(
_internal_upgradeStarGift(account: self.account, formId: formId, reference: reference, keepOriginalInfo: keepOriginalInfo).startStrict(next: { [weak self] result in
(_internal_upgradeStarGift(
account: self.account,
formId: formId,
reference: reference,
keepOriginalInfo: keepOriginalInfo
)
|> deliverOn(self.queue)).startStrict(next: { [weak self] result in
guard let self else {
return
}
@ -1509,39 +1529,89 @@ private final class ProfileGiftsContextImpl {
}
}
func updateStarGiftResellPrice(slug: String, price: Int64?) {
self.actionDisposable.set(
_internal_updateStarGiftResalePrice(account: self.account, slug: slug, price: price).startStrict()
)
if let index = self.gifts.firstIndex(where: { gift in
if case let .unique(uniqueGift) = gift.gift, uniqueGift.slug == slug {
return true
func updateStarGiftResellPrice(reference: StarGiftReference, price: Int64?, id: Int64?) -> Signal<Never, UpdateStarGiftPriceError> {
return Signal { [weak self] subscriber in
guard let self else {
return EmptyDisposable
}
return false
}) {
if case let .unique(uniqueGift) = self.gifts[index].gift {
let updatedUniqueGift = uniqueGift.withResellStars(price)
let updatedGift = self.gifts[index].withGift(.unique(updatedUniqueGift))
self.gifts[index] = updatedGift
var saveToProfile = false
if let gift = self.gifts.first(where: { $0.reference == reference }) {
if !gift.savedToProfile {
saveToProfile = true
}
} else if let gift = self.filteredGifts.first(where: { $0.reference == reference }) {
if !gift.savedToProfile {
saveToProfile = true
}
}
var signal = _internal_updateStarGiftResalePrice(account: self.account, reference: reference, price: price)
if saveToProfile {
signal = _internal_updateStarGiftAddedToProfile(account: self.account, reference: reference, added: true)
|> castError(UpdateStarGiftPriceError.self)
|> then(signal)
}
let disposable = MetaDisposable()
disposable.set(
(signal
|> deliverOn(self.queue)).startStrict(error: { error in
subscriber.putError(error)
}, completed: {
if let index = self.gifts.firstIndex(where: { gift in
if gift.reference == reference {
return true
}
switch gift.gift {
case .generic(let gift):
if gift.id == id {
return true
}
case .unique(let uniqueGift):
if uniqueGift.id == id {
return true
}
}
return false
}) {
if case let .unique(uniqueGift) = self.gifts[index].gift {
let updatedUniqueGift = uniqueGift.withResellStars(price)
let updatedGift = self.gifts[index].withGift(.unique(updatedUniqueGift)).withSavedToProfile(true)
self.gifts[index] = updatedGift
}
}
if let index = self.filteredGifts.firstIndex(where: { gift in
if gift.reference == reference {
return true
}
switch gift.gift {
case .generic(let gift):
if gift.id == id {
return true
}
case .unique(let uniqueGift):
if uniqueGift.id == id {
return true
}
}
return false
}) {
if case let .unique(uniqueGift) = self.filteredGifts[index].gift {
let updatedUniqueGift = uniqueGift.withResellStars(price)
let updatedGift = self.filteredGifts[index].withGift(.unique(updatedUniqueGift)).withSavedToProfile(true)
self.filteredGifts[index] = updatedGift
}
}
self.pushState()
subscriber.putCompletion()
})
)
return disposable
}
if let index = self.filteredGifts.firstIndex(where: { gift in
if case let .unique(uniqueGift) = gift.gift, uniqueGift.slug == slug {
return true
}
return false
}) {
if case let .unique(uniqueGift) = self.filteredGifts[index].gift {
let updatedUniqueGift = uniqueGift.withResellStars(price)
let updatedGift = self.filteredGifts[index].withGift(.unique(updatedUniqueGift))
self.filteredGifts[index] = updatedGift
}
}
self.pushState()
}
func toggleStarGiftsNotifications(enabled: Bool) {
@ -1638,6 +1708,8 @@ public final class ProfileGiftsContext {
case upgradeStars
case transferStars
case giftAddress
case canTransferDate
case canResaleDate
}
public let gift: TelegramCore.StarGift
@ -1654,6 +1726,8 @@ public final class ProfileGiftsContext {
public let canExportDate: Int32?
public let upgradeStars: Int64?
public let transferStars: Int64?
public let canTransferDate: Int32?
public let canResaleDate: Int32?
fileprivate let _fromPeerId: EnginePeer.Id?
@ -1675,7 +1749,9 @@ public final class ProfileGiftsContext {
canUpgrade: Bool,
canExportDate: Int32?,
upgradeStars: Int64?,
transferStars: Int64?
transferStars: Int64?,
canTransferDate: Int32?,
canResaleDate: Int32?
) {
self.gift = gift
self.reference = reference
@ -1692,6 +1768,8 @@ public final class ProfileGiftsContext {
self.canExportDate = canExportDate
self.upgradeStars = upgradeStars
self.transferStars = transferStars
self.canTransferDate = canTransferDate
self.canResaleDate = canResaleDate
}
public init(from decoder: Decoder) throws {
@ -1718,6 +1796,8 @@ public final class ProfileGiftsContext {
self.canExportDate = try container.decodeIfPresent(Int32.self, forKey: .canExportDate)
self.upgradeStars = try container.decodeIfPresent(Int64.self, forKey: .upgradeStars)
self.transferStars = try container.decodeIfPresent(Int64.self, forKey: .transferStars)
self.canTransferDate = try container.decodeIfPresent(Int32.self, forKey: .canTransferDate)
self.canResaleDate = try container.decodeIfPresent(Int32.self, forKey: .canResaleDate)
}
public func encode(to encoder: Encoder) throws {
@ -1737,6 +1817,8 @@ public final class ProfileGiftsContext {
try container.encodeIfPresent(self.canExportDate, forKey: .canExportDate)
try container.encodeIfPresent(self.upgradeStars, forKey: .upgradeStars)
try container.encodeIfPresent(self.transferStars, forKey: .transferStars)
try container.encodeIfPresent(self.canTransferDate, forKey: .canTransferDate)
try container.encodeIfPresent(self.canResaleDate, forKey: .canResaleDate)
}
public func withGift(_ gift: TelegramCore.StarGift) -> StarGift {
@ -1754,7 +1836,9 @@ public final class ProfileGiftsContext {
canUpgrade: self.canUpgrade,
canExportDate: self.canExportDate,
upgradeStars: self.upgradeStars,
transferStars: self.transferStars
transferStars: self.transferStars,
canTransferDate: self.canTransferDate,
canResaleDate: self.canResaleDate
)
}
@ -1773,7 +1857,9 @@ public final class ProfileGiftsContext {
canUpgrade: self.canUpgrade,
canExportDate: self.canExportDate,
upgradeStars: self.upgradeStars,
transferStars: self.transferStars
transferStars: self.transferStars,
canTransferDate: self.canTransferDate,
canResaleDate: self.canResaleDate
)
}
@ -1792,7 +1878,9 @@ public final class ProfileGiftsContext {
canUpgrade: self.canUpgrade,
canExportDate: self.canExportDate,
upgradeStars: self.upgradeStars,
transferStars: self.transferStars
transferStars: self.transferStars,
canTransferDate: self.canTransferDate,
canResaleDate: self.canResaleDate
)
}
fileprivate func withFromPeer(_ fromPeer: EnginePeer?) -> StarGift {
@ -1810,7 +1898,9 @@ public final class ProfileGiftsContext {
canUpgrade: self.canUpgrade,
canExportDate: self.canExportDate,
upgradeStars: self.upgradeStars,
transferStars: self.transferStars
transferStars: self.transferStars,
canTransferDate: self.canTransferDate,
canResaleDate: self.canResaleDate
)
}
}
@ -1909,6 +1999,12 @@ public final class ProfileGiftsContext {
}
}
public func removeStarGift(gift: TelegramCore.StarGift) {
self.impl.with { impl in
impl.removeStarGift(gift: gift)
}
}
public func transferStarGift(prepaid: Bool, reference: StarGiftReference, peerId: EnginePeer.Id) -> Signal<Never, TransferStarGiftError> {
return Signal { subscriber in
let disposable = MetaDisposable()
@ -1939,9 +2035,17 @@ public final class ProfileGiftsContext {
}
}
public func updateStarGiftResellPrice(slug: String, price: Int64?) {
self.impl.with { impl in
impl.updateStarGiftResellPrice(slug: slug, price: price)
public func updateStarGiftResellPrice(reference: StarGiftReference, price: Int64?, id: Int64? = nil) -> Signal<Never, UpdateStarGiftPriceError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.updateStarGiftResellPrice(reference: reference, price: price, id: id).start(error: { error in
subscriber.putError(error)
}, completed: {
subscriber.putCompletion()
}))
}
return disposable
}
}
@ -1975,7 +2079,7 @@ public final class ProfileGiftsContext {
extension ProfileGiftsContext.State.StarGift {
init?(apiSavedStarGift: Api.SavedStarGift, peerId: EnginePeer.Id, transaction: Transaction) {
switch apiSavedStarGift {
case let .savedStarGift(flags, fromId, date, apiGift, message, msgId, savedId, convertStars, upgradeStars, canExportDate, transferStars):
case let .savedStarGift(flags, fromId, date, apiGift, message, msgId, savedId, convertStars, upgradeStars, canExportDate, transferStars, canTransferAt, canResaleAt):
guard let gift = StarGift(apiStarGift: apiGift) else {
return nil
}
@ -2019,6 +2123,8 @@ extension ProfileGiftsContext.State.StarGift {
self.canExportDate = canExportDate
self.upgradeStars = upgradeStars
self.transferStars = transferStars
self.canTransferDate = canTransferAt
self.canResaleDate = canResaleAt
}
}
}
@ -2082,10 +2188,12 @@ public enum StarGiftReference: Equatable, Hashable, Codable {
case messageId
case peerId
case id
case slug
}
case message(messageId: EngineMessage.Id)
case peer(peerId: EnginePeer.Id, id: Int64)
case slug(slug: String)
public enum DecodingError: Error {
case generic
@ -2100,6 +2208,8 @@ public enum StarGiftReference: Equatable, Hashable, Codable {
self = .message(messageId: try container.decode(EngineMessage.Id.self, forKey: .messageId))
case 1:
self = .peer(peerId: try container.decode(EnginePeer.Id.self, forKey: .peerId), id: try container.decode(Int64.self, forKey: .id))
case 2:
self = .slug(slug: try container.decode(String.self, forKey: .slug))
default:
throw DecodingError.generic
}
@ -2116,6 +2226,9 @@ public enum StarGiftReference: Equatable, Hashable, Codable {
try container.encode(1 as Int32, forKey: .type)
try container.encode(peerId, forKey: .peerId)
try container.encode(id, forKey: .id)
case let .slug(slug):
try container.encode(2 as Int32, forKey: .type)
try container.encode(slug, forKey: .slug)
}
}
}
@ -2130,6 +2243,8 @@ extension StarGiftReference {
return nil
}
return .inputSavedStarGiftChat(peer: inputPeer, savedId: id)
case let .slug(slug):
return .inputSavedStarGiftSlug(slug: slug)
}
}
}
@ -2265,19 +2380,31 @@ func _internal_toggleStarGiftsNotifications(account: Account, peerId: EnginePeer
}
}
func _internal_updateStarGiftResalePrice(account: Account, slug: String, price: Int64?) -> Signal<Never, NoError> {
return account.network.request(Api.functions.payments.updateStarGiftPrice(slug: slug, resellStars: price ?? 0))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
return .single(nil)
func _internal_updateStarGiftResalePrice(account: Account, reference: StarGiftReference, price: Int64?) -> Signal<Never, UpdateStarGiftPriceError> {
return account.postbox.transaction { transaction in
return reference.apiStarGiftReference(transaction: transaction)
}
|> mapToSignal { updates -> Signal<Void, NoError> in
if let updates {
account.stateManager.addUpdates(updates)
|> castError(UpdateStarGiftPriceError.self)
|> mapToSignal { starGift in
guard let starGift else {
return .complete()
}
return .complete()
return account.network.request(Api.functions.payments.updateStarGiftPrice(stargift: starGift, resellStars: price ?? 0))
|> mapError { error -> UpdateStarGiftPriceError in
if error.errorDescription.hasPrefix("STARGIFT_RESELL_TOO_EARLY_") {
let timeout = String(error.errorDescription[error.errorDescription.index(error.errorDescription.startIndex, offsetBy: "STARGIFT_RESELL_TOO_EARLY_".count)...])
if let value = Int32(timeout) {
return .starGiftResellTooEarly(value)
}
}
return .generic
}
|> mapToSignal { updates -> Signal<Void, UpdateStarGiftPriceError> in
account.stateManager.addUpdates(updates)
return .complete()
}
|> ignoreValues
}
|> ignoreValues
}
public extension StarGift.UniqueGift {
@ -2469,6 +2596,11 @@ private final class ResaleGiftsContextImpl {
self.loadMore()
}
func removeStarGift(gift: TelegramCore.StarGift) {
self.gifts.removeAll(where: { $0 == gift })
self.pushState()
}
func updateSorting(_ sorting: ResaleGiftsContext.Sorting) {
guard self.sorting != sorting else {
return
@ -2479,6 +2611,66 @@ private final class ResaleGiftsContextImpl {
self.loadMore()
}
func buyStarGift(slug: String, peerId: EnginePeer.Id) -> Signal<Never, BuyStarGiftError> {
return _internal_buyStarGift(account: self.account, slug: slug, peerId: peerId)
|> afterCompleted { [weak self] in
guard let self else {
return
}
self.queue.async {
if let count = self.count {
self.count = max(0, count - 1)
}
self.gifts.removeAll(where: { gift in
if case let .unique(uniqueGift) = gift, uniqueGift.slug == slug {
return true
}
return false
})
self.pushState()
}
}
}
func updateStarGiftResellPrice(slug: String, price: Int64?) -> Signal<Never, UpdateStarGiftPriceError> {
return Signal { [weak self] subscriber in
guard let self else {
return EmptyDisposable
}
let disposable = MetaDisposable()
disposable.set(
(_internal_updateStarGiftResalePrice(
account: self.account,
reference: .slug(slug: slug),
price: price
)
|> deliverOn(self.queue)).startStrict(error: { error in
subscriber.putError(error)
}, completed: {
if let index = self.gifts.firstIndex(where: { gift in
if case let .unique(uniqueGift) = gift, uniqueGift.slug == slug {
return true
}
return false
}) {
if let price {
if case let .unique(uniqueGift) = self.gifts[index] {
self.gifts[index] = .unique(uniqueGift.withResellStars(price))
}
} else {
self.gifts.remove(at: index)
}
}
self.pushState()
subscriber.putCompletion()
})
)
return disposable
}
}
private func pushState() {
let state = ResaleGiftsContext.State(
@ -2567,6 +2759,41 @@ public final class ResaleGiftsContext {
impl.updateFilterAttributes(attributes)
}
}
public func buyStarGift(slug: String, peerId: EnginePeer.Id) -> Signal<Never, BuyStarGiftError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.buyStarGift(slug: slug, peerId: peerId).start(error: { error in
subscriber.putError(error)
}, completed: {
subscriber.putCompletion()
}))
}
return disposable
}
}
public func updateStarGiftResellPrice(slug: String, price: Int64?) -> Signal<Never, UpdateStarGiftPriceError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.updateStarGiftResellPrice(slug: slug, price: price).start(error: { error in
subscriber.putError(error)
}, completed: {
subscriber.putCompletion()
}))
}
return disposable
}
}
public func removeStarGift(gift: TelegramCore.StarGift) {
self.impl.with { impl in
impl.removeStarGift(gift: gift)
}
}
public var currentState: ResaleGiftsContext.State? {
var state: ResaleGiftsContext.State?

View File

@ -653,6 +653,9 @@ private extension StarsContext.State.Transaction {
if (apiFlags & (1 << 21)) != 0 {
flags.insert(.isBusinessTransfer)
}
if (apiFlags & (1 << 22)) != 0 {
flags.insert(.isStarGiftResale)
}
let media = extendedMedia.flatMap({ $0.compactMap { textMediaAndExpirationTimerFromApiMedia($0, PeerId(0)).media } }) ?? []
let _ = subscriptionPeriod
@ -706,6 +709,7 @@ public final class StarsContext {
public static let isStarGiftUpgrade = Flags(rawValue: 1 << 6)
public static let isPaidMessage = Flags(rawValue: 1 << 7)
public static let isBusinessTransfer = Flags(rawValue: 1 << 8)
public static let isStarGiftResale = Flags(rawValue: 1 << 9)
}
public enum Peer: Equatable {
@ -1527,7 +1531,7 @@ func _internal_sendStarsPaymentForm(account: Account, formId: Int64, source: Bot
case .giftCode, .stars, .starsGift, .starsChatSubscription, .starGift, .starGiftUpgrade, .starGiftTransfer, .premiumGift, .starGiftResale:
receiptMessageId = nil
}
} else if case let .starGiftUnique(gift, _, _, savedToProfile, canExportDate, transferStars, _, peerId, _, savedId, _) = action.action, case let .Id(messageId) = message.id {
} else if case let .starGiftUnique(gift, _, _, savedToProfile, canExportDate, transferStars, _, peerId, _, savedId, _, canTransferDate, canResaleDate) = action.action, case let .Id(messageId) = message.id {
let reference: StarGiftReference
if let peerId, let savedId {
reference = .peer(peerId: peerId, id: savedId)
@ -1548,7 +1552,9 @@ func _internal_sendStarsPaymentForm(account: Account, formId: Int64, source: Bot
canUpgrade: false,
canExportDate: canExportDate,
upgradeStars: nil,
transferStars: transferStars
transferStars: transferStars,
canTransferDate: canTransferDate,
canResaleDate: canResaleDate
)
}
}

View File

@ -153,8 +153,8 @@ public extension TelegramEngine {
return _internal_toggleStarGiftsNotifications(account: self.account, peerId: peerId, enabled: enabled)
}
public func updateStarGiftResalePrice(slug: String, price: Int64?) -> Signal<Never, NoError> {
return _internal_updateStarGiftResalePrice(account: self.account, slug: slug, price: price)
public func updateStarGiftResalePrice(reference: StarGiftReference, price: Int64?) -> Signal<Never, UpdateStarGiftPriceError> {
return _internal_updateStarGiftResalePrice(account: self.account, reference: reference, price: price)
}
}
}

View File

@ -0,0 +1,19 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
func _internal_toggleAutoTranslation(account: Account, peerId: PeerId, enabled: Bool) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> Signal<Void, NoError> in
if let peer = transaction.getPeer(peerId), let inputChannel = apiInputChannel(peer) {
return account.network.request(Api.functions.channels.toggleAutotranslation(channel: inputChannel, enabled: enabled ? .boolTrue : .boolFalse)) |> `catch` { _ in .complete() } |> map { updates -> Void in
account.stateManager.addUpdates(updates)
}
} else {
return .complete()
}
}
|> switchToLatest
|> ignoreValues
}

View File

@ -94,6 +94,7 @@ public enum AdminLogEventAction {
case changeStatus(prev: PeerEmojiStatus?, new: PeerEmojiStatus?)
case changeEmojiPack(prev: StickerPackReference?, new: StickerPackReference?)
case participantSubscriptionExtended(prev: RenderedChannelParticipant, new: RenderedChannelParticipant)
case toggleAutoTranslation(Bool)
}
public enum ChannelAdminLogEventError {
@ -457,6 +458,8 @@ func channelAdminLogEvents(accountPeerId: PeerId, postbox: Postbox, network: Net
if let prevPeer = peers[prevParticipant.peerId], let newPeer = peers[newParticipant.peerId] {
action = .participantSubscriptionExtended(prev: RenderedChannelParticipant(participant: prevParticipant, peer: prevPeer), new: RenderedChannelParticipant(participant: newParticipant, peer: newPeer))
}
case let .channelAdminLogEventActionToggleAutotranslation(newValue):
action = .toggleAutoTranslation(boolFromApiValue(newValue))
}
let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))
if let action = action {

View File

@ -1693,6 +1693,10 @@ public extension TelegramEngine {
let _ = _internal_removeChatManagingBot(account: self.account, chatId: chatId).startStandalone()
}
public func toggleAutoTranslation(peerId: EnginePeer.Id, enabled: Bool) -> Signal<Never, NoError> {
return _internal_toggleAutoTranslation(account: self.account, peerId: peerId, enabled: enabled)
}
public func resolveMessageLink(slug: String) -> Signal<TelegramResolvedMessageLink?, NoError> {
return self.account.network.request(Api.functions.account.resolveBusinessChatLink(slug: slug))
|> map(Optional.init)

View File

@ -1165,7 +1165,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_Sent(authorName, starsPrice)._tuple, body: bodyAttributes, argumentAttributes: attributes)
}
}
case let .starGiftUnique(gift, isUpgrade, _, _, _, _, _, peerId, senderId, _, _):
case let .starGiftUnique(gift, isUpgrade, _, _, _, _, _, peerId, senderId, _, resaleStars, _, _):
if case let .unique(gift) = gift {
if !forAdditionalServiceMessage && !"".isEmpty {
attributedString = NSAttributedString(string: "\(gift.title) #\(presentationStringsFormattedNumber(gift.number, dateTimeFormat.groupingSeparator))", font: titleFont, textColor: primaryTextColor)
@ -1188,7 +1188,17 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
if message.id.peerId.isTelegramNotifications && senderId == nil {
attributedString = NSAttributedString(string: strings.Notification_StarsGift_SentSomeone, font: titleFont, textColor: primaryTextColor)
} else if message.author?.id == accountPeerId {
attributedString = NSAttributedString(string: strings.Notification_StarsGift_TransferYou, font: titleFont, textColor: primaryTextColor)
if let resaleStars {
let starsString = strings.Notification_StarsGift_Bought_Stars(Int32(resaleStars))
if message.id.peerId == accountPeerId {
attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_BoughtForYouself(starsString)._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes])
} else {
let giftTitle = "\(gift.title) #\(presentationStringsFormattedNumber(gift.number, dateTimeFormat.groupingSeparator))"
attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_BoughtYou(giftTitle, starsString)._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes, 1: boldAttributes])
}
} else {
attributedString = NSAttributedString(string: strings.Notification_StarsGift_TransferYou, font: titleFont, textColor: primaryTextColor)
}
} else if let senderId, let peer = message.peers[senderId] {
if let peerId, let targetPeer = message.peers[peerId] {
if senderId == accountPeerId {
@ -1210,8 +1220,16 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_Transfer(peerName)._tuple, body: bodyAttributes, argumentAttributes: attributes)
}
} else {
let attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: peerIds)
attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_Transfer(peerName)._tuple, body: bodyAttributes, argumentAttributes: attributes)
var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: peerIds)
if let resaleStars {
let starsString = strings.Notification_StarsGift_Bought_Stars(Int32(resaleStars))
let giftTitle = "\(gift.title) #\(presentationStringsFormattedNumber(gift.number, dateTimeFormat.groupingSeparator))"
attributes[1] = boldAttributes
attributes[2] = boldAttributes
attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_Bought(peerName, giftTitle, starsString)._tuple, body: bodyAttributes, argumentAttributes: attributes)
} else {
attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_Transfer(peerName)._tuple, body: bodyAttributes, argumentAttributes: attributes)
}
}
}
}

View File

@ -1959,6 +1959,7 @@ public class CameraScreenImpl: ViewController, CameraScreen {
}
},
nil,
1,
{}
)
} else {
@ -1995,6 +1996,7 @@ public class CameraScreenImpl: ViewController, CameraScreen {
}
},
nil,
self.controller?.remainingStoryCount,
{}
)
}
@ -3374,7 +3376,7 @@ public class CameraScreenImpl: ViewController, CameraScreen {
self.transitionOut = transitionOut
}
}
fileprivate let completion: (Signal<CameraScreenImpl.Result, NoError>, ResultTransition?, @escaping () -> Void) -> Void
fileprivate let completion: (Signal<CameraScreenImpl.Result, NoError>, ResultTransition?, Int32?, @escaping () -> Void) -> Void
public var transitionedIn: () -> Void = {}
public var transitionedOut: () -> Void = {}
@ -3382,6 +3384,7 @@ public class CameraScreenImpl: ViewController, CameraScreen {
private let postingAvailabilityPromise = Promise<StoriesUploadAvailability>()
private var postingAvailabilityDisposable: Disposable?
private var remainingStoryCount: Int32?
private var codeDisposable: Disposable?
private var resolveCodeDisposable: Disposable?
@ -3419,7 +3422,7 @@ public class CameraScreenImpl: ViewController, CameraScreen {
holder: CameraHolder? = nil,
transitionIn: TransitionIn?,
transitionOut: @escaping (Bool) -> TransitionOut?,
completion: @escaping (Signal<CameraScreenImpl.Result, NoError>, ResultTransition?, @escaping () -> Void) -> Void
completion: @escaping (Signal<CameraScreenImpl.Result, NoError>, ResultTransition?, Int32?, @escaping () -> Void) -> Void
) {
self.context = context
self.mode = mode
@ -3469,7 +3472,11 @@ public class CameraScreenImpl: ViewController, CameraScreen {
}
self.postingAvailabilityDisposable = (self.postingAvailabilityPromise.get()
|> deliverOnMainQueue).start(next: { [weak self] availability in
guard let self, availability != .available else {
guard let self else {
return
}
if case let .available(remainingCount) = availability {
self.remainingStoryCount = remainingCount
return
}
self.node.postingAvailable = false
@ -3635,7 +3642,11 @@ public class CameraScreenImpl: ViewController, CameraScreen {
if self.cameraState.isCollageEnabled {
selectionLimit = 6
} else {
selectionLimit = 10
if let remainingStoryCount = self.remainingStoryCount {
selectionLimit = min(Int(remainingStoryCount), 10)
} else {
selectionLimit = 10
}
}
}
controller = self.context.sharedContext.makeStoryMediaPickerScreen(
@ -3700,10 +3711,10 @@ public class CameraScreenImpl: ViewController, CameraScreen {
)
self.present(alertController, in: .window(.root))
} else {
self.completion(.single(.asset(asset)), resultTransition, dismissed)
self.completion(.single(.asset(asset)), resultTransition, self.remainingStoryCount, dismissed)
}
} else if let draft = result as? MediaEditorDraft {
self.completion(.single(.draft(draft)), resultTransition, dismissed)
self.completion(.single(.draft(draft)), resultTransition, self.remainingStoryCount, dismissed)
}
}
}
@ -3749,7 +3760,7 @@ public class CameraScreenImpl: ViewController, CameraScreen {
}
} else {
if let assets = results as? [PHAsset] {
self.completion(.single(.assets(assets)), nil, {
self.completion(.single(.assets(assets)), nil, self.remainingStoryCount, {
})
}

View File

@ -560,7 +560,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
buttonTitle = item.presentationData.strings.Notification_StarGift_View
}
}
case let .starGiftUnique(gift, isUpgrade, _, _, _, _, isRefunded, _, _, _, _):
case let .starGiftUnique(gift, isUpgrade, _, _, _, _, isRefunded, _, _, _, _, _, _):
if case let .unique(uniqueGift) = gift {
isStarGift = true

View File

@ -2282,6 +2282,33 @@ struct ChatRecentActionsEntry: Comparable, Identifiable {
let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil))
case let .toggleAutoTranslation(value):
var peers = SimpleDictionary<PeerId, Peer>()
var author: Peer?
if let peer = self.entry.peers[self.entry.event.peerId] {
author = peer
peers[peer.id] = peer
}
var text: String = ""
var entities: [MessageTextEntity] = []
if value {
appendAttributedText(text: self.presentationData.strings.Channel_AdminLog_MessageToggleAutoTranslateOn(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? ""), generateEntities: { index in
if index == 0, let author = author {
return [.TextMention(peerId: author.id)]
}
return []
}, to: &text, entities: &entities)
} else {
appendAttributedText(text: self.presentationData.strings.Channel_AdminLog_MessageToggleAutoTranslateOff(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? ""), generateEntities: { index in
if index == 0, let author = author {
return [.TextMention(peerId: author.id)]
}
return []
}, to: &text, entities: &entities)
}
let action = TelegramMediaActionType.customText(text: text, entities: entities, additionalAttributes: nil)
let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil))
}
}
}

View File

@ -101,7 +101,7 @@ private final class QuickShareToastScreenComponent: Component {
}
func animateIn() {
guard let component = self.component else {
guard let component = self.component, let environment = self.environment else {
return
}
func generateAvatarParabollicMotionKeyframes(from sourcePoint: CGPoint, to targetPosition: CGPoint, elevation: CGFloat) -> [CGPoint] {
@ -153,7 +153,7 @@ private final class QuickShareToastScreenComponent: Component {
playIconAnimation(0.2)
}
let offset = self.bounds.height - self.backgroundView.frame.minY
let offset = self.bounds.height - environment.inputHeight - self.backgroundView.frame.minY
self.backgroundView.layer.animatePosition(from: CGPoint(x: 0.0, y: offset), to: CGPoint(), duration: 0.35, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true, completion: { _ in
if component.peer.id != component.context.account.peerId {
playIconAnimation(0.1)
@ -203,12 +203,8 @@ private final class QuickShareToastScreenComponent: Component {
let contentInsets = UIEdgeInsets(top: 10.0, left: 12.0, bottom: 10.0, right: 10.0)
let tabBarHeight: CGFloat
if !environment.safeInsets.left.isZero {
tabBarHeight = 34.0 + environment.safeInsets.bottom
} else {
tabBarHeight = 49.0 + environment.safeInsets.bottom
}
let tabBarHeight = 49.0 + max(environment.safeInsets.bottom, environment.inputHeight)
let containerInsets = UIEdgeInsets(
top: environment.safeInsets.top,
left: environment.safeInsets.left + 12.0,
@ -394,8 +390,12 @@ public final class QuickShareToastScreen: ViewControllerComponentContainer {
super.dismiss()
}
private var didCommit = false
public func dismissWithCommitAction() {
self.action(.commit)
if !self.didCommit {
self.didCommit = true
self.action(.commit)
}
self.dismiss()
}

View File

@ -866,7 +866,8 @@ public final class GiftItemComponent: Component {
return (TelegramTextAttributes.URL, contents)
}
)
let labelText = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString("#\(resellPrice)", attributes: attributes))
let dateTimeFormat = component.context.sharedContext.currentPresentationData.with { $0 }.dateTimeFormat
let labelText = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString("#\(presentationStringsFormattedNumber(Int32(resellPrice), dateTimeFormat.groupingSeparator))", attributes: attributes))
if let range = labelText.string.range(of: "#") {
labelText.addAttribute(NSAttributedString.Key.font, value: Font.semibold(10.0), range: NSRange(range, in: labelText.string))
labelText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: true)), range: NSRange(range, in: labelText.string))

View File

@ -370,18 +370,10 @@ final class GiftOptionsScreenComponent: Component {
var isSoldOut = false
switch gift {
case let .generic(gift):
if let availability = gift.availability, availability.resale > 0 {
//TODO:localize
//TODO:unmock
ribbon = GiftItemComponent.Ribbon(
text: "resale",
color: .green
)
} else if let _ = gift.soldOut {
if let _ = gift.soldOut {
if let availability = gift.availability, availability.resale > 0 {
//TODO:localize
ribbon = GiftItemComponent.Ribbon(
text: "resale",
text: environment.strings.Gift_Options_Gift_Resale,
color: .green
)
} else {
@ -415,7 +407,7 @@ final class GiftOptionsScreenComponent: Component {
let subject: GiftItemComponent.Subject
switch gift {
case let .generic(gift):
if let availability = gift.availability, let minResaleStars = availability.minResaleStars {
if let availability = gift.availability, availability.remains == 0, let minResaleStars = availability.minResaleStars {
subject = .starGift(gift: gift, price: "⭐️ \(minResaleStars)+")
} else {
subject = .starGift(gift: gift, price: "⭐️ \(gift.price)")
@ -450,7 +442,7 @@ final class GiftOptionsScreenComponent: Component {
mainController = controller
}
if case let .generic(gift) = gift {
if let availability = gift.availability, availability.remains == 0 || (availability.resale > 0) {
if let availability = gift.availability, availability.remains == 0 {
if availability.resale > 0 {
let storeController = component.context.sharedContext.makeGiftStoreController(
context: component.context,
@ -1296,7 +1288,7 @@ final class GiftOptionsScreenComponent: Component {
starsAmountsSet.insert(gift.price)
if let availability = gift.availability {
hasLimited = true
if availability.resale > 0 {
if availability.remains == 0 && availability.resale > 0 {
hasResale = true
}
}
@ -1317,10 +1309,9 @@ final class GiftOptionsScreenComponent: Component {
))
if hasResale {
//TODO:localize
tabSelectorItems.append(TabSelectorComponent.Item(
id: AnyHashable(StarsFilter.resale.rawValue),
title: "Resale"
title: strings.Gift_Options_Gift_Filter_Resale
))
}

View File

@ -82,6 +82,7 @@ final class GiftSetupScreenComponent: Component {
private let navigationTitle = ComponentView<Empty>()
private let remainingCount = ComponentView<Empty>()
private let resaleSection = ComponentView<Empty>()
private let introContent = ComponentView<Empty>()
private let introSection = ComponentView<Empty>()
private let starsSection = ComponentView<Empty>()
@ -465,7 +466,6 @@ final class GiftSetupScreenComponent: Component {
self.inProgress = false
self.state?.updated()
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
var errorText: String?
switch error {
case .starGiftOutOfStock:
@ -788,6 +788,59 @@ final class GiftSetupScreenComponent: Component {
contentHeight += sectionSpacing
}
if case let .starGift(starGift, _) = component.subject, let availability = starGift.availability, availability.resale > 0 {
let resaleSectionSize = self.resaleSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
header: nil,
footer: nil,
items: [
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(string: environment.strings.Gift_Send_AvailableForResale, font: Font.regular(presentationData.listsFontSize.baseDisplaySize), textColor: environment.theme.list.itemPrimaryTextColor))
)
)),
], alignment: .left, spacing: 2.0)),
accessory: .custom(ListActionItemComponent.CustomAccessory(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: presentationStringsFormattedNumber(Int32(availability.resale), environment.dateTimeFormat.groupingSeparator),
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemSecondaryTextColor
)),
maximumNumberOfLines: 0
))), insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 16.0))),
action: { [weak self] _ in
guard let self, let component = self.component, let controller = environment.controller() else {
return
}
let storeController = component.context.sharedContext.makeGiftStoreController(
context: component.context,
peerId: component.peerId,
gift: starGift
)
controller.push(storeController)
}
)))
]
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
)
let resaleSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: resaleSectionSize)
if let resaleSectionView = self.resaleSection.view {
if resaleSectionView.superview == nil {
self.scrollView.addSubview(resaleSectionView)
}
transition.setFrame(view: resaleSectionView, frame: resaleSectionFrame)
}
contentHeight += resaleSectionSize.height
contentHeight += sectionSpacing
}
let giftConfiguration = GiftConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 })
var introSectionItems: [AnyComponentWithIdentity<Empty>] = []
@ -1717,6 +1770,8 @@ public final class GiftSetupScreen: ViewControllerComponentContainer {
self.title = ""
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.context.sharedContext.currentPresentationData.with { $0 }.strings.Common_Back, style: .plain, target: nil, action: nil)
self.scrollToTop = { [weak self] in
guard let self, let componentView = self.node.hostView.componentView as? GiftSetupScreenComponent.View else {
return

View File

@ -218,8 +218,7 @@ private final class GiftAttributeListContextItemNode: ASDisplayNode, ContextMenu
let selectedAttributes = Set(item.selectedAttributes)
//TODO:localize
let selectAllAction = ContextMenuActionItem(text: "Select All", icon: { theme in
let selectAllAction = ContextMenuActionItem(text: presentationData.strings.Gift_Store_SelectAll, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor)
}, iconPosition: .left, action: { _, f in
getController()?.dismiss(result: .dismissWithoutContent, completion: nil)
@ -248,7 +247,7 @@ private final class GiftAttributeListContextItemNode: ASDisplayNode, ContextMenu
}
let nopAction: ((ContextControllerProtocol?, @escaping (ContextMenuActionResult) -> Void) -> Void)? = nil
let emptyResultsAction = ContextMenuActionItem(text: "No Results", textFont: .small, icon: { _ in return nil }, action: nopAction)
let emptyResultsAction = ContextMenuActionItem(text: presentationData.strings.Gift_Store_NoResults, textFont: .small, icon: { _ in return nil }, action: nopAction)
let emptyResultsActionNode = ContextControllerActionsListActionItemNode(context: item.context, getController: getController, requestDismiss: actionSelected, requestUpdateAction: { _, _ in }, item: emptyResultsAction)
actionNodes.append(emptyResultsActionNode)
@ -412,12 +411,6 @@ private final class GiftAttributeListContextItemNode: ASDisplayNode, ContextMenu
}
func actionNode(at point: CGPoint) -> ContextActionNodeProtocol {
// for actionNode in self.actionNodes {
// let frame = actionNode.convert(actionNode.bounds, to: self)
// if frame.contains(point) {
// return actionNode
// }
// }
return self
}

View File

@ -72,7 +72,7 @@ final class GiftStoreScreenComponent: Component {
private let loadingNode: LoadingShimmerNode
private let emptyResultsAnimation = ComponentView<Empty>()
private let emptyResultsTitle = ComponentView<Empty>()
private let emptyResultsAction = ComponentView<Empty>()
private let clearFilters = ComponentView<Empty>()
private let topPanel = ComponentView<Empty>()
private let topSeparator = ComponentView<Empty>()
@ -154,6 +154,7 @@ final class GiftStoreScreenComponent: Component {
}
let availableWidth = self.scrollView.bounds.width
let availableHeight = self.scrollView.bounds.height
let contentOffset = self.scrollView.contentOffset.y
let topPanelAlpha = min(20.0, max(0.0, contentOffset)) / 20.0
@ -213,8 +214,8 @@ final class GiftStoreScreenComponent: Component {
font: .monospaced,
color: ribbonColor
)
let subject: GiftItemComponent.Subject = .uniqueGift(gift: uniqueGift, price: "⭐️\(uniqueGift.resellStars ?? 0)")
let subject: GiftItemComponent.Subject = .uniqueGift(gift: uniqueGift, price: "⭐️\(presentationStringsFormattedNumber(Int32(uniqueGift.resellStars ?? 0), environment.dateTimeFormat.groupingSeparator))")
let _ = visibleItem.update(
transition: itemTransition,
component: AnyComponent(
@ -241,7 +242,13 @@ final class GiftStoreScreenComponent: Component {
}
let giftController = GiftViewScreen(
context: component.context,
subject: .uniqueGift(uniqueGift, state.peerId)
subject: .uniqueGift(uniqueGift, state.peerId),
buyGift: { slug, peerId in
return self.state?.starGiftsContext.buyStarGift(slug: slug, peerId: peerId) ?? .complete()
},
updateResellStars: { price in
return self.state?.starGiftsContext.updateStarGiftResellPrice(slug: uniqueGift.slug, price: price) ?? .complete()
}
)
mainController.push(giftController)
}
@ -288,6 +295,136 @@ final class GiftStoreScreenComponent: Component {
}
}
let fadeTransition = ComponentTransition.easeInOut(duration: 0.25)
let emptyResultsActionSize = self.clearFilters.update(
transition: .immediate,
component: AnyComponent(
PlainButtonComponent(
content: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(string: environment.strings.Gift_Store_ClearFilters, font: Font.regular(17.0), textColor: environment.theme.list.itemAccentColor)),
horizontalAlignment: .center,
maximumNumberOfLines: 0
)
),
effectAlignment: .center,
action: { [weak self] in
guard let self else {
return
}
self.state?.starGiftsContext.updateFilterAttributes([])
},
animateScale: false
)
),
environment: {},
containerSize: CGSize(width: availableWidth - 44.0 * 2.0, height: 100.0)
)
var showClearFilters = false
if let filterAttributes = self.state?.starGiftsState?.filterAttributes, !filterAttributes.isEmpty {
showClearFilters = true
}
let topInset: CGFloat = environment.navigationHeight + 39.0
let bottomInset: CGFloat = environment.safeInsets.bottom
var emptyResultsActionFrame = CGRect(
origin: CGPoint(
x: floorToScreenPixels((availableWidth - emptyResultsActionSize.width) / 2.0),
y: max(self.scrollView.contentSize.height - 8.0, availableHeight - bottomInset - emptyResultsActionSize.height - 16.0)
),
size: emptyResultsActionSize
)
if let effectiveGifts = self.effectiveGifts, effectiveGifts.isEmpty && self.state?.starGiftsState?.dataState != .loading {
let emptyAnimationHeight = 148.0
let visibleHeight = availableHeight
let emptyAnimationSpacing: CGFloat = 20.0
let emptyTextSpacing: CGFloat = 18.0
let emptyResultsTitleSize = self.emptyResultsTitle.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(string: environment.strings.Gift_Store_EmptyResults, font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor)),
horizontalAlignment: .center
)
),
environment: {},
containerSize: CGSize(width: availableWidth, height: 100.0)
)
let emptyResultsAnimationSize = self.emptyResultsAnimation.update(
transition: .immediate,
component: AnyComponent(LottieComponent(
content: LottieComponent.AppBundleContent(name: "ChatListNoResults")
)),
environment: {},
containerSize: CGSize(width: emptyAnimationHeight, height: emptyAnimationHeight)
)
let emptyTotalHeight = emptyAnimationHeight + emptyAnimationSpacing + emptyResultsTitleSize.height + emptyResultsActionSize.height + emptyTextSpacing
let emptyAnimationY = topInset + floorToScreenPixels((visibleHeight - topInset - bottomInset - emptyTotalHeight) / 2.0)
let emptyResultsAnimationFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableWidth - emptyResultsAnimationSize.width) / 2.0), y: emptyAnimationY), size: emptyResultsAnimationSize)
let emptyResultsTitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableWidth - emptyResultsTitleSize.width) / 2.0), y: emptyResultsAnimationFrame.maxY + emptyAnimationSpacing), size: emptyResultsTitleSize)
emptyResultsActionFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableWidth - emptyResultsActionSize.width) / 2.0), y: emptyResultsTitleFrame.maxY + emptyTextSpacing), size: emptyResultsActionSize)
if let view = self.emptyResultsAnimation.view as? LottieComponent.View {
if view.superview == nil {
view.alpha = 0.0
fadeTransition.setAlpha(view: view, alpha: 1.0)
self.insertSubview(view, belowSubview: self.loadingNode.view)
view.playOnce()
}
view.bounds = CGRect(origin: .zero, size: emptyResultsAnimationFrame.size)
ComponentTransition.immediate.setPosition(view: view, position: emptyResultsAnimationFrame.center)
}
if let view = self.emptyResultsTitle.view {
if view.superview == nil {
view.alpha = 0.0
fadeTransition.setAlpha(view: view, alpha: 1.0)
self.insertSubview(view, belowSubview: self.loadingNode.view)
}
view.bounds = CGRect(origin: .zero, size: emptyResultsTitleFrame.size)
ComponentTransition.immediate.setPosition(view: view, position: emptyResultsTitleFrame.center)
}
} else {
if let view = self.emptyResultsAnimation.view {
fadeTransition.setAlpha(view: view, alpha: 0.0, completion: { _ in
view.removeFromSuperview()
})
}
if let view = self.emptyResultsTitle.view {
fadeTransition.setAlpha(view: view, alpha: 0.0, completion: { _ in
view.removeFromSuperview()
})
}
}
if showClearFilters {
if let view = self.clearFilters.view {
if view.superview == nil {
view.alpha = 0.0
fadeTransition.setAlpha(view: view, alpha: 1.0)
self.insertSubview(view, belowSubview: self.loadingNode.view)
}
view.bounds = CGRect(origin: .zero, size: emptyResultsActionFrame.size)
ComponentTransition.immediate.setPosition(view: view, position: emptyResultsActionFrame.center)
view.alpha = self.state?.starGiftsState?.attributes.isEmpty == true ? 0.0 : 1.0
}
} else {
if let view = self.clearFilters.view {
fadeTransition.setAlpha(view: view, alpha: 0.0, completion: { _ in
view.removeFromSuperview()
})
}
}
let bottomContentOffset = max(0.0, self.scrollView.contentSize.height - self.scrollView.contentOffset.y - self.scrollView.frame.height)
if interactive, bottomContentOffset < 320.0 {
self.state?.starGiftsContext.loadMore()
@ -302,21 +439,21 @@ final class GiftStoreScreenComponent: Component {
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: "Sort by Price", icon: { theme in
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_Store_SortByPrice, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Peer Info/SortValue"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.default)
self?.state?.starGiftsContext.updateSorting(.value)
})))
items.append(.action(ContextMenuActionItem(text: "Sort by Date", icon: { theme in
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_Store_SortByDate, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Peer Info/SortDate"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.default)
self?.state?.starGiftsContext.updateSorting(.date)
})))
items.append(.action(ContextMenuActionItem(text: "Sort by Number", icon: { theme in
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_Store_SortByNumber, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Peer Info/SortNumber"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.default)
@ -354,17 +491,18 @@ final class GiftStoreScreenComponent: Component {
}
}
//TODO:localize
var items: [ContextMenuItem] = []
items.append(.custom(SearchContextItem(
context: component.context,
placeholder: "Search",
value: "",
valueChanged: { value in
searchQueryPromise.set(value)
}
), false))
items.append(.separator)
if modelAttributes.count >= 8 {
items.append(.custom(SearchContextItem(
context: component.context,
placeholder: presentationData.strings.Gift_Store_Search,
value: "",
valueChanged: { value in
searchQueryPromise.set(value)
}
), false))
items.append(.separator)
}
items.append(.custom(GiftAttributeListContextItem(
context: component.context,
attributes: modelAttributes,
@ -444,17 +582,18 @@ final class GiftStoreScreenComponent: Component {
}
}
//TODO:localize
var items: [ContextMenuItem] = []
items.append(.custom(SearchContextItem(
context: component.context,
placeholder: "Search",
value: "",
valueChanged: { value in
searchQueryPromise.set(value)
}
), false))
items.append(.separator)
if backdropAttributes.count >= 8 {
items.append(.custom(SearchContextItem(
context: component.context,
placeholder: presentationData.strings.Gift_Store_Search,
value: "",
valueChanged: { value in
searchQueryPromise.set(value)
}
), false))
items.append(.separator)
}
items.append(.custom(GiftAttributeListContextItem(
context: component.context,
attributes: backdropAttributes,
@ -534,17 +673,18 @@ final class GiftStoreScreenComponent: Component {
}
}
//TODO:localize
var items: [ContextMenuItem] = []
items.append(.custom(SearchContextItem(
context: component.context,
placeholder: "Search",
value: "",
valueChanged: { value in
searchQueryPromise.set(value)
}
), false))
items.append(.separator)
if patternAttributes.count >= 8 {
items.append(.custom(SearchContextItem(
context: component.context,
placeholder: presentationData.strings.Gift_Store_Search,
value: "",
valueChanged: { value in
searchQueryPromise.set(value)
}
), false))
items.append(.separator)
}
items.append(.custom(GiftAttributeListContextItem(
context: component.context,
attributes: patternAttributes,
@ -669,35 +809,7 @@ final class GiftStoreScreenComponent: Component {
transition.setFrame(view: topPanelView, frame: topPanelFrame)
transition.setFrame(view: topSeparatorView, frame: topSeparatorFrame)
}
// let cancelButtonSize = self.cancelButton.update(
// transition: transition,
// component: AnyComponent(
// PlainButtonComponent(
// content: AnyComponent(
// MultilineTextComponent(
// text: .plain(NSAttributedString(string: strings.Common_Cancel, font: Font.regular(17.0), textColor: theme.rootController.navigationBar.accentTextColor)),
// horizontalAlignment: .center
// )
// ),
// effectAlignment: .center,
// action: {
// controller()?.dismiss()
// },
// animateScale: false
// )
// ),
// environment: {},
// containerSize: CGSize(width: availableSize.width, height: 100.0)
// )
// let cancelButtonFrame = CGRect(origin: CGPoint(x: environment.safeInsets.left + 16.0, y: environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) / 2.0 - cancelButtonSize.height / 2.0), size: cancelButtonSize)
// if let cancelButtonView = self.cancelButton.view {
// if cancelButtonView.superview == nil {
// self.addSubview(cancelButtonView)
// }
// transition.setFrame(view: cancelButtonView, frame: cancelButtonFrame)
// }
let balanceTitleSize = self.balanceTitle.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
@ -777,7 +889,7 @@ final class GiftStoreScreenComponent: Component {
let subtitleSize = self.subtitle.update(
transition: transition,
component: AnyComponent(BalancedTextComponent(
text: .plain(NSAttributedString(string: "\(effectiveCount) for resale", font: Font.regular(13.0), textColor: theme.rootController.navigationBar.secondaryTextColor)),
text: .plain(NSAttributedString(string: environment.strings.Gift_Store_ForResale(effectiveCount), font: Font.regular(13.0), textColor: theme.rootController.navigationBar.secondaryTextColor)),
horizontalAlignment: .center,
maximumNumberOfLines: 1
)),
@ -795,18 +907,18 @@ final class GiftStoreScreenComponent: Component {
let optionSpacing: CGFloat = 10.0
let optionWidth = (availableSize.width - sideInset * 2.0 - optionSpacing * 2.0) / 3.0
var sortingTitle = "Date"
var sortingTitle = environment.strings.Gift_Store_Sort_Date
var sortingIcon: String = "Peer Info/SortDate"
if let sorting = self.state?.starGiftsState?.sorting {
switch sorting {
case .date:
sortingTitle = "Date"
sortingTitle = environment.strings.Gift_Store_Sort_Date
sortingIcon = "Peer Info/SortDate"
case .value:
sortingTitle = "Price"
sortingTitle = environment.strings.Gift_Store_Sort_Price
sortingIcon = "Peer Info/SortValue"
case .number:
sortingTitle = "Number"
sortingTitle = environment.strings.Gift_Store_Sort_Number
sortingIcon = "Peer Info/SortNumber"
}
}
@ -823,13 +935,13 @@ final class GiftStoreScreenComponent: Component {
}
))
var modelTitle = "Model"
var backdropTitle = "Backdrop"
var symbolTitle = "Symbol"
var modelTitle = environment.strings.Gift_Store_Filter_Model
var backdropTitle = environment.strings.Gift_Store_Filter_Backdrop
var symbolTitle = environment.strings.Gift_Store_Filter_Symbol
if let filterAttributes = self.state?.starGiftsState?.filterAttributes {
var modelCount = 0
var backdropCount = 0
var symbolCount = 0
var modelCount: Int32 = 0
var backdropCount: Int32 = 0
var symbolCount: Int32 = 0
for attribute in filterAttributes {
switch attribute {
@ -843,25 +955,13 @@ final class GiftStoreScreenComponent: Component {
}
if modelCount > 0 {
if modelCount > 1 {
modelTitle = "\(modelCount) Models"
} else {
modelTitle = "1 Model"
}
modelTitle = environment.strings.Gift_Store_Filter_Selected_Model(modelCount)
}
if backdropCount > 0 {
if backdropCount > 1 {
backdropTitle = "\(backdropCount) Backdrops"
} else {
backdropTitle = "1 Backdrop"
}
backdropTitle = environment.strings.Gift_Store_Filter_Selected_Backdrop(modelCount)
}
if symbolCount > 0 {
if symbolCount > 1 {
symbolTitle = "\(symbolCount) Symbols"
} else {
symbolTitle = "1 Symbol"
}
symbolTitle = environment.strings.Gift_Store_Filter_Selected_Symbol(modelCount)
}
}
@ -966,118 +1066,7 @@ final class GiftStoreScreenComponent: Component {
loadingTransition.setAlpha(view: self.loadingNode.view, alpha: 0.0)
}
transition.setFrame(view: self.loadingNode.view, frame: CGRect(origin: CGPoint(x: 0.0, y: environment.navigationHeight + 39.0 + 7.0), size: availableSize))
let fadeTransition = ComponentTransition.easeInOut(duration: 0.25)
if let effectiveGifts = self.effectiveGifts, effectiveGifts.isEmpty && self.state?.starGiftsState?.dataState != .loading {
let sideInset: CGFloat = 44.0
let emptyAnimationHeight = 148.0
let topInset: CGFloat = environment.navigationHeight + 39.0
let bottomInset: CGFloat = environment.safeInsets.bottom
let visibleHeight = availableSize.height
let emptyAnimationSpacing: CGFloat = 20.0
let emptyTextSpacing: CGFloat = 18.0
let emptyResultsTitleSize = self.emptyResultsTitle.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(string: "No Matching Gifts", font: Font.semibold(17.0), textColor: theme.list.itemPrimaryTextColor)),
horizontalAlignment: .center
)
),
environment: {},
containerSize: availableSize
)
let emptyResultsActionSize = self.emptyResultsAction.update(
transition: .immediate,
component: AnyComponent(
PlainButtonComponent(
content: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(string: "Clear Filters", font: Font.regular(17.0), textColor: theme.list.itemAccentColor)),
horizontalAlignment: .center,
maximumNumberOfLines: 0
)
),
effectAlignment: .center,
action: { [weak self] in
guard let self else {
return
}
self.state?.starGiftsContext.updateFilterAttributes([])
},
animateScale: false
)
),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: visibleHeight)
)
let emptyResultsAnimationSize = self.emptyResultsAnimation.update(
transition: .immediate,
component: AnyComponent(LottieComponent(
content: LottieComponent.AppBundleContent(name: "ChatListNoResults")
)),
environment: {},
containerSize: CGSize(width: emptyAnimationHeight, height: emptyAnimationHeight)
)
let emptyTotalHeight = emptyAnimationHeight + emptyAnimationSpacing + emptyResultsTitleSize.height + emptyResultsActionSize.height + emptyTextSpacing
let emptyAnimationY = topInset + floorToScreenPixels((visibleHeight - topInset - bottomInset - emptyTotalHeight) / 2.0)
let emptyResultsAnimationFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - emptyResultsAnimationSize.width) / 2.0), y: emptyAnimationY), size: emptyResultsAnimationSize)
let emptyResultsTitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - emptyResultsTitleSize.width) / 2.0), y: emptyResultsAnimationFrame.maxY + emptyAnimationSpacing), size: emptyResultsTitleSize)
let emptyResultsActionFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - emptyResultsActionSize.width) / 2.0), y: emptyResultsTitleFrame.maxY + emptyTextSpacing), size: emptyResultsActionSize)
if let view = self.emptyResultsAnimation.view as? LottieComponent.View {
if view.superview == nil {
view.alpha = 0.0
fadeTransition.setAlpha(view: view, alpha: 1.0)
self.insertSubview(view, belowSubview: self.loadingNode.view)
view.playOnce()
}
view.bounds = CGRect(origin: .zero, size: emptyResultsAnimationFrame.size)
ComponentTransition.immediate.setPosition(view: view, position: emptyResultsAnimationFrame.center)
}
if let view = self.emptyResultsTitle.view {
if view.superview == nil {
view.alpha = 0.0
fadeTransition.setAlpha(view: view, alpha: 1.0)
self.insertSubview(view, belowSubview: self.loadingNode.view)
}
view.bounds = CGRect(origin: .zero, size: emptyResultsTitleFrame.size)
ComponentTransition.immediate.setPosition(view: view, position: emptyResultsTitleFrame.center)
}
if let view = self.emptyResultsAction.view {
if view.superview == nil {
view.alpha = 0.0
fadeTransition.setAlpha(view: view, alpha: 1.0)
self.insertSubview(view, belowSubview: self.loadingNode.view)
}
view.bounds = CGRect(origin: .zero, size: emptyResultsActionFrame.size)
ComponentTransition.immediate.setPosition(view: view, position: emptyResultsActionFrame.center)
view.alpha = self.state?.starGiftsState?.attributes.isEmpty == true ? 0.0 : 1.0
}
} else {
if let view = self.emptyResultsAnimation.view {
fadeTransition.setAlpha(view: view, alpha: 0.0, completion: { _ in
view.removeFromSuperview()
})
}
if let view = self.emptyResultsTitle.view {
fadeTransition.setAlpha(view: view, alpha: 0.0, completion: { _ in
view.removeFromSuperview()
})
}
if let view = self.emptyResultsAction.view {
fadeTransition.setAlpha(view: view, alpha: 0.0, completion: { _ in
view.removeFromSuperview()
})
}
}
return availableSize
}
}

View File

@ -10,10 +10,10 @@ import AccountContext
import TelegramPresentationData
final class PriceButtonComponent: Component {
let price: Int64
let price: String
init(
price: Int64
price: String
) {
self.price = price
}
@ -54,7 +54,7 @@ final class PriceButtonComponent: Component {
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "\(component.price)",
string: component.price,
font: Font.semibold(11.0),
textColor: UIColor(rgb: 0xffffff)
))

View File

@ -407,7 +407,6 @@ private final class GiftViewSheetContent: CombinedComponent {
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let mode = ContactSelectionControllerMode.starsGifting(birthdays: nil, hasActions: false, showSelf: true, selfSubtitle: presentationData.strings.Premium_Gift_ContactSelection_BuySelf)
//TODO:localize
let controller = self.context.sharedContext.makeContactSelectionController(ContactSelectionControllerParams(
context: context,
mode: mode,
@ -444,7 +443,19 @@ private final class GiftViewSheetContent: CombinedComponent {
self.updated()
self.buyDisposable = (self.buyGift(uniqueGift.slug, recipientPeerId)
|> deliverOnMainQueue).start(completed: { [weak self, weak starsContext] in
|> deliverOnMainQueue).start(error: { [weak self] error in
guard let self, let controller = self.getController() else {
return
}
self.inProgress = false
self.updated()
let errorText = presentationData.strings.Gift_Send_ErrorUnknown
let alertController = textAlertController(context: context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})], parseMarkdown: true)
controller.present(alertController, in: .window(.root))
}, completed: { [weak self, weak starsContext] in
guard let self, let controller = self.getController() as? GiftViewScreen else {
return
}
@ -460,40 +471,22 @@ private final class GiftViewSheetContent: CombinedComponent {
if let navigationController = controller.navigationController as? NavigationController {
if recipientPeerId == self.context.account.peerId {
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId))
|> deliverOnMainQueue).start(next: { [weak navigationController] peer in
guard let peer, let navigationController else {
return
controller.dismissAnimated()
navigationController.view.addSubview(ConfettiView(frame: navigationController.view.bounds))
Queue.mainQueue().after(0.5, {
if let lastController = navigationController.viewControllers.last as? ViewController, let animationFile {
let resultController = UndoOverlayController(
presentationData: presentationData,
content: .sticker(context: context, file: animationFile, loop: false, title: presentationData.strings.Gift_View_Resale_SuccessYou_Title, text: presentationData.strings.Gift_View_Resale_SuccessYou_Text(giftTitle).string, undoText: nil, customAction: nil),
elevatedLayout: lastController is ChatController,
action: { _ in
return true
}
)
lastController.present(resultController, in: .window(.root))
}
var controllers = Array(navigationController.viewControllers.prefix(1))
if let controller = context.sharedContext.makePeerInfoController(
context: context,
updatedPresentationData: nil,
peer: peer._asPeer(),
mode: .myProfileGifts,
avatarInitiallyExpanded: false,
fromChat: false,
requestsContext: nil
) {
controllers.append(controller)
}
navigationController.setViewControllers(controllers, animated: true)
navigationController.view.addSubview(ConfettiView(frame: navigationController.view.bounds))
Queue.mainQueue().after(0.5, {
if let lastController = navigationController.viewControllers.last as? ViewController, let animationFile {
let resultController = UndoOverlayController(
presentationData: presentationData,
content: .sticker(context: context, file: animationFile, loop: false, title: "Gift Acquired", text: "\(giftTitle) is now yours.", undoText: nil, customAction: nil),
elevatedLayout: lastController is ChatController,
action: { _ in
return true
}
)
lastController.present(resultController, in: .window(.root))
}
})
})
} else {
var controllers = Array(navigationController.viewControllers.prefix(1))
@ -508,7 +501,7 @@ private final class GiftViewSheetContent: CombinedComponent {
if let peer, let lastController = navigationController?.viewControllers.last as? ViewController, let animationFile {
let resultController = UndoOverlayController(
presentationData: presentationData,
content: .sticker(context: context, file: animationFile, loop: false, title: "Gift Sent", text: "\(peer.compactDisplayTitle) has been notified about your gift.", undoText: nil, customAction: nil),
content: .sticker(context: context, file: animationFile, loop: false, title: presentationData.strings.Gift_View_Resale_Success_Title, text: presentationData.strings.Gift_View_Resale_Success_Text(peer.compactDisplayTitle).string, undoText: nil, customAction: nil),
elevatedLayout: lastController is ChatController,
action: { _ in
return true
@ -521,7 +514,6 @@ private final class GiftViewSheetContent: CombinedComponent {
}
}
controller.animateSuccess()
self.updated(transition: .spring(duration: 0.4))
Queue.mainQueue().after(0.5) {
@ -884,17 +876,16 @@ private final class GiftViewSheetContent: CombinedComponent {
headerSubject = nil
}
var ownerPeerId: EnginePeer.Id
var ownerPeerId: EnginePeer.Id?
if let uniqueGift, case let .peerId(peerId) = uniqueGift.owner {
ownerPeerId = peerId
} else {
ownerPeerId = component.context.account.peerId
}
let wearOwnerPeerId = ownerPeerId ?? component.context.account.peerId
var wearPeerNameChild: _UpdatedChildComponent?
if showWearPreview, let uniqueGift {
var peerName = ""
if let ownerPeer = state.peerMap[ownerPeerId] {
if let ownerPeer = state.peerMap[wearOwnerPeerId] {
peerName = ownerPeer.displayTitle(strings: strings, displayOrder: nameDisplayOrder)
}
wearPeerNameChild = wearPeerName.update(
@ -1004,7 +995,7 @@ private final class GiftViewSheetContent: CombinedComponent {
}
if let wearPeerNameChild {
if let ownerPeer = state.peerMap[ownerPeerId] {
if let ownerPeer = state.peerMap[wearOwnerPeerId] {
let wearAvatar = wearAvatar.update(
component: AvatarComponent(
context: component.context,
@ -1488,8 +1479,7 @@ private final class GiftViewSheetContent: CombinedComponent {
if !soldOut {
if let uniqueGift {
if case let .uniqueGift(_, recipientPeerIdValue) = component.subject, let _ = recipientPeerIdValue, let recipientPeerId = state.recipientPeerId {
//TODO:localize
if !"".isEmpty, case let .uniqueGift(_, recipientPeerIdValue) = component.subject, let _ = recipientPeerIdValue, let recipientPeerId = state.recipientPeerId {
if let peer = state.peerMap[recipientPeerId] {
tableItems.append(.init(
id: "recipient",
@ -1815,7 +1805,7 @@ private final class GiftViewSheetContent: CombinedComponent {
}
let canWear: Bool
if isChannelGift, case let .channel(channel) = state.peerMap[ownerPeerId] {
if isChannelGift, case let .channel(channel) = state.peerMap[wearOwnerPeerId] {
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 })
let requiredLevel = Int(BoostSubject.wearGift.requiredLevel(group: false, context: component.context, configuration: premiumConfiguration))
if let boostLevel = channel.approximateBoostLevel {
@ -1853,12 +1843,11 @@ private final class GiftViewSheetContent: CombinedComponent {
)
buttonOriginX += buttonWidth + buttonSpacing
//TODO:localize
let resellButton = resellButton.update(
component: PlainButtonComponent(
content: AnyComponent(
HeaderButtonComponent(
title: uniqueGift.resellStars == nil ? "sell" : "unlist",
title: uniqueGift.resellStars == nil ? strings.Gift_View_Sell : strings.Gift_View_Unlist,
iconName: uniqueGift.resellStars == nil ? "Premium/Collectible/Sell" : "Premium/Collectible/Unlist"
)
),
@ -2232,31 +2221,30 @@ private final class GiftViewSheetContent: CombinedComponent {
if let uniqueGift {
resellStars = uniqueGift.resellStars
if incoming, let resellStars {
let priceButton = priceButton.update(
component: PlainButtonComponent(
content: AnyComponent(
PriceButtonComponent(price: resellStars)
if let resellStars {
if incoming || ownerPeerId == component.context.account.peerId {
let priceButton = priceButton.update(
component: PlainButtonComponent(
content: AnyComponent(
PriceButtonComponent(price: presentationStringsFormattedNumber(Int32(resellStars), environment.dateTimeFormat.groupingSeparator))
),
effectAlignment: .center,
action: {
component.resellGift(true)
},
animateScale: false
),
effectAlignment: .center,
action: {
component.resellGift(true)
},
animateScale: false
),
availableSize: CGSize(width: 120.0, height: 30.0),
transition: context.transition
)
context.add(priceButton
.position(CGPoint(x: environment.safeInsets.left + 16.0 + priceButton.size.width / 2.0, y: 28.0))
.appear(.default(scale: true, alpha: true))
.disappear(.default(scale: true, alpha: true))
)
}
if !incoming, let _ = resellStars {
availableSize: CGSize(width: 150.0, height: 30.0),
transition: context.transition
)
context.add(priceButton
.position(CGPoint(x: environment.safeInsets.left + 16.0 + priceButton.size.width / 2.0, y: 28.0))
.appear(.default(scale: true, alpha: true))
.disappear(.default(scale: true, alpha: true))
)
}
if case let .uniqueGift(_, recipientPeerId) = component.subject, recipientPeerId != nil {
} else {
} else if ownerPeerId != component.context.account.peerId {
selling = true
}
}
@ -2270,14 +2258,13 @@ private final class GiftViewSheetContent: CombinedComponent {
var addressToOpen: String?
var descriptionText: String
if let uniqueGift, selling {
//TODO:localize
let ownerName: String
if case let .peerId(peerId) = uniqueGift.owner {
ownerName = state.peerMap[peerId]?.compactDisplayTitle ?? ""
} else {
ownerName = ""
}
descriptionText = "\(ownerName) is selling this gift and you can buy it."
descriptionText = strings.Gift_View_SellingGiftInfo(ownerName).string
} else if let uniqueGift, let address = uniqueGift.giftAddress, case .address = uniqueGift.owner {
addressToOpen = address
descriptionText = strings.Gift_View_TonGiftAddressInfo
@ -2361,7 +2348,7 @@ private final class GiftViewSheetContent: CombinedComponent {
let requiredLevel = Int(BoostSubject.wearGift.requiredLevel(group: false, context: component.context, configuration: premiumConfiguration))
var canWear = true
if isChannelGift, case let .channel(channel) = state.peerMap[ownerPeerId], (channel.approximateBoostLevel ?? 0) < requiredLevel {
if isChannelGift, case let .channel(channel) = state.peerMap[wearOwnerPeerId], (channel.approximateBoostLevel ?? 0) < requiredLevel {
canWear = false
buttonContent = AnyComponentWithIdentity(
id: AnyHashable("wear_channel"),
@ -2421,7 +2408,7 @@ private final class GiftViewSheetContent: CombinedComponent {
if isChannelGift {
state.levelsDisposable.set(combineLatest(
queue: Queue.mainQueue(),
context.engine.peers.getChannelBoostStatus(peerId: ownerPeerId),
context.engine.peers.getChannelBoostStatus(peerId: wearOwnerPeerId),
context.engine.peers.getMyBoostStatus()
).startStandalone(next: { [weak controller] boostStatus, myBoostStatus in
guard let controller, let boostStatus, let myBoostStatus else {
@ -2429,7 +2416,7 @@ private final class GiftViewSheetContent: CombinedComponent {
}
component.cancel(true)
let levelsController = context.sharedContext.makePremiumBoostLevelsController(context: context, peerId: ownerPeerId, subject: .wearGift, boostStatus: boostStatus, myBoostStatus: myBoostStatus, forceDark: false, openStats: nil)
let levelsController = context.sharedContext.makePremiumBoostLevelsController(context: context, peerId: wearOwnerPeerId, subject: .wearGift, boostStatus: boostStatus, myBoostStatus: myBoostStatus, forceDark: false, openStats: nil)
controller.push(levelsController)
HapticFeedback().impact(.light)
@ -2574,8 +2561,7 @@ private final class GiftViewSheetContent: CombinedComponent {
if state.cachedStarImage == nil || state.cachedStarImage?.1 !== theme {
state.cachedStarImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: theme.list.itemCheckColors.foregroundColor)!, theme)
}
//TODO:localize
var upgradeString = "Buy for"
var upgradeString = strings.Gift_View_BuyFor
upgradeString += " # \(presentationStringsFormattedNumber(Int32(resellStars), environment.dateTimeFormat.groupingSeparator))"
let buttonTitle = subject.arguments?.upgradeStars != nil ? strings.Gift_Upgrade_Confirm : upgradeString
@ -2763,6 +2749,11 @@ private final class GiftViewSheetComponent: CombinedComponent {
if let controller = controller() as? GiftViewScreen {
controller.dismissAllTooltips()
}
},
willDismiss: {
if let controller = controller() as? GiftViewScreen {
controller.dismissBalanceOverlay()
}
}
),
environment: {
@ -2829,7 +2820,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
case upgradePreview([StarGift.UniqueGift.Attribute], String)
case wearPreview(StarGift.UniqueGift)
var arguments: (peerId: EnginePeer.Id?, fromPeerId: EnginePeer.Id?, fromPeerName: String?, messageId: EngineMessage.Id?, reference: StarGiftReference?, incoming: Bool, gift: StarGift, date: Int32, convertStars: Int64?, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, pinnedToTop: Bool?, converted: Bool, upgraded: Bool, refunded: Bool, canUpgrade: Bool, upgradeStars: Int64?, transferStars: Int64?, resellStars: Int64?, canExportDate: Int32?, upgradeMessageId: Int32?)? {
var arguments: (peerId: EnginePeer.Id?, fromPeerId: EnginePeer.Id?, fromPeerName: String?, messageId: EngineMessage.Id?, reference: StarGiftReference?, incoming: Bool, gift: StarGift, date: Int32, convertStars: Int64?, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, pinnedToTop: Bool?, converted: Bool, upgraded: Bool, refunded: Bool, canUpgrade: Bool, upgradeStars: Int64?, transferStars: Int64?, resellStars: Int64?, canExportDate: Int32?, upgradeMessageId: Int32?, canTransferDate: Int32?, canResaleDate: Int32?)? {
switch self {
case let .message(message):
if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction {
@ -2841,8 +2832,8 @@ public class GiftViewScreen: ViewControllerComponentContainer {
} else {
reference = .message(messageId: message.id)
}
return (message.id.peerId, senderId ?? message.author?.id, message.author?.compactDisplayTitle, message.id, reference, message.flags.contains(.Incoming), gift, message.timestamp, convertStars, text, entities, nameHidden, savedToProfile, nil, converted, upgraded, isRefunded, canUpgrade, upgradeStars, nil, nil, nil, upgradeMessageId)
case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, _, peerId, senderId, savedId, _):
return (message.id.peerId, senderId ?? message.author?.id, message.author?.compactDisplayTitle, message.id, reference, message.flags.contains(.Incoming), gift, message.timestamp, convertStars, text, entities, nameHidden, savedToProfile, nil, converted, upgraded, isRefunded, canUpgrade, upgradeStars, nil, nil, nil, upgradeMessageId, nil, nil)
case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, _, peerId, senderId, savedId, _, canTransferDate, canResaleDate):
var reference: StarGiftReference
if let peerId, let savedId {
reference = .peer(peerId: peerId, id: savedId)
@ -2866,13 +2857,13 @@ public class GiftViewScreen: ViewControllerComponentContainer {
if case let .unique(uniqueGift) = gift {
resellStars = uniqueGift.resellStars
}
return (message.id.peerId, senderId ?? message.author?.id, message.author?.compactDisplayTitle, message.id, reference, incoming, gift, message.timestamp, nil, nil, nil, false, savedToProfile, nil, false, false, false, false, nil, transferStars, resellStars, canExportDate, nil)
return (message.id.peerId, senderId ?? message.author?.id, message.author?.compactDisplayTitle, message.id, reference, incoming, gift, message.timestamp, nil, nil, nil, false, savedToProfile, nil, false, false, false, false, nil, transferStars, resellStars, canExportDate, nil, canTransferDate, canResaleDate)
default:
return nil
}
}
case let .uniqueGift(gift, _), let .wearPreview(gift):
return (nil, nil, nil, nil, nil, false, .unique(gift), 0, nil, nil, nil, false, false, nil, false, false, false, false, nil, nil, gift.resellStars, nil, nil)
return (nil, nil, nil, nil, nil, false, .unique(gift), 0, nil, nil, nil, false, false, nil, false, false, false, false, nil, nil, gift.resellStars, nil, nil, nil, nil)
case let .profileGift(peerId, gift):
var messageId: EngineMessage.Id?
if case let .message(messageIdValue) = gift.reference {
@ -2882,7 +2873,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
if case let .unique(uniqueGift) = gift.gift {
resellStars = uniqueGift.resellStars
}
return (peerId, gift.fromPeer?.id, gift.fromPeer?.compactDisplayTitle, messageId, gift.reference, false, gift.gift, gift.date, gift.convertStars, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, gift.pinnedToTop, false, false, false, gift.canUpgrade, gift.upgradeStars, gift.transferStars, resellStars, gift.canExportDate, nil)
return (peerId, gift.fromPeer?.id, gift.fromPeer?.compactDisplayTitle, messageId, gift.reference, false, gift.gift, gift.date, gift.convertStars, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, gift.pinnedToTop, false, false, false, gift.canUpgrade, gift.upgradeStars, gift.transferStars, resellStars, gift.canExportDate, nil, gift.canTransferDate, gift.canResaleDate)
case .soldOutGift:
return nil
case .upgradePreview:
@ -2920,14 +2911,14 @@ public class GiftViewScreen: ViewControllerComponentContainer {
transferGift: ((Bool, EnginePeer.Id) -> Signal<Never, TransferStarGiftError>)? = nil,
upgradeGift: ((Int64?, Bool) -> Signal<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError>)? = nil,
buyGift: ((String, EnginePeer.Id) -> Signal<Never, BuyStarGiftError>)? = nil,
updateResellStars: ((Int64?) -> Void)? = nil,
updateResellStars: ((Int64?) -> Signal<Never, UpdateStarGiftPriceError>)? = nil,
togglePinnedToTop: ((Bool) -> Bool)? = nil,
shareStory: ((StarGift.UniqueGift) -> Void)? = nil
) {
self.context = context
self.subject = subject
var openPeerImpl: ((EnginePeer) -> Void)?
var openPeerImpl: ((EnginePeer, Bool) -> Void)?
var openAddressImpl: ((String) -> Void)?
var copyAddressImpl: ((String) -> Void)?
var updateSavedToProfileImpl: ((Bool) -> Void)?
@ -2950,7 +2941,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
context: context,
subject: subject,
openPeer: { peerId in
openPeerImpl?(peerId)
openPeerImpl?(peerId, false)
},
openAddress: { address in
openAddressImpl?(address)
@ -3009,21 +3000,27 @@ public class GiftViewScreen: ViewControllerComponentContainer {
self.navigationPresentation = .flatModal
self.automaticallyControlPresentationContextLayout = false
openPeerImpl = { [weak self] peer in
openPeerImpl = { [weak self] peer, gifts in
guard let self, let navigationController = self.navigationController as? NavigationController else {
return
}
self.dismissAllTooltips()
let _ = (context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: peer.id)
)
|> deliverOnMainQueue).start(next: { peer in
guard let peer else {
return
if gifts {
if let controller = context.sharedContext.makePeerInfoController(
context: context,
updatedPresentationData: nil,
peer: peer._asPeer(),
mode: .gifts,
avatarInitiallyExpanded: false,
fromChat: false,
requestsContext: nil
) {
self.push(controller)
}
} else {
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, chatController: nil, context: context, chatLocation: .peer(peer), subject: nil, botStart: nil, updateTextInputState: nil, keepStack: .always, useExisting: true, purposefulAction: nil, scrollToEndIfExists: false, activateMessageSearch: nil, animated: true))
})
}
}
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
@ -3379,7 +3376,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
guard let peer else {
return
}
openPeerImpl?(peer)
openPeerImpl?(peer, false)
Queue.mainQueue().after(0.6) {
self?.dismiss(animated: false, completion: nil)
}
@ -3397,54 +3394,75 @@ public class GiftViewScreen: ViewControllerComponentContainer {
}
resellGiftImpl = { [weak self] update in
guard let self, let arguments = self.subject.arguments, case let .profileGift(peerId, currentSubject) = self.subject, case let .unique(gift) = arguments.gift else {
guard let self, let arguments = self.subject.arguments, case let .unique(gift) = arguments.gift else {
return
}
self.dismissAllTooltips()
//TODO:localize
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
if let canResaleDate = arguments.canResaleDate, currentTime < canResaleDate {
let dateString = stringForFullDate(timestamp: canResaleDate, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat)
let controller = textAlertController(
context: self.context,
title: presentationData.strings.Gift_Resale_Unavailable_Title,
text: presentationData.strings.Gift_Resale_Unavailable_Text(dateString).string,
actions: [
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})
],
parseMarkdown: true
)
self.present(controller, in: .window(.root))
return
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let giftTitle = "\(gift.title) #\(presentationStringsFormattedNumber(gift.number, presentationData.dateTimeFormat.groupingSeparator))"
let reference = arguments.reference ?? .slug(slug: gift.slug)
if let resellStars = gift.resellStars, resellStars > 0, !update {
let alertController = textAlertController(
context: context,
title: "Unlist This Item?",
text: "It will no longer be for sale.",
title: presentationData.strings.Gift_View_Resale_Unlist_Title,
text: presentationData.strings.Gift_View_Resale_Unlist_Text,
actions: [
TextAlertAction(type: .defaultAction, title: "Unlist", action: { [weak self] in
TextAlertAction(type: .defaultAction, title: presentationData.strings.Gift_View_Resale_Unlist_Unlist, action: { [weak self] in
guard let self else {
return
}
self.subject = .profileGift(peerId, currentSubject.withGift(.unique(gift.withResellStars(nil))))
let giftTitle = "\(gift.title) #\(gift.number)"
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let text = "\(giftTitle) is removed from sale."
let tooltipController = UndoOverlayController(
presentationData: presentationData,
content: .universalImage(
image: generateTintedImage(image: UIImage(bundleImageName: "Premium/Collectible/Unlist"), color: .white)!,
size: nil,
title: nil,
text: text,
customUndoText: nil,
timeout: 3.0
),
position: .bottom,
animateInAsReplacement: false,
appearance: UndoOverlayController.Appearance(sideInset: 16.0, bottomInset: 62.0),
action: { action in
return false
let _ = ((updateResellStars?(nil) ?? context.engine.payments.updateStarGiftResalePrice(reference: reference, price: nil))
|> deliverOnMainQueue).startStandalone(error: { error in
}, completed: {
switch self.subject {
case let .profileGift(peerId, currentSubject):
self.subject = .profileGift(peerId, currentSubject.withGift(.unique(gift.withResellStars(nil))))
case let .uniqueGift(_, recipientPeerId):
self.subject = .uniqueGift(gift.withResellStars(nil), recipientPeerId)
default:
break
}
)
self.present(tooltipController, in: .window(.root))
if let updateResellStars {
updateResellStars(nil)
} else {
let _ = (context.engine.payments.updateStarGiftResalePrice(slug: gift.slug, price: nil)
|> deliverOnMainQueue).startStandalone()
}
let text = presentationData.strings.Gift_View_Resale_Unlist_Success(giftTitle).string
let tooltipController = UndoOverlayController(
presentationData: presentationData,
content: .universalImage(
image: generateTintedImage(image: UIImage(bundleImageName: "Premium/Collectible/Unlist"), color: .white)!,
size: nil,
title: nil,
text: text,
customUndoText: nil,
timeout: 3.0
),
position: .bottom,
animateInAsReplacement: false,
appearance: UndoOverlayController.Appearance(sideInset: 16.0, bottomInset: 62.0),
action: { action in
return false
}
)
self.present(tooltipController, in: .window(.root))
})
}),
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
})
@ -3458,41 +3476,48 @@ public class GiftViewScreen: ViewControllerComponentContainer {
return
}
self.subject = .profileGift(peerId, currentSubject.withGift(.unique(gift.withResellStars(price))))
let giftTitle = "\(gift.title) #\(gift.number)"
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var text = "\(giftTitle) is now for sale!"
if update {
text = "\(giftTitle) is relisted for \(price) Stars."
}
let tooltipController = UndoOverlayController(
presentationData: presentationData,
content: .universalImage(
image: generateTintedImage(image: UIImage(bundleImageName: "Premium/Collectible/Sell"), color: .white)!,
size: nil,
title: nil,
text: text,
customUndoText: nil,
timeout: 3.0
),
position: .bottom,
animateInAsReplacement: false,
appearance: UndoOverlayController.Appearance(sideInset: 16.0, bottomInset: 62.0),
action: { action in
return false
let _ = ((updateResellStars?(price) ?? context.engine.payments.updateStarGiftResalePrice(reference: reference, price: price))
|> deliverOnMainQueue).startStandalone(error: { error in
}, completed: { [weak self] in
guard let self else {
return
}
)
self.present(tooltipController, in: .window(.root))
if let updateResellStars {
updateResellStars(price)
} else {
let _ = (context.engine.payments.updateStarGiftResalePrice(slug: gift.slug, price: price)
|> deliverOnMainQueue).startStandalone()
}
switch self.subject {
case let .profileGift(peerId, currentSubject):
self.subject = .profileGift(peerId, currentSubject.withGift(.unique(gift.withResellStars(price))))
case let .uniqueGift(_, recipientPeerId):
self.subject = .uniqueGift(gift.withResellStars(price), recipientPeerId)
default:
break
}
var text = presentationData.strings.Gift_View_Resale_List_Success(giftTitle).string
if update {
let starsString = presentationData.strings.Gift_View_Resale_Relist_Success_Stars(Int32(price))
text = presentationData.strings.Gift_View_Resale_Relist_Success(giftTitle, starsString).string
}
let tooltipController = UndoOverlayController(
presentationData: presentationData,
content: .universalImage(
image: generateTintedImage(image: UIImage(bundleImageName: "Premium/Collectible/Sell"), color: .white)!,
size: nil,
title: nil,
text: text,
customUndoText: nil,
timeout: 3.0
),
position: .bottom,
animateInAsReplacement: false,
appearance: UndoOverlayController.Appearance(sideInset: 16.0, bottomInset: 62.0),
action: { action in
return false
}
)
self.present(tooltipController, in: .window(.root))
})
})
self.push(resellController)
}
@ -3571,6 +3596,16 @@ 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)
})))
}
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_View_Context_CopyLink, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] c, _ in
@ -3607,6 +3642,27 @@ public class GiftViewScreen: ViewControllerComponentContainer {
}
}
if let _ = arguments.resellStars, case let .uniqueGift(uniqueGift, recipientPeerId) = subject, let _ = recipientPeerId {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_View_Context_ViewInProfile, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Peer Info/ShowIcon"), color: theme.contextMenu.primaryColor)
}, action: { c, _ in
c?.dismiss(completion: nil)
if case let .peerId(peerId) = uniqueGift.owner {
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let self, let peer else {
return
}
openPeerImpl?(peer, true)
Queue.mainQueue().after(0.6) {
self.dismiss(animated: false, completion: nil)
}
})
}
})))
}
let contextController = ContextController(presentationData: presentationData, source: .reference(GiftViewContextReferenceContentSource(controller: self, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
self.presentInGlobalOverlay(contextController)
})

View File

@ -352,6 +352,8 @@ public final class MediaEditor {
return state.position
}
}
public var maxDuration: Double = 60.0
public var duration: Double? {
if let stickerEntity = self.stickerEntity {
@ -360,7 +362,7 @@ public final class MediaEditor {
if let trimRange = self.values.videoTrimRange {
return trimRange.upperBound - trimRange.lowerBound
} else {
return min(60.0, self.playerPlaybackState.duration)
return min(self.maxDuration, self.playerPlaybackState.duration)
}
} else {
return nil
@ -369,7 +371,7 @@ public final class MediaEditor {
public var mainVideoDuration: Double? {
if self.player != nil {
return min(60.0, self.playerPlaybackState.duration)
return min(self.maxDuration, self.playerPlaybackState.duration)
} else {
return nil
}
@ -377,7 +379,7 @@ public final class MediaEditor {
public var additionalVideoDuration: Double? {
if let additionalPlayer = self.additionalPlayers.first {
return min(60.0, additionalPlayer.currentItem?.asset.duration.seconds ?? 0.0)
return min(self.maxDuration, additionalPlayer.currentItem?.asset.duration.seconds ?? 0.0)
} else {
return nil
}
@ -385,7 +387,15 @@ public final class MediaEditor {
public var originalDuration: Double? {
if self.player != nil || !self.additionalPlayers.isEmpty {
return min(60.0, self.playerPlaybackState.duration)
return self.playerPlaybackState.duration
} else {
return nil
}
}
public var originalCappedDuration: Double? {
if self.player != nil || !self.additionalPlayers.isEmpty {
return min(self.maxDuration, self.playerPlaybackState.duration)
} else {
return nil
}
@ -530,10 +540,11 @@ public final class MediaEditor {
}
}
public init(context: AccountContext, mode: Mode, subject: Subject, values: MediaEditorValues? = nil, hasHistogram: Bool = false) {
public init(context: AccountContext, mode: Mode, subject: Subject, values: MediaEditorValues? = nil, hasHistogram: Bool = false, isStandalone: Bool = false) {
self.context = context
self.mode = mode
self.subject = subject
if let values {
self.values = values
self.updateRenderChain()
@ -581,6 +592,9 @@ public final class MediaEditor {
}
self.valuesPromise.set(.single(self.values))
if isStandalone, let device = MTLCreateSystemDefaultDevice() {
self.renderer.setupForStandaloneDevice(device: device)
}
self.renderer.addRenderChain(self.renderChain)
if hasHistogram {
self.renderer.addRenderPass(self.histogramCalculationPass)
@ -611,7 +625,7 @@ public final class MediaEditor {
}
public func replaceSource(_ image: UIImage, additionalImage: UIImage?, time: CMTime, mirror: Bool) {
guard let renderTarget = self.previewView, let device = renderTarget.mtlDevice, let texture = loadTexture(image: image, device: device) else {
guard let device = self.renderer.effectiveDevice, let texture = loadTexture(image: image, device: device) else {
return
}
let additionalTexture = additionalImage.flatMap { loadTexture(image: $0, device: device) }
@ -974,6 +988,8 @@ public final class MediaEditor {
if let trimRange = self.values.videoTrimRange {
player.currentItem?.forwardPlaybackEndTime = CMTime(seconds: trimRange.upperBound, preferredTimescale: CMTimeScale(1000))
// additionalPlayer?.currentItem?.forwardPlaybackEndTime = CMTime(seconds: trimRange.upperBound, preferredTimescale: CMTimeScale(1000))
} else if let duration = player.currentItem?.duration.seconds, duration > self.maxDuration {
player.currentItem?.forwardPlaybackEndTime = CMTime(seconds: self.maxDuration, preferredTimescale: CMTimeScale(1000))
}
if let initialSeekPosition = self.initialSeekPosition {

View File

@ -125,7 +125,7 @@ final class MediaEditorRenderer {
func addRenderPass(_ renderPass: RenderPass) {
self.renderPasses.append(renderPass)
if let device = self.renderTarget?.mtlDevice, let library = self.library {
if let device = self.effectiveDevice, let library = self.library {
renderPass.setup(device: device, library: library)
}
}
@ -160,6 +160,14 @@ final class MediaEditorRenderer {
self.renderPasses.forEach { $0.setup(device: device, library: library) }
}
var effectiveDevice: MTLDevice? {
if let device = self.renderTarget?.mtlDevice {
return device
} else {
return self.device
}
}
private func setup() {
guard let device = self.renderTarget?.mtlDevice else {
return
@ -180,6 +188,11 @@ final class MediaEditorRenderer {
self.commonSetup(device: device)
}
func setupForStandaloneDevice(device: MTLDevice) {
self.device = device
self.commonSetup(device: device)
}
func setRate(_ rate: Float) {
self.textureSource?.setRate(rate)
}
@ -240,15 +253,7 @@ final class MediaEditorRenderer {
}
func renderFrame() {
let device: MTLDevice?
if let renderTarget = self.renderTarget {
device = renderTarget.mtlDevice
} else if let currentDevice = self.device {
device = currentDevice
} else {
device = nil
}
guard let device = device,
guard let device = self.effectiveDevice,
let commandQueue = self.commandQueue,
let textureCache = self.textureCache,
let commandBuffer = commandQueue.makeCommandBuffer(),
@ -366,7 +371,7 @@ final class MediaEditorRenderer {
}
func finalRenderedImage(mirror: Bool = false) -> UIImage? {
if let finalTexture = self.resultTexture, let device = self.renderTarget?.mtlDevice {
if let finalTexture = self.resultTexture, let device = self.effectiveDevice {
return getTextureImage(device: device, texture: finalTexture, mirror: mirror)
} else {
return nil

View File

@ -909,7 +909,7 @@ public final class MediaEditorValues: Codable, Equatable {
return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset)
}
func withUpdatedVideoTrimRange(_ videoTrimRange: Range<Double>) -> MediaEditorValues {
public func withUpdatedVideoTrimRange(_ videoTrimRange: Range<Double>) -> MediaEditorValues {
return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset)
}

View File

@ -1,45 +0,0 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import AccountContext
import TextFormat
public extension MediaEditorScreenImpl {
static func makeEditVideoCoverController(
context: AccountContext,
video: MediaEditorScreenImpl.Subject,
completed: @escaping () -> Void = {},
willDismiss: @escaping () -> Void = {},
update: @escaping (Disposable?) -> Void
) -> MediaEditorScreenImpl? {
let controller = MediaEditorScreenImpl(
context: context,
mode: .storyEditor,
subject: .single(video),
isEditing: true,
isEditingCover: true,
forwardSource: nil,
initialCaption: nil,
initialPrivacy: nil,
initialMediaAreas: nil,
initialVideoPosition: 0.0,
transitionIn: .noAnimation,
transitionOut: { finished, isNew in
return nil
},
completion: { result, commit in
if let _ = result.coverTimestamp {
}
commit({})
}
)
controller.willDismiss = willDismiss
controller.navigationPresentation = .flatModal
return controller
}
}

View File

@ -97,7 +97,7 @@ public extension MediaEditorScreenImpl {
var updateProgressImpl: ((Float) -> Void)?
let controller = MediaEditorScreenImpl(
context: context,
mode: .storyEditor,
mode: .storyEditor(remainingCount: 1),
subject: subject,
isEditing: !repost,
isEditingCover: cover,
@ -122,7 +122,10 @@ public extension MediaEditorScreenImpl {
return transitionOut
}
},
completion: { result, commit in
completion: { results, commit in
guard let result = results.first else {
return
}
let entities = generateChatInputTextEntities(result.caption)
if repost {

View File

@ -0,0 +1,837 @@
import Foundation
import UIKit
import Display
import AVFoundation
import SwiftSignalKit
import TelegramCore
import TextFormat
import Photos
import MediaEditor
import DrawingUI
extension MediaEditorScreenImpl {
func requestStoryCompletion(animated: Bool) {
guard let mediaEditor = self.node.mediaEditor, !self.didComplete else {
return
}
self.didComplete = true
self.updateMediaEditorEntities()
mediaEditor.stop()
mediaEditor.invalidate()
self.node.entitiesView.invalidate()
if let navigationController = self.navigationController as? NavigationController {
navigationController.updateRootContainerTransitionOffset(0.0, transition: .immediate)
}
var multipleItems: [EditingItem] = []
var isLongVideo = false
if self.node.items.count > 1 {
multipleItems = self.node.items.filter({ $0.isEnabled })
} else if case let .asset(asset) = self.node.subject {
let duration: Double
if let playerDuration = mediaEditor.duration {
duration = playerDuration
} else {
duration = asset.duration
}
if duration > storyMaxVideoDuration {
let originalDuration = mediaEditor.originalDuration ?? asset.duration
let values = mediaEditor.values
let storyCount = min(storyMaxCombinedVideoCount, Int(ceil(duration / storyMaxVideoDuration)))
var start = values.videoTrimRange?.lowerBound ?? 0
for i in 0 ..< storyCount {
let trimmedValues = values.withUpdatedVideoTrimRange(start ..< min(start + storyMaxVideoDuration, originalDuration))
var editingItem = EditingItem(asset: asset)
if i == 0 {
editingItem.caption = self.node.getCaption()
}
editingItem.values = trimmedValues
multipleItems.append(editingItem)
start += storyMaxVideoDuration
}
isLongVideo = true
}
}
if multipleItems.count > 1 {
self.processMultipleItems(items: multipleItems, isLongVideo: isLongVideo)
} else {
self.processSingleItem()
}
self.dismissAllTooltips()
}
private func processSingleItem() {
guard let mediaEditor = self.node.mediaEditor, let subject = self.node.subject, let actualSubject = self.node.actualSubject else {
return
}
var caption = self.node.getCaption()
caption = convertMarkdownToAttributes(caption)
var hasEntityChanges = false
let randomId: Int64
if case let .draft(_, id) = actualSubject, let id {
randomId = id
} else {
randomId = Int64.random(in: .min ... .max)
}
let codableEntities = mediaEditor.values.entities
var mediaAreas: [MediaArea] = []
if case let .draft(draft, _) = actualSubject {
if draft.values.entities != codableEntities {
hasEntityChanges = true
}
} else {
mediaAreas = self.initialMediaAreas ?? []
}
var stickers: [TelegramMediaFile] = []
for entity in codableEntities {
switch entity {
case let .sticker(stickerEntity):
if case let .file(file, fileType) = stickerEntity.content, case .sticker = fileType {
stickers.append(file.media)
}
case let .text(textEntity):
if let subEntities = textEntity.renderSubEntities {
for entity in subEntities {
if let stickerEntity = entity as? DrawingStickerEntity, case let .file(file, fileType) = stickerEntity.content, case .sticker = fileType {
stickers.append(file.media)
}
}
}
default:
break
}
if let mediaArea = entity.mediaArea {
mediaAreas.append(mediaArea)
}
}
var hasAnyChanges = self.node.hasAnyChanges
if self.isEditingStoryCover {
hasAnyChanges = false
}
if self.isEmbeddedEditor && !(hasAnyChanges || hasEntityChanges) {
self.saveDraft(id: randomId, isEdit: true)
self.completion([MediaEditorScreenImpl.Result(media: nil, mediaAreas: [], caption: caption, coverTimestamp: mediaEditor.values.coverImageTimestamp, options: self.state.privacy, stickers: stickers, randomId: randomId)], { [weak self] finished in
self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in
self?.dismiss()
Queue.mainQueue().justDispatch {
finished()
}
})
})
return
}
if !(self.isEditingStory || self.isEditingStoryCover) {
let privacy = self.state.privacy
let _ = updateMediaEditorStoredStateInteractively(engine: self.context.engine, { current in
if let current {
return current.withUpdatedPrivacy(privacy)
} else {
return MediaEditorStoredState(privacy: privacy, textSettings: nil)
}
}).start()
}
if mediaEditor.resultIsVideo {
self.saveDraft(id: randomId)
var firstFrame: Signal<(UIImage?, UIImage?), NoError>
let firstFrameTime: CMTime
if let coverImageTimestamp = mediaEditor.values.coverImageTimestamp {
firstFrameTime = CMTime(seconds: coverImageTimestamp, preferredTimescale: CMTimeScale(60))
} else {
firstFrameTime = CMTime(seconds: mediaEditor.values.videoTrimRange?.lowerBound ?? 0.0, preferredTimescale: CMTimeScale(60))
}
let videoResult: Signal<MediaResult.VideoResult, NoError>
var videoIsMirrored = false
let duration: Double
switch subject {
case let .empty(dimensions):
let image = generateImage(dimensions.cgSize, opaque: false, scale: 1.0, rotatedContext: { size, context in
context.clear(CGRect(origin: .zero, size: size))
})!
let tempImagePath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).jpg"
if let data = image.jpegData(compressionQuality: 0.85) {
try? data.write(to: URL(fileURLWithPath: tempImagePath))
}
videoResult = .single(.imageFile(path: tempImagePath))
duration = 3.0
firstFrame = .single((image, nil))
case let .image(image, _, _, _):
let tempImagePath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).jpg"
if let data = image.jpegData(compressionQuality: 0.85) {
try? data.write(to: URL(fileURLWithPath: tempImagePath))
}
videoResult = .single(.imageFile(path: tempImagePath))
duration = 5.0
firstFrame = .single((image, nil))
case let .video(path, _, mirror, additionalPath, _, _, durationValue, _, _):
videoIsMirrored = mirror
videoResult = .single(.videoFile(path: path))
if let videoTrimRange = mediaEditor.values.videoTrimRange {
duration = videoTrimRange.upperBound - videoTrimRange.lowerBound
} else {
duration = durationValue
}
var additionalPath = additionalPath
if additionalPath == nil, let valuesAdditionalPath = mediaEditor.values.additionalVideoPath {
additionalPath = valuesAdditionalPath
}
firstFrame = Signal<(UIImage?, UIImage?), NoError> { subscriber in
let avAsset = AVURLAsset(url: URL(fileURLWithPath: path))
let avAssetGenerator = AVAssetImageGenerator(asset: avAsset)
avAssetGenerator.appliesPreferredTrackTransform = true
avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)], completionHandler: { _, cgImage, _, _, _ in
if let cgImage {
if let additionalPath {
let avAsset = AVURLAsset(url: URL(fileURLWithPath: additionalPath))
let avAssetGenerator = AVAssetImageGenerator(asset: avAsset)
avAssetGenerator.appliesPreferredTrackTransform = true
avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)], completionHandler: { _, additionalCGImage, _, _, _ in
if let additionalCGImage {
subscriber.putNext((UIImage(cgImage: cgImage), UIImage(cgImage: additionalCGImage)))
subscriber.putCompletion()
} else {
subscriber.putNext((UIImage(cgImage: cgImage), nil))
subscriber.putCompletion()
}
})
} else {
subscriber.putNext((UIImage(cgImage: cgImage), nil))
subscriber.putCompletion()
}
}
})
return ActionDisposable {
avAssetGenerator.cancelAllCGImageGeneration()
}
}
case let .videoCollage(items):
var maxDurationItem: (Double, Subject.VideoCollageItem)?
for item in items {
switch item.content {
case .image:
break
case let .video(_, duration):
if let (maxDuration, _) = maxDurationItem {
if duration > maxDuration {
maxDurationItem = (duration, item)
}
} else {
maxDurationItem = (duration, item)
}
case let .asset(asset):
if let (maxDuration, _) = maxDurationItem {
if asset.duration > maxDuration {
maxDurationItem = (asset.duration, item)
}
} else {
maxDurationItem = (asset.duration, item)
}
}
}
guard let (maxDuration, mainItem) = maxDurationItem else {
fatalError()
}
switch mainItem.content {
case let .video(path, _):
videoResult = .single(.videoFile(path: path))
case let .asset(asset):
videoResult = .single(.asset(localIdentifier: asset.localIdentifier))
default:
fatalError()
}
let image = generateImage(storyDimensions, opaque: false, scale: 1.0, rotatedContext: { size, context in
context.clear(CGRect(origin: .zero, size: size))
})!
firstFrame = .single((image, nil))
if let videoTrimRange = mediaEditor.values.videoTrimRange {
duration = videoTrimRange.upperBound - videoTrimRange.lowerBound
} else {
duration = min(maxDuration, storyMaxVideoDuration)
}
case let .asset(asset):
videoResult = .single(.asset(localIdentifier: asset.localIdentifier))
if asset.mediaType == .video {
if let videoTrimRange = mediaEditor.values.videoTrimRange {
duration = videoTrimRange.upperBound - videoTrimRange.lowerBound
} else {
duration = min(asset.duration, storyMaxVideoDuration)
}
} else {
duration = 5.0
}
var additionalPath: String?
if let valuesAdditionalPath = mediaEditor.values.additionalVideoPath {
additionalPath = valuesAdditionalPath
}
firstFrame = Signal<(UIImage?, UIImage?), NoError> { subscriber in
if asset.mediaType == .video {
PHImageManager.default().requestAVAsset(forVideo: asset, options: nil) { avAsset, _, _ in
if let avAsset {
let avAssetGenerator = AVAssetImageGenerator(asset: avAsset)
avAssetGenerator.appliesPreferredTrackTransform = true
avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)], completionHandler: { _, cgImage, _, _, _ in
if let cgImage {
if let additionalPath {
let avAsset = AVURLAsset(url: URL(fileURLWithPath: additionalPath))
let avAssetGenerator = AVAssetImageGenerator(asset: avAsset)
avAssetGenerator.appliesPreferredTrackTransform = true
avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)], completionHandler: { _, additionalCGImage, _, _, _ in
if let additionalCGImage {
subscriber.putNext((UIImage(cgImage: cgImage), UIImage(cgImage: additionalCGImage)))
subscriber.putCompletion()
} else {
subscriber.putNext((UIImage(cgImage: cgImage), nil))
subscriber.putCompletion()
}
})
} else {
subscriber.putNext((UIImage(cgImage: cgImage), nil))
subscriber.putCompletion()
}
}
})
}
}
} else {
let options = PHImageRequestOptions()
options.deliveryMode = .highQualityFormat
PHImageManager.default().requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .default, options: options) { image, _ in
if let image {
if let additionalPath {
let avAsset = AVURLAsset(url: URL(fileURLWithPath: additionalPath))
let avAssetGenerator = AVAssetImageGenerator(asset: avAsset)
avAssetGenerator.appliesPreferredTrackTransform = true
avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)], completionHandler: { _, additionalCGImage, _, _, _ in
if let additionalCGImage {
subscriber.putNext((image, UIImage(cgImage: additionalCGImage)))
subscriber.putCompletion()
} else {
subscriber.putNext((image, nil))
subscriber.putCompletion()
}
})
} else {
subscriber.putNext((image, nil))
subscriber.putCompletion()
}
}
}
}
return EmptyDisposable
}
case let .draft(draft, _):
let draftPath = draft.fullPath(engine: context.engine)
if draft.isVideo {
videoResult = .single(.videoFile(path: draftPath))
if let videoTrimRange = mediaEditor.values.videoTrimRange {
duration = videoTrimRange.upperBound - videoTrimRange.lowerBound
} else {
duration = min(draft.duration ?? 5.0, storyMaxVideoDuration)
}
firstFrame = Signal<(UIImage?, UIImage?), NoError> { subscriber in
let avAsset = AVURLAsset(url: URL(fileURLWithPath: draftPath))
let avAssetGenerator = AVAssetImageGenerator(asset: avAsset)
avAssetGenerator.appliesPreferredTrackTransform = true
avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)], completionHandler: { _, cgImage, _, _, _ in
if let cgImage {
subscriber.putNext((UIImage(cgImage: cgImage), nil))
subscriber.putCompletion()
}
})
return ActionDisposable {
avAssetGenerator.cancelAllCGImageGeneration()
}
}
} else {
videoResult = .single(.imageFile(path: draftPath))
duration = 5.0
if let image = UIImage(contentsOfFile: draftPath) {
firstFrame = .single((image, nil))
} else {
firstFrame = .single((UIImage(), nil))
}
}
case .message, .gift:
let peerId: EnginePeer.Id
if case let .message(messageIds) = subject {
peerId = messageIds.first!.peerId
} else {
peerId = self.context.account.peerId
}
let isNightTheme = mediaEditor.values.nightTheme
let wallpaper = getChatWallpaperImage(context: self.context, peerId: peerId)
|> map { _, image, nightImage -> UIImage? in
if isNightTheme {
return nightImage ?? image
} else {
return image
}
}
videoResult = wallpaper
|> mapToSignal { image in
if let image {
let tempImagePath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).jpg"
if let data = image.jpegData(compressionQuality: 0.85) {
try? data.write(to: URL(fileURLWithPath: tempImagePath))
}
return .single(.imageFile(path: tempImagePath))
} else {
return .complete()
}
}
firstFrame = wallpaper
|> map { image in
return (image, nil)
}
duration = 5.0
case .sticker:
let image = generateImage(storyDimensions, contextGenerator: { size, context in
context.clear(CGRect(origin: .zero, size: size))
}, opaque: false, scale: 1.0)
let tempImagePath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).png"
if let data = image?.pngData() {
try? data.write(to: URL(fileURLWithPath: tempImagePath))
}
videoResult = .single(.imageFile(path: tempImagePath))
duration = 3.0
firstFrame = .single((image, nil))
case .assets:
fatalError()
}
let _ = combineLatest(queue: Queue.mainQueue(), firstFrame, videoResult)
.start(next: { [weak self] images, videoResult in
if let self {
let (image, additionalImage) = images
var currentImage = mediaEditor.resultImage
if let image {
mediaEditor.replaceSource(image, additionalImage: additionalImage, time: firstFrameTime, mirror: true)
if let updatedImage = mediaEditor.getResultImage(mirror: videoIsMirrored) {
currentImage = updatedImage
}
}
var inputImage: UIImage
if let currentImage {
inputImage = currentImage
} else if let image {
inputImage = image
} else {
inputImage = UIImage()
}
var values = mediaEditor.values
if case .avatarEditor = self.mode, values.videoTrimRange == nil && duration > avatarMaxVideoDuration {
values = values.withUpdatedVideoTrimRange(0 ..< avatarMaxVideoDuration)
}
makeEditorImageComposition(context: self.node.ciContext, postbox: self.context.account.postbox, inputImage: inputImage, dimensions: storyDimensions, values: values, time: firstFrameTime, textScale: 2.0, completion: { [weak self] coverImage in
if let self {
self.willComplete(coverImage, true, { [weak self] in
guard let self else {
return
}
Logger.shared.log("MediaEditor", "Completed with video \(videoResult)")
self.completion([MediaEditorScreenImpl.Result(media: .video(video: videoResult, coverImage: coverImage, values: values, duration: duration, dimensions: values.resultDimensions), mediaAreas: mediaAreas, caption: caption, coverTimestamp: values.coverImageTimestamp, options: self.state.privacy, stickers: stickers, randomId: randomId)], { [weak self] finished in
self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in
self?.dismiss()
Queue.mainQueue().justDispatch {
finished()
}
})
})
})
}
})
}
})
if case let .draft(draft, id) = actualSubject, id == nil {
removeStoryDraft(engine: self.context.engine, path: draft.path, delete: false)
}
} else if let image = mediaEditor.resultImage {
self.saveDraft(id: randomId)
var values = mediaEditor.values
var outputDimensions: CGSize?
if case .avatarEditor = self.mode {
outputDimensions = CGSize(width: 640.0, height: 640.0)
values = values.withUpdatedQualityPreset(.profile)
}
makeEditorImageComposition(
context: self.node.ciContext,
postbox: self.context.account.postbox,
inputImage: image,
dimensions: storyDimensions,
outputDimensions: outputDimensions,
values: values,
time: .zero,
textScale: 2.0,
completion: { [weak self] resultImage in
if let self, let resultImage {
self.willComplete(resultImage, false, { [weak self] in
guard let self else {
return
}
Logger.shared.log("MediaEditor", "Completed with image \(resultImage)")
self.completion([MediaEditorScreenImpl.Result(media: .image(image: resultImage, dimensions: PixelDimensions(resultImage.size)), mediaAreas: mediaAreas, caption: caption, coverTimestamp: nil, options: self.state.privacy, stickers: stickers, randomId: randomId)], { [weak self] finished in
self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in
self?.dismiss()
Queue.mainQueue().justDispatch {
finished()
}
})
})
if case let .draft(draft, id) = actualSubject, id == nil {
removeStoryDraft(engine: self.context.engine, path: draft.path, delete: true)
}
})
}
})
}
}
private func processMultipleItems(items: [EditingItem], isLongVideo: Bool) {
guard !items.isEmpty else {
return
}
var items = items
if let mediaEditor = self.node.mediaEditor, case let .asset(asset) = self.node.subject, let currentItemIndex = items.firstIndex(where: { $0.asset.localIdentifier == asset.localIdentifier }) {
var updatedCurrentItem = items[currentItemIndex]
updatedCurrentItem.caption = self.node.getCaption()
updatedCurrentItem.values = mediaEditor.values
items[currentItemIndex] = updatedCurrentItem
}
let multipleResults = Atomic<[MediaEditorScreenImpl.Result]>(value: [])
let totalItems = items.count
let dispatchGroup = DispatchGroup()
let privacy = self.state.privacy
if !(self.isEditingStory || self.isEditingStoryCover) {
let _ = updateMediaEditorStoredStateInteractively(engine: self.context.engine, { current in
if let current {
return current.withUpdatedPrivacy(privacy)
} else {
return MediaEditorStoredState(privacy: privacy, textSettings: nil)
}
}).start()
}
var order: [Int64] = []
for (index, item) in items.enumerated() {
guard item.isEnabled else {
continue
}
dispatchGroup.enter()
let randomId = Int64.random(in: .min ... .max)
order.append(randomId)
if item.asset.mediaType == .video {
processVideoItem(item: item, index: index, randomId: randomId, isLongVideo: isLongVideo) { result in
let _ = multipleResults.modify { results in
var updatedResults = results
updatedResults.append(result)
return updatedResults
}
dispatchGroup.leave()
}
} else if item.asset.mediaType == .image {
processImageItem(item: item, index: index, randomId: randomId) { result in
let _ = multipleResults.modify { results in
var updatedResults = results
updatedResults.append(result)
return updatedResults
}
dispatchGroup.leave()
}
} else {
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: .main) {
let results = multipleResults.with { $0 }
if results.count == totalItems {
var orderedResults: [MediaEditorScreenImpl.Result] = []
for id in order {
if let item = results.first(where: { $0.randomId == id }) {
orderedResults.append(item)
}
}
self.completion(results, { [weak self] finished in
self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in
self?.dismiss()
Queue.mainQueue().justDispatch {
finished()
}
})
})
}
}
}
private func processVideoItem(item: EditingItem, index: Int, randomId: Int64, isLongVideo: Bool, completion: @escaping (MediaEditorScreenImpl.Result) -> Void) {
let asset = item.asset
let itemMediaEditor = setupMediaEditorForItem(item: item)
var caption = item.caption
caption = convertMarkdownToAttributes(caption)
var mediaAreas: [MediaArea] = []
var stickers: [TelegramMediaFile] = []
if let entities = item.values?.entities {
for entity in entities {
if let mediaArea = entity.mediaArea {
mediaAreas.append(mediaArea)
}
extractStickersFromEntity(entity, into: &stickers)
}
}
let firstFrameTime: CMTime
if let coverImageTimestamp = item.values?.coverImageTimestamp, !isLongVideo || index == 0 {
firstFrameTime = CMTime(seconds: coverImageTimestamp, preferredTimescale: CMTimeScale(60))
} else {
firstFrameTime = CMTime(seconds: item.values?.videoTrimRange?.lowerBound ?? 0.0, preferredTimescale: CMTimeScale(60))
}
PHImageManager.default().requestAVAsset(forVideo: asset, options: nil) { [weak self] avAsset, _, _ in
guard let avAsset else {
DispatchQueue.main.async {
if let self {
completion(self.createEmptyResult(randomId: randomId))
}
}
return
}
let duration: Double
if let videoTrimRange = item.values?.videoTrimRange {
duration = videoTrimRange.upperBound - videoTrimRange.lowerBound
} else {
duration = min(asset.duration, storyMaxVideoDuration)
}
let avAssetGenerator = AVAssetImageGenerator(asset: avAsset)
avAssetGenerator.appliesPreferredTrackTransform = true
avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)]) { [weak self] _, cgImage, _, _, _ in
guard let self else {
return
}
DispatchQueue.main.async {
if let cgImage {
let image = UIImage(cgImage: cgImage)
itemMediaEditor.replaceSource(image, additionalImage: nil, time: firstFrameTime, mirror: false)
if let resultImage = itemMediaEditor.resultImage {
makeEditorImageComposition(
context: self.node.ciContext,
postbox: self.context.account.postbox,
inputImage: resultImage,
dimensions: storyDimensions,
values: itemMediaEditor.values,
time: firstFrameTime,
textScale: 2.0
) { coverImage in
if let coverImage = coverImage {
let result = MediaEditorScreenImpl.Result(
media: .video(
video: .asset(localIdentifier: asset.localIdentifier),
coverImage: coverImage,
values: itemMediaEditor.values,
duration: duration,
dimensions: itemMediaEditor.values.resultDimensions
),
mediaAreas: mediaAreas,
caption: caption,
coverTimestamp: itemMediaEditor.values.coverImageTimestamp,
options: self.state.privacy,
stickers: stickers,
randomId: randomId
)
completion(result)
} else {
completion(self.createEmptyResult(randomId: randomId))
}
}
} else {
completion(self.createEmptyResult(randomId: randomId))
}
} else {
completion(self.createEmptyResult(randomId: randomId))
}
}
}
}
}
private func processImageItem(item: EditingItem, index: Int, randomId: Int64, completion: @escaping (MediaEditorScreenImpl.Result) -> Void) {
let asset = item.asset
let itemMediaEditor = setupMediaEditorForItem(item: item)
var caption = item.caption
caption = convertMarkdownToAttributes(caption)
var mediaAreas: [MediaArea] = []
var stickers: [TelegramMediaFile] = []
if let entities = item.values?.entities {
for entity in entities {
if let mediaArea = entity.mediaArea {
mediaAreas.append(mediaArea)
}
extractStickersFromEntity(entity, into: &stickers)
}
}
let options = PHImageRequestOptions()
options.deliveryMode = .highQualityFormat
options.isNetworkAccessAllowed = true
PHImageManager.default().requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .default, options: options) { [weak self] image, _ in
guard let self else {
return
}
DispatchQueue.main.async {
if let image {
itemMediaEditor.replaceSource(image, additionalImage: nil, time: .zero, mirror: false)
if let resultImage = itemMediaEditor.resultImage {
makeEditorImageComposition(
context: self.node.ciContext,
postbox: self.context.account.postbox,
inputImage: resultImage,
dimensions: storyDimensions,
values: itemMediaEditor.values,
time: .zero,
textScale: 2.0
) { resultImage in
if let resultImage = resultImage {
let result = MediaEditorScreenImpl.Result(
media: .image(
image: resultImage,
dimensions: PixelDimensions(resultImage.size)
),
mediaAreas: mediaAreas,
caption: caption,
coverTimestamp: nil,
options: self.state.privacy,
stickers: stickers,
randomId: randomId
)
completion(result)
} else {
completion(self.createEmptyResult(randomId: randomId))
}
}
} else {
completion(self.createEmptyResult(randomId: randomId))
}
} else {
completion(self.createEmptyResult(randomId: randomId))
}
}
}
}
private func setupMediaEditorForItem(item: EditingItem) -> MediaEditor {
var values = item.values
if values?.videoTrimRange == nil {
values = values?.withUpdatedVideoTrimRange(0 ..< storyMaxVideoDuration)
}
return MediaEditor(
context: self.context,
mode: .default,
subject: .asset(item.asset),
values: values,
hasHistogram: false,
isStandalone: true
)
}
private func extractStickersFromEntity(_ entity: CodableDrawingEntity, into stickers: inout [TelegramMediaFile]) {
switch entity {
case let .sticker(stickerEntity):
if case let .file(file, fileType) = stickerEntity.content, case .sticker = fileType {
stickers.append(file.media)
}
case let .text(textEntity):
if let subEntities = textEntity.renderSubEntities {
for entity in subEntities {
if let stickerEntity = entity as? DrawingStickerEntity, case let .file(file, fileType) = stickerEntity.content, case .sticker = fileType {
stickers.append(file.media)
}
}
}
default:
break
}
}
private func createEmptyResult(randomId: Int64) -> MediaEditorScreenImpl.Result {
let emptyImage = UIImage()
return MediaEditorScreenImpl.Result(
media: .image(
image: emptyImage,
dimensions: PixelDimensions(emptyImage.size)
),
mediaAreas: [],
caption: NSAttributedString(),
coverTimestamp: nil,
options: self.state.privacy,
stickers: [],
randomId: randomId
)
}
func updateMediaEditorEntities() {
guard let mediaEditor = self.node.mediaEditor else {
return
}
let entities = self.node.entitiesView.entities.filter { !($0 is DrawingMediaEntity) }
let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.node.entitiesView)
mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities)
}
}

View File

@ -145,23 +145,11 @@ final class SelectionPanelComponent: Component {
selectionLayer.lineWidth = lineWidth
selectionLayer.frame = selectionFrame
selectionLayer.path = CGPath(roundedRect: CGRect(origin: .zero, size: selectionFrame.size).insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0), cornerWidth: 6.0, cornerHeight: 6.0, transform: nil)
// if !transition.animation.isImmediate {
// let initialPath = CGPath(roundedRect: CGRect(origin: .zero, size: selectionFrame.size).insetBy(dx: 0.0, dy: 0.0), cornerWidth: 6.0, cornerHeight: 6.0, transform: nil)
// selectionLayer.animate(from: initialPath, to: selectionLayer.path as AnyObject, keyPath: "path", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2)
// selectionLayer.animateShapeLineWidth(from: 0.0, to: lineWidth, duration: 0.2)
// }
}
} else if let selectionLayer = self.selectionLayer {
self.selectionLayer = nil
selectionLayer.removeFromSuperlayer()
// let targetPath = CGPath(roundedRect: CGRect(origin: .zero, size: selectionFrame.size).insetBy(dx: 0.0, dy: 0.0), cornerWidth: 6.0, cornerHeight: 6.0, transform: nil)
// selectionLayer.animate(from: selectionLayer.path, to: targetPath, keyPath: "path", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2, removeOnCompletion: false)
// selectionLayer.animateShapeLineWidth(from: selectionLayer.lineWidth, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
// selectionLayer.removeFromSuperlayer()
// })
}
}
}
@ -373,11 +361,96 @@ final class SelectionPanelComponent: Component {
}
func animateIn(from buttonView: SelectionPanelButtonContentComponent.View) {
guard let component = self.component else {
return
}
self.scrollView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
let buttonFrame = buttonView.convert(buttonView.bounds, to: self)
let fromPoint = CGPoint(x: buttonFrame.center.x - self.scrollView.center.x, y: buttonFrame.center.y - self.scrollView.center.y)
self.scrollView.layer.animatePosition(from: fromPoint, to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
self.scrollView.layer.animateBounds(from: CGRect(origin: CGPoint(x: buttonFrame.minX - self.scrollView.frame.minX, y: buttonFrame.minY - self.scrollView.frame.minY), size: buttonFrame.size), to: self.scrollView.bounds, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
self.backgroundMaskPanelView.layer.animatePosition(from: fromPoint, to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
self.backgroundMaskPanelView.layer.animate(from: NSNumber(value: Float(16.5)), to: NSNumber(value: Float(self.backgroundMaskPanelView.layer.cornerRadius)), keyPath: "cornerRadius", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.4)
self.backgroundMaskPanelView.layer.animateBounds(from: CGRect(origin: .zero, size: CGSize(width: 33.0, height: 33.0)), to: self.backgroundMaskPanelView.bounds, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
let mainCircleDelay: Double = 0.02
let backgroundWidth = self.backgroundMaskPanelView.frame.width
for item in component.items {
guard let itemView = self.itemViews[item.asset.localIdentifier] else {
continue
}
let distance = abs(itemView.frame.center.x - backgroundWidth)
let distanceNorm = distance / backgroundWidth
let adjustedDistanceNorm = distanceNorm
let itemDelay = mainCircleDelay + adjustedDistanceNorm * 0.14
itemView.isHidden = true
Queue.mainQueue().after(itemDelay * UIView.animationDurationFactor()) { [weak itemView] in
guard let itemView else {
return
}
itemView.isHidden = false
itemView.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4)
}
}
}
func animateOut(to buttonView: SelectionPanelButtonContentComponent.View, completion: @escaping () -> Void) {
completion()
guard let component = self.component else {
completion()
return
}
self.scrollView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
let buttonFrame = buttonView.convert(buttonView.bounds, to: self)
let scrollButtonFrame = buttonView.convert(buttonView.bounds, to: self.scrollView)
let toPoint = CGPoint(x: buttonFrame.center.x - self.scrollView.center.x, y: buttonFrame.center.y - self.scrollView.center.y)
self.scrollView.layer.animatePosition(from: .zero, to: toPoint, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
self.scrollView.layer.animateBounds(from: self.scrollView.bounds, to: CGRect(origin: CGPoint(x: (buttonFrame.minX - self.scrollView.frame.minX) / 2.0, y: (buttonFrame.minY - self.scrollView.frame.minY) / 2.0), size: buttonFrame.size), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
self.backgroundMaskPanelView.layer.animatePosition(from: .zero, to: toPoint, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true)
self.backgroundMaskPanelView.layer.animate(from: NSNumber(value: Float(self.backgroundMaskPanelView.layer.cornerRadius)), to: NSNumber(value: Float(16.5)), keyPath: "cornerRadius", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.4, removeOnCompletion: false)
self.backgroundMaskPanelView.layer.animateBounds(from: self.backgroundMaskPanelView.bounds, to: CGRect(origin: .zero, size: CGSize(width: 33.0, height: 33.0)), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { finished in
if finished {
completion()
self.backgroundMaskPanelView.layer.removeAllAnimations()
for (_, itemView) in self.itemViews {
itemView.layer.removeAllAnimations()
}
}
})
let mainCircleDelay: Double = 0.0
let backgroundWidth = self.backgroundMaskPanelView.frame.width
for item in component.items {
guard let itemView = self.itemViews[item.asset.localIdentifier] else {
continue
}
let distance = abs(itemView.frame.center.x - backgroundWidth)
let distanceNorm = distance / backgroundWidth
let adjustedDistanceNorm = distanceNorm
let itemDelay = mainCircleDelay + adjustedDistanceNorm * 0.05
Queue.mainQueue().after(itemDelay * UIView.animationDurationFactor()) { [weak itemView] in
guard let itemView else {
return
}
itemView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
}
itemView.layer.animatePosition(from: itemView.center, to: scrollButtonFrame.center, duration: 0.4)
}
}
func update(component: SelectionPanelComponent, availableSize: CGSize, state: EmptyComponentState, transition: ComponentTransition) -> CGSize {

View File

@ -84,6 +84,7 @@ public final class MediaScrubberComponent: Component {
let position: Double
let minDuration: Double
let maxDuration: Double
let segmentDuration: Double?
let isPlaying: Bool
let tracks: [Track]
@ -112,6 +113,7 @@ public final class MediaScrubberComponent: Component {
position: Double,
minDuration: Double,
maxDuration: Double,
segmentDuration: Double? = nil,
isPlaying: Bool,
tracks: [Track],
isCollage: Bool,
@ -135,6 +137,7 @@ public final class MediaScrubberComponent: Component {
self.position = position
self.minDuration = minDuration
self.maxDuration = maxDuration
self.segmentDuration = segmentDuration
self.isPlaying = isPlaying
self.tracks = tracks
self.isCollage = isCollage
@ -171,6 +174,9 @@ public final class MediaScrubberComponent: Component {
if lhs.maxDuration != rhs.maxDuration {
return false
}
if lhs.segmentDuration != rhs.segmentDuration {
return false
}
if lhs.isPlaying != rhs.isPlaying {
return false
}
@ -624,6 +630,7 @@ public final class MediaScrubberComponent: Component {
isSelected: isSelected,
availableSize: availableSize,
duration: self.duration,
segmentDuration: lowestVideoId == track.id ? component.segmentDuration : nil,
transition: trackTransition
)
trackLayout[id] = (CGRect(origin: CGPoint(x: 0.0, y: totalHeight), size: trackSize), trackTransition, animateTrackIn)
@ -675,6 +682,7 @@ public final class MediaScrubberComponent: Component {
isSelected: false,
availableSize: availableSize,
duration: self.duration,
segmentDuration: nil,
transition: trackTransition
)
trackTransition.setFrame(view: trackView, frame: CGRect(origin: .zero, size: trackSize))
@ -955,6 +963,9 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega
fileprivate let audioContentMaskView: UIImageView
fileprivate let audioIconView: UIImageView
fileprivate let audioTitle = ComponentView<Empty>()
fileprivate var segmentTitles: [Int32: ComponentView<Empty>] = [:]
fileprivate var segmentLayers: [Int32: SimpleLayer] = [:]
fileprivate let videoTransparentFramesContainer = UIView()
fileprivate var videoTransparentFrameLayers: [VideoFrameLayer] = []
@ -1142,6 +1153,7 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega
isSelected: Bool,
availableSize: CGSize,
duration: Double,
segmentDuration: Double?,
transition: ComponentTransition
) -> CGSize {
let previousParams = self.params
@ -1477,6 +1489,86 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega
transition.setFrame(view: self.vibrancyView, frame: CGRect(origin: .zero, size: containerFrame.size))
transition.setFrame(view: self.vibrancyContainer, frame: CGRect(origin: .zero, size: containerFrame.size))
var segmentCount = 0
var segmentOrigin: CGFloat = 0.0
var segmentWidth: CGFloat = 0.0
if let segmentDuration {
if duration > segmentDuration {
let fraction = segmentDuration / duration
segmentCount = Int(ceil(duration / segmentDuration)) - 1
segmentWidth = floorToScreenPixels(containerFrame.width * fraction)
}
if let trimRange = track.trimRange {
if trimRange.lowerBound > 0.0 {
let fraction = trimRange.lowerBound / duration
segmentOrigin = floorToScreenPixels(containerFrame.width * fraction)
}
let actualSegmentCount = Int(ceil((trimRange.upperBound - trimRange.lowerBound) / segmentDuration)) - 1
segmentCount = min(actualSegmentCount, segmentCount)
}
}
var validIds = Set<Int32>()
var segmentFrame = CGRect(x: segmentOrigin + segmentWidth, y: 0.0, width: 1.0, height: containerFrame.size.height)
for i in 0 ..< min(segmentCount, 2) {
let id = Int32(i)
validIds.insert(id)
let segmentLayer: SimpleLayer
let segmentTitle: ComponentView<Empty>
var segmentTransition = transition
if let currentLayer = self.segmentLayers[id], let currentTitle = self.segmentTitles[id] {
segmentLayer = currentLayer
segmentTitle = currentTitle
} else {
segmentTransition = .immediate
segmentLayer = SimpleLayer()
segmentLayer.backgroundColor = UIColor.white.cgColor
segmentTitle = ComponentView<Empty>()
self.segmentLayers[id] = segmentLayer
self.segmentTitles[id] = segmentTitle
self.containerView.layer.addSublayer(segmentLayer)
}
transition.setFrame(layer: segmentLayer, frame: segmentFrame)
let segmentTitleSize = segmentTitle.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: "#\(i + 2)", font: Font.semibold(11.0), textColor: .white)),
textShadowColor: UIColor(rgb: 0x000000, alpha: 0.4),
textShadowBlur: 1.0
)),
environment: {},
containerSize: containerFrame.size
)
if let view = segmentTitle.view {
if view.superview == nil {
self.containerView.addSubview(view)
}
segmentTransition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: segmentFrame.maxX + 2.0, y: 2.0), size: segmentTitleSize))
}
segmentFrame.origin.x += segmentWidth
}
var removeIds: [Int32] = []
for (id, segmentLayer) in self.segmentLayers {
if !validIds.contains(id) {
removeIds.append(id)
segmentLayer.removeFromSuperlayer()
if let segmentTitle = self.segmentTitles[id] {
segmentTitle.view?.removeFromSuperview()
}
}
}
for id in removeIds {
self.segmentLayers.removeValue(forKey: id)
self.segmentTitles.removeValue(forKey: id)
}
return scrubberSize
}
}

View File

@ -615,6 +615,8 @@ private final class PeerInfoInteraction {
let openBirthdayContextMenu: (ASDisplayNode, ContextGesture?) -> Void
let editingOpenAffiliateProgram: () -> Void
let editingOpenVerifyAccounts: () -> Void
let editingToggleAutoTranslate: (Bool) -> Void
let displayAutoTranslateLocked: () -> Void
let getController: () -> ViewController?
init(
@ -686,6 +688,8 @@ private final class PeerInfoInteraction {
openBirthdayContextMenu: @escaping (ASDisplayNode, ContextGesture?) -> Void,
editingOpenAffiliateProgram: @escaping () -> Void,
editingOpenVerifyAccounts: @escaping () -> Void,
editingToggleAutoTranslate: @escaping (Bool) -> Void,
displayAutoTranslateLocked: @escaping () -> Void,
getController: @escaping () -> ViewController?
) {
self.openUsername = openUsername
@ -756,6 +760,8 @@ private final class PeerInfoInteraction {
self.openBirthdayContextMenu = openBirthdayContextMenu
self.editingOpenAffiliateProgram = editingOpenAffiliateProgram
self.editingOpenVerifyAccounts = editingOpenVerifyAccounts
self.editingToggleAutoTranslate = editingToggleAutoTranslate
self.displayAutoTranslateLocked = displayAutoTranslateLocked
self.getController = getController
}
}
@ -2005,7 +2011,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
return result
}
private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatLocation: ChatLocation, context: AccountContext, presentationData: PresentationData, interaction: PeerInfoInteraction) -> [(AnyHashable, [PeerInfoScreenItem])] {
private func editingItems(data: PeerInfoScreenData?, boostStatus: ChannelBoostStatus?, state: PeerInfoState, chatLocation: ChatLocation, context: AccountContext, presentationData: PresentationData, interaction: PeerInfoInteraction) -> [(AnyHashable, [PeerInfoScreenItem])] {
enum Section: Int, CaseIterable {
case notifications
case groupLocation
@ -2159,6 +2165,7 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL
let ItemRecentActions = 12
let ItemAffiliatePrograms = 13
let ItemPostSuggestionsSettings = 14
let ItemPeerAutoTranslate = 15
let isCreator = channel.flags.contains(.isCreator)
@ -2278,6 +2285,19 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL
items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemPeerColor, label: .image(colorImage, colorImage.size), additionalBadgeIcon: boostIcon, text: presentationData.strings.Channel_Info_AppearanceItem, icon: UIImage(bundleImageName: "Chat/Info/NameColorIcon"), action: {
interaction.editingOpenNameColorSetup()
}))
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
var isLocked = true
if let boostLevel = boostStatus?.level, boostLevel >= BoostSubject.autoTranslate.requiredLevel(group: false, context: context, configuration: premiumConfiguration) {
isLocked = false
}
items[.peerSettings]!.append(PeerInfoScreenSwitchItem(id: ItemPeerAutoTranslate, text: presentationData.strings.Channel_Info_AutoTranslate, value: channel.flags.contains(.autoTranslateEnabled), icon: UIImage(bundleImageName: "Settings/Menu/AutoTranslate"), isLocked: isLocked, toggled: { value in
if isLocked {
interaction.displayAutoTranslateLocked()
} else {
interaction.editingToggleAutoTranslate(value)
}
}))
}
var canEditMembers = false
@ -3207,6 +3227,16 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
return
}
self.editingOpenVerifyAccounts()
}, editingToggleAutoTranslate: { [weak self] isEnabled in
guard let self else {
return
}
self.toggleAutoTranslate(isEnabled: isEnabled)
}, displayAutoTranslateLocked: { [weak self] in
guard let self else {
return
}
self.displayAutoTranslateLocked()
},
getController: { [weak self] in
return self?.controller
@ -9148,6 +9178,28 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}
}
private func toggleAutoTranslate(isEnabled: Bool) {
self.activeActionDisposable.set(self.context.engine.peers.toggleAutoTranslation(peerId: self.peerId, enabled: isEnabled).start())
}
private func displayAutoTranslateLocked() {
let _ = combineLatest(
queue: Queue.mainQueue(),
context.engine.peers.getChannelBoostStatus(peerId: self.peerId),
context.engine.peers.getMyBoostStatus()
).startStandalone(next: { [weak self] boostStatus, myBoostStatus in
guard let self, let controller = self.controller, let boostStatus, let myBoostStatus else {
return
}
let boostController = self.context.sharedContext.makePremiumBoostLevelsController(context: self.context, peerId: self.peerId, subject: .autoTranslate, boostStatus: boostStatus, myBoostStatus: myBoostStatus, forceDark: false, openStats: { [weak self] in
if let self {
self.openStats(section: .boosts, boostStatus: boostStatus)
}
})
controller.push(boostController)
})
}
private func toggleForumTopics(isEnabled: Bool) {
guard let data = self.data, let peer = data.peer else {
return
@ -11888,7 +11940,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}
var validEditingSections: [AnyHashable] = []
let editItems = (self.isSettings || self.isMyProfile) ? settingsEditingItems(data: self.data, state: self.state, context: self.context, presentationData: self.presentationData, interaction: self.interaction, isMyProfile: self.isMyProfile) : editingItems(data: self.data, state: self.state, chatLocation: self.chatLocation, context: self.context, presentationData: self.presentationData, interaction: self.interaction)
let editItems = (self.isSettings || self.isMyProfile) ? settingsEditingItems(data: self.data, state: self.state, context: self.context, presentationData: self.presentationData, interaction: self.interaction, isMyProfile: self.isMyProfile) : editingItems(data: self.data, boostStatus: self.boostStatus, state: self.state, chatLocation: self.chatLocation, context: self.context, presentationData: self.presentationData, interaction: self.interaction)
for (sectionId, sectionItems) in editItems {
var insets = UIEdgeInsets()

View File

@ -201,7 +201,10 @@ extension PeerInfoScreenImpl {
commit()
}
},
completion: { [weak self] result, commit in
completion: { [weak self] results, commit in
guard let result = results.first else {
return
}
switch result.media {
case let .image(image, _):
resultImage = image
@ -217,7 +220,7 @@ extension PeerInfoScreenImpl {
break
}
dismissImpl?()
} as (MediaEditorScreenImpl.Result, @escaping (@escaping () -> Void) -> Void) -> Void
} as ([MediaEditorScreenImpl.Result], @escaping (@escaping () -> Void) -> Void) -> Void
)
editorController.cancelled = { _ in
cancelled()

View File

@ -502,8 +502,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
resellPrice = gift.resellStars
if let _ = resellPrice {
//TODO:localize
ribbonText = "sale"
ribbonText = params.presentationData.strings.PeerInfo_Gifts_Sale
ribbonFont = .larger
ribbonColor = .green
ribbonOutline = params.presentationData.theme.list.blocksBackgroundColor
@ -607,10 +606,10 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
return self.profileGifts.buyStarGift(slug: slug, peerId: peerId)
},
updateResellStars: { [weak self] price in
guard let self, case let .unique(uniqueGift) = product.gift else {
return
guard let self, let reference = product.reference else {
return .never()
}
self.profileGifts.updateStarGiftResellPrice(slug: uniqueGift.slug, price: price)
return self.profileGifts.updateStarGiftResellPrice(reference: reference, price: price)
},
togglePinnedToTop: { [weak self] pinnedToTop in
guard let self else {
@ -1479,6 +1478,8 @@ private extension StarGiftReference {
return "m_\(messageId.id)"
case let .peer(peerId, id):
return "p_\(peerId.toInt64())_\(id)"
case let .slug(slug):
return "s_\(slug)"
}
}
}

View File

@ -173,6 +173,8 @@ private final class StarsTransactionSheetContent: CombinedComponent {
let spaceRegex = try? NSRegularExpression(pattern: "\\[(.*?)\\]", options: [])
let giftCompositionExternalState = GiftCompositionComponent.ExternalState()
return { context in
let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
let controller = environment.controller
@ -366,8 +368,14 @@ private final class StarsTransactionSheetContent: CombinedComponent {
}
case let .transaction(transaction, parentPeer):
if let starGift = transaction.starGift {
titleText = strings.Stars_Transaction_Gift_Title
descriptionText = ""
switch starGift {
case .generic:
titleText = strings.Stars_Transaction_Gift_Title
descriptionText = ""
case let .unique(gift):
titleText = gift.title
descriptionText = "\(strings.Gift_Unique_Collectible) #\(presentationStringsFormattedNumber(gift.number, dateTimeFormat.groupingSeparator))"
}
count = transaction.count
transactionId = transaction.id
date = transaction.date
@ -665,14 +673,23 @@ private final class StarsTransactionSheetContent: CombinedComponent {
}
} else {
amountText = "+ \(formattedAmount)"
countColor = theme.list.itemDisclosureActions.constructive.fillColor
if case .unique = giftAnimationSubject {
countColor = .white
} else {
countColor = theme.list.itemDisclosureActions.constructive.fillColor
}
}
var titleFont = Font.bold(25.0)
if case .unique = giftAnimationSubject {
titleFont = Font.bold(20.0)
}
let title = title.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(
string: titleText,
font: Font.bold(25.0),
font: titleFont,
textColor: headerTextColor,
paragraphAlignment: .center
)),
@ -723,7 +740,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
if let giftAnimationSubject {
let animationHeight: CGFloat
if case .unique = giftAnimationSubject {
animationHeight = 240.0
animationHeight = 268.0
} else {
animationHeight = 210.0
}
@ -731,7 +748,8 @@ private final class StarsTransactionSheetContent: CombinedComponent {
component: GiftCompositionComponent(
context: component.context,
theme: theme,
subject: giftAnimationSubject
subject: giftAnimationSubject,
externalState: giftCompositionExternalState
),
availableSize: CGSize(width: context.availableSize.width, height: animationHeight),
transition: .immediate
@ -816,6 +834,14 @@ private final class StarsTransactionSheetContent: CombinedComponent {
MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Stars_Transaction_GiftUpgrade, font: tableFont, textColor: tableTextColor)))
)
))
} else if case .unique = giftAnimationSubject {
tableItems.append(.init(
id: "reason",
title: strings.Stars_Transaction_Giveaway_Reason,
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: count < StarsAmount.zero ? strings.Stars_Transaction_GiftPurchase : strings.Stars_Transaction_GiftSale, font: tableFont, textColor: tableTextColor)))
)
))
}
if isGift, toPeer == nil {
@ -1073,7 +1099,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
}
}
if let starRefPeerId = transaction.starrefPeerId, let starRefPeer = state.peerMap[starRefPeerId] {
if !transaction.flags.contains(.isPaidMessage) {
if !transaction.flags.contains(.isPaidMessage) && !transaction.flags.contains(.isStarGiftResale) {
tableItems.append(.init(
id: "to",
title: strings.StarsTransaction_StarRefReason_Affiliate,
@ -1104,7 +1130,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
))
}
if let toPeer {
if let toPeer, !transaction.flags.contains(.isStarGiftResale) {
tableItems.append(.init(
id: "referred",
title: transaction.flags.contains(.isPaidMessage) ? strings.Stars_Transaction_From : strings.StarsTransaction_StarRefReason_Referred,
@ -1136,7 +1162,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
}
}
if let starrefCommissionPermille = transaction.starrefCommissionPermille, transaction.starrefPeerId != nil {
if transaction.flags.contains(.isPaidMessage) {
if transaction.flags.contains(.isPaidMessage) || transaction.flags.contains(.isStarGiftResale) {
var totalStars = transaction.count
if let starrefCount = transaction.starrefAmount {
totalStars = totalStars + starrefCount
@ -1300,13 +1326,29 @@ private final class StarsTransactionSheetContent: CombinedComponent {
)
var originY: CGFloat = 156.0
if let _ = giftAnimationSubject {
originY += 18.0
switch giftAnimationSubject {
case .generic:
originY += 20.0
case .unique:
originY += 34.0
default:
break
}
context.add(title
.position(CGPoint(x: context.availableSize.width / 2.0, y: originY))
)
originY += 21.0
if case .unique = giftAnimationSubject {
originY += 17.0
} else {
originY += 21.0
}
let vibrantColor: UIColor
if let previewPatternColor = giftCompositionExternalState.previewPatternColor {
vibrantColor = previewPatternColor.withMultiplied(hue: 1.0, saturation: 1.02, brightness: 1.25).mixedWith(UIColor.white, alpha: 0.3)
} else {
vibrantColor = UIColor.white.withAlphaComponent(0.6)
}
var descriptionSize: CGSize = .zero
if !descriptionText.isEmpty {
@ -1316,8 +1358,18 @@ private final class StarsTransactionSheetContent: CombinedComponent {
if state.cachedChevronImage == nil || state.cachedChevronImage?.1 !== environment.theme {
state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: linkColor)!, theme)
}
var textFont = Font.regular(15.0)
let boldTextFont = Font.semibold(15.0)
var textColor = theme.actionSheet.secondaryTextColor
if case .unique = giftAnimationSubject {
textFont = Font.regular(13.0)
textColor = vibrantColor
} else if countOnTop && !isSubscriber {
textColor = theme.list.itemPrimaryTextColor
}
let linkColor = theme.actionSheet.controlAccentColor
let textColor = countOnTop && !isSubscriber ? theme.list.itemPrimaryTextColor : textColor
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
})
@ -1362,7 +1414,13 @@ private final class StarsTransactionSheetContent: CombinedComponent {
context.add(description
.position(CGPoint(x: context.availableSize.width / 2.0, y: descriptionOrigin + description.size.height / 2.0))
)
originY += description.size.height + 10.0
originY += description.size.height
if case .unique = giftAnimationSubject {
originY += 6.0
} else {
originY += 10.0
}
}
let amountSpacing: CGFloat = countBackgroundColor != nil ? 4.0 : 1.0

View File

@ -317,9 +317,18 @@ final class StarsTransactionsListPanelComponent: Component {
uniqueGift = gift
} else {
itemTitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast)
itemSubtitle = item.count > StarsAmount.zero ? environment.strings.Stars_Intro_Transaction_ConvertedGift : environment.strings.Stars_Intro_Transaction_Gift
if case let .generic(gift) = starGift {
switch starGift {
case let .generic(gift):
itemFile = gift.file
itemSubtitle = item.count > StarsAmount.zero ? environment.strings.Stars_Intro_Transaction_ConvertedGift : environment.strings.Stars_Intro_Transaction_Gift
case let .unique(gift):
for attribute in gift.attributes {
if case let .model(_, file, _) = attribute {
itemFile = file
break
}
}
itemSubtitle = item.count > StarsAmount.zero ? environment.strings.Stars_Intro_Transaction_GiftSale : environment.strings.Stars_Intro_Transaction_GiftPurchase
}
}
} else if let _ = item.giveawayMessageId {

View File

@ -148,10 +148,9 @@ private final class SheetContent: CombinedComponent {
maxAmount = withdrawConfiguration.maxPaidMediaAmount.flatMap { StarsAmount(value: $0, nanos: 0) }
amountLabel = nil
case let .starGiftResell(update):
//TODO:localize
titleString = update ? "Edit Price" : "Sell Gift"
amountTitle = "PRICE IN STARS"
amountPlaceholder = "Enter Price"
titleString = update ? environment.strings.Stars_SellGift_EditTitle : environment.strings.Stars_SellGift_Title
amountTitle = environment.strings.Stars_SellGift_AmountTitle
amountPlaceholder = environment.strings.Stars_SellGift_AmountPlaceholder
minAmount = StarsAmount(value: resaleConfiguration.starGiftResaleMinAmount, nanos: 0)
maxAmount = StarsAmount(value: resaleConfiguration.starGiftResaleMaxAmount, nanos: 0)
@ -283,12 +282,13 @@ private final class SheetContent: CombinedComponent {
maximumNumberOfLines: 0
))
case .starGiftResell:
//TODO:localize
let amountInfoString: NSAttributedString
if let value = state.amount?.value, value > 0 {
amountInfoString = NSAttributedString(attributedString: parseMarkdownIntoAttributedString("You will receive **\(Int32(floor(Float(value) * 0.8))) Stars**.", attributes: amountMarkdownAttributes, textAlignment: .natural))
let starsValue = Int32(floor(Float(value) * Float(resaleConfiguration.paidMessageCommissionPermille) / 1000.0))
let starsString = environment.strings.Stars_SellGift_AmountInfo_Stars(starsValue)
amountInfoString = NSAttributedString(attributedString: parseMarkdownIntoAttributedString(environment.strings.Stars_SellGift_AmountInfo(starsString).string, attributes: amountMarkdownAttributes, textAlignment: .natural))
} else {
amountInfoString = NSAttributedString(attributedString: parseMarkdownIntoAttributedString("You will receive **80%**.", attributes: amountMarkdownAttributes, textAlignment: .natural))
amountInfoString = NSAttributedString(attributedString: parseMarkdownIntoAttributedString(environment.strings.Stars_SellGift_AmountInfo("\(resaleConfiguration.paidMessageCommissionPermille / 10)%").string, attributes: amountMarkdownAttributes, textAlignment: .natural))
}
amountFooter = AnyComponent(MultilineTextComponent(
text: .plain(amountInfoString),
@ -361,11 +361,10 @@ private final class SheetContent: CombinedComponent {
if case .paidMedia = component.mode {
buttonString = environment.strings.Stars_PaidContent_Create
} else if case .starGiftResell = component.mode {
//TODO:localize
if let amount = state.amount, amount.value > 0 {
buttonString = "Sell for # \(presentationStringsFormattedNumber(amount, environment.dateTimeFormat.groupingSeparator))"
buttonString = "\(environment.strings.Stars_SellGift_SellFor) # \(presentationStringsFormattedNumber(amount, environment.dateTimeFormat.groupingSeparator))"
} else {
buttonString = "Sell"
buttonString = environment.strings.Stars_SellGift_Sell
}
} else if case .paidMessages = component.mode {
//TODO:localize

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "price (2).pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "translation.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -977,8 +977,8 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
var setPresentationCall: ((PresentationCall?) -> Void)?
let sharedContext = SharedAccountContextImpl(mainWindow: self.mainWindow, sharedContainerPath: legacyBasePath, basePath: rootPath, encryptionParameters: encryptionParameters, accountManager: accountManager, appLockContext: appLockContext, notificationController: nil, applicationBindings: applicationBindings, initialPresentationDataAndSettings: initialPresentationDataAndSettings, networkArguments: networkArguments, hasInAppPurchases: buildConfig.isAppStoreBuild && buildConfig.apiId == 1, rootPath: rootPath, legacyBasePath: legacyBasePath, apsNotificationToken: self.notificationTokenPromise.get() |> map(Optional.init), voipNotificationToken: self.voipTokenPromise.get() |> map(Optional.init), firebaseSecretStream: self.firebaseSecretStream.get(), setNotificationCall: { call in
setPresentationCall?(call)
}, navigateToChat: { accountId, peerId, messageId in
self.openChatWhenReady(accountId: accountId, peerId: peerId, threadId: nil, messageId: messageId, storyId: nil)
}, navigateToChat: { accountId, peerId, messageId, alwaysKeepMessageId in
self.openChatWhenReady(accountId: accountId, peerId: peerId, threadId: nil, messageId: messageId, storyId: nil, alwaysKeepMessageId: alwaysKeepMessageId)
}, displayUpgradeProgress: { progress in
if let progress = progress {
if self.dataImportSplash == nil {
@ -2736,7 +2736,7 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
}))
}
private func openChatWhenReady(accountId: AccountRecordId?, peerId: PeerId, threadId: Int64?, messageId: MessageId? = nil, activateInput: Bool = false, storyId: StoryId?, openAppIfAny: Bool = false) {
private func openChatWhenReady(accountId: AccountRecordId?, peerId: PeerId, threadId: Int64?, messageId: MessageId? = nil, activateInput: Bool = false, storyId: StoryId?, openAppIfAny: Bool = false, alwaysKeepMessageId: Bool = false) {
let signal = self.sharedContextPromise.get()
|> take(1)
|> deliverOnMainQueue
@ -2755,7 +2755,7 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
}
self.openChatWhenReadyDisposable.set((signal
|> deliverOnMainQueue).start(next: { context in
context.openChatWithPeerId(peerId: peerId, threadId: threadId, messageId: messageId, activateInput: activateInput, storyId: storyId, openAppIfAny: openAppIfAny)
context.openChatWithPeerId(peerId: peerId, threadId: threadId, messageId: messageId, activateInput: activateInput, storyId: storyId, openAppIfAny: openAppIfAny, alwaysKeepMessageId: alwaysKeepMessageId)
}))
}

View File

@ -896,7 +896,7 @@ final class AuthorizedApplicationContext {
}))
}
func openChatWithPeerId(peerId: PeerId, threadId: Int64?, messageId: MessageId? = nil, activateInput: Bool = false, storyId: StoryId?, openAppIfAny: Bool = false) {
func openChatWithPeerId(peerId: PeerId, threadId: Int64?, messageId: MessageId? = nil, activateInput: Bool = false, storyId: StoryId?, openAppIfAny: Bool = false, alwaysKeepMessageId: Bool = false) {
if let storyId {
var controllers = self.rootController.viewControllers
controllers = controllers.filter { c in
@ -950,7 +950,7 @@ final class AuthorizedApplicationContext {
if openAppIfAny, case let .user(user) = peer, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp), let parentController = self.rootController.viewControllers.last as? ViewController {
self.context.sharedContext.openWebApp(context: self.context, parentController: parentController, updatedPresentationData: nil, botPeer: peer, chatPeer: nil, threadId: nil, buttonText: "", url: "", simple: true, source: .generic, skipTermsOfService: true, payload: nil)
} else {
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: self.rootController, context: self.context, chatLocation: chatLocation, subject: isOutgoingMessage ? messageId.flatMap { .message(id: .id($0), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false) } : nil, activateInput: activateInput ? .text : nil))
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: self.rootController, context: self.context, chatLocation: chatLocation, subject: alwaysKeepMessageId || isOutgoingMessage ? messageId.flatMap { .message(id: .id($0), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false) } : nil, activateInput: activateInput ? .text : nil))
}
})
}

View File

@ -677,17 +677,22 @@ extension ChatControllerImpl {
let isHidden = self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.TranslationHidden(id: peerId))
|> distinctUntilChanged
let hasAutoTranslate = self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.AutoTranslateEnabled(id: peerId))
|> distinctUntilChanged
self.translationStateDisposable = (combineLatest(
queue: .concurrentDefaultQueue(),
isPremium,
isHidden,
hasAutoTranslate,
ApplicationSpecificNotice.translationSuggestion(accountManager: self.context.sharedContext.accountManager)
) |> mapToSignal { isPremium, isHidden, counterAndTimestamp -> Signal<ChatPresentationTranslationState?, NoError> in
) |> mapToSignal { isPremium, isHidden, hasAutoTranslate, counterAndTimestamp -> Signal<ChatPresentationTranslationState?, NoError> in
var maybeSuggestPremium = false
if counterAndTimestamp.0 >= 3 {
maybeSuggestPremium = true
}
if (isPremium || maybeSuggestPremium) && !isHidden {
if (isPremium || maybeSuggestPremium || hasAutoTranslate) && !isHidden {
return chatTranslationState(context: context, peerId: peerId)
|> map { translationState -> ChatPresentationTranslationState? in
if let translationState, !translationState.fromLang.isEmpty && (translationState.fromLang != baseLanguageCode || translationState.isEnabled) {

View File

@ -1315,13 +1315,13 @@ extension ChatControllerImpl {
)
}
return nil
}, completion: { result, commit in
if case let .image(image, _) = result.media {
}, completion: { results, commit in
if case let .image(image, _) = results.first?.media {
completion(image)
commit({})
}
dismissImpl?()
} as (MediaEditorScreenImpl.Result, @escaping (@escaping () -> Void) -> Void) -> Void
} as ([MediaEditorScreenImpl.Result], @escaping (@escaping () -> Void) -> Void) -> Void
)
editorController.cancelled = { _ in
cancelled()
@ -1930,17 +1930,17 @@ extension ChatControllerImpl {
)
}
return nil
}, completion: { [weak self] result, commit in
}, completion: { [weak self] results, commit in
dismissImpl?()
self?.chatDisplayNode.dismissInput()
Queue.mainQueue().after(0.1) {
commit({})
if case let .sticker(file, _) = result.media {
if case let .sticker(file, _) = results.first?.media {
self?.enqueueStickerFile(file)
}
}
} as (MediaEditorScreenImpl.Result, @escaping (@escaping () -> Void) -> Void) -> Void
} as ([MediaEditorScreenImpl.Result], @escaping (@escaping () -> Void) -> Void) -> Void
)
editorController.cancelled = { _ in
cancelled()

View File

@ -1946,12 +1946,14 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
}
var audioTranscriptionProvidedByBoost = false
var autoTranslate = false
var isCopyProtectionEnabled: Bool = data.initialData?.peer?.isCopyProtectionEnabled ?? false
for entry in view.additionalData {
if case let .peer(_, maybePeer) = entry, let peer = maybePeer {
isCopyProtectionEnabled = peer.isCopyProtectionEnabled
if let channel = peer as? TelegramChannel, let boostLevel = channel.approximateBoostLevel {
if boostLevel >= premiumConfiguration.minGroupAudioTranscriptionLevel {
if let channel = peer as? TelegramChannel {
autoTranslate = channel.flags.contains(.autoTranslateEnabled)
if let boostLevel = channel.approximateBoostLevel, boostLevel >= premiumConfiguration.minGroupAudioTranscriptionLevel {
audioTranscriptionProvidedByBoost = true
}
}
@ -1964,7 +1966,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
)
var translateToLanguage: (fromLang: String, toLang: String)?
if let translationState, isPremium && translationState.isEnabled {
if let translationState, (isPremium || autoTranslate) && translationState.isEnabled {
var languageCode = translationState.toLang ?? chatPresentationData.strings.baseLanguageCode
let rawSuffix = "-raw"
if languageCode.hasSuffix(rawSuffix) {

View File

@ -1124,7 +1124,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
let sendGiftTitle: String
var isIncoming = message.effectivelyIncoming(context.account.peerId)
for media in message.media {
if let action = media as? TelegramMediaAction, case let .starGiftUnique(_, isUpgrade, _, _, _, _, _, _, _, _, _) = action.action {
if let action = media as? TelegramMediaAction, case let .starGiftUnique(_, isUpgrade, _, _, _, _, _, _, _, _, _, _, _) = action.action {
if isUpgrade && message.author?.id == context.account.peerId {
isIncoming = true
}

View File

@ -187,19 +187,27 @@ final class ChatTranslationPanelNode: ASDisplayNode {
}
let isPremium = self.chatInterfaceState?.isPremium ?? false
if isPremium {
var translationAvailable = isPremium
if let channel = self.chatInterfaceState?.renderedPeer?.chatMainPeer as? TelegramChannel, channel.flags.contains(.autoTranslateEnabled) {
translationAvailable = true
}
if translationAvailable {
self.interfaceInteraction?.toggleTranslation(translationState.isEnabled ? .original : .translated)
} else if !translationState.isEnabled {
let context = self.context
var replaceImpl: ((ViewController) -> Void)?
let controller = PremiumDemoScreen(context: context, subject: .translation, action: {
let controller = PremiumIntroScreen(context: context, source: .translation)
replaceImpl?(controller)
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
if !isPremium {
let context = self.context
var replaceImpl: ((ViewController) -> Void)?
let controller = PremiumDemoScreen(context: context, subject: .translation, action: {
let controller = PremiumIntroScreen(context: context, source: .translation)
replaceImpl?(controller)
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
}
self.interfaceInteraction?.chatController()?.push(controller)
}
self.interfaceInteraction?.chatController()?.push(controller)
}
}

View File

@ -39,7 +39,7 @@ public func makeTempContext(
firebaseSecretStream: .never(),
setNotificationCall: { _ in
},
navigateToChat: { _, _, _ in
navigateToChat: { _, _, _, _ in
}, displayUpgradeProgress: { _ in
},
appDelegate: nil

View File

@ -140,7 +140,7 @@ public final class NotificationViewControllerImpl {
return nil
})
sharedAccountContext = SharedAccountContextImpl(mainWindow: nil, sharedContainerPath: self.initializationData.appGroupPath, basePath: rootPath, encryptionParameters: ValueBoxEncryptionParameters(forceEncryptionIfNoSet: false, key: ValueBoxEncryptionParameters.Key(data: self.initializationData.encryptionParameters.0)!, salt: ValueBoxEncryptionParameters.Salt(data: self.initializationData.encryptionParameters.1)!), accountManager: accountManager, appLockContext: appLockContext, notificationController: nil, applicationBindings: applicationBindings, initialPresentationDataAndSettings: initialPresentationDataAndSettings!, networkArguments: NetworkInitializationArguments(apiId: self.initializationData.apiId, apiHash: self.initializationData.apiHash, languagesCategory: self.initializationData.languagesCategory, appVersion: self.initializationData.appVersion, voipMaxLayer: 0, voipVersions: [], appData: .single(self.initializationData.bundleData), externalRequestVerificationStream: .never(), externalRecaptchaRequestVerification: { _, _ in return .never() }, autolockDeadine: .single(nil), encryptionProvider: OpenSSLEncryptionProvider(), deviceModelName: nil, useBetaFeatures: self.initializationData.useBetaFeatures, isICloudEnabled: false), hasInAppPurchases: false, rootPath: rootPath, legacyBasePath: nil, apsNotificationToken: .never(), voipNotificationToken: .never(), firebaseSecretStream: .never(), setNotificationCall: { _ in }, navigateToChat: { _, _, _ in }, appDelegate: nil)
sharedAccountContext = SharedAccountContextImpl(mainWindow: nil, sharedContainerPath: self.initializationData.appGroupPath, basePath: rootPath, encryptionParameters: ValueBoxEncryptionParameters(forceEncryptionIfNoSet: false, key: ValueBoxEncryptionParameters.Key(data: self.initializationData.encryptionParameters.0)!, salt: ValueBoxEncryptionParameters.Salt(data: self.initializationData.encryptionParameters.1)!), accountManager: accountManager, appLockContext: appLockContext, notificationController: nil, applicationBindings: applicationBindings, initialPresentationDataAndSettings: initialPresentationDataAndSettings!, networkArguments: NetworkInitializationArguments(apiId: self.initializationData.apiId, apiHash: self.initializationData.apiHash, languagesCategory: self.initializationData.languagesCategory, appVersion: self.initializationData.appVersion, voipMaxLayer: 0, voipVersions: [], appData: .single(self.initializationData.bundleData), externalRequestVerificationStream: .never(), externalRecaptchaRequestVerification: { _, _ in return .never() }, autolockDeadine: .single(nil), encryptionProvider: OpenSSLEncryptionProvider(), deviceModelName: nil, useBetaFeatures: self.initializationData.useBetaFeatures, isICloudEnabled: false), hasInAppPurchases: false, rootPath: rootPath, legacyBasePath: nil, apsNotificationToken: .never(), voipNotificationToken: .never(), firebaseSecretStream: .never(), setNotificationCall: { _ in }, navigateToChat: { _, _, _, _ in }, appDelegate: nil)
presentationDataPromise.set(sharedAccountContext!.presentationData)
}

View File

@ -133,7 +133,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
}
}
private let navigateToChatImpl: (AccountRecordId, PeerId, MessageId?) -> Void
private let navigateToChatImpl: (AccountRecordId, PeerId, MessageId?, Bool) -> Void
private let apsNotificationToken: Signal<Data?, NoError>
private let voipNotificationToken: Signal<Data?, NoError>
@ -268,7 +268,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
private let energyUsageAutomaticDisposable = MetaDisposable()
init(mainWindow: Window1?, sharedContainerPath: String, basePath: String, encryptionParameters: ValueBoxEncryptionParameters, accountManager: AccountManager<TelegramAccountManagerTypes>, appLockContext: AppLockContext, notificationController: NotificationContainerController?, applicationBindings: TelegramApplicationBindings, initialPresentationDataAndSettings: InitialPresentationDataAndSettings, networkArguments: NetworkInitializationArguments, hasInAppPurchases: Bool, rootPath: String, legacyBasePath: String?, apsNotificationToken: Signal<Data?, NoError>, voipNotificationToken: Signal<Data?, NoError>, firebaseSecretStream: Signal<[String: String], NoError>, setNotificationCall: @escaping (PresentationCall?) -> Void, navigateToChat: @escaping (AccountRecordId, PeerId, MessageId?) -> Void, displayUpgradeProgress: @escaping (Float?) -> Void = { _ in }, appDelegate: AppDelegate?) {
init(mainWindow: Window1?, sharedContainerPath: String, basePath: String, encryptionParameters: ValueBoxEncryptionParameters, accountManager: AccountManager<TelegramAccountManagerTypes>, appLockContext: AppLockContext, notificationController: NotificationContainerController?, applicationBindings: TelegramApplicationBindings, initialPresentationDataAndSettings: InitialPresentationDataAndSettings, networkArguments: NetworkInitializationArguments, hasInAppPurchases: Bool, rootPath: String, legacyBasePath: String?, apsNotificationToken: Signal<Data?, NoError>, voipNotificationToken: Signal<Data?, NoError>, firebaseSecretStream: Signal<[String: String], NoError>, setNotificationCall: @escaping (PresentationCall?) -> Void, navigateToChat: @escaping (AccountRecordId, PeerId, MessageId?, Bool) -> Void, displayUpgradeProgress: @escaping (Float?) -> Void = { _ in }, appDelegate: AppDelegate?) {
assert(Queue.mainQueue().isCurrent())
precondition(!testHasInstance)
@ -1760,7 +1760,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
}
public func navigateToChat(accountId: AccountRecordId, peerId: PeerId, messageId: MessageId?) {
self.navigateToChatImpl(accountId, peerId, messageId)
self.navigateToChatImpl(accountId, peerId, messageId, true)
}
public func messageFromPreloadedChatHistoryViewForLocation(id: MessageId, location: ChatHistoryLocationInput, context: AccountContext, chatLocation: ChatLocation, subject: ChatControllerSubject?, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>, tag: HistoryViewInputTag?) -> Signal<(MessageIndex?, Bool), NoError> {
@ -2991,8 +2991,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
let _ = (combineLatest(
queue: Queue.mainQueue(),
controller.result,
options.get())
|> take(1)).startStandalone(next: { [weak controller] result, options in
options.get() |> distinctUntilChanged
)).startStandalone(next: { [weak controller] result, options in
if let (peers, _, _, _, _, _) = result, let contactPeer = peers.first, case let .peer(peer, _, _) = contactPeer, let starsContext = context.starsContext {
if case .starGiftTransfer = source {
presentTransferAlertImpl?(EnginePeer(peer))
@ -3462,9 +3462,9 @@ public final class SharedAccountContextImpl: SharedAccountContext {
)
}
return nil
}, completion: { result, commit in
completion(result, commit)
} as (MediaEditorScreenImpl.Result, @escaping (@escaping () -> Void) -> Void) -> Void
}, completion: { results, commit in
completion(results.first!, commit)
} as ([MediaEditorScreenImpl.Result], @escaping (@escaping () -> Void) -> Void) -> Void
)
editorController.cancelled = { _ in
cancelled()
@ -3526,13 +3526,13 @@ public final class SharedAccountContextImpl: SharedAccountContext {
)
}
return nil
}, completion: { result, commit in
if case let .sticker(file, emoji) = result.media {
}, completion: { results, commit in
if case let .sticker(file, emoji) = results.first?.media {
completion(file, emoji, {
commit({})
})
}
} as (MediaEditorScreenImpl.Result, @escaping (@escaping () -> Void) -> Void) -> Void
} as ([MediaEditorScreenImpl.Result], @escaping (@escaping () -> Void) -> Void) -> Void
)
editorController.cancelled = { _ in
cancelled()
@ -3551,7 +3551,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
}
let editorController = MediaEditorScreenImpl(
context: context,
mode: .storyEditor,
mode: .storyEditor(remainingCount: 1),
subject: subject,
customTarget: nil,
initialCaption: text.flatMap { NSAttributedString(string: $0) },
@ -3559,13 +3559,10 @@ public final class SharedAccountContextImpl: SharedAccountContext {
transitionIn: nil,
transitionOut: { finished, isNew in
return nil
}, completion: { result, commit in
completion(result, commit)
} as (MediaEditorScreenImpl.Result, @escaping (@escaping () -> Void) -> Void) -> Void
}, completion: { results, commit in
completion(results.first!, commit)
} as ([MediaEditorScreenImpl.Result], @escaping (@escaping () -> Void) -> Void) -> Void
)
// editorController.cancelled = { _ in
// cancelled()
// }
return editorController
}
@ -3726,13 +3723,16 @@ public final class SharedAccountContextImpl: SharedAccountContext {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = MediaEditorScreenImpl(
context: context,
mode: .storyEditor,
mode: .storyEditor(remainingCount: 1),
subject: editorSubject,
transitionIn: nil,
transitionOut: { _, _ in
return nil
},
completion: { [weak parentController] result, commit in
completion: { [weak parentController] results, commit in
guard let result = results.first else {
return
}
let targetPeerId: EnginePeer.Id
let target: Stories.PendingTarget
if let sendAsPeerId = result.options.sendAsPeerId {

View File

@ -346,7 +346,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
return nil
}
},
completion: { result, resultTransition, dismissed in
completion: { result, resultTransition, storyRemainingCount, dismissed in
let subject: Signal<MediaEditorScreenImpl.Subject?, NoError> = result
|> map { value -> MediaEditorScreenImpl.Subject? in
func editorPIPPosition(_ position: CameraScreenImpl.PIPPosition) -> MediaEditorScreenImpl.PIPPosition {
@ -422,7 +422,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
let controller = MediaEditorScreenImpl(
context: context,
mode: .storyEditor,
mode: .storyEditor(remainingCount: storyRemainingCount ?? 1),
subject: subject,
customTarget: mediaEditorCustomTarget,
transitionIn: transitionIn,
@ -444,7 +444,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
} else {
return nil
}
}, completion: { [weak self] result, commit in
}, completion: { [weak self] results, commit in
guard let self else {
dismissCameraImpl?()
commit({})
@ -453,7 +453,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
if let customTarget, case .botPreview = customTarget {
externalState.storyTarget = customTarget
self.proceedWithStoryUpload(target: customTarget, results: [result], existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit)
self.proceedWithStoryUpload(target: customTarget, results: results, existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit)
dismissCameraImpl?()
return
@ -464,7 +464,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
target = .peer(id)
targetPeerId = id
} else {
if let sendAsPeerId = result.options.sendAsPeerId {
if let sendAsPeerId = results.first?.options.sendAsPeerId {
target = .peer(sendAsPeerId)
targetPeerId = sendAsPeerId
} else {
@ -486,12 +486,12 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
externalState.isPeerArchived = channel.storiesHidden ?? false
}
self.proceedWithStoryUpload(target: target, results: [result], existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit)
self.proceedWithStoryUpload(target: target, results: results, existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit)
dismissCameraImpl?()
})
}
} as (MediaEditorScreenImpl.Result, @escaping (@escaping () -> Void) -> Void) -> Void
} as ([MediaEditorScreenImpl.Result], @escaping (@escaping () -> Void) -> Void) -> Void
)
controller.cancelled = { showDraftTooltip in
if showDraftTooltip {

View File

@ -180,10 +180,17 @@ public func chatTranslationState(context: AccountContext, peerId: EnginePeer.Id)
baseLang = String(baseLang.dropLast(rawSuffix.count))
}
return context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.translationSettings])
|> mapToSignal { sharedData in
let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.translationSettings]?.get(TranslationSettings.self) ?? TranslationSettings.defaultSettings
if !settings.translateChats {
return combineLatest(
context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.translationSettings])
|> map { sharedData -> TranslationSettings in
return sharedData.entries[ApplicationSpecificSharedDataKeys.translationSettings]?.get(TranslationSettings.self) ?? TranslationSettings.defaultSettings
},
context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.AutoTranslateEnabled(id: peerId))
)
|> mapToSignal { settings, autoTranslateEnabled in
if !settings.translateChats && !autoTranslateEnabled {
return .single(nil)
}
@ -286,12 +293,22 @@ public func chatTranslationState(context: AccountContext, peerId: EnginePeer.Id)
if loggingEnabled {
Logger.shared.log("ChatTranslation", "Ended with: \(fromLang)")
}
let isEnabled: Bool
if let currentIsEnabled = cached?.isEnabled {
isEnabled = currentIsEnabled
} else if autoTranslateEnabled {
isEnabled = true
} else {
isEnabled = false
}
let state = ChatTranslationState(
baseLang: baseLang,
fromLang: fromLang,
timestamp: currentTime,
toLang: cached?.toLang,
isEnabled: cached?.isEnabled ?? false
isEnabled: isEnabled
)
let _ = updateChatTranslationState(engine: context.engine, peerId: peerId, state: state).start()
if !dontTranslateLanguages.contains(fromLang) {

View File

@ -124,6 +124,7 @@ public enum DeviceModel: CaseIterable, Equatable {
case iPhone16Plus
case iPhone16Pro
case iPhone16ProMax
case iPhone16e
case unknown(String)
@ -235,6 +236,8 @@ public enum DeviceModel: CaseIterable, Equatable {
return ["iPhone17,1"]
case .iPhone16ProMax:
return ["iPhone17,2"]
case .iPhone16e:
return ["iPhone17,5"]
case let .unknown(modelId):
return [modelId]
}
@ -348,6 +351,8 @@ public enum DeviceModel: CaseIterable, Equatable {
return "iPhone 16 Pro"
case .iPhone16ProMax:
return "iPhone 16 Pro Max"
case .iPhone16e:
return "iPhone 16e"
case let .unknown(modelId):
if modelId.hasPrefix("iPhone") {
return "Unknown iPhone"

View File

@ -103,7 +103,6 @@ private final class SheetContent: CombinedComponent {
.position(CGPoint(x: environment.safeInsets.left + 16.0 + closeButton.size.width / 2.0, y: 28.0))
)
//TODO:localize
let title = title.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(string: strings.WebApp_ImportData_Title, font: titleFont, textColor: textColor)),