diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 74cf0c9035..9d693b6bb7 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -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 %@."; diff --git a/submodules/AccountContext/Sources/Premium.swift b/submodules/AccountContext/Sources/Premium.swift index 790a21b077..2dea082727 100644 --- a/submodules/AccountContext/Sources/Premium.swift +++ b/submodules/AccountContext/Sources/Premium.swift @@ -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, diff --git a/submodules/Camera/Sources/Camera.swift b/submodules/Camera/Sources/Camera.swift index cbc9466f9c..b3e34d4cd2 100644 --- a/submodules/Camera/Sources/Camera.swift +++ b/submodules/Camera/Sources/Camera.swift @@ -173,8 +173,11 @@ private final class CameraContext { self.positionValue = configuration.position self._positionPromise = ValuePromise(configuration.position) +#if targetEnvironment(simulator) +#else self.setDualCameraEnabled(configuration.isDualEnabled, change: false) - +#endif + NotificationCenter.default.addObserver( self, selector: #selector(self.sessionRuntimeError), diff --git a/submodules/Components/SheetComponent/Sources/SheetComponent.swift b/submodules/Components/SheetComponent/Sources/SheetComponent.swift index 7e61cbe1d5..e95713a3a3 100644 --- a/submodules/Components/SheetComponent/Sources/SheetComponent.swift +++ b/submodules/Components/SheetComponent/Sources/SheetComponent.swift @@ -67,6 +67,7 @@ public final class SheetComponent: Component { public let externalState: ExternalState? public let animateOut: ActionSlot> public let onPan: () -> Void + public let willDismiss: () -> Void public init( content: AnyComponent, @@ -76,7 +77,8 @@ public final class SheetComponent: Component { isScrollEnabled: Bool = true, externalState: ExternalState? = nil, animateOut: ActionSlot>, - onPan: @escaping () -> Void = {} + onPan: @escaping () -> Void = {}, + willDismiss: @escaping () -> Void = {} ) { self.content = content self.backgroundColor = backgroundColor @@ -86,6 +88,7 @@ public final class SheetComponent: 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: 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: 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) }) diff --git a/submodules/Display/Source/Navigation/NavigationModalContainer.swift b/submodules/Display/Source/Navigation/NavigationModalContainer.swift index 3a86497e78..7b1b433995 100644 --- a/submodules/Display/Source/Navigation/NavigationModalContainer.swift +++ b/submodules/Display/Source/Navigation/NavigationModalContainer.swift @@ -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 diff --git a/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift b/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift index 7fd1c4f3b4..4833ed62b3 100644 --- a/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift +++ b/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift @@ -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 { diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index 9ea769f836..3b9bcc6b5e 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -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?() }) diff --git a/submodules/MtProtoKit/Sources/MTApiEnvironment.m b/submodules/MtProtoKit/Sources/MTApiEnvironment.m index 399acc5097..a7eea0c54c 100644 --- a/submodules/MtProtoKit/Sources/MTApiEnvironment.m +++ b/submodules/MtProtoKit/Sources/MTApiEnvironment.m @@ -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"; diff --git a/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift b/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift index aae7f44fe6..d9c5782795 100644 --- a/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift @@ -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 diff --git a/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift b/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift index e231483dcb..f737897e8f 100644 --- a/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift +++ b/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift @@ -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()) } }) } diff --git a/submodules/TelegramCore/Sources/ApiUtils/ApiGroupOrChannel.swift b/submodules/TelegramCore/Sources/ApiUtils/ApiGroupOrChannel.swift index 122d2f95a5..dc9280893c 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/ApiGroupOrChannel.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/ApiGroupOrChannel.swift @@ -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 diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift index 2cf9cd110d..4c3e0a445f 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift @@ -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): diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChannel.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChannel.swift index 77c9b3d4ca..6fb4634d4f 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChannel.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChannel.swift @@ -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 { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift index e1cf771e0c..ce4796607e 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift @@ -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) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift index f5738a162c..36f3679bed 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift @@ -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 + } + } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index cfaf228f4f..d3954aecf4 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -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 in diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift index 3425c1f487..32aa9cdc41 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift @@ -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 { 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 { + 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 { 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 { + 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 { - return account.network.request(Api.functions.payments.updateStarGiftPrice(slug: slug, resellStars: price ?? 0)) - |> map(Optional.init) - |> `catch` { _ -> Signal in - return .single(nil) +func _internal_updateStarGiftResalePrice(account: Account, reference: StarGiftReference, price: Int64?) -> Signal { + return account.postbox.transaction { transaction in + return reference.apiStarGiftReference(transaction: transaction) } - |> mapToSignal { updates -> Signal 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 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 { + 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 { + 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 { + 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 { + 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? diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift index 59e0d2849f..464f5f757d 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift @@ -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 ) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift index 4677f08a9e..2fcf1f873a 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift @@ -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 { - return _internal_updateStarGiftResalePrice(account: self.account, slug: slug, price: price) + public func updateStarGiftResalePrice(reference: StarGiftReference, price: Int64?) -> Signal { + return _internal_updateStarGiftResalePrice(account: self.account, reference: reference, price: price) } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/AutoTranslate.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/AutoTranslate.swift new file mode 100644 index 0000000000..cd513ab5f1 --- /dev/null +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/AutoTranslate.swift @@ -0,0 +1,19 @@ +import Foundation +import Postbox +import SwiftSignalKit +import TelegramApi +import MtProtoKit + +func _internal_toggleAutoTranslation(account: Account, peerId: PeerId, enabled: Bool) -> Signal { + return account.postbox.transaction { transaction -> Signal 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 +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift index 39e07ac740..6937e7b94a 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift @@ -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 { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index a8b746b691..06ac1460c1 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -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 { + return _internal_toggleAutoTranslation(account: self.account, peerId: peerId, enabled: enabled) + } + public func resolveMessageLink(slug: String) -> Signal { return self.account.network.request(Api.functions.account.resolveBusinessChatLink(slug: slug)) |> map(Optional.init) diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index 49ecafc847..812d3a26ff 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -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) + } } } } diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift index f2cf4d1cd2..bda8dd68ee 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift @@ -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, ResultTransition?, @escaping () -> Void) -> Void + fileprivate let completion: (Signal, 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() 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, ResultTransition?, @escaping () -> Void) -> Void + completion: @escaping (Signal, 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, { }) } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift index b0cc08742c..75c0a68835 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift @@ -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 diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift index 8b3855f064..30cff91d9b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift @@ -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() + 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)) } } } diff --git a/submodules/TelegramUI/Components/Chat/QuickShareScreen/Sources/QuickShareToastScreen.swift b/submodules/TelegramUI/Components/Chat/QuickShareScreen/Sources/QuickShareToastScreen.swift index e1c71a1a22..723007aa78 100644 --- a/submodules/TelegramUI/Components/Chat/QuickShareScreen/Sources/QuickShareToastScreen.swift +++ b/submodules/TelegramUI/Components/Chat/QuickShareScreen/Sources/QuickShareToastScreen.swift @@ -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() } diff --git a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift index 379755dd6b..001fa6f4fe 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift @@ -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)) diff --git a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift index b327c778ce..52bd22d3ee 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift @@ -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 )) } diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift index 76e2d6e3fa..3c325c54bb 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift @@ -82,6 +82,7 @@ final class GiftSetupScreenComponent: Component { private let navigationTitle = ComponentView() private let remainingCount = ComponentView() + private let resaleSection = ComponentView() private let introContent = ComponentView() private let introSection = ComponentView() private let starsSection = ComponentView() @@ -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] = [] @@ -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 diff --git a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftAttributeListContextItem.swift b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftAttributeListContextItem.swift index a55122afa6..bbb4658b85 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftAttributeListContextItem.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftAttributeListContextItem.swift @@ -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 } diff --git a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift index 46fee26be0..8dc7594761 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift @@ -72,7 +72,7 @@ final class GiftStoreScreenComponent: Component { private let loadingNode: LoadingShimmerNode private let emptyResultsAnimation = ComponentView() private let emptyResultsTitle = ComponentView() - private let emptyResultsAction = ComponentView() + private let clearFilters = ComponentView() private let topPanel = ComponentView() private let topSeparator = ComponentView() @@ -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 } } diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/ButtonsComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/ButtonsComponent.swift index 12336824b9..ac59b7a2ce 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/ButtonsComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/ButtonsComponent.swift @@ -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) )) diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index 3125cacc51..b0ced0d654 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -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)? = nil, upgradeGift: ((Int64?, Bool) -> Signal)? = nil, buyGift: ((String, EnginePeer.Id) -> Signal)? = nil, - updateResellStars: ((Int64?) -> Void)? = nil, + updateResellStars: ((Int64?) -> Signal)? = 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) }) diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift index 2c9d6e16cf..1f77d01911 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift @@ -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 { diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderer.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderer.swift index 4792af7622..9595d22207 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderer.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderer.swift @@ -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 diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift index d0b1e891aa..47b4d4c792 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift @@ -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) -> MediaEditorValues { + public func withUpdatedVideoTrimRange(_ videoTrimRange: Range) -> 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) } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditCover.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditCover.swift deleted file mode 100644 index 7310f6d9a3..0000000000 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditCover.swift +++ /dev/null @@ -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 - } -} diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift index 1b053546ee..0b3e30a019 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift @@ -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 { diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index ef0262ea56..3cee6aa8cd 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -327,7 +327,7 @@ final class MediaEditorScreenComponent: Component { private let switchCameraButton = ComponentView() private let selectionButton = ComponentView() - private let selectionPanel = ComponentView() + private var selectionPanel: ComponentView? private let textCancelButton = ComponentView() private let textDoneButton = ComponentView() @@ -338,7 +338,7 @@ final class MediaEditorScreenComponent: Component { private var isEditingCaption = false private var currentInputMode: MessageInputPanelComponent.InputMode = .text - private var isSelectionPanelOpen = false + fileprivate var isSelectionPanelOpen = false private var didInitializeInputMediaNodeDataPromise = false private var inputMediaNodeData: ChatEntityKeyboardInputNode.InputData? @@ -577,6 +577,11 @@ final class MediaEditorScreenComponent: Component { view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) view.layer.animateScale(from: 0.6, to: 1.0, duration: 0.2) } + + if let view = self.selectionButton.view { + view.layer.animateAlpha(from: 0.0, to: view.alpha, duration: 0.2) + view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) + } } } @@ -589,14 +594,14 @@ final class MediaEditorScreenComponent: Component { transition.setScale(view: view, scale: 0.1) } - let buttons = [ + let toolbarButtons = [ self.drawButton, self.textButton, self.stickerButton, self.toolsButton ] - for button in buttons { + for button in toolbarButtons { if let view = button.view { view.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: 64.0), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) view.layer.animateAlpha(from: view.alpha, to: 0.0, duration: 0.15, removeOnCompletion: false) @@ -617,19 +622,17 @@ final class MediaEditorScreenComponent: Component { } } - if let view = self.saveButton.view { - transition.setAlpha(view: view, alpha: 0.0) - transition.setScale(view: view, scale: 0.1) - } + let topButtons = [ + self.saveButton, + self.muteButton, + self.playbackButton + ] - if let view = self.muteButton.view { - transition.setAlpha(view: view, alpha: 0.0) - transition.setScale(view: view, scale: 0.1) - } - - if let view = self.playbackButton.view { - transition.setAlpha(view: view, alpha: 0.0) - transition.setScale(view: view, scale: 0.1) + for button in topButtons { + if let view = button.view { + transition.setAlpha(view: view, alpha: 0.0) + transition.setScale(view: view, scale: 0.1) + } } if let view = self.scrubber?.view { @@ -638,35 +641,30 @@ final class MediaEditorScreenComponent: Component { view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2) } - if let view = self.undoButton.view { - transition.setAlpha(view: view, alpha: 0.0) - transition.setScale(view: view, scale: 0.1) - } + let stickerButtons = [ + self.undoButton, + self.eraseButton, + self.restoreButton, + self.outlineButton, + self.cutoutButton + ] - if let view = self.eraseButton.view { - transition.setAlpha(view: view, alpha: 0.0) - transition.setScale(view: view, scale: 0.1) - } - - if let view = self.restoreButton.view { - transition.setAlpha(view: view, alpha: 0.0) - transition.setScale(view: view, scale: 0.1) - } - - if let view = self.outlineButton.view { - transition.setAlpha(view: view, alpha: 0.0) - transition.setScale(view: view, scale: 0.1) - } - - if let view = self.cutoutButton.view { - transition.setAlpha(view: view, alpha: 0.0) - transition.setScale(view: view, scale: 0.1) + for button in stickerButtons { + if let view = button.view { + transition.setAlpha(view: view, alpha: 0.0) + transition.setScale(view: view, scale: 0.1) + } } if let view = self.textSize.view { transition.setAlpha(view: view, alpha: 0.0) transition.setScale(view: view, scale: 0.1) } + + if let view = self.selectionButton.view { + transition.setAlpha(view: view, alpha: 0.0) + transition.setScale(view: view, scale: 0.1) + } } func animateOutToTool(inPlace: Bool, transition: ComponentTransition) { @@ -2000,120 +1998,6 @@ final class MediaEditorScreenComponent: Component { transition.setScale(view: switchCameraButtonView, scale: isRecordingAdditionalVideo ? 1.0 : 0.01) transition.setAlpha(view: switchCameraButtonView, alpha: isRecordingAdditionalVideo ? 1.0 : 0.0) } - - if controller.node.items.count > 1 { - let selectionButtonSize = self.selectionButton.update( - transition: transition, - component: AnyComponent(PlainButtonComponent( - content: AnyComponent( - SelectionPanelButtonContentComponent( - count: Int32(controller.node.items.count(where: { $0.isEnabled })), - isSelected: self.isSelectionPanelOpen, - tag: nil - ) - ), - effectAlignment: .center, - action: { [weak self] in - if let self { - self.isSelectionPanelOpen = !self.isSelectionPanelOpen - self.state?.updated() - } - }, - animateAlpha: false - )), - environment: {}, - containerSize: CGSize(width: 33.0, height: 33.0) - ) - let selectionButtonFrame = CGRect( - origin: CGPoint(x: availableSize.width - selectionButtonSize.width - 12.0, y: inputPanelFrame.minY - selectionButtonSize.height - 3.0), - size: selectionButtonSize - ) - if let selectionButtonView = self.selectionButton.view as? PlainButtonComponent.View { - if selectionButtonView.superview == nil { - self.addSubview(selectionButtonView) - } - transition.setPosition(view: selectionButtonView, position: selectionButtonFrame.center) - transition.setBounds(view: selectionButtonView, bounds: CGRect(origin: .zero, size: selectionButtonFrame.size)) - transition.setScale(view: selectionButtonView, scale: displayTopButtons ? 1.0 : 0.01) - transition.setAlpha(view: selectionButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities ? 1.0 : 0.0) - - if self.isSelectionPanelOpen { - let selectionPanelFrame = CGRect( - origin: CGPoint(x: 12.0, y: inputPanelFrame.minY - selectionButtonSize.height - 3.0 - 130.0), - size: CGSize(width: availableSize.width - 24.0, height: 120.0) - ) - - var selectedItemId = "" - if case let .asset(asset) = controller.node.subject { - selectedItemId = asset.localIdentifier - } - - let _ = self.selectionPanel.update( - transition: transition, - component: AnyComponent( - SelectionPanelComponent( - previewContainerView: controller.node.previewContentContainerView, - frame: selectionPanelFrame, - items: controller.node.items, - selectedItemId: selectedItemId, - itemTapped: { [weak self, weak controller] id in - guard let self, let controller else { - return - } - self.isSelectionPanelOpen = false - self.state?.updated() - - if let id { - controller.node.switchToItem(id) - } - }, - itemSelectionToggled: { [weak self, weak controller] id in - guard let self, let controller else { - return - } - if let itemIndex = controller.node.items.firstIndex(where: { $0.asset.localIdentifier == id }) { - controller.node.items[itemIndex].isEnabled = !controller.node.items[itemIndex].isEnabled - } - self.state?.updated(transition: .spring(duration: 0.3)) - }, - itemReordered: { [weak self, weak controller] fromId, toId in - guard let self, let controller else { - return - } - guard let fromIndex = controller.node.items.firstIndex(where: { $0.asset.localIdentifier == fromId }), let toIndex = controller.node.items.firstIndex(where: { $0.asset.localIdentifier == toId }), toIndex < controller.node.items.count else { - return - } - let fromItem = controller.node.items[fromIndex] - let toItem = controller.node.items[toIndex] - controller.node.items[fromIndex] = toItem - controller.node.items[toIndex] = fromItem - self.state?.updated(transition: .spring(duration: 0.3)) - } - ) - ), - environment: {}, - containerSize: availableSize - ) - if let selectionPanelView = self.selectionPanel.view as? SelectionPanelComponent.View { - if selectionPanelView.superview == nil { - self.insertSubview(selectionPanelView, belowSubview: selectionButtonView) - if let buttonView = selectionButtonView.contentView as? SelectionPanelButtonContentComponent.View { - selectionPanelView.animateIn(from: buttonView) - } - } - selectionPanelView.frame = CGRect(origin: .zero, size: availableSize) - } - } else if let selectionPanelView = self.selectionPanel.view as? SelectionPanelComponent.View { - if let buttonView = selectionButtonView.contentView as? SelectionPanelButtonContentComponent.View { - selectionPanelView.animateOut(to: buttonView, completion: { [weak selectionPanelView] in - selectionPanelView?.removeFromSuperview() - }) - } else { - selectionPanelView.removeFromSuperview() - } - } - } - } } else { inputPanelSize = CGSize(width: 0.0, height: 12.0) } @@ -2121,20 +2005,32 @@ final class MediaEditorScreenComponent: Component { if case .stickerEditor = controller.mode { } else { + var selectionButtonInset: CGFloat = 0.0 + if let playerState = state.playerState { let scrubberInset: CGFloat = 9.0 let minDuration: Double let maxDuration: Double + var segmentDuration: Double? if playerState.isAudioOnly { minDuration = 5.0 maxDuration = 15.0 } else { minDuration = 1.0 if case .avatarEditor = controller.mode { - maxDuration = 10.0 + maxDuration = avatarMaxVideoDuration } else { - maxDuration = storyMaxVideoDuration + if controller.node.items.count > 0 { + maxDuration = storyMaxVideoDuration + } else { + if case let .storyEditor(remainingCount) = controller.mode, remainingCount > 1 { + maxDuration = min(storyMaxCombinedVideoDuration, Double(remainingCount) * storyMaxVideoDuration) + segmentDuration = storyMaxVideoDuration + } else { + maxDuration = storyMaxVideoDuration + } + } } } @@ -2209,6 +2105,7 @@ final class MediaEditorScreenComponent: Component { position: playerState.position, minDuration: minDuration, maxDuration: maxDuration, + segmentDuration: segmentDuration, isPlaying: playerState.isPlaying, tracks: visibleTracks, isCollage: isCollage, @@ -2348,6 +2245,7 @@ final class MediaEditorScreenComponent: Component { } let scrubberFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - scrubberSize.width) / 2.0), y: availableSize.height - environment.safeInsets.bottom - scrubberSize.height + controlsBottomInset - inputPanelSize.height + 3.0 - scrubberBottomOffset), size: scrubberSize) + selectionButtonInset = scrubberSize.height + 11.0 if let scrubberView = scrubber.view { var animateIn = false if scrubberView.superview == nil { @@ -2392,6 +2290,146 @@ final class MediaEditorScreenComponent: Component { } } } + + if controller.node.items.count > 1 { + let selectionButtonSize = self.selectionButton.update( + transition: transition, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent( + SelectionPanelButtonContentComponent( + count: Int32(controller.node.items.count(where: { $0.isEnabled })), + isSelected: self.isSelectionPanelOpen, + tag: nil + ) + ), + effectAlignment: .center, + action: { [weak self, weak controller] in + if let self, let controller { + self.isSelectionPanelOpen = !self.isSelectionPanelOpen + if let mediaEditor = controller.node.mediaEditor { + if self.isSelectionPanelOpen { + mediaEditor.maybePauseVideo() + } else { + Queue.mainQueue().after(0.1) { + mediaEditor.maybeUnpauseVideo() + } + } + } + self.state?.updated(transition: .spring(duration: 0.3)) + + controller.hapticFeedback.impact(.light) + } + }, + animateAlpha: false + )), + environment: {}, + containerSize: CGSize(width: 33.0, height: 33.0) + ) + let selectionButtonFrame = CGRect( + origin: CGPoint(x: availableSize.width - selectionButtonSize.width - 12.0, y: availableSize.height - environment.safeInsets.bottom - selectionButtonSize.height + controlsBottomInset - inputPanelSize.height - 3.0 - selectionButtonInset), + size: selectionButtonSize + ) + if let selectionButtonView = self.selectionButton.view as? PlainButtonComponent.View { + if selectionButtonView.superview == nil { + self.addSubview(selectionButtonView) + } + transition.setPosition(view: selectionButtonView, position: selectionButtonFrame.center) + transition.setBounds(view: selectionButtonView, bounds: CGRect(origin: .zero, size: selectionButtonFrame.size)) + transition.setScale(view: selectionButtonView, scale: displayTopButtons && !isRecordingAdditionalVideo ? 1.0 : 0.01) + transition.setAlpha(view: selectionButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities && !isRecordingAdditionalVideo ? 1.0 : 0.0) + + if self.isSelectionPanelOpen { + let selectionPanelFrame = CGRect( + origin: CGPoint(x: 12.0, y: selectionButtonFrame.minY - 130.0), + size: CGSize(width: availableSize.width - 24.0, height: 120.0) + ) + + var selectedItemId = "" + if case let .asset(asset) = controller.node.subject { + selectedItemId = asset.localIdentifier + } + + let selectionPanel: ComponentView + if let current = self.selectionPanel { + selectionPanel = current + } else { + selectionPanel = ComponentView() + self.selectionPanel = selectionPanel + } + + let _ = selectionPanel.update( + transition: transition, + component: AnyComponent( + SelectionPanelComponent( + previewContainerView: controller.node.previewContentContainerView, + frame: selectionPanelFrame, + items: controller.node.items, + selectedItemId: selectedItemId, + itemTapped: { [weak self, weak controller] id in + guard let self, let controller else { + return + } + self.isSelectionPanelOpen = false + self.state?.updated(transition: id == nil ? .spring(duration: 0.3) : .immediate) + + if let id { + controller.node.switchToItem(id) + + controller.hapticFeedback.impact(.light) + } + }, + itemSelectionToggled: { [weak self, weak controller] id in + guard let self, let controller else { + return + } + if let itemIndex = controller.node.items.firstIndex(where: { $0.asset.localIdentifier == id }) { + controller.node.items[itemIndex].isEnabled = !controller.node.items[itemIndex].isEnabled + } + self.state?.updated(transition: .spring(duration: 0.3)) + }, + itemReordered: { [weak self, weak controller] fromId, toId in + guard let self, let controller else { + return + } + guard let fromIndex = controller.node.items.firstIndex(where: { $0.asset.localIdentifier == fromId }), let toIndex = controller.node.items.firstIndex(where: { $0.asset.localIdentifier == toId }), toIndex < controller.node.items.count else { + return + } + let fromItem = controller.node.items[fromIndex] + let toItem = controller.node.items[toIndex] + controller.node.items[fromIndex] = toItem + controller.node.items[toIndex] = fromItem + self.state?.updated(transition: .spring(duration: 0.3)) + + controller.hapticFeedback.tap() + } + ) + ), + environment: {}, + containerSize: availableSize + ) + if let selectionPanelView = selectionPanel.view as? SelectionPanelComponent.View { + if selectionPanelView.superview == nil { + self.insertSubview(selectionPanelView, belowSubview: selectionButtonView) + if let buttonView = selectionButtonView.contentView as? SelectionPanelButtonContentComponent.View { + selectionPanelView.animateIn(from: buttonView) + } + } + selectionPanelView.frame = CGRect(origin: .zero, size: availableSize) + } + } else if let selectionPanel = self.selectionPanel { + self.selectionPanel = nil + if let selectionPanelView = selectionPanel.view as? SelectionPanelComponent.View { + if !transition.animation.isImmediate, let buttonView = selectionButtonView.contentView as? SelectionPanelButtonContentComponent.View { + selectionPanelView.animateOut(to: buttonView, completion: { [weak selectionPanelView] in + selectionPanelView?.removeFromSuperview() + }) + } else { + selectionPanelView.removeFromSuperview() + } + } + } + } + } } if case .stickerEditor = controller.mode { @@ -2806,6 +2844,10 @@ final class MediaEditorScreenComponent: Component { let storyDimensions = CGSize(width: 1080.0, height: 1920.0) let storyMaxVideoDuration: Double = 60.0 +let storyMaxCombinedVideoCount: Int = 3 +let storyMaxCombinedVideoDuration: Double = storyMaxVideoDuration * Double(storyMaxCombinedVideoCount) + +let avatarMaxVideoDuration: Double = 10.0 public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UIDropInteractionDelegate { public enum Mode { @@ -2816,7 +2858,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID case businessIntro } - case storyEditor + case storyEditor(remainingCount: Int32) case stickerEditor(mode: StickerEditorMode) case botPreview case avatarEditor @@ -3474,6 +3516,12 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID values: initialValues, hasHistogram: true ) + if case let .storyEditor(remainingCount) = controller.mode, self.items.isEmpty { + mediaEditor.maxDuration = min(storyMaxCombinedVideoDuration, Double(remainingCount) * storyMaxVideoDuration) + } else if case .avatarEditor = controller.mode { + mediaEditor.maxDuration = avatarMaxVideoDuration + } + if case .avatarEditor = controller.mode { mediaEditor.setVideoIsMuted(true) } else if case let .coverEditor(dimensions) = controller.mode { @@ -3607,7 +3655,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID } } else if case let .gift(gift) = subject { isGift = true - let media: [Media] = [TelegramMediaAction(action: .starGiftUnique(gift: .unique(gift), isUpgrade: false, isTransferred: false, savedToProfile: false, canExportDate: nil, transferStars: nil, isRefunded: false, peerId: nil, senderId: nil, savedId: nil, resaleStars: nil))] + let media: [Media] = [TelegramMediaAction(action: .starGiftUnique(gift: .unique(gift), isUpgrade: false, isTransferred: false, savedToProfile: false, canExportDate: nil, transferStars: nil, isRefunded: false, peerId: nil, senderId: nil, savedId: nil, resaleStars: nil, canTransferDate: nil, canResaleDate: nil))] let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: self.context.account.peerId, namespace: Namespaces.Message.Cloud, id: -1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: media, peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) messages = .single([message]) } else { @@ -4027,7 +4075,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID } if gestureRecognizer === self.dismissPanGestureRecognizer { let location = gestureRecognizer.location(in: self.entitiesView) - if self.controller?.isEmbeddedEditor == true || self.isDisplayingTool != nil || self.entitiesView.hasSelection || self.entitiesView.getView(at: location) != nil { + if self.controller?.isEmbeddedEditor == true || self.isDisplayingTool != nil || self.entitiesView.hasSelection || self.entitiesView.getView(at: location) != nil || self.componentHostView?.isSelectionPanelOpen == true { return false } return true @@ -4188,7 +4236,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID private var previousRotateTimestamp: Double? @objc func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { - guard !self.isCollageTimelineOpen else { + guard !self.isCollageTimelineOpen && !(self.componentHostView?.isSelectionPanelOpen ?? false) else { return } if gestureRecognizer.numberOfTouches == 2, let subject = self.subject, !self.entitiesView.hasSelection { @@ -5060,7 +5108,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID var audioTrimRange: Range? var audioOffset: Double? - if let videoDuration = mediaEditor.originalDuration { + if let videoDuration = mediaEditor.originalCappedDuration { if let videoStart = mediaEditor.values.videoTrimRange?.lowerBound { audioOffset = -videoStart } else if let _ = mediaEditor.values.additionalVideoPath, let videoStart = mediaEditor.values.additionalVideoTrimRange?.lowerBound { @@ -5381,7 +5429,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID var updatedCurrentItem = self.items[currentItemIndex] updatedCurrentItem.caption = self.getCaption() - if mediaEditor.values.hasChanges && updatedCurrentItem.values != mediaEditor.values { + if (mediaEditor.values.hasChanges && updatedCurrentItem.values != mediaEditor.values) || updatedCurrentItem.values?.gradientColors == nil { updatedCurrentItem.values = mediaEditor.values updatedCurrentItem.version += 1 @@ -6509,18 +6557,20 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID fileprivate let customTarget: EnginePeer.Id? let forwardSource: (EnginePeer, EngineStoryItem)? - fileprivate let initialCaption: NSAttributedString? - fileprivate let initialPrivacy: EngineStoryPrivacy? - fileprivate let initialMediaAreas: [MediaArea]? - fileprivate let initialVideoPosition: Double? - fileprivate let initialLink: (url: String, name: String?)? + let initialCaption: NSAttributedString? + let initialPrivacy: EngineStoryPrivacy? + let initialMediaAreas: [MediaArea]? + let initialVideoPosition: Double? + let initialLink: (url: String, name: String?)? fileprivate let transitionIn: TransitionIn? fileprivate let transitionOut: (Bool, Bool?) -> TransitionOut? + var didComplete = false + public var cancelled: (Bool) -> Void = { _ in } public var willComplete: (UIImage?, Bool, @escaping () -> Void) -> Void - public var completion: (MediaEditorScreenImpl.Result, @escaping (@escaping () -> Void) -> Void) -> Void + public var completion: ([MediaEditorScreenImpl.Result], @escaping (@escaping () -> Void) -> Void) -> Void public var dismissed: () -> Void = { } public var willDismiss: () -> Void = { } public var sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)? @@ -6529,7 +6579,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID private var closeFriends = Promise<[EnginePeer]>() private let storiesBlockedPeers: BlockedPeersContext - private let hapticFeedback = HapticFeedback() + fileprivate let hapticFeedback = HapticFeedback() private var audioSessionDisposable: Disposable? private let postingAvailabilityPromise = Promise() @@ -6554,7 +6604,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID transitionIn: TransitionIn?, transitionOut: @escaping (Bool, Bool?) -> TransitionOut?, willComplete: @escaping (UIImage?, Bool, @escaping () -> Void) -> Void = { _, _, commit in commit() }, - completion: @escaping (MediaEditorScreenImpl.Result, @escaping (@escaping () -> Void) -> Void) -> Void + completion: @escaping ([MediaEditorScreenImpl.Result], @escaping (@escaping () -> Void) -> Void) -> Void ) { self.context = context self.mode = mode @@ -6679,7 +6729,10 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID } 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 .available = availability { return } @@ -6741,7 +6794,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID } } - fileprivate var isEmbeddedEditor: Bool { + var isEmbeddedEditor: Bool { return self.isEditingStory || self.isEditingStoryCover || self.forwardSource != nil } @@ -6765,9 +6818,26 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID coverImage = nil } + var storyCount: Int32 = 0 + if self.node.items.count > 0 { + storyCount = Int32(self.node.items.count(where: { $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 { + storyCount = Int32(min(storyMaxCombinedVideoCount, Int(ceil(duration / storyMaxVideoDuration)))) + } + } + } + let stateContext = ShareWithPeersScreen.StateContext( context: self.context, - subject: .stories(editing: false, count: Int32(self.node.items.count(where: { $0.isEnabled }))), + subject: .stories(editing: false, count: storyCount), editing: false, initialPeerIds: Set(privacy.privacy.additionallyIncludePeers), closeFriends: self.closeFriends.get(), @@ -6977,7 +7047,6 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID let hasPremium = self.context.isPremium let presentationData = self.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme) - let title = presentationData.strings.Story_Editor_ExpirationText let currentValue = self.state.privacy.timeout let emptyAction: ((ContextMenuActionItem.Action) -> Void)? = nil @@ -6994,62 +7063,56 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID ) } - var items: [ContextMenuItem] = [] - items.append(.action(ContextMenuActionItem(text: title, textLayout: .multiline, textFont: .small, icon: { _ in return nil }, action: emptyAction))) + let timeoutOptions: [(hours: Int, requiresPremium: Bool)] = [ + (6, true), + (12, true), + (24, false), + (48, true) + ] - items.append(.action(ContextMenuActionItem(text: presentationData.strings.Story_Editor_ExpirationValue(6), icon: { theme in - if !hasPremium { - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/TextLockIcon"), color: theme.contextMenu.secondaryColor) - } else { - return currentValue == 3600 * 6 ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil - } - }, action: { [weak self] _, a in - a(.default) - - if hasPremium { - updateTimeout(3600 * 6) - } else { - self?.presentTimeoutPremiumSuggestion() - } - }))) - items.append(.action(ContextMenuActionItem(text: presentationData.strings.Story_Editor_ExpirationValue(12), icon: { theme in - if !hasPremium { - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/TextLockIcon"), color: theme.contextMenu.secondaryColor) - } else { - return currentValue == 3600 * 12 ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil - } - }, action: { [weak self] _, a in - a(.default) - - if hasPremium { - updateTimeout(3600 * 12) - } else { - self?.presentTimeoutPremiumSuggestion() - } - }))) - items.append(.action(ContextMenuActionItem(text: presentationData.strings.Story_Editor_ExpirationValue(24), icon: { theme in - return currentValue == 86400 ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil - }, action: { _, a in - a(.default) - - updateTimeout(86400) - }))) - items.append(.action(ContextMenuActionItem(text: presentationData.strings.Story_Editor_ExpirationValue(48), icon: { theme in - if !hasPremium { - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/TextLockIcon"), color: theme.contextMenu.secondaryColor) - } else { - return currentValue == 86400 * 2 ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil - } - }, action: { [weak self] _, a in - a(.default) - - if hasPremium { - updateTimeout(86400 * 2) - } else { - self?.presentTimeoutPremiumSuggestion() - } - }))) + var items: [ContextMenuItem] = [ + .action(ContextMenuActionItem( + text: presentationData.strings.Story_Editor_ExpirationText, + textLayout: .multiline, + textFont: .small, + icon: { _ in return nil }, + action: emptyAction + )) + ] + for option in timeoutOptions { + let text = presentationData.strings.Story_Editor_ExpirationValue(Int32(option.hours)) + let value = option.hours * 3600 + + items.append(.action(ContextMenuActionItem( + text: text, + icon: { theme in + if option.requiresPremium && !hasPremium { + return generateTintedImage( + image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/TextLockIcon"), + color: theme.contextMenu.secondaryColor + ) + } else if currentValue == value { + return generateTintedImage( + image: UIImage(bundleImageName: "Chat/Context Menu/Check"), + color: theme.contextMenu.primaryColor + ) + } else { + return nil + } + }, + action: { [weak self] _, a in + a(.default) + + if !option.requiresPremium || hasPremium { + updateTimeout(value) + } else { + self?.presentTimeoutPremiumSuggestion() + } + } + ))) + } + let contextController = ContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: self, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) self.present(contextController, in: .window(.root)) } @@ -7332,462 +7395,6 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID } return true } - - private var didComplete = false - func requestStoryCompletion(animated: Bool) { - guard let mediaEditor = self.node.mediaEditor, let subject = self.node.subject, let actualSubject = self.node.actualSubject, !self.didComplete else { - return - } - - self.didComplete = true - - self.dismissAllTooltips() - - mediaEditor.stop() - mediaEditor.invalidate() - self.node.entitiesView.invalidate() - - let context = self.context - if let navigationController = self.navigationController as? NavigationController { - navigationController.updateRootContainerTransitionOffset(0.0, transition: .immediate) - } - - 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) - - 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) - } - - 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 - 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() - } - - makeEditorImageComposition(context: self.node.ciContext, postbox: self.context.account.postbox, inputImage: inputImage, dimensions: storyDimensions, values: mediaEditor.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: mediaEditor.values, duration: duration, dimensions: mediaEditor.values.resultDimensions), mediaAreas: 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() - } - }) - }) - }) - } - }) - } - }) - - 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) - } - }) - } - }) - } - } - } func requestStickerCompletion(animated: Bool) { guard let mediaEditor = self.node.mediaEditor else { @@ -7812,10 +7419,8 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID navigationController.updateRootContainerTransitionOffset(0.0, transition: .immediate) } - 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) - + self.updateMediaEditorEntities() + if let image = mediaEditor.resultImage { let values = mediaEditor.values.withUpdatedQualityPreset(.sticker) makeEditorImageComposition(context: self.node.ciContext, postbox: self.context.account.postbox, inputImage: image, dimensions: storyDimensions, outputDimensions: CGSize(width: 512, height: 512), values: values, time: .zero, textScale: 2.0, completion: { [weak self] resultImage in @@ -7836,23 +7441,14 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID if let navigationController = self.navigationController as? NavigationController { navigationController.updateRootContainerTransitionOffset(0.0, transition: .immediate) } - - 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) - + + self.updateMediaEditorEntities() + if let image = mediaEditor.resultImage { let values = mediaEditor.values.withUpdatedCoverDimensions(dimensions) makeEditorImageComposition(context: self.node.ciContext, postbox: self.context.account.postbox, inputImage: image, dimensions: storyDimensions, outputDimensions: dimensions.aspectFitted(CGSize(width: 1080, height: 1080)), values: values, time: .zero, textScale: 2.0, completion: { [weak self] resultImage in if let self, let resultImage { - #if DEBUG - if let data = resultImage.jpegData(compressionQuality: 0.7) { - let path = NSTemporaryDirectory() + "\(Int(Date().timeIntervalSince1970)).jpg" - try? data.write(to: URL(fileURLWithPath: path)) - } - #endif - - self.completion(MediaEditorScreenImpl.Result(media: .image(image: resultImage, dimensions: PixelDimensions(resultImage.size))), { [weak self] finished in + self.completion([MediaEditorScreenImpl.Result(media: .image(image: resultImage, dimensions: PixelDimensions(resultImage.size)))], { [weak self] finished in self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in self?.dismiss() Queue.mainQueue().justDispatch { @@ -7955,7 +7551,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID if isVideo { self.uploadSticker(file, action: .send) } else { - self.completion(MediaEditorScreenImpl.Result( + self.completion([MediaEditorScreenImpl.Result( media: .sticker(file: file, emoji: self.effectiveStickerEmoji()), mediaAreas: [], caption: NSAttributedString(), @@ -7963,7 +7559,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID options: MediaEditorResultPrivacy(sendAsPeerId: nil, privacy: EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []), timeout: 0, isForwardingDisabled: false, pin: false), stickers: [], randomId: 0 - ), { [weak self] finished in + )], { [weak self] finished in self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in self?.dismiss() Queue.mainQueue().justDispatch { @@ -8376,7 +7972,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID result = MediaEditorScreenImpl.Result() } - self.completion(result, { [weak self] finished in + self.completion([result], { [weak self] finished in self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in guard let self else { return @@ -8441,12 +8037,8 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID return } - let context = self.context + self.updateMediaEditorEntities() - 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) - let isSticker = toStickerResource != nil if !isSticker { self.previousSavedValues = mediaEditor.values @@ -8475,6 +8067,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID }) } + let context = self.context if mediaEditor.resultIsVideo { if !isSticker { mediaEditor.maybePauseVideo() @@ -8696,7 +8289,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID self.node.updateEditProgress(progress, cancel: cancel) } - fileprivate func dismissAllTooltips() { + func dismissAllTooltips() { self.window?.forEachController({ controller in if let controller = controller as? TooltipScreen { controller.dismiss() diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorStoryCompletion.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorStoryCompletion.swift new file mode 100644 index 0000000000..9c6d56a8cc --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorStoryCompletion.swift @@ -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 + 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) + } +} diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/SelectionPanelComponent.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/SelectionPanelComponent.swift index b405b061f7..b915534b4f 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/SelectionPanelComponent.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/SelectionPanelComponent.swift @@ -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 { diff --git a/submodules/TelegramUI/Components/MediaScrubberComponent/Sources/MediaScrubberComponent.swift b/submodules/TelegramUI/Components/MediaScrubberComponent/Sources/MediaScrubberComponent.swift index 3a5b90a3b3..7793a3f9ff 100644 --- a/submodules/TelegramUI/Components/MediaScrubberComponent/Sources/MediaScrubberComponent.swift +++ b/submodules/TelegramUI/Components/MediaScrubberComponent/Sources/MediaScrubberComponent.swift @@ -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() + + fileprivate var segmentTitles: [Int32: ComponentView] = [:] + 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() + 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 + + 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() + + 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 } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index c8c38b1516..764de13781 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -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() diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenAvatarSetup.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenAvatarSetup.swift index c25ad5dee1..00f309ace7 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenAvatarSetup.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenAvatarSetup.swift @@ -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() diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift index 3ca1bfee0b..9dcf4c1f25 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift @@ -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)" } } } diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift index 8b4021c46c..940c8c781b 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift @@ -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 diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift index f9aacdbe72..9d29afc635 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift @@ -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 { diff --git a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift index 5912a035b5..128e1954f2 100644 --- a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift @@ -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 diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/PriceTag.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/PriceTag.imageset/Contents.json new file mode 100644 index 0000000000..62a7cd1e1d --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/PriceTag.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "price (2).pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/PriceTag.imageset/price (2).pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/PriceTag.imageset/price (2).pdf new file mode 100644 index 0000000000..8a4812504c Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/PriceTag.imageset/price (2).pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Menu/AutoTranslate.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/Menu/AutoTranslate.imageset/Contents.json new file mode 100644 index 0000000000..3bc8fbe27a --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/Menu/AutoTranslate.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "translation.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Menu/AutoTranslate.imageset/translation.pdf b/submodules/TelegramUI/Images.xcassets/Settings/Menu/AutoTranslate.imageset/translation.pdf new file mode 100644 index 0000000000..1715735f63 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Settings/Menu/AutoTranslate.imageset/translation.pdf differ diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index d07a2bf88d..c22df4eb3e 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -977,8 +977,8 @@ private func extractAccountManagerState(records: AccountRecordsView 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 take(1) |> deliverOnMainQueue @@ -2755,7 +2755,7 @@ private func extractAccountManagerState(records: AccountRecordsView 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) })) } diff --git a/submodules/TelegramUI/Sources/ApplicationContext.swift b/submodules/TelegramUI/Sources/ApplicationContext.swift index 1f0026c3e4..f27d52dbb9 100644 --- a/submodules/TelegramUI/Sources/ApplicationContext.swift +++ b/submodules/TelegramUI/Sources/ApplicationContext.swift @@ -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)) } }) } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift index 041a84c198..33e9eee299 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift @@ -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 in + ) |> mapToSignal { isPremium, isHidden, hasAutoTranslate, counterAndTimestamp -> Signal 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) { diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift index 94670977dc..9540acbeab 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift @@ -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() diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 89a9ef7aed..723f3a53c3 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -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) { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 63b7c1c3d4..a1650a467d 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -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 } diff --git a/submodules/TelegramUI/Sources/ChatTranslationPanelNode.swift b/submodules/TelegramUI/Sources/ChatTranslationPanelNode.swift index fb7481b9f0..ae413aabd2 100644 --- a/submodules/TelegramUI/Sources/ChatTranslationPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTranslationPanelNode.swift @@ -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) } } diff --git a/submodules/TelegramUI/Sources/MakeTempAccountContext.swift b/submodules/TelegramUI/Sources/MakeTempAccountContext.swift index 89f69e5ddb..670abb783b 100644 --- a/submodules/TelegramUI/Sources/MakeTempAccountContext.swift +++ b/submodules/TelegramUI/Sources/MakeTempAccountContext.swift @@ -39,7 +39,7 @@ public func makeTempContext( firebaseSecretStream: .never(), setNotificationCall: { _ in }, - navigateToChat: { _, _, _ in + navigateToChat: { _, _, _, _ in }, displayUpgradeProgress: { _ in }, appDelegate: nil diff --git a/submodules/TelegramUI/Sources/NotificationContentContext.swift b/submodules/TelegramUI/Sources/NotificationContentContext.swift index 973a939726..5fa8c6edf8 100644 --- a/submodules/TelegramUI/Sources/NotificationContentContext.swift +++ b/submodules/TelegramUI/Sources/NotificationContentContext.swift @@ -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) } diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 498f83a202..b6c6367094 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -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 private let voipNotificationToken: Signal @@ -268,7 +268,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { private let energyUsageAutomaticDisposable = MetaDisposable() - init(mainWindow: Window1?, sharedContainerPath: String, basePath: String, encryptionParameters: ValueBoxEncryptionParameters, accountManager: AccountManager, appLockContext: AppLockContext, notificationController: NotificationContainerController?, applicationBindings: TelegramApplicationBindings, initialPresentationDataAndSettings: InitialPresentationDataAndSettings, networkArguments: NetworkInitializationArguments, hasInAppPurchases: Bool, rootPath: String, legacyBasePath: String?, apsNotificationToken: Signal, voipNotificationToken: Signal, 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, appLockContext: AppLockContext, notificationController: NotificationContainerController?, applicationBindings: TelegramApplicationBindings, initialPresentationDataAndSettings: InitialPresentationDataAndSettings, networkArguments: NetworkInitializationArguments, hasInAppPurchases: Bool, rootPath: String, legacyBasePath: String?, apsNotificationToken: Signal, voipNotificationToken: Signal, 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, 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 { diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index 3a6662cddd..077ff118fa 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -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 = 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 { diff --git a/submodules/TranslateUI/Sources/ChatTranslation.swift b/submodules/TranslateUI/Sources/ChatTranslation.swift index f6bdf92ec2..9111cbf26e 100644 --- a/submodules/TranslateUI/Sources/ChatTranslation.swift +++ b/submodules/TranslateUI/Sources/ChatTranslation.swift @@ -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) { diff --git a/submodules/Utils/DeviceModel/Sources/DeviceModel.swift b/submodules/Utils/DeviceModel/Sources/DeviceModel.swift index e74039e14e..572a05d9ae 100644 --- a/submodules/Utils/DeviceModel/Sources/DeviceModel.swift +++ b/submodules/Utils/DeviceModel/Sources/DeviceModel.swift @@ -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" diff --git a/submodules/WebUI/Sources/WebAppSecureStorageTransferScreen.swift b/submodules/WebUI/Sources/WebAppSecureStorageTransferScreen.swift index e64a909355..d3245faa72 100644 --- a/submodules/WebUI/Sources/WebAppSecureStorageTransferScreen.swift +++ b/submodules/WebUI/Sources/WebAppSecureStorageTransferScreen.swift @@ -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)),