mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 21:45:19 +00:00
Merge commit '9e0600edfa45ce017d315be9101615dc480717d8' into post-suggestion
# Conflicts: # submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift
This commit is contained in:
commit
b92293efe9
@ -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 %@.";
|
||||
|
@ -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,
|
||||
|
@ -173,8 +173,11 @@ private final class CameraContext {
|
||||
self.positionValue = configuration.position
|
||||
self._positionPromise = ValuePromise<Camera.Position>(configuration.position)
|
||||
|
||||
#if targetEnvironment(simulator)
|
||||
#else
|
||||
self.setDualCameraEnabled(configuration.isDualEnabled, change: false)
|
||||
|
||||
#endif
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(self.sessionRuntimeError),
|
||||
|
@ -67,6 +67,7 @@ public final class SheetComponent<ChildEnvironmentType: Equatable>: Component {
|
||||
public let externalState: ExternalState?
|
||||
public let animateOut: ActionSlot<Action<()>>
|
||||
public let onPan: () -> Void
|
||||
public let willDismiss: () -> Void
|
||||
|
||||
public init(
|
||||
content: AnyComponent<ChildEnvironmentType>,
|
||||
@ -76,7 +77,8 @@ public final class SheetComponent<ChildEnvironmentType: Equatable>: Component {
|
||||
isScrollEnabled: Bool = true,
|
||||
externalState: ExternalState? = nil,
|
||||
animateOut: ActionSlot<Action<()>>,
|
||||
onPan: @escaping () -> Void = {}
|
||||
onPan: @escaping () -> Void = {},
|
||||
willDismiss: @escaping () -> Void = {}
|
||||
) {
|
||||
self.content = content
|
||||
self.backgroundColor = backgroundColor
|
||||
@ -86,6 +88,7 @@ public final class SheetComponent<ChildEnvironmentType: Equatable>: Component {
|
||||
self.externalState = externalState
|
||||
self.animateOut = animateOut
|
||||
self.onPan = onPan
|
||||
self.willDismiss = willDismiss
|
||||
}
|
||||
|
||||
public static func ==(lhs: SheetComponent, rhs: SheetComponent) -> Bool {
|
||||
@ -222,6 +225,7 @@ public final class SheetComponent<ChildEnvironmentType: Equatable>: Component {
|
||||
let currentContentOffset = scrollView.contentOffset
|
||||
targetContentOffset.pointee = currentContentOffset
|
||||
if velocity.y > 300.0 {
|
||||
self.component?.willDismiss()
|
||||
self.animateOut(initialVelocity: initialVelocity, completion: {
|
||||
self.dismiss?(false)
|
||||
})
|
||||
@ -233,6 +237,7 @@ public final class SheetComponent<ChildEnvironmentType: Equatable>: Component {
|
||||
scrollView.setContentOffset(CGPoint(x: 0.0, y: scrollView.contentSize.height - scrollView.contentInset.top), animated: true)
|
||||
}
|
||||
} else {
|
||||
self.component?.willDismiss()
|
||||
self.animateOut(initialVelocity: initialVelocity, completion: {
|
||||
self.dismiss?(false)
|
||||
})
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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?()
|
||||
})
|
||||
|
@ -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";
|
||||
|
@ -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
|
||||
|
@ -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())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1702,7 +1702,7 @@ func _internal_editStoryPrivacy(account: Account, id: Int32, privacy: EngineStor
|
||||
}
|
||||
|
||||
public enum StoriesUploadAvailability {
|
||||
case available
|
||||
case available(remainingCount: Int32)
|
||||
case weeklyLimit
|
||||
case monthlyLimit
|
||||
case expiringLimit
|
||||
@ -1729,10 +1729,9 @@ func _internal_checkStoriesUploadAvailability(account: Account, target: Stories.
|
||||
|
||||
return account.network.request(Api.functions.stories.canSendStory(peer: inputPeer))
|
||||
|> map { result -> StoriesUploadAvailability in
|
||||
if result == .boolTrue {
|
||||
return .available
|
||||
} else {
|
||||
return .unknownLimit
|
||||
switch result {
|
||||
case let .canSendStoryCount(countRemains):
|
||||
return .available(remainingCount: countRemains)
|
||||
}
|
||||
}
|
||||
|> `catch` { error -> Signal<StoriesUploadAvailability, NoError> in
|
||||
|
@ -849,6 +849,11 @@ public enum BuyStarGiftError {
|
||||
case generic
|
||||
}
|
||||
|
||||
public enum UpdateStarGiftPriceError {
|
||||
case generic
|
||||
case starGiftResellTooEarly(Int32)
|
||||
}
|
||||
|
||||
public enum UpgradeStarGiftError {
|
||||
case generic
|
||||
}
|
||||
@ -963,7 +968,7 @@ func _internal_upgradeStarGift(account: Account, formId: Int64?, reference: Star
|
||||
case let .updateNewMessage(message, _, _):
|
||||
if let message = StoreMessage(apiMessage: message, accountPeerId: account.peerId, peerIsForum: false) {
|
||||
for media in message.media {
|
||||
if let action = media as? TelegramMediaAction, case let .starGiftUnique(gift, _, _, savedToProfile, canExportDate, transferStars, _, peerId, _, savedId, _) = action.action, case let .Id(messageId) = message.id {
|
||||
if let action = media as? TelegramMediaAction, case let .starGiftUnique(gift, _, _, savedToProfile, canExportDate, transferStars, _, peerId, _, savedId, _, canTransferDate, canResaleDate) = action.action, case let .Id(messageId) = message.id {
|
||||
let reference: StarGiftReference
|
||||
if let peerId, let savedId {
|
||||
reference = .peer(peerId: peerId, id: savedId)
|
||||
@ -984,7 +989,9 @@ func _internal_upgradeStarGift(account: Account, formId: Int64?, reference: Star
|
||||
canUpgrade: false,
|
||||
canExportDate: canExportDate,
|
||||
upgradeStars: nil,
|
||||
transferStars: transferStars
|
||||
transferStars: transferStars,
|
||||
canTransferDate: canTransferDate,
|
||||
canResaleDate: canResaleDate
|
||||
))
|
||||
}
|
||||
}
|
||||
@ -1475,11 +1482,18 @@ private final class ProfileGiftsContextImpl {
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
self.pushState()
|
||||
|
||||
return _internal_buyStarGift(account: self.account, slug: slug, peerId: peerId)
|
||||
}
|
||||
|
||||
func removeStarGift(gift: TelegramCore.StarGift) {
|
||||
self.gifts.removeAll(where: { $0.gift == gift })
|
||||
self.filteredGifts.removeAll(where: { $0.gift == gift })
|
||||
self.pushState()
|
||||
}
|
||||
|
||||
func upgradeStarGift(formId: Int64?, reference: StarGiftReference, keepOriginalInfo: Bool) -> Signal<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError> {
|
||||
return Signal { [weak self] subscriber in
|
||||
guard let self else {
|
||||
@ -1487,7 +1501,13 @@ private final class ProfileGiftsContextImpl {
|
||||
}
|
||||
let disposable = MetaDisposable()
|
||||
disposable.set(
|
||||
_internal_upgradeStarGift(account: self.account, formId: formId, reference: reference, keepOriginalInfo: keepOriginalInfo).startStrict(next: { [weak self] result in
|
||||
(_internal_upgradeStarGift(
|
||||
account: self.account,
|
||||
formId: formId,
|
||||
reference: reference,
|
||||
keepOriginalInfo: keepOriginalInfo
|
||||
)
|
||||
|> deliverOn(self.queue)).startStrict(next: { [weak self] result in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
@ -1509,39 +1529,89 @@ private final class ProfileGiftsContextImpl {
|
||||
}
|
||||
}
|
||||
|
||||
func updateStarGiftResellPrice(slug: String, price: Int64?) {
|
||||
self.actionDisposable.set(
|
||||
_internal_updateStarGiftResalePrice(account: self.account, slug: slug, price: price).startStrict()
|
||||
)
|
||||
|
||||
|
||||
if let index = self.gifts.firstIndex(where: { gift in
|
||||
if case let .unique(uniqueGift) = gift.gift, uniqueGift.slug == slug {
|
||||
return true
|
||||
func updateStarGiftResellPrice(reference: StarGiftReference, price: Int64?, id: Int64?) -> Signal<Never, UpdateStarGiftPriceError> {
|
||||
return Signal { [weak self] subscriber in
|
||||
guard let self else {
|
||||
return EmptyDisposable
|
||||
}
|
||||
return false
|
||||
}) {
|
||||
if case let .unique(uniqueGift) = self.gifts[index].gift {
|
||||
let updatedUniqueGift = uniqueGift.withResellStars(price)
|
||||
let updatedGift = self.gifts[index].withGift(.unique(updatedUniqueGift))
|
||||
self.gifts[index] = updatedGift
|
||||
|
||||
var saveToProfile = false
|
||||
if let gift = self.gifts.first(where: { $0.reference == reference }) {
|
||||
if !gift.savedToProfile {
|
||||
saveToProfile = true
|
||||
}
|
||||
} else if let gift = self.filteredGifts.first(where: { $0.reference == reference }) {
|
||||
if !gift.savedToProfile {
|
||||
saveToProfile = true
|
||||
}
|
||||
}
|
||||
|
||||
var signal = _internal_updateStarGiftResalePrice(account: self.account, reference: reference, price: price)
|
||||
if saveToProfile {
|
||||
signal = _internal_updateStarGiftAddedToProfile(account: self.account, reference: reference, added: true)
|
||||
|> castError(UpdateStarGiftPriceError.self)
|
||||
|> then(signal)
|
||||
}
|
||||
|
||||
let disposable = MetaDisposable()
|
||||
disposable.set(
|
||||
(signal
|
||||
|> deliverOn(self.queue)).startStrict(error: { error in
|
||||
subscriber.putError(error)
|
||||
}, completed: {
|
||||
if let index = self.gifts.firstIndex(where: { gift in
|
||||
if gift.reference == reference {
|
||||
return true
|
||||
}
|
||||
switch gift.gift {
|
||||
case .generic(let gift):
|
||||
if gift.id == id {
|
||||
return true
|
||||
}
|
||||
case .unique(let uniqueGift):
|
||||
if uniqueGift.id == id {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}) {
|
||||
if case let .unique(uniqueGift) = self.gifts[index].gift {
|
||||
let updatedUniqueGift = uniqueGift.withResellStars(price)
|
||||
let updatedGift = self.gifts[index].withGift(.unique(updatedUniqueGift)).withSavedToProfile(true)
|
||||
self.gifts[index] = updatedGift
|
||||
}
|
||||
}
|
||||
|
||||
if let index = self.filteredGifts.firstIndex(where: { gift in
|
||||
if gift.reference == reference {
|
||||
return true
|
||||
}
|
||||
switch gift.gift {
|
||||
case .generic(let gift):
|
||||
if gift.id == id {
|
||||
return true
|
||||
}
|
||||
case .unique(let uniqueGift):
|
||||
if uniqueGift.id == id {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}) {
|
||||
if case let .unique(uniqueGift) = self.filteredGifts[index].gift {
|
||||
let updatedUniqueGift = uniqueGift.withResellStars(price)
|
||||
let updatedGift = self.filteredGifts[index].withGift(.unique(updatedUniqueGift)).withSavedToProfile(true)
|
||||
self.filteredGifts[index] = updatedGift
|
||||
}
|
||||
}
|
||||
|
||||
self.pushState()
|
||||
|
||||
subscriber.putCompletion()
|
||||
})
|
||||
)
|
||||
return disposable
|
||||
}
|
||||
|
||||
if let index = self.filteredGifts.firstIndex(where: { gift in
|
||||
if case let .unique(uniqueGift) = gift.gift, uniqueGift.slug == slug {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}) {
|
||||
if case let .unique(uniqueGift) = self.filteredGifts[index].gift {
|
||||
let updatedUniqueGift = uniqueGift.withResellStars(price)
|
||||
let updatedGift = self.filteredGifts[index].withGift(.unique(updatedUniqueGift))
|
||||
self.filteredGifts[index] = updatedGift
|
||||
}
|
||||
}
|
||||
|
||||
self.pushState()
|
||||
}
|
||||
|
||||
func toggleStarGiftsNotifications(enabled: Bool) {
|
||||
@ -1638,6 +1708,8 @@ public final class ProfileGiftsContext {
|
||||
case upgradeStars
|
||||
case transferStars
|
||||
case giftAddress
|
||||
case canTransferDate
|
||||
case canResaleDate
|
||||
}
|
||||
|
||||
public let gift: TelegramCore.StarGift
|
||||
@ -1654,6 +1726,8 @@ public final class ProfileGiftsContext {
|
||||
public let canExportDate: Int32?
|
||||
public let upgradeStars: Int64?
|
||||
public let transferStars: Int64?
|
||||
public let canTransferDate: Int32?
|
||||
public let canResaleDate: Int32?
|
||||
|
||||
fileprivate let _fromPeerId: EnginePeer.Id?
|
||||
|
||||
@ -1675,7 +1749,9 @@ public final class ProfileGiftsContext {
|
||||
canUpgrade: Bool,
|
||||
canExportDate: Int32?,
|
||||
upgradeStars: Int64?,
|
||||
transferStars: Int64?
|
||||
transferStars: Int64?,
|
||||
canTransferDate: Int32?,
|
||||
canResaleDate: Int32?
|
||||
) {
|
||||
self.gift = gift
|
||||
self.reference = reference
|
||||
@ -1692,6 +1768,8 @@ public final class ProfileGiftsContext {
|
||||
self.canExportDate = canExportDate
|
||||
self.upgradeStars = upgradeStars
|
||||
self.transferStars = transferStars
|
||||
self.canTransferDate = canTransferDate
|
||||
self.canResaleDate = canResaleDate
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
@ -1718,6 +1796,8 @@ public final class ProfileGiftsContext {
|
||||
self.canExportDate = try container.decodeIfPresent(Int32.self, forKey: .canExportDate)
|
||||
self.upgradeStars = try container.decodeIfPresent(Int64.self, forKey: .upgradeStars)
|
||||
self.transferStars = try container.decodeIfPresent(Int64.self, forKey: .transferStars)
|
||||
self.canTransferDate = try container.decodeIfPresent(Int32.self, forKey: .canTransferDate)
|
||||
self.canResaleDate = try container.decodeIfPresent(Int32.self, forKey: .canResaleDate)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
@ -1737,6 +1817,8 @@ public final class ProfileGiftsContext {
|
||||
try container.encodeIfPresent(self.canExportDate, forKey: .canExportDate)
|
||||
try container.encodeIfPresent(self.upgradeStars, forKey: .upgradeStars)
|
||||
try container.encodeIfPresent(self.transferStars, forKey: .transferStars)
|
||||
try container.encodeIfPresent(self.canTransferDate, forKey: .canTransferDate)
|
||||
try container.encodeIfPresent(self.canResaleDate, forKey: .canResaleDate)
|
||||
}
|
||||
|
||||
public func withGift(_ gift: TelegramCore.StarGift) -> StarGift {
|
||||
@ -1754,7 +1836,9 @@ public final class ProfileGiftsContext {
|
||||
canUpgrade: self.canUpgrade,
|
||||
canExportDate: self.canExportDate,
|
||||
upgradeStars: self.upgradeStars,
|
||||
transferStars: self.transferStars
|
||||
transferStars: self.transferStars,
|
||||
canTransferDate: self.canTransferDate,
|
||||
canResaleDate: self.canResaleDate
|
||||
)
|
||||
}
|
||||
|
||||
@ -1773,7 +1857,9 @@ public final class ProfileGiftsContext {
|
||||
canUpgrade: self.canUpgrade,
|
||||
canExportDate: self.canExportDate,
|
||||
upgradeStars: self.upgradeStars,
|
||||
transferStars: self.transferStars
|
||||
transferStars: self.transferStars,
|
||||
canTransferDate: self.canTransferDate,
|
||||
canResaleDate: self.canResaleDate
|
||||
)
|
||||
}
|
||||
|
||||
@ -1792,7 +1878,9 @@ public final class ProfileGiftsContext {
|
||||
canUpgrade: self.canUpgrade,
|
||||
canExportDate: self.canExportDate,
|
||||
upgradeStars: self.upgradeStars,
|
||||
transferStars: self.transferStars
|
||||
transferStars: self.transferStars,
|
||||
canTransferDate: self.canTransferDate,
|
||||
canResaleDate: self.canResaleDate
|
||||
)
|
||||
}
|
||||
fileprivate func withFromPeer(_ fromPeer: EnginePeer?) -> StarGift {
|
||||
@ -1810,7 +1898,9 @@ public final class ProfileGiftsContext {
|
||||
canUpgrade: self.canUpgrade,
|
||||
canExportDate: self.canExportDate,
|
||||
upgradeStars: self.upgradeStars,
|
||||
transferStars: self.transferStars
|
||||
transferStars: self.transferStars,
|
||||
canTransferDate: self.canTransferDate,
|
||||
canResaleDate: self.canResaleDate
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1909,6 +1999,12 @@ public final class ProfileGiftsContext {
|
||||
}
|
||||
}
|
||||
|
||||
public func removeStarGift(gift: TelegramCore.StarGift) {
|
||||
self.impl.with { impl in
|
||||
impl.removeStarGift(gift: gift)
|
||||
}
|
||||
}
|
||||
|
||||
public func transferStarGift(prepaid: Bool, reference: StarGiftReference, peerId: EnginePeer.Id) -> Signal<Never, TransferStarGiftError> {
|
||||
return Signal { subscriber in
|
||||
let disposable = MetaDisposable()
|
||||
@ -1939,9 +2035,17 @@ public final class ProfileGiftsContext {
|
||||
}
|
||||
}
|
||||
|
||||
public func updateStarGiftResellPrice(slug: String, price: Int64?) {
|
||||
self.impl.with { impl in
|
||||
impl.updateStarGiftResellPrice(slug: slug, price: price)
|
||||
public func updateStarGiftResellPrice(reference: StarGiftReference, price: Int64?, id: Int64? = nil) -> Signal<Never, UpdateStarGiftPriceError> {
|
||||
return Signal { subscriber in
|
||||
let disposable = MetaDisposable()
|
||||
self.impl.with { impl in
|
||||
disposable.set(impl.updateStarGiftResellPrice(reference: reference, price: price, id: id).start(error: { error in
|
||||
subscriber.putError(error)
|
||||
}, completed: {
|
||||
subscriber.putCompletion()
|
||||
}))
|
||||
}
|
||||
return disposable
|
||||
}
|
||||
}
|
||||
|
||||
@ -1975,7 +2079,7 @@ public final class ProfileGiftsContext {
|
||||
extension ProfileGiftsContext.State.StarGift {
|
||||
init?(apiSavedStarGift: Api.SavedStarGift, peerId: EnginePeer.Id, transaction: Transaction) {
|
||||
switch apiSavedStarGift {
|
||||
case let .savedStarGift(flags, fromId, date, apiGift, message, msgId, savedId, convertStars, upgradeStars, canExportDate, transferStars):
|
||||
case let .savedStarGift(flags, fromId, date, apiGift, message, msgId, savedId, convertStars, upgradeStars, canExportDate, transferStars, canTransferAt, canResaleAt):
|
||||
guard let gift = StarGift(apiStarGift: apiGift) else {
|
||||
return nil
|
||||
}
|
||||
@ -2019,6 +2123,8 @@ extension ProfileGiftsContext.State.StarGift {
|
||||
self.canExportDate = canExportDate
|
||||
self.upgradeStars = upgradeStars
|
||||
self.transferStars = transferStars
|
||||
self.canTransferDate = canTransferAt
|
||||
self.canResaleDate = canResaleAt
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2082,10 +2188,12 @@ public enum StarGiftReference: Equatable, Hashable, Codable {
|
||||
case messageId
|
||||
case peerId
|
||||
case id
|
||||
case slug
|
||||
}
|
||||
|
||||
case message(messageId: EngineMessage.Id)
|
||||
case peer(peerId: EnginePeer.Id, id: Int64)
|
||||
case slug(slug: String)
|
||||
|
||||
public enum DecodingError: Error {
|
||||
case generic
|
||||
@ -2100,6 +2208,8 @@ public enum StarGiftReference: Equatable, Hashable, Codable {
|
||||
self = .message(messageId: try container.decode(EngineMessage.Id.self, forKey: .messageId))
|
||||
case 1:
|
||||
self = .peer(peerId: try container.decode(EnginePeer.Id.self, forKey: .peerId), id: try container.decode(Int64.self, forKey: .id))
|
||||
case 2:
|
||||
self = .slug(slug: try container.decode(String.self, forKey: .slug))
|
||||
default:
|
||||
throw DecodingError.generic
|
||||
}
|
||||
@ -2116,6 +2226,9 @@ public enum StarGiftReference: Equatable, Hashable, Codable {
|
||||
try container.encode(1 as Int32, forKey: .type)
|
||||
try container.encode(peerId, forKey: .peerId)
|
||||
try container.encode(id, forKey: .id)
|
||||
case let .slug(slug):
|
||||
try container.encode(2 as Int32, forKey: .type)
|
||||
try container.encode(slug, forKey: .slug)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2130,6 +2243,8 @@ extension StarGiftReference {
|
||||
return nil
|
||||
}
|
||||
return .inputSavedStarGiftChat(peer: inputPeer, savedId: id)
|
||||
case let .slug(slug):
|
||||
return .inputSavedStarGiftSlug(slug: slug)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2265,19 +2380,31 @@ func _internal_toggleStarGiftsNotifications(account: Account, peerId: EnginePeer
|
||||
}
|
||||
}
|
||||
|
||||
func _internal_updateStarGiftResalePrice(account: Account, slug: String, price: Int64?) -> Signal<Never, NoError> {
|
||||
return account.network.request(Api.functions.payments.updateStarGiftPrice(slug: slug, resellStars: price ?? 0))
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
|
||||
return .single(nil)
|
||||
func _internal_updateStarGiftResalePrice(account: Account, reference: StarGiftReference, price: Int64?) -> Signal<Never, UpdateStarGiftPriceError> {
|
||||
return account.postbox.transaction { transaction in
|
||||
return reference.apiStarGiftReference(transaction: transaction)
|
||||
}
|
||||
|> mapToSignal { updates -> Signal<Void, NoError> in
|
||||
if let updates {
|
||||
account.stateManager.addUpdates(updates)
|
||||
|> castError(UpdateStarGiftPriceError.self)
|
||||
|> mapToSignal { starGift in
|
||||
guard let starGift else {
|
||||
return .complete()
|
||||
}
|
||||
return .complete()
|
||||
return account.network.request(Api.functions.payments.updateStarGiftPrice(stargift: starGift, resellStars: price ?? 0))
|
||||
|> mapError { error -> UpdateStarGiftPriceError in
|
||||
if error.errorDescription.hasPrefix("STARGIFT_RESELL_TOO_EARLY_") {
|
||||
let timeout = String(error.errorDescription[error.errorDescription.index(error.errorDescription.startIndex, offsetBy: "STARGIFT_RESELL_TOO_EARLY_".count)...])
|
||||
if let value = Int32(timeout) {
|
||||
return .starGiftResellTooEarly(value)
|
||||
}
|
||||
}
|
||||
return .generic
|
||||
}
|
||||
|> mapToSignal { updates -> Signal<Void, UpdateStarGiftPriceError> in
|
||||
account.stateManager.addUpdates(updates)
|
||||
return .complete()
|
||||
}
|
||||
|> ignoreValues
|
||||
}
|
||||
|> ignoreValues
|
||||
}
|
||||
|
||||
public extension StarGift.UniqueGift {
|
||||
@ -2469,6 +2596,11 @@ private final class ResaleGiftsContextImpl {
|
||||
self.loadMore()
|
||||
}
|
||||
|
||||
func removeStarGift(gift: TelegramCore.StarGift) {
|
||||
self.gifts.removeAll(where: { $0 == gift })
|
||||
self.pushState()
|
||||
}
|
||||
|
||||
func updateSorting(_ sorting: ResaleGiftsContext.Sorting) {
|
||||
guard self.sorting != sorting else {
|
||||
return
|
||||
@ -2479,6 +2611,66 @@ private final class ResaleGiftsContextImpl {
|
||||
|
||||
self.loadMore()
|
||||
}
|
||||
|
||||
func buyStarGift(slug: String, peerId: EnginePeer.Id) -> Signal<Never, BuyStarGiftError> {
|
||||
return _internal_buyStarGift(account: self.account, slug: slug, peerId: peerId)
|
||||
|> afterCompleted { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.queue.async {
|
||||
if let count = self.count {
|
||||
self.count = max(0, count - 1)
|
||||
}
|
||||
self.gifts.removeAll(where: { gift in
|
||||
if case let .unique(uniqueGift) = gift, uniqueGift.slug == slug {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
self.pushState()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateStarGiftResellPrice(slug: String, price: Int64?) -> Signal<Never, UpdateStarGiftPriceError> {
|
||||
return Signal { [weak self] subscriber in
|
||||
guard let self else {
|
||||
return EmptyDisposable
|
||||
}
|
||||
let disposable = MetaDisposable()
|
||||
disposable.set(
|
||||
(_internal_updateStarGiftResalePrice(
|
||||
account: self.account,
|
||||
reference: .slug(slug: slug),
|
||||
price: price
|
||||
)
|
||||
|> deliverOn(self.queue)).startStrict(error: { error in
|
||||
subscriber.putError(error)
|
||||
}, completed: {
|
||||
if let index = self.gifts.firstIndex(where: { gift in
|
||||
if case let .unique(uniqueGift) = gift, uniqueGift.slug == slug {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}) {
|
||||
if let price {
|
||||
if case let .unique(uniqueGift) = self.gifts[index] {
|
||||
self.gifts[index] = .unique(uniqueGift.withResellStars(price))
|
||||
}
|
||||
} else {
|
||||
self.gifts.remove(at: index)
|
||||
}
|
||||
}
|
||||
|
||||
self.pushState()
|
||||
|
||||
subscriber.putCompletion()
|
||||
})
|
||||
)
|
||||
return disposable
|
||||
}
|
||||
}
|
||||
|
||||
private func pushState() {
|
||||
let state = ResaleGiftsContext.State(
|
||||
@ -2567,6 +2759,41 @@ public final class ResaleGiftsContext {
|
||||
impl.updateFilterAttributes(attributes)
|
||||
}
|
||||
}
|
||||
|
||||
public func buyStarGift(slug: String, peerId: EnginePeer.Id) -> Signal<Never, BuyStarGiftError> {
|
||||
return Signal { subscriber in
|
||||
let disposable = MetaDisposable()
|
||||
self.impl.with { impl in
|
||||
disposable.set(impl.buyStarGift(slug: slug, peerId: peerId).start(error: { error in
|
||||
subscriber.putError(error)
|
||||
}, completed: {
|
||||
subscriber.putCompletion()
|
||||
}))
|
||||
}
|
||||
return disposable
|
||||
}
|
||||
}
|
||||
|
||||
public func updateStarGiftResellPrice(slug: String, price: Int64?) -> Signal<Never, UpdateStarGiftPriceError> {
|
||||
return Signal { subscriber in
|
||||
let disposable = MetaDisposable()
|
||||
self.impl.with { impl in
|
||||
disposable.set(impl.updateStarGiftResellPrice(slug: slug, price: price).start(error: { error in
|
||||
subscriber.putError(error)
|
||||
}, completed: {
|
||||
subscriber.putCompletion()
|
||||
}))
|
||||
}
|
||||
return disposable
|
||||
}
|
||||
}
|
||||
|
||||
public func removeStarGift(gift: TelegramCore.StarGift) {
|
||||
self.impl.with { impl in
|
||||
impl.removeStarGift(gift: gift)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public var currentState: ResaleGiftsContext.State? {
|
||||
var state: ResaleGiftsContext.State?
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -153,8 +153,8 @@ public extension TelegramEngine {
|
||||
return _internal_toggleStarGiftsNotifications(account: self.account, peerId: peerId, enabled: enabled)
|
||||
}
|
||||
|
||||
public func updateStarGiftResalePrice(slug: String, price: Int64?) -> Signal<Never, NoError> {
|
||||
return _internal_updateStarGiftResalePrice(account: self.account, slug: slug, price: price)
|
||||
public func updateStarGiftResalePrice(reference: StarGiftReference, price: Int64?) -> Signal<Never, UpdateStarGiftPriceError> {
|
||||
return _internal_updateStarGiftResalePrice(account: self.account, reference: reference, price: price)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,19 @@
|
||||
import Foundation
|
||||
import Postbox
|
||||
import SwiftSignalKit
|
||||
import TelegramApi
|
||||
import MtProtoKit
|
||||
|
||||
func _internal_toggleAutoTranslation(account: Account, peerId: PeerId, enabled: Bool) -> Signal<Never, NoError> {
|
||||
return account.postbox.transaction { transaction -> Signal<Void, NoError> in
|
||||
if let peer = transaction.getPeer(peerId), let inputChannel = apiInputChannel(peer) {
|
||||
return account.network.request(Api.functions.channels.toggleAutotranslation(channel: inputChannel, enabled: enabled ? .boolTrue : .boolFalse)) |> `catch` { _ in .complete() } |> map { updates -> Void in
|
||||
account.stateManager.addUpdates(updates)
|
||||
}
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
|> switchToLatest
|
||||
|> ignoreValues
|
||||
}
|
@ -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 {
|
||||
|
@ -1693,6 +1693,10 @@ public extension TelegramEngine {
|
||||
let _ = _internal_removeChatManagingBot(account: self.account, chatId: chatId).startStandalone()
|
||||
}
|
||||
|
||||
public func toggleAutoTranslation(peerId: EnginePeer.Id, enabled: Bool) -> Signal<Never, NoError> {
|
||||
return _internal_toggleAutoTranslation(account: self.account, peerId: peerId, enabled: enabled)
|
||||
}
|
||||
|
||||
public func resolveMessageLink(slug: String) -> Signal<TelegramResolvedMessageLink?, NoError> {
|
||||
return self.account.network.request(Api.functions.account.resolveBusinessChatLink(slug: slug))
|
||||
|> map(Optional.init)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1959,6 +1959,7 @@ public class CameraScreenImpl: ViewController, CameraScreen {
|
||||
}
|
||||
},
|
||||
nil,
|
||||
1,
|
||||
{}
|
||||
)
|
||||
} else {
|
||||
@ -1995,6 +1996,7 @@ public class CameraScreenImpl: ViewController, CameraScreen {
|
||||
}
|
||||
},
|
||||
nil,
|
||||
self.controller?.remainingStoryCount,
|
||||
{}
|
||||
)
|
||||
}
|
||||
@ -3374,7 +3376,7 @@ public class CameraScreenImpl: ViewController, CameraScreen {
|
||||
self.transitionOut = transitionOut
|
||||
}
|
||||
}
|
||||
fileprivate let completion: (Signal<CameraScreenImpl.Result, NoError>, ResultTransition?, @escaping () -> Void) -> Void
|
||||
fileprivate let completion: (Signal<CameraScreenImpl.Result, NoError>, ResultTransition?, Int32?, @escaping () -> Void) -> Void
|
||||
public var transitionedIn: () -> Void = {}
|
||||
public var transitionedOut: () -> Void = {}
|
||||
|
||||
@ -3382,6 +3384,7 @@ public class CameraScreenImpl: ViewController, CameraScreen {
|
||||
|
||||
private let postingAvailabilityPromise = Promise<StoriesUploadAvailability>()
|
||||
private var postingAvailabilityDisposable: Disposable?
|
||||
private var remainingStoryCount: Int32?
|
||||
|
||||
private var codeDisposable: Disposable?
|
||||
private var resolveCodeDisposable: Disposable?
|
||||
@ -3419,7 +3422,7 @@ public class CameraScreenImpl: ViewController, CameraScreen {
|
||||
holder: CameraHolder? = nil,
|
||||
transitionIn: TransitionIn?,
|
||||
transitionOut: @escaping (Bool) -> TransitionOut?,
|
||||
completion: @escaping (Signal<CameraScreenImpl.Result, NoError>, ResultTransition?, @escaping () -> Void) -> Void
|
||||
completion: @escaping (Signal<CameraScreenImpl.Result, NoError>, ResultTransition?, Int32?, @escaping () -> Void) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.mode = mode
|
||||
@ -3469,7 +3472,11 @@ public class CameraScreenImpl: ViewController, CameraScreen {
|
||||
}
|
||||
self.postingAvailabilityDisposable = (self.postingAvailabilityPromise.get()
|
||||
|> deliverOnMainQueue).start(next: { [weak self] availability in
|
||||
guard let self, availability != .available else {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if case let .available(remainingCount) = availability {
|
||||
self.remainingStoryCount = remainingCount
|
||||
return
|
||||
}
|
||||
self.node.postingAvailable = false
|
||||
@ -3635,7 +3642,11 @@ public class CameraScreenImpl: ViewController, CameraScreen {
|
||||
if self.cameraState.isCollageEnabled {
|
||||
selectionLimit = 6
|
||||
} else {
|
||||
selectionLimit = 10
|
||||
if let remainingStoryCount = self.remainingStoryCount {
|
||||
selectionLimit = min(Int(remainingStoryCount), 10)
|
||||
} else {
|
||||
selectionLimit = 10
|
||||
}
|
||||
}
|
||||
}
|
||||
controller = self.context.sharedContext.makeStoryMediaPickerScreen(
|
||||
@ -3700,10 +3711,10 @@ public class CameraScreenImpl: ViewController, CameraScreen {
|
||||
)
|
||||
self.present(alertController, in: .window(.root))
|
||||
} else {
|
||||
self.completion(.single(.asset(asset)), resultTransition, dismissed)
|
||||
self.completion(.single(.asset(asset)), resultTransition, self.remainingStoryCount, dismissed)
|
||||
}
|
||||
} else if let draft = result as? MediaEditorDraft {
|
||||
self.completion(.single(.draft(draft)), resultTransition, dismissed)
|
||||
self.completion(.single(.draft(draft)), resultTransition, self.remainingStoryCount, dismissed)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3749,7 +3760,7 @@ public class CameraScreenImpl: ViewController, CameraScreen {
|
||||
}
|
||||
} else {
|
||||
if let assets = results as? [PHAsset] {
|
||||
self.completion(.single(.assets(assets)), nil, {
|
||||
self.completion(.single(.assets(assets)), nil, self.remainingStoryCount, {
|
||||
|
||||
})
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -2282,6 +2282,33 @@ struct ChatRecentActionsEntry: Comparable, Identifiable {
|
||||
|
||||
let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
|
||||
return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil))
|
||||
case let .toggleAutoTranslation(value):
|
||||
var peers = SimpleDictionary<PeerId, Peer>()
|
||||
var author: Peer?
|
||||
if let peer = self.entry.peers[self.entry.event.peerId] {
|
||||
author = peer
|
||||
peers[peer.id] = peer
|
||||
}
|
||||
var text: String = ""
|
||||
var entities: [MessageTextEntity] = []
|
||||
if value {
|
||||
appendAttributedText(text: self.presentationData.strings.Channel_AdminLog_MessageToggleAutoTranslateOn(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? ""), generateEntities: { index in
|
||||
if index == 0, let author = author {
|
||||
return [.TextMention(peerId: author.id)]
|
||||
}
|
||||
return []
|
||||
}, to: &text, entities: &entities)
|
||||
} else {
|
||||
appendAttributedText(text: self.presentationData.strings.Channel_AdminLog_MessageToggleAutoTranslateOff(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? ""), generateEntities: { index in
|
||||
if index == 0, let author = author {
|
||||
return [.TextMention(peerId: author.id)]
|
||||
}
|
||||
return []
|
||||
}, to: &text, entities: &entities)
|
||||
}
|
||||
let action = TelegramMediaActionType.customText(text: text, entities: entities, additionalAttributes: nil)
|
||||
let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
|
||||
return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
))
|
||||
}
|
||||
|
||||
|
@ -82,6 +82,7 @@ final class GiftSetupScreenComponent: Component {
|
||||
|
||||
private let navigationTitle = ComponentView<Empty>()
|
||||
private let remainingCount = ComponentView<Empty>()
|
||||
private let resaleSection = ComponentView<Empty>()
|
||||
private let introContent = ComponentView<Empty>()
|
||||
private let introSection = ComponentView<Empty>()
|
||||
private let starsSection = ComponentView<Empty>()
|
||||
@ -465,7 +466,6 @@ final class GiftSetupScreenComponent: Component {
|
||||
self.inProgress = false
|
||||
self.state?.updated()
|
||||
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||
var errorText: String?
|
||||
switch error {
|
||||
case .starGiftOutOfStock:
|
||||
@ -788,6 +788,59 @@ final class GiftSetupScreenComponent: Component {
|
||||
contentHeight += sectionSpacing
|
||||
}
|
||||
|
||||
if case let .starGift(starGift, _) = component.subject, let availability = starGift.availability, availability.resale > 0 {
|
||||
let resaleSectionSize = self.resaleSection.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(ListSectionComponent(
|
||||
theme: environment.theme,
|
||||
header: nil,
|
||||
footer: nil,
|
||||
items: [
|
||||
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent(
|
||||
theme: environment.theme,
|
||||
title: AnyComponent(VStack([
|
||||
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(
|
||||
MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: environment.strings.Gift_Send_AvailableForResale, font: Font.regular(presentationData.listsFontSize.baseDisplaySize), textColor: environment.theme.list.itemPrimaryTextColor))
|
||||
)
|
||||
)),
|
||||
], alignment: .left, spacing: 2.0)),
|
||||
accessory: .custom(ListActionItemComponent.CustomAccessory(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: presentationStringsFormattedNumber(Int32(availability.resale), environment.dateTimeFormat.groupingSeparator),
|
||||
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
|
||||
textColor: environment.theme.list.itemSecondaryTextColor
|
||||
)),
|
||||
maximumNumberOfLines: 0
|
||||
))), insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 16.0))),
|
||||
action: { [weak self] _ in
|
||||
guard let self, let component = self.component, let controller = environment.controller() else {
|
||||
return
|
||||
}
|
||||
let storeController = component.context.sharedContext.makeGiftStoreController(
|
||||
context: component.context,
|
||||
peerId: component.peerId,
|
||||
gift: starGift
|
||||
)
|
||||
controller.push(storeController)
|
||||
}
|
||||
)))
|
||||
]
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
|
||||
)
|
||||
let resaleSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: resaleSectionSize)
|
||||
if let resaleSectionView = self.resaleSection.view {
|
||||
if resaleSectionView.superview == nil {
|
||||
self.scrollView.addSubview(resaleSectionView)
|
||||
}
|
||||
transition.setFrame(view: resaleSectionView, frame: resaleSectionFrame)
|
||||
}
|
||||
contentHeight += resaleSectionSize.height
|
||||
contentHeight += sectionSpacing
|
||||
}
|
||||
|
||||
let giftConfiguration = GiftConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 })
|
||||
|
||||
var introSectionItems: [AnyComponentWithIdentity<Empty>] = []
|
||||
@ -1717,6 +1770,8 @@ public final class GiftSetupScreen: ViewControllerComponentContainer {
|
||||
|
||||
self.title = ""
|
||||
|
||||
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.context.sharedContext.currentPresentationData.with { $0 }.strings.Common_Back, style: .plain, target: nil, action: nil)
|
||||
|
||||
self.scrollToTop = { [weak self] in
|
||||
guard let self, let componentView = self.node.hostView.componentView as? GiftSetupScreenComponent.View else {
|
||||
return
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -72,7 +72,7 @@ final class GiftStoreScreenComponent: Component {
|
||||
private let loadingNode: LoadingShimmerNode
|
||||
private let emptyResultsAnimation = ComponentView<Empty>()
|
||||
private let emptyResultsTitle = ComponentView<Empty>()
|
||||
private let emptyResultsAction = ComponentView<Empty>()
|
||||
private let clearFilters = ComponentView<Empty>()
|
||||
|
||||
private let topPanel = ComponentView<Empty>()
|
||||
private let topSeparator = ComponentView<Empty>()
|
||||
@ -154,6 +154,7 @@ final class GiftStoreScreenComponent: Component {
|
||||
}
|
||||
|
||||
let availableWidth = self.scrollView.bounds.width
|
||||
let availableHeight = self.scrollView.bounds.height
|
||||
let contentOffset = self.scrollView.contentOffset.y
|
||||
|
||||
let topPanelAlpha = min(20.0, max(0.0, contentOffset)) / 20.0
|
||||
@ -213,8 +214,8 @@ final class GiftStoreScreenComponent: Component {
|
||||
font: .monospaced,
|
||||
color: ribbonColor
|
||||
)
|
||||
|
||||
let subject: GiftItemComponent.Subject = .uniqueGift(gift: uniqueGift, price: "⭐️\(uniqueGift.resellStars ?? 0)")
|
||||
|
||||
let subject: GiftItemComponent.Subject = .uniqueGift(gift: uniqueGift, price: "⭐️\(presentationStringsFormattedNumber(Int32(uniqueGift.resellStars ?? 0), environment.dateTimeFormat.groupingSeparator))")
|
||||
let _ = visibleItem.update(
|
||||
transition: itemTransition,
|
||||
component: AnyComponent(
|
||||
@ -241,7 +242,13 @@ final class GiftStoreScreenComponent: Component {
|
||||
}
|
||||
let giftController = GiftViewScreen(
|
||||
context: component.context,
|
||||
subject: .uniqueGift(uniqueGift, state.peerId)
|
||||
subject: .uniqueGift(uniqueGift, state.peerId),
|
||||
buyGift: { slug, peerId in
|
||||
return self.state?.starGiftsContext.buyStarGift(slug: slug, peerId: peerId) ?? .complete()
|
||||
},
|
||||
updateResellStars: { price in
|
||||
return self.state?.starGiftsContext.updateStarGiftResellPrice(slug: uniqueGift.slug, price: price) ?? .complete()
|
||||
}
|
||||
)
|
||||
mainController.push(giftController)
|
||||
}
|
||||
@ -288,6 +295,136 @@ final class GiftStoreScreenComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
let fadeTransition = ComponentTransition.easeInOut(duration: 0.25)
|
||||
let emptyResultsActionSize = self.clearFilters.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
PlainButtonComponent(
|
||||
content: AnyComponent(
|
||||
MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: environment.strings.Gift_Store_ClearFilters, font: Font.regular(17.0), textColor: environment.theme.list.itemAccentColor)),
|
||||
horizontalAlignment: .center,
|
||||
maximumNumberOfLines: 0
|
||||
)
|
||||
),
|
||||
effectAlignment: .center,
|
||||
action: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.state?.starGiftsContext.updateFilterAttributes([])
|
||||
},
|
||||
animateScale: false
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableWidth - 44.0 * 2.0, height: 100.0)
|
||||
)
|
||||
|
||||
var showClearFilters = false
|
||||
if let filterAttributes = self.state?.starGiftsState?.filterAttributes, !filterAttributes.isEmpty {
|
||||
showClearFilters = true
|
||||
}
|
||||
|
||||
let topInset: CGFloat = environment.navigationHeight + 39.0
|
||||
let bottomInset: CGFloat = environment.safeInsets.bottom
|
||||
|
||||
var emptyResultsActionFrame = CGRect(
|
||||
origin: CGPoint(
|
||||
x: floorToScreenPixels((availableWidth - emptyResultsActionSize.width) / 2.0),
|
||||
y: max(self.scrollView.contentSize.height - 8.0, availableHeight - bottomInset - emptyResultsActionSize.height - 16.0)
|
||||
),
|
||||
size: emptyResultsActionSize
|
||||
)
|
||||
|
||||
if let effectiveGifts = self.effectiveGifts, effectiveGifts.isEmpty && self.state?.starGiftsState?.dataState != .loading {
|
||||
let emptyAnimationHeight = 148.0
|
||||
let visibleHeight = availableHeight
|
||||
let emptyAnimationSpacing: CGFloat = 20.0
|
||||
let emptyTextSpacing: CGFloat = 18.0
|
||||
|
||||
let emptyResultsTitleSize = self.emptyResultsTitle.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: environment.strings.Gift_Store_EmptyResults, font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor)),
|
||||
horizontalAlignment: .center
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableWidth, height: 100.0)
|
||||
)
|
||||
|
||||
let emptyResultsAnimationSize = self.emptyResultsAnimation.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(LottieComponent(
|
||||
content: LottieComponent.AppBundleContent(name: "ChatListNoResults")
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: emptyAnimationHeight, height: emptyAnimationHeight)
|
||||
)
|
||||
|
||||
let emptyTotalHeight = emptyAnimationHeight + emptyAnimationSpacing + emptyResultsTitleSize.height + emptyResultsActionSize.height + emptyTextSpacing
|
||||
let emptyAnimationY = topInset + floorToScreenPixels((visibleHeight - topInset - bottomInset - emptyTotalHeight) / 2.0)
|
||||
|
||||
let emptyResultsAnimationFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableWidth - emptyResultsAnimationSize.width) / 2.0), y: emptyAnimationY), size: emptyResultsAnimationSize)
|
||||
|
||||
let emptyResultsTitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableWidth - emptyResultsTitleSize.width) / 2.0), y: emptyResultsAnimationFrame.maxY + emptyAnimationSpacing), size: emptyResultsTitleSize)
|
||||
|
||||
emptyResultsActionFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableWidth - emptyResultsActionSize.width) / 2.0), y: emptyResultsTitleFrame.maxY + emptyTextSpacing), size: emptyResultsActionSize)
|
||||
|
||||
if let view = self.emptyResultsAnimation.view as? LottieComponent.View {
|
||||
if view.superview == nil {
|
||||
view.alpha = 0.0
|
||||
fadeTransition.setAlpha(view: view, alpha: 1.0)
|
||||
self.insertSubview(view, belowSubview: self.loadingNode.view)
|
||||
view.playOnce()
|
||||
}
|
||||
view.bounds = CGRect(origin: .zero, size: emptyResultsAnimationFrame.size)
|
||||
ComponentTransition.immediate.setPosition(view: view, position: emptyResultsAnimationFrame.center)
|
||||
}
|
||||
if let view = self.emptyResultsTitle.view {
|
||||
if view.superview == nil {
|
||||
view.alpha = 0.0
|
||||
fadeTransition.setAlpha(view: view, alpha: 1.0)
|
||||
self.insertSubview(view, belowSubview: self.loadingNode.view)
|
||||
}
|
||||
view.bounds = CGRect(origin: .zero, size: emptyResultsTitleFrame.size)
|
||||
ComponentTransition.immediate.setPosition(view: view, position: emptyResultsTitleFrame.center)
|
||||
}
|
||||
} else {
|
||||
if let view = self.emptyResultsAnimation.view {
|
||||
fadeTransition.setAlpha(view: view, alpha: 0.0, completion: { _ in
|
||||
view.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
if let view = self.emptyResultsTitle.view {
|
||||
fadeTransition.setAlpha(view: view, alpha: 0.0, completion: { _ in
|
||||
view.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if showClearFilters {
|
||||
if let view = self.clearFilters.view {
|
||||
if view.superview == nil {
|
||||
view.alpha = 0.0
|
||||
fadeTransition.setAlpha(view: view, alpha: 1.0)
|
||||
self.insertSubview(view, belowSubview: self.loadingNode.view)
|
||||
}
|
||||
view.bounds = CGRect(origin: .zero, size: emptyResultsActionFrame.size)
|
||||
ComponentTransition.immediate.setPosition(view: view, position: emptyResultsActionFrame.center)
|
||||
|
||||
view.alpha = self.state?.starGiftsState?.attributes.isEmpty == true ? 0.0 : 1.0
|
||||
}
|
||||
} else {
|
||||
if let view = self.clearFilters.view {
|
||||
fadeTransition.setAlpha(view: view, alpha: 0.0, completion: { _ in
|
||||
view.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let bottomContentOffset = max(0.0, self.scrollView.contentSize.height - self.scrollView.contentOffset.y - self.scrollView.frame.height)
|
||||
if interactive, bottomContentOffset < 320.0 {
|
||||
self.state?.starGiftsContext.loadMore()
|
||||
@ -302,21 +439,21 @@ final class GiftStoreScreenComponent: Component {
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
var items: [ContextMenuItem] = []
|
||||
items.append(.action(ContextMenuActionItem(text: "Sort by Price", icon: { theme in
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_Store_SortByPrice, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Peer Info/SortValue"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] _, f in
|
||||
f(.default)
|
||||
|
||||
self?.state?.starGiftsContext.updateSorting(.value)
|
||||
})))
|
||||
items.append(.action(ContextMenuActionItem(text: "Sort by Date", icon: { theme in
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_Store_SortByDate, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Peer Info/SortDate"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] _, f in
|
||||
f(.default)
|
||||
|
||||
self?.state?.starGiftsContext.updateSorting(.date)
|
||||
})))
|
||||
items.append(.action(ContextMenuActionItem(text: "Sort by Number", icon: { theme in
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_Store_SortByNumber, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Peer Info/SortNumber"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] _, f in
|
||||
f(.default)
|
||||
@ -354,17 +491,18 @@ final class GiftStoreScreenComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
//TODO:localize
|
||||
var items: [ContextMenuItem] = []
|
||||
items.append(.custom(SearchContextItem(
|
||||
context: component.context,
|
||||
placeholder: "Search",
|
||||
value: "",
|
||||
valueChanged: { value in
|
||||
searchQueryPromise.set(value)
|
||||
}
|
||||
), false))
|
||||
items.append(.separator)
|
||||
if modelAttributes.count >= 8 {
|
||||
items.append(.custom(SearchContextItem(
|
||||
context: component.context,
|
||||
placeholder: presentationData.strings.Gift_Store_Search,
|
||||
value: "",
|
||||
valueChanged: { value in
|
||||
searchQueryPromise.set(value)
|
||||
}
|
||||
), false))
|
||||
items.append(.separator)
|
||||
}
|
||||
items.append(.custom(GiftAttributeListContextItem(
|
||||
context: component.context,
|
||||
attributes: modelAttributes,
|
||||
@ -444,17 +582,18 @@ final class GiftStoreScreenComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
//TODO:localize
|
||||
var items: [ContextMenuItem] = []
|
||||
items.append(.custom(SearchContextItem(
|
||||
context: component.context,
|
||||
placeholder: "Search",
|
||||
value: "",
|
||||
valueChanged: { value in
|
||||
searchQueryPromise.set(value)
|
||||
}
|
||||
), false))
|
||||
items.append(.separator)
|
||||
if backdropAttributes.count >= 8 {
|
||||
items.append(.custom(SearchContextItem(
|
||||
context: component.context,
|
||||
placeholder: presentationData.strings.Gift_Store_Search,
|
||||
value: "",
|
||||
valueChanged: { value in
|
||||
searchQueryPromise.set(value)
|
||||
}
|
||||
), false))
|
||||
items.append(.separator)
|
||||
}
|
||||
items.append(.custom(GiftAttributeListContextItem(
|
||||
context: component.context,
|
||||
attributes: backdropAttributes,
|
||||
@ -534,17 +673,18 @@ final class GiftStoreScreenComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
//TODO:localize
|
||||
var items: [ContextMenuItem] = []
|
||||
items.append(.custom(SearchContextItem(
|
||||
context: component.context,
|
||||
placeholder: "Search",
|
||||
value: "",
|
||||
valueChanged: { value in
|
||||
searchQueryPromise.set(value)
|
||||
}
|
||||
), false))
|
||||
items.append(.separator)
|
||||
if patternAttributes.count >= 8 {
|
||||
items.append(.custom(SearchContextItem(
|
||||
context: component.context,
|
||||
placeholder: presentationData.strings.Gift_Store_Search,
|
||||
value: "",
|
||||
valueChanged: { value in
|
||||
searchQueryPromise.set(value)
|
||||
}
|
||||
), false))
|
||||
items.append(.separator)
|
||||
}
|
||||
items.append(.custom(GiftAttributeListContextItem(
|
||||
context: component.context,
|
||||
attributes: patternAttributes,
|
||||
@ -669,35 +809,7 @@ final class GiftStoreScreenComponent: Component {
|
||||
transition.setFrame(view: topPanelView, frame: topPanelFrame)
|
||||
transition.setFrame(view: topSeparatorView, frame: topSeparatorFrame)
|
||||
}
|
||||
|
||||
// let cancelButtonSize = self.cancelButton.update(
|
||||
// transition: transition,
|
||||
// component: AnyComponent(
|
||||
// PlainButtonComponent(
|
||||
// content: AnyComponent(
|
||||
// MultilineTextComponent(
|
||||
// text: .plain(NSAttributedString(string: strings.Common_Cancel, font: Font.regular(17.0), textColor: theme.rootController.navigationBar.accentTextColor)),
|
||||
// horizontalAlignment: .center
|
||||
// )
|
||||
// ),
|
||||
// effectAlignment: .center,
|
||||
// action: {
|
||||
// controller()?.dismiss()
|
||||
// },
|
||||
// animateScale: false
|
||||
// )
|
||||
// ),
|
||||
// environment: {},
|
||||
// containerSize: CGSize(width: availableSize.width, height: 100.0)
|
||||
// )
|
||||
// let cancelButtonFrame = CGRect(origin: CGPoint(x: environment.safeInsets.left + 16.0, y: environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) / 2.0 - cancelButtonSize.height / 2.0), size: cancelButtonSize)
|
||||
// if let cancelButtonView = self.cancelButton.view {
|
||||
// if cancelButtonView.superview == nil {
|
||||
// self.addSubview(cancelButtonView)
|
||||
// }
|
||||
// transition.setFrame(view: cancelButtonView, frame: cancelButtonFrame)
|
||||
// }
|
||||
|
||||
|
||||
let balanceTitleSize = self.balanceTitle.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
@ -777,7 +889,7 @@ final class GiftStoreScreenComponent: Component {
|
||||
let subtitleSize = self.subtitle.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(BalancedTextComponent(
|
||||
text: .plain(NSAttributedString(string: "\(effectiveCount) for resale", font: Font.regular(13.0), textColor: theme.rootController.navigationBar.secondaryTextColor)),
|
||||
text: .plain(NSAttributedString(string: environment.strings.Gift_Store_ForResale(effectiveCount), font: Font.regular(13.0), textColor: theme.rootController.navigationBar.secondaryTextColor)),
|
||||
horizontalAlignment: .center,
|
||||
maximumNumberOfLines: 1
|
||||
)),
|
||||
@ -795,18 +907,18 @@ final class GiftStoreScreenComponent: Component {
|
||||
let optionSpacing: CGFloat = 10.0
|
||||
let optionWidth = (availableSize.width - sideInset * 2.0 - optionSpacing * 2.0) / 3.0
|
||||
|
||||
var sortingTitle = "Date"
|
||||
var sortingTitle = environment.strings.Gift_Store_Sort_Date
|
||||
var sortingIcon: String = "Peer Info/SortDate"
|
||||
if let sorting = self.state?.starGiftsState?.sorting {
|
||||
switch sorting {
|
||||
case .date:
|
||||
sortingTitle = "Date"
|
||||
sortingTitle = environment.strings.Gift_Store_Sort_Date
|
||||
sortingIcon = "Peer Info/SortDate"
|
||||
case .value:
|
||||
sortingTitle = "Price"
|
||||
sortingTitle = environment.strings.Gift_Store_Sort_Price
|
||||
sortingIcon = "Peer Info/SortValue"
|
||||
case .number:
|
||||
sortingTitle = "Number"
|
||||
sortingTitle = environment.strings.Gift_Store_Sort_Number
|
||||
sortingIcon = "Peer Info/SortNumber"
|
||||
}
|
||||
}
|
||||
@ -823,13 +935,13 @@ final class GiftStoreScreenComponent: Component {
|
||||
}
|
||||
))
|
||||
|
||||
var modelTitle = "Model"
|
||||
var backdropTitle = "Backdrop"
|
||||
var symbolTitle = "Symbol"
|
||||
var modelTitle = environment.strings.Gift_Store_Filter_Model
|
||||
var backdropTitle = environment.strings.Gift_Store_Filter_Backdrop
|
||||
var symbolTitle = environment.strings.Gift_Store_Filter_Symbol
|
||||
if let filterAttributes = self.state?.starGiftsState?.filterAttributes {
|
||||
var modelCount = 0
|
||||
var backdropCount = 0
|
||||
var symbolCount = 0
|
||||
var modelCount: Int32 = 0
|
||||
var backdropCount: Int32 = 0
|
||||
var symbolCount: Int32 = 0
|
||||
|
||||
for attribute in filterAttributes {
|
||||
switch attribute {
|
||||
@ -843,25 +955,13 @@ final class GiftStoreScreenComponent: Component {
|
||||
}
|
||||
|
||||
if modelCount > 0 {
|
||||
if modelCount > 1 {
|
||||
modelTitle = "\(modelCount) Models"
|
||||
} else {
|
||||
modelTitle = "1 Model"
|
||||
}
|
||||
modelTitle = environment.strings.Gift_Store_Filter_Selected_Model(modelCount)
|
||||
}
|
||||
if backdropCount > 0 {
|
||||
if backdropCount > 1 {
|
||||
backdropTitle = "\(backdropCount) Backdrops"
|
||||
} else {
|
||||
backdropTitle = "1 Backdrop"
|
||||
}
|
||||
backdropTitle = environment.strings.Gift_Store_Filter_Selected_Backdrop(modelCount)
|
||||
}
|
||||
if symbolCount > 0 {
|
||||
if symbolCount > 1 {
|
||||
symbolTitle = "\(symbolCount) Symbols"
|
||||
} else {
|
||||
symbolTitle = "1 Symbol"
|
||||
}
|
||||
symbolTitle = environment.strings.Gift_Store_Filter_Selected_Symbol(modelCount)
|
||||
}
|
||||
}
|
||||
|
||||
@ -966,118 +1066,7 @@ final class GiftStoreScreenComponent: Component {
|
||||
loadingTransition.setAlpha(view: self.loadingNode.view, alpha: 0.0)
|
||||
}
|
||||
transition.setFrame(view: self.loadingNode.view, frame: CGRect(origin: CGPoint(x: 0.0, y: environment.navigationHeight + 39.0 + 7.0), size: availableSize))
|
||||
|
||||
let fadeTransition = ComponentTransition.easeInOut(duration: 0.25)
|
||||
if let effectiveGifts = self.effectiveGifts, effectiveGifts.isEmpty && self.state?.starGiftsState?.dataState != .loading {
|
||||
let sideInset: CGFloat = 44.0
|
||||
let emptyAnimationHeight = 148.0
|
||||
let topInset: CGFloat = environment.navigationHeight + 39.0
|
||||
let bottomInset: CGFloat = environment.safeInsets.bottom
|
||||
let visibleHeight = availableSize.height
|
||||
let emptyAnimationSpacing: CGFloat = 20.0
|
||||
let emptyTextSpacing: CGFloat = 18.0
|
||||
|
||||
let emptyResultsTitleSize = self.emptyResultsTitle.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: "No Matching Gifts", font: Font.semibold(17.0), textColor: theme.list.itemPrimaryTextColor)),
|
||||
horizontalAlignment: .center
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
let emptyResultsActionSize = self.emptyResultsAction.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
PlainButtonComponent(
|
||||
content: AnyComponent(
|
||||
MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: "Clear Filters", font: Font.regular(17.0), textColor: theme.list.itemAccentColor)),
|
||||
horizontalAlignment: .center,
|
||||
maximumNumberOfLines: 0
|
||||
)
|
||||
),
|
||||
effectAlignment: .center,
|
||||
action: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.state?.starGiftsContext.updateFilterAttributes([])
|
||||
},
|
||||
animateScale: false
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: visibleHeight)
|
||||
)
|
||||
let emptyResultsAnimationSize = self.emptyResultsAnimation.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(LottieComponent(
|
||||
content: LottieComponent.AppBundleContent(name: "ChatListNoResults")
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: emptyAnimationHeight, height: emptyAnimationHeight)
|
||||
)
|
||||
|
||||
let emptyTotalHeight = emptyAnimationHeight + emptyAnimationSpacing + emptyResultsTitleSize.height + emptyResultsActionSize.height + emptyTextSpacing
|
||||
let emptyAnimationY = topInset + floorToScreenPixels((visibleHeight - topInset - bottomInset - emptyTotalHeight) / 2.0)
|
||||
|
||||
let emptyResultsAnimationFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - emptyResultsAnimationSize.width) / 2.0), y: emptyAnimationY), size: emptyResultsAnimationSize)
|
||||
|
||||
let emptyResultsTitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - emptyResultsTitleSize.width) / 2.0), y: emptyResultsAnimationFrame.maxY + emptyAnimationSpacing), size: emptyResultsTitleSize)
|
||||
|
||||
let emptyResultsActionFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - emptyResultsActionSize.width) / 2.0), y: emptyResultsTitleFrame.maxY + emptyTextSpacing), size: emptyResultsActionSize)
|
||||
|
||||
if let view = self.emptyResultsAnimation.view as? LottieComponent.View {
|
||||
if view.superview == nil {
|
||||
view.alpha = 0.0
|
||||
fadeTransition.setAlpha(view: view, alpha: 1.0)
|
||||
self.insertSubview(view, belowSubview: self.loadingNode.view)
|
||||
view.playOnce()
|
||||
}
|
||||
view.bounds = CGRect(origin: .zero, size: emptyResultsAnimationFrame.size)
|
||||
ComponentTransition.immediate.setPosition(view: view, position: emptyResultsAnimationFrame.center)
|
||||
}
|
||||
if let view = self.emptyResultsTitle.view {
|
||||
if view.superview == nil {
|
||||
view.alpha = 0.0
|
||||
fadeTransition.setAlpha(view: view, alpha: 1.0)
|
||||
self.insertSubview(view, belowSubview: self.loadingNode.view)
|
||||
}
|
||||
view.bounds = CGRect(origin: .zero, size: emptyResultsTitleFrame.size)
|
||||
ComponentTransition.immediate.setPosition(view: view, position: emptyResultsTitleFrame.center)
|
||||
}
|
||||
if let view = self.emptyResultsAction.view {
|
||||
if view.superview == nil {
|
||||
view.alpha = 0.0
|
||||
fadeTransition.setAlpha(view: view, alpha: 1.0)
|
||||
self.insertSubview(view, belowSubview: self.loadingNode.view)
|
||||
}
|
||||
view.bounds = CGRect(origin: .zero, size: emptyResultsActionFrame.size)
|
||||
ComponentTransition.immediate.setPosition(view: view, position: emptyResultsActionFrame.center)
|
||||
|
||||
view.alpha = self.state?.starGiftsState?.attributes.isEmpty == true ? 0.0 : 1.0
|
||||
}
|
||||
} else {
|
||||
if let view = self.emptyResultsAnimation.view {
|
||||
fadeTransition.setAlpha(view: view, alpha: 0.0, completion: { _ in
|
||||
view.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
if let view = self.emptyResultsTitle.view {
|
||||
fadeTransition.setAlpha(view: view, alpha: 0.0, completion: { _ in
|
||||
view.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
if let view = self.emptyResultsAction.view {
|
||||
fadeTransition.setAlpha(view: view, alpha: 0.0, completion: { _ in
|
||||
view.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
))
|
||||
|
@ -407,7 +407,6 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
||||
let mode = ContactSelectionControllerMode.starsGifting(birthdays: nil, hasActions: false, showSelf: true, selfSubtitle: presentationData.strings.Premium_Gift_ContactSelection_BuySelf)
|
||||
|
||||
//TODO:localize
|
||||
let controller = self.context.sharedContext.makeContactSelectionController(ContactSelectionControllerParams(
|
||||
context: context,
|
||||
mode: mode,
|
||||
@ -444,7 +443,19 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
self.updated()
|
||||
|
||||
self.buyDisposable = (self.buyGift(uniqueGift.slug, recipientPeerId)
|
||||
|> deliverOnMainQueue).start(completed: { [weak self, weak starsContext] in
|
||||
|> deliverOnMainQueue).start(error: { [weak self] error in
|
||||
guard let self, let controller = self.getController() else {
|
||||
return
|
||||
}
|
||||
|
||||
self.inProgress = false
|
||||
self.updated()
|
||||
|
||||
let errorText = presentationData.strings.Gift_Send_ErrorUnknown
|
||||
|
||||
let alertController = textAlertController(context: context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})], parseMarkdown: true)
|
||||
controller.present(alertController, in: .window(.root))
|
||||
}, completed: { [weak self, weak starsContext] in
|
||||
guard let self, let controller = self.getController() as? GiftViewScreen else {
|
||||
return
|
||||
}
|
||||
@ -460,40 +471,22 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
|
||||
if let navigationController = controller.navigationController as? NavigationController {
|
||||
if recipientPeerId == self.context.account.peerId {
|
||||
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId))
|
||||
|> deliverOnMainQueue).start(next: { [weak navigationController] peer in
|
||||
guard let peer, let navigationController else {
|
||||
return
|
||||
controller.dismissAnimated()
|
||||
|
||||
navigationController.view.addSubview(ConfettiView(frame: navigationController.view.bounds))
|
||||
|
||||
Queue.mainQueue().after(0.5, {
|
||||
if let lastController = navigationController.viewControllers.last as? ViewController, let animationFile {
|
||||
let resultController = UndoOverlayController(
|
||||
presentationData: presentationData,
|
||||
content: .sticker(context: context, file: animationFile, loop: false, title: presentationData.strings.Gift_View_Resale_SuccessYou_Title, text: presentationData.strings.Gift_View_Resale_SuccessYou_Text(giftTitle).string, undoText: nil, customAction: nil),
|
||||
elevatedLayout: lastController is ChatController,
|
||||
action: { _ in
|
||||
return true
|
||||
}
|
||||
)
|
||||
lastController.present(resultController, in: .window(.root))
|
||||
}
|
||||
|
||||
var controllers = Array(navigationController.viewControllers.prefix(1))
|
||||
if let controller = context.sharedContext.makePeerInfoController(
|
||||
context: context,
|
||||
updatedPresentationData: nil,
|
||||
peer: peer._asPeer(),
|
||||
mode: .myProfileGifts,
|
||||
avatarInitiallyExpanded: false,
|
||||
fromChat: false,
|
||||
requestsContext: nil
|
||||
) {
|
||||
controllers.append(controller)
|
||||
}
|
||||
navigationController.setViewControllers(controllers, animated: true)
|
||||
navigationController.view.addSubview(ConfettiView(frame: navigationController.view.bounds))
|
||||
|
||||
Queue.mainQueue().after(0.5, {
|
||||
if let lastController = navigationController.viewControllers.last as? ViewController, let animationFile {
|
||||
let resultController = UndoOverlayController(
|
||||
presentationData: presentationData,
|
||||
content: .sticker(context: context, file: animationFile, loop: false, title: "Gift Acquired", text: "\(giftTitle) is now yours.", undoText: nil, customAction: nil),
|
||||
elevatedLayout: lastController is ChatController,
|
||||
action: { _ in
|
||||
return true
|
||||
}
|
||||
)
|
||||
lastController.present(resultController, in: .window(.root))
|
||||
}
|
||||
})
|
||||
})
|
||||
} else {
|
||||
var controllers = Array(navigationController.viewControllers.prefix(1))
|
||||
@ -508,7 +501,7 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
if let peer, let lastController = navigationController?.viewControllers.last as? ViewController, let animationFile {
|
||||
let resultController = UndoOverlayController(
|
||||
presentationData: presentationData,
|
||||
content: .sticker(context: context, file: animationFile, loop: false, title: "Gift Sent", text: "\(peer.compactDisplayTitle) has been notified about your gift.", undoText: nil, customAction: nil),
|
||||
content: .sticker(context: context, file: animationFile, loop: false, title: presentationData.strings.Gift_View_Resale_Success_Title, text: presentationData.strings.Gift_View_Resale_Success_Text(peer.compactDisplayTitle).string, undoText: nil, customAction: nil),
|
||||
elevatedLayout: lastController is ChatController,
|
||||
action: { _ in
|
||||
return true
|
||||
@ -521,7 +514,6 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
}
|
||||
}
|
||||
|
||||
controller.animateSuccess()
|
||||
self.updated(transition: .spring(duration: 0.4))
|
||||
|
||||
Queue.mainQueue().after(0.5) {
|
||||
@ -884,17 +876,16 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
headerSubject = nil
|
||||
}
|
||||
|
||||
var ownerPeerId: EnginePeer.Id
|
||||
var ownerPeerId: EnginePeer.Id?
|
||||
if let uniqueGift, case let .peerId(peerId) = uniqueGift.owner {
|
||||
ownerPeerId = peerId
|
||||
} else {
|
||||
ownerPeerId = component.context.account.peerId
|
||||
}
|
||||
let wearOwnerPeerId = ownerPeerId ?? component.context.account.peerId
|
||||
|
||||
var wearPeerNameChild: _UpdatedChildComponent?
|
||||
if showWearPreview, let uniqueGift {
|
||||
var peerName = ""
|
||||
if let ownerPeer = state.peerMap[ownerPeerId] {
|
||||
if let ownerPeer = state.peerMap[wearOwnerPeerId] {
|
||||
peerName = ownerPeer.displayTitle(strings: strings, displayOrder: nameDisplayOrder)
|
||||
}
|
||||
wearPeerNameChild = wearPeerName.update(
|
||||
@ -1004,7 +995,7 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
}
|
||||
|
||||
if let wearPeerNameChild {
|
||||
if let ownerPeer = state.peerMap[ownerPeerId] {
|
||||
if let ownerPeer = state.peerMap[wearOwnerPeerId] {
|
||||
let wearAvatar = wearAvatar.update(
|
||||
component: AvatarComponent(
|
||||
context: component.context,
|
||||
@ -1488,8 +1479,7 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
|
||||
if !soldOut {
|
||||
if let uniqueGift {
|
||||
if case let .uniqueGift(_, recipientPeerIdValue) = component.subject, let _ = recipientPeerIdValue, let recipientPeerId = state.recipientPeerId {
|
||||
//TODO:localize
|
||||
if !"".isEmpty, case let .uniqueGift(_, recipientPeerIdValue) = component.subject, let _ = recipientPeerIdValue, let recipientPeerId = state.recipientPeerId {
|
||||
if let peer = state.peerMap[recipientPeerId] {
|
||||
tableItems.append(.init(
|
||||
id: "recipient",
|
||||
@ -1815,7 +1805,7 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
}
|
||||
|
||||
let canWear: Bool
|
||||
if isChannelGift, case let .channel(channel) = state.peerMap[ownerPeerId] {
|
||||
if isChannelGift, case let .channel(channel) = state.peerMap[wearOwnerPeerId] {
|
||||
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 })
|
||||
let requiredLevel = Int(BoostSubject.wearGift.requiredLevel(group: false, context: component.context, configuration: premiumConfiguration))
|
||||
if let boostLevel = channel.approximateBoostLevel {
|
||||
@ -1853,12 +1843,11 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
)
|
||||
buttonOriginX += buttonWidth + buttonSpacing
|
||||
|
||||
//TODO:localize
|
||||
let resellButton = resellButton.update(
|
||||
component: PlainButtonComponent(
|
||||
content: AnyComponent(
|
||||
HeaderButtonComponent(
|
||||
title: uniqueGift.resellStars == nil ? "sell" : "unlist",
|
||||
title: uniqueGift.resellStars == nil ? strings.Gift_View_Sell : strings.Gift_View_Unlist,
|
||||
iconName: uniqueGift.resellStars == nil ? "Premium/Collectible/Sell" : "Premium/Collectible/Unlist"
|
||||
)
|
||||
),
|
||||
@ -2232,31 +2221,30 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
if let uniqueGift {
|
||||
resellStars = uniqueGift.resellStars
|
||||
|
||||
if incoming, let resellStars {
|
||||
let priceButton = priceButton.update(
|
||||
component: PlainButtonComponent(
|
||||
content: AnyComponent(
|
||||
PriceButtonComponent(price: resellStars)
|
||||
if let resellStars {
|
||||
if incoming || ownerPeerId == component.context.account.peerId {
|
||||
let priceButton = priceButton.update(
|
||||
component: PlainButtonComponent(
|
||||
content: AnyComponent(
|
||||
PriceButtonComponent(price: presentationStringsFormattedNumber(Int32(resellStars), environment.dateTimeFormat.groupingSeparator))
|
||||
),
|
||||
effectAlignment: .center,
|
||||
action: {
|
||||
component.resellGift(true)
|
||||
},
|
||||
animateScale: false
|
||||
),
|
||||
effectAlignment: .center,
|
||||
action: {
|
||||
component.resellGift(true)
|
||||
},
|
||||
animateScale: false
|
||||
),
|
||||
availableSize: CGSize(width: 120.0, height: 30.0),
|
||||
transition: context.transition
|
||||
)
|
||||
context.add(priceButton
|
||||
.position(CGPoint(x: environment.safeInsets.left + 16.0 + priceButton.size.width / 2.0, y: 28.0))
|
||||
.appear(.default(scale: true, alpha: true))
|
||||
.disappear(.default(scale: true, alpha: true))
|
||||
)
|
||||
}
|
||||
|
||||
if !incoming, let _ = resellStars {
|
||||
availableSize: CGSize(width: 150.0, height: 30.0),
|
||||
transition: context.transition
|
||||
)
|
||||
context.add(priceButton
|
||||
.position(CGPoint(x: environment.safeInsets.left + 16.0 + priceButton.size.width / 2.0, y: 28.0))
|
||||
.appear(.default(scale: true, alpha: true))
|
||||
.disappear(.default(scale: true, alpha: true))
|
||||
)
|
||||
}
|
||||
if case let .uniqueGift(_, recipientPeerId) = component.subject, recipientPeerId != nil {
|
||||
} else {
|
||||
} else if ownerPeerId != component.context.account.peerId {
|
||||
selling = true
|
||||
}
|
||||
}
|
||||
@ -2270,14 +2258,13 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
var addressToOpen: String?
|
||||
var descriptionText: String
|
||||
if let uniqueGift, selling {
|
||||
//TODO:localize
|
||||
let ownerName: String
|
||||
if case let .peerId(peerId) = uniqueGift.owner {
|
||||
ownerName = state.peerMap[peerId]?.compactDisplayTitle ?? ""
|
||||
} else {
|
||||
ownerName = ""
|
||||
}
|
||||
descriptionText = "\(ownerName) is selling this gift and you can buy it."
|
||||
descriptionText = strings.Gift_View_SellingGiftInfo(ownerName).string
|
||||
} else if let uniqueGift, let address = uniqueGift.giftAddress, case .address = uniqueGift.owner {
|
||||
addressToOpen = address
|
||||
descriptionText = strings.Gift_View_TonGiftAddressInfo
|
||||
@ -2361,7 +2348,7 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
let requiredLevel = Int(BoostSubject.wearGift.requiredLevel(group: false, context: component.context, configuration: premiumConfiguration))
|
||||
|
||||
var canWear = true
|
||||
if isChannelGift, case let .channel(channel) = state.peerMap[ownerPeerId], (channel.approximateBoostLevel ?? 0) < requiredLevel {
|
||||
if isChannelGift, case let .channel(channel) = state.peerMap[wearOwnerPeerId], (channel.approximateBoostLevel ?? 0) < requiredLevel {
|
||||
canWear = false
|
||||
buttonContent = AnyComponentWithIdentity(
|
||||
id: AnyHashable("wear_channel"),
|
||||
@ -2421,7 +2408,7 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
if isChannelGift {
|
||||
state.levelsDisposable.set(combineLatest(
|
||||
queue: Queue.mainQueue(),
|
||||
context.engine.peers.getChannelBoostStatus(peerId: ownerPeerId),
|
||||
context.engine.peers.getChannelBoostStatus(peerId: wearOwnerPeerId),
|
||||
context.engine.peers.getMyBoostStatus()
|
||||
).startStandalone(next: { [weak controller] boostStatus, myBoostStatus in
|
||||
guard let controller, let boostStatus, let myBoostStatus else {
|
||||
@ -2429,7 +2416,7 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
}
|
||||
component.cancel(true)
|
||||
|
||||
let levelsController = context.sharedContext.makePremiumBoostLevelsController(context: context, peerId: ownerPeerId, subject: .wearGift, boostStatus: boostStatus, myBoostStatus: myBoostStatus, forceDark: false, openStats: nil)
|
||||
let levelsController = context.sharedContext.makePremiumBoostLevelsController(context: context, peerId: wearOwnerPeerId, subject: .wearGift, boostStatus: boostStatus, myBoostStatus: myBoostStatus, forceDark: false, openStats: nil)
|
||||
controller.push(levelsController)
|
||||
|
||||
HapticFeedback().impact(.light)
|
||||
@ -2574,8 +2561,7 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
if state.cachedStarImage == nil || state.cachedStarImage?.1 !== theme {
|
||||
state.cachedStarImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: theme.list.itemCheckColors.foregroundColor)!, theme)
|
||||
}
|
||||
//TODO:localize
|
||||
var upgradeString = "Buy for"
|
||||
var upgradeString = strings.Gift_View_BuyFor
|
||||
upgradeString += " # \(presentationStringsFormattedNumber(Int32(resellStars), environment.dateTimeFormat.groupingSeparator))"
|
||||
|
||||
let buttonTitle = subject.arguments?.upgradeStars != nil ? strings.Gift_Upgrade_Confirm : upgradeString
|
||||
@ -2763,6 +2749,11 @@ private final class GiftViewSheetComponent: CombinedComponent {
|
||||
if let controller = controller() as? GiftViewScreen {
|
||||
controller.dismissAllTooltips()
|
||||
}
|
||||
},
|
||||
willDismiss: {
|
||||
if let controller = controller() as? GiftViewScreen {
|
||||
controller.dismissBalanceOverlay()
|
||||
}
|
||||
}
|
||||
),
|
||||
environment: {
|
||||
@ -2829,7 +2820,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
||||
case upgradePreview([StarGift.UniqueGift.Attribute], String)
|
||||
case wearPreview(StarGift.UniqueGift)
|
||||
|
||||
var arguments: (peerId: EnginePeer.Id?, fromPeerId: EnginePeer.Id?, fromPeerName: String?, messageId: EngineMessage.Id?, reference: StarGiftReference?, incoming: Bool, gift: StarGift, date: Int32, convertStars: Int64?, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, pinnedToTop: Bool?, converted: Bool, upgraded: Bool, refunded: Bool, canUpgrade: Bool, upgradeStars: Int64?, transferStars: Int64?, resellStars: Int64?, canExportDate: Int32?, upgradeMessageId: Int32?)? {
|
||||
var arguments: (peerId: EnginePeer.Id?, fromPeerId: EnginePeer.Id?, fromPeerName: String?, messageId: EngineMessage.Id?, reference: StarGiftReference?, incoming: Bool, gift: StarGift, date: Int32, convertStars: Int64?, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, pinnedToTop: Bool?, converted: Bool, upgraded: Bool, refunded: Bool, canUpgrade: Bool, upgradeStars: Int64?, transferStars: Int64?, resellStars: Int64?, canExportDate: Int32?, upgradeMessageId: Int32?, canTransferDate: Int32?, canResaleDate: Int32?)? {
|
||||
switch self {
|
||||
case let .message(message):
|
||||
if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction {
|
||||
@ -2841,8 +2832,8 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
||||
} else {
|
||||
reference = .message(messageId: message.id)
|
||||
}
|
||||
return (message.id.peerId, senderId ?? message.author?.id, message.author?.compactDisplayTitle, message.id, reference, message.flags.contains(.Incoming), gift, message.timestamp, convertStars, text, entities, nameHidden, savedToProfile, nil, converted, upgraded, isRefunded, canUpgrade, upgradeStars, nil, nil, nil, upgradeMessageId)
|
||||
case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, _, peerId, senderId, savedId, _):
|
||||
return (message.id.peerId, senderId ?? message.author?.id, message.author?.compactDisplayTitle, message.id, reference, message.flags.contains(.Incoming), gift, message.timestamp, convertStars, text, entities, nameHidden, savedToProfile, nil, converted, upgraded, isRefunded, canUpgrade, upgradeStars, nil, nil, nil, upgradeMessageId, nil, nil)
|
||||
case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, _, peerId, senderId, savedId, _, canTransferDate, canResaleDate):
|
||||
var reference: StarGiftReference
|
||||
if let peerId, let savedId {
|
||||
reference = .peer(peerId: peerId, id: savedId)
|
||||
@ -2866,13 +2857,13 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
||||
if case let .unique(uniqueGift) = gift {
|
||||
resellStars = uniqueGift.resellStars
|
||||
}
|
||||
return (message.id.peerId, senderId ?? message.author?.id, message.author?.compactDisplayTitle, message.id, reference, incoming, gift, message.timestamp, nil, nil, nil, false, savedToProfile, nil, false, false, false, false, nil, transferStars, resellStars, canExportDate, nil)
|
||||
return (message.id.peerId, senderId ?? message.author?.id, message.author?.compactDisplayTitle, message.id, reference, incoming, gift, message.timestamp, nil, nil, nil, false, savedToProfile, nil, false, false, false, false, nil, transferStars, resellStars, canExportDate, nil, canTransferDate, canResaleDate)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
case let .uniqueGift(gift, _), let .wearPreview(gift):
|
||||
return (nil, nil, nil, nil, nil, false, .unique(gift), 0, nil, nil, nil, false, false, nil, false, false, false, false, nil, nil, gift.resellStars, nil, nil)
|
||||
return (nil, nil, nil, nil, nil, false, .unique(gift), 0, nil, nil, nil, false, false, nil, false, false, false, false, nil, nil, gift.resellStars, nil, nil, nil, nil)
|
||||
case let .profileGift(peerId, gift):
|
||||
var messageId: EngineMessage.Id?
|
||||
if case let .message(messageIdValue) = gift.reference {
|
||||
@ -2882,7 +2873,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
||||
if case let .unique(uniqueGift) = gift.gift {
|
||||
resellStars = uniqueGift.resellStars
|
||||
}
|
||||
return (peerId, gift.fromPeer?.id, gift.fromPeer?.compactDisplayTitle, messageId, gift.reference, false, gift.gift, gift.date, gift.convertStars, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, gift.pinnedToTop, false, false, false, gift.canUpgrade, gift.upgradeStars, gift.transferStars, resellStars, gift.canExportDate, nil)
|
||||
return (peerId, gift.fromPeer?.id, gift.fromPeer?.compactDisplayTitle, messageId, gift.reference, false, gift.gift, gift.date, gift.convertStars, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, gift.pinnedToTop, false, false, false, gift.canUpgrade, gift.upgradeStars, gift.transferStars, resellStars, gift.canExportDate, nil, gift.canTransferDate, gift.canResaleDate)
|
||||
case .soldOutGift:
|
||||
return nil
|
||||
case .upgradePreview:
|
||||
@ -2920,14 +2911,14 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
||||
transferGift: ((Bool, EnginePeer.Id) -> Signal<Never, TransferStarGiftError>)? = nil,
|
||||
upgradeGift: ((Int64?, Bool) -> Signal<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError>)? = nil,
|
||||
buyGift: ((String, EnginePeer.Id) -> Signal<Never, BuyStarGiftError>)? = nil,
|
||||
updateResellStars: ((Int64?) -> Void)? = nil,
|
||||
updateResellStars: ((Int64?) -> Signal<Never, UpdateStarGiftPriceError>)? = nil,
|
||||
togglePinnedToTop: ((Bool) -> Bool)? = nil,
|
||||
shareStory: ((StarGift.UniqueGift) -> Void)? = nil
|
||||
) {
|
||||
self.context = context
|
||||
self.subject = subject
|
||||
|
||||
var openPeerImpl: ((EnginePeer) -> Void)?
|
||||
var openPeerImpl: ((EnginePeer, Bool) -> Void)?
|
||||
var openAddressImpl: ((String) -> Void)?
|
||||
var copyAddressImpl: ((String) -> Void)?
|
||||
var updateSavedToProfileImpl: ((Bool) -> Void)?
|
||||
@ -2950,7 +2941,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
||||
context: context,
|
||||
subject: subject,
|
||||
openPeer: { peerId in
|
||||
openPeerImpl?(peerId)
|
||||
openPeerImpl?(peerId, false)
|
||||
},
|
||||
openAddress: { address in
|
||||
openAddressImpl?(address)
|
||||
@ -3009,21 +3000,27 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
||||
self.navigationPresentation = .flatModal
|
||||
self.automaticallyControlPresentationContextLayout = false
|
||||
|
||||
openPeerImpl = { [weak self] peer in
|
||||
openPeerImpl = { [weak self] peer, gifts in
|
||||
guard let self, let navigationController = self.navigationController as? NavigationController else {
|
||||
return
|
||||
}
|
||||
self.dismissAllTooltips()
|
||||
|
||||
let _ = (context.engine.data.get(
|
||||
TelegramEngine.EngineData.Item.Peer.Peer(id: peer.id)
|
||||
)
|
||||
|> deliverOnMainQueue).start(next: { peer in
|
||||
guard let peer else {
|
||||
return
|
||||
if gifts {
|
||||
if let controller = context.sharedContext.makePeerInfoController(
|
||||
context: context,
|
||||
updatedPresentationData: nil,
|
||||
peer: peer._asPeer(),
|
||||
mode: .gifts,
|
||||
avatarInitiallyExpanded: false,
|
||||
fromChat: false,
|
||||
requestsContext: nil
|
||||
) {
|
||||
self.push(controller)
|
||||
}
|
||||
} else {
|
||||
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, chatController: nil, context: context, chatLocation: .peer(peer), subject: nil, botStart: nil, updateTextInputState: nil, keepStack: .always, useExisting: true, purposefulAction: nil, scrollToEndIfExists: false, activateMessageSearch: nil, animated: true))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
||||
@ -3379,7 +3376,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
||||
guard let peer else {
|
||||
return
|
||||
}
|
||||
openPeerImpl?(peer)
|
||||
openPeerImpl?(peer, false)
|
||||
Queue.mainQueue().after(0.6) {
|
||||
self?.dismiss(animated: false, completion: nil)
|
||||
}
|
||||
@ -3397,54 +3394,75 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
||||
}
|
||||
|
||||
resellGiftImpl = { [weak self] update in
|
||||
guard let self, let arguments = self.subject.arguments, case let .profileGift(peerId, currentSubject) = self.subject, case let .unique(gift) = arguments.gift else {
|
||||
guard let self, let arguments = self.subject.arguments, case let .unique(gift) = arguments.gift else {
|
||||
return
|
||||
}
|
||||
|
||||
self.dismissAllTooltips()
|
||||
|
||||
//TODO:localize
|
||||
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
|
||||
if let canResaleDate = arguments.canResaleDate, currentTime < canResaleDate {
|
||||
let dateString = stringForFullDate(timestamp: canResaleDate, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat)
|
||||
let controller = textAlertController(
|
||||
context: self.context,
|
||||
title: presentationData.strings.Gift_Resale_Unavailable_Title,
|
||||
text: presentationData.strings.Gift_Resale_Unavailable_Text(dateString).string,
|
||||
actions: [
|
||||
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})
|
||||
],
|
||||
parseMarkdown: true
|
||||
)
|
||||
self.present(controller, in: .window(.root))
|
||||
return
|
||||
}
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let giftTitle = "\(gift.title) #\(presentationStringsFormattedNumber(gift.number, presentationData.dateTimeFormat.groupingSeparator))"
|
||||
let reference = arguments.reference ?? .slug(slug: gift.slug)
|
||||
|
||||
if let resellStars = gift.resellStars, resellStars > 0, !update {
|
||||
let alertController = textAlertController(
|
||||
context: context,
|
||||
title: "Unlist This Item?",
|
||||
text: "It will no longer be for sale.",
|
||||
title: presentationData.strings.Gift_View_Resale_Unlist_Title,
|
||||
text: presentationData.strings.Gift_View_Resale_Unlist_Text,
|
||||
actions: [
|
||||
TextAlertAction(type: .defaultAction, title: "Unlist", action: { [weak self] in
|
||||
TextAlertAction(type: .defaultAction, title: presentationData.strings.Gift_View_Resale_Unlist_Unlist, action: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
self.subject = .profileGift(peerId, currentSubject.withGift(.unique(gift.withResellStars(nil))))
|
||||
|
||||
let giftTitle = "\(gift.title) #\(gift.number)"
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let text = "\(giftTitle) is removed from sale."
|
||||
let tooltipController = UndoOverlayController(
|
||||
presentationData: presentationData,
|
||||
content: .universalImage(
|
||||
image: generateTintedImage(image: UIImage(bundleImageName: "Premium/Collectible/Unlist"), color: .white)!,
|
||||
size: nil,
|
||||
title: nil,
|
||||
text: text,
|
||||
customUndoText: nil,
|
||||
timeout: 3.0
|
||||
),
|
||||
position: .bottom,
|
||||
animateInAsReplacement: false,
|
||||
appearance: UndoOverlayController.Appearance(sideInset: 16.0, bottomInset: 62.0),
|
||||
action: { action in
|
||||
return false
|
||||
let _ = ((updateResellStars?(nil) ?? context.engine.payments.updateStarGiftResalePrice(reference: reference, price: nil))
|
||||
|> deliverOnMainQueue).startStandalone(error: { error in
|
||||
|
||||
}, completed: {
|
||||
switch self.subject {
|
||||
case let .profileGift(peerId, currentSubject):
|
||||
self.subject = .profileGift(peerId, currentSubject.withGift(.unique(gift.withResellStars(nil))))
|
||||
case let .uniqueGift(_, recipientPeerId):
|
||||
self.subject = .uniqueGift(gift.withResellStars(nil), recipientPeerId)
|
||||
default:
|
||||
break
|
||||
}
|
||||
)
|
||||
self.present(tooltipController, in: .window(.root))
|
||||
|
||||
if let updateResellStars {
|
||||
updateResellStars(nil)
|
||||
} else {
|
||||
let _ = (context.engine.payments.updateStarGiftResalePrice(slug: gift.slug, price: nil)
|
||||
|> deliverOnMainQueue).startStandalone()
|
||||
}
|
||||
|
||||
let text = presentationData.strings.Gift_View_Resale_Unlist_Success(giftTitle).string
|
||||
let tooltipController = UndoOverlayController(
|
||||
presentationData: presentationData,
|
||||
content: .universalImage(
|
||||
image: generateTintedImage(image: UIImage(bundleImageName: "Premium/Collectible/Unlist"), color: .white)!,
|
||||
size: nil,
|
||||
title: nil,
|
||||
text: text,
|
||||
customUndoText: nil,
|
||||
timeout: 3.0
|
||||
),
|
||||
position: .bottom,
|
||||
animateInAsReplacement: false,
|
||||
appearance: UndoOverlayController.Appearance(sideInset: 16.0, bottomInset: 62.0),
|
||||
action: { action in
|
||||
return false
|
||||
}
|
||||
)
|
||||
self.present(tooltipController, in: .window(.root))
|
||||
})
|
||||
}),
|
||||
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
|
||||
})
|
||||
@ -3458,41 +3476,48 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
||||
return
|
||||
}
|
||||
|
||||
self.subject = .profileGift(peerId, currentSubject.withGift(.unique(gift.withResellStars(price))))
|
||||
|
||||
let giftTitle = "\(gift.title) #\(gift.number)"
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
var text = "\(giftTitle) is now for sale!"
|
||||
if update {
|
||||
text = "\(giftTitle) is relisted for \(price) Stars."
|
||||
}
|
||||
|
||||
|
||||
let tooltipController = UndoOverlayController(
|
||||
presentationData: presentationData,
|
||||
content: .universalImage(
|
||||
image: generateTintedImage(image: UIImage(bundleImageName: "Premium/Collectible/Sell"), color: .white)!,
|
||||
size: nil,
|
||||
title: nil,
|
||||
text: text,
|
||||
customUndoText: nil,
|
||||
timeout: 3.0
|
||||
),
|
||||
position: .bottom,
|
||||
animateInAsReplacement: false,
|
||||
appearance: UndoOverlayController.Appearance(sideInset: 16.0, bottomInset: 62.0),
|
||||
action: { action in
|
||||
return false
|
||||
let _ = ((updateResellStars?(price) ?? context.engine.payments.updateStarGiftResalePrice(reference: reference, price: price))
|
||||
|> deliverOnMainQueue).startStandalone(error: { error in
|
||||
|
||||
}, completed: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
)
|
||||
self.present(tooltipController, in: .window(.root))
|
||||
|
||||
if let updateResellStars {
|
||||
updateResellStars(price)
|
||||
} else {
|
||||
let _ = (context.engine.payments.updateStarGiftResalePrice(slug: gift.slug, price: price)
|
||||
|> deliverOnMainQueue).startStandalone()
|
||||
}
|
||||
|
||||
switch self.subject {
|
||||
case let .profileGift(peerId, currentSubject):
|
||||
self.subject = .profileGift(peerId, currentSubject.withGift(.unique(gift.withResellStars(price))))
|
||||
case let .uniqueGift(_, recipientPeerId):
|
||||
self.subject = .uniqueGift(gift.withResellStars(price), recipientPeerId)
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
var text = presentationData.strings.Gift_View_Resale_List_Success(giftTitle).string
|
||||
if update {
|
||||
let starsString = presentationData.strings.Gift_View_Resale_Relist_Success_Stars(Int32(price))
|
||||
text = presentationData.strings.Gift_View_Resale_Relist_Success(giftTitle, starsString).string
|
||||
}
|
||||
|
||||
let tooltipController = UndoOverlayController(
|
||||
presentationData: presentationData,
|
||||
content: .universalImage(
|
||||
image: generateTintedImage(image: UIImage(bundleImageName: "Premium/Collectible/Sell"), color: .white)!,
|
||||
size: nil,
|
||||
title: nil,
|
||||
text: text,
|
||||
customUndoText: nil,
|
||||
timeout: 3.0
|
||||
),
|
||||
position: .bottom,
|
||||
animateInAsReplacement: false,
|
||||
appearance: UndoOverlayController.Appearance(sideInset: 16.0, bottomInset: 62.0),
|
||||
action: { action in
|
||||
return false
|
||||
}
|
||||
)
|
||||
self.present(tooltipController, in: .window(.root))
|
||||
})
|
||||
})
|
||||
self.push(resellController)
|
||||
}
|
||||
@ -3571,6 +3596,16 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
||||
})))
|
||||
}
|
||||
|
||||
if case let .unique(gift) = arguments.gift, let resellStars = gift.resellStars, resellStars > 0 {
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_View_Context_ChangePrice, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/PriceTag"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { c, _ in
|
||||
c?.dismiss(completion: nil)
|
||||
|
||||
resellGiftImpl?(true)
|
||||
})))
|
||||
}
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_View_Context_CopyLink, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] c, _ in
|
||||
@ -3607,6 +3642,27 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
||||
}
|
||||
}
|
||||
|
||||
if let _ = arguments.resellStars, case let .uniqueGift(uniqueGift, recipientPeerId) = subject, let _ = recipientPeerId {
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_View_Context_ViewInProfile, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Peer Info/ShowIcon"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { c, _ in
|
||||
c?.dismiss(completion: nil)
|
||||
|
||||
if case let .peerId(peerId) = uniqueGift.owner {
|
||||
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peer in
|
||||
guard let self, let peer else {
|
||||
return
|
||||
}
|
||||
openPeerImpl?(peer, true)
|
||||
Queue.mainQueue().after(0.6) {
|
||||
self.dismiss(animated: false, completion: nil)
|
||||
}
|
||||
})
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
let contextController = ContextController(presentationData: presentationData, source: .reference(GiftViewContextReferenceContentSource(controller: self, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
|
||||
self.presentInGlobalOverlay(contextController)
|
||||
})
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -909,7 +909,7 @@ public final class MediaEditorValues: Codable, Equatable {
|
||||
return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset)
|
||||
}
|
||||
|
||||
func withUpdatedVideoTrimRange(_ videoTrimRange: Range<Double>) -> MediaEditorValues {
|
||||
public func withUpdatedVideoTrimRange(_ videoTrimRange: Range<Double>) -> MediaEditorValues {
|
||||
return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,837 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AVFoundation
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import TextFormat
|
||||
import Photos
|
||||
import MediaEditor
|
||||
import DrawingUI
|
||||
|
||||
extension MediaEditorScreenImpl {
|
||||
func requestStoryCompletion(animated: Bool) {
|
||||
guard let mediaEditor = self.node.mediaEditor, !self.didComplete else {
|
||||
return
|
||||
}
|
||||
|
||||
self.didComplete = true
|
||||
|
||||
self.updateMediaEditorEntities()
|
||||
|
||||
mediaEditor.stop()
|
||||
mediaEditor.invalidate()
|
||||
self.node.entitiesView.invalidate()
|
||||
|
||||
if let navigationController = self.navigationController as? NavigationController {
|
||||
navigationController.updateRootContainerTransitionOffset(0.0, transition: .immediate)
|
||||
}
|
||||
|
||||
var multipleItems: [EditingItem] = []
|
||||
var isLongVideo = false
|
||||
if self.node.items.count > 1 {
|
||||
multipleItems = self.node.items.filter({ $0.isEnabled })
|
||||
} else if case let .asset(asset) = self.node.subject {
|
||||
let duration: Double
|
||||
if let playerDuration = mediaEditor.duration {
|
||||
duration = playerDuration
|
||||
} else {
|
||||
duration = asset.duration
|
||||
}
|
||||
if duration > storyMaxVideoDuration {
|
||||
let originalDuration = mediaEditor.originalDuration ?? asset.duration
|
||||
let values = mediaEditor.values
|
||||
|
||||
let storyCount = min(storyMaxCombinedVideoCount, Int(ceil(duration / storyMaxVideoDuration)))
|
||||
var start = values.videoTrimRange?.lowerBound ?? 0
|
||||
for i in 0 ..< storyCount {
|
||||
let trimmedValues = values.withUpdatedVideoTrimRange(start ..< min(start + storyMaxVideoDuration, originalDuration))
|
||||
|
||||
var editingItem = EditingItem(asset: asset)
|
||||
if i == 0 {
|
||||
editingItem.caption = self.node.getCaption()
|
||||
}
|
||||
editingItem.values = trimmedValues
|
||||
multipleItems.append(editingItem)
|
||||
|
||||
start += storyMaxVideoDuration
|
||||
}
|
||||
isLongVideo = true
|
||||
}
|
||||
}
|
||||
|
||||
if multipleItems.count > 1 {
|
||||
self.processMultipleItems(items: multipleItems, isLongVideo: isLongVideo)
|
||||
} else {
|
||||
self.processSingleItem()
|
||||
}
|
||||
|
||||
self.dismissAllTooltips()
|
||||
}
|
||||
|
||||
private func processSingleItem() {
|
||||
guard let mediaEditor = self.node.mediaEditor, let subject = self.node.subject, let actualSubject = self.node.actualSubject else {
|
||||
return
|
||||
}
|
||||
|
||||
var caption = self.node.getCaption()
|
||||
caption = convertMarkdownToAttributes(caption)
|
||||
|
||||
var hasEntityChanges = false
|
||||
let randomId: Int64
|
||||
if case let .draft(_, id) = actualSubject, let id {
|
||||
randomId = id
|
||||
} else {
|
||||
randomId = Int64.random(in: .min ... .max)
|
||||
}
|
||||
|
||||
let codableEntities = mediaEditor.values.entities
|
||||
var mediaAreas: [MediaArea] = []
|
||||
if case let .draft(draft, _) = actualSubject {
|
||||
if draft.values.entities != codableEntities {
|
||||
hasEntityChanges = true
|
||||
}
|
||||
} else {
|
||||
mediaAreas = self.initialMediaAreas ?? []
|
||||
}
|
||||
|
||||
var stickers: [TelegramMediaFile] = []
|
||||
for entity in codableEntities {
|
||||
switch entity {
|
||||
case let .sticker(stickerEntity):
|
||||
if case let .file(file, fileType) = stickerEntity.content, case .sticker = fileType {
|
||||
stickers.append(file.media)
|
||||
}
|
||||
case let .text(textEntity):
|
||||
if let subEntities = textEntity.renderSubEntities {
|
||||
for entity in subEntities {
|
||||
if let stickerEntity = entity as? DrawingStickerEntity, case let .file(file, fileType) = stickerEntity.content, case .sticker = fileType {
|
||||
stickers.append(file.media)
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
if let mediaArea = entity.mediaArea {
|
||||
mediaAreas.append(mediaArea)
|
||||
}
|
||||
}
|
||||
|
||||
var hasAnyChanges = self.node.hasAnyChanges
|
||||
if self.isEditingStoryCover {
|
||||
hasAnyChanges = false
|
||||
}
|
||||
|
||||
if self.isEmbeddedEditor && !(hasAnyChanges || hasEntityChanges) {
|
||||
self.saveDraft(id: randomId, isEdit: true)
|
||||
|
||||
self.completion([MediaEditorScreenImpl.Result(media: nil, mediaAreas: [], caption: caption, coverTimestamp: mediaEditor.values.coverImageTimestamp, options: self.state.privacy, stickers: stickers, randomId: randomId)], { [weak self] finished in
|
||||
self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in
|
||||
self?.dismiss()
|
||||
Queue.mainQueue().justDispatch {
|
||||
finished()
|
||||
}
|
||||
})
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if !(self.isEditingStory || self.isEditingStoryCover) {
|
||||
let privacy = self.state.privacy
|
||||
let _ = updateMediaEditorStoredStateInteractively(engine: self.context.engine, { current in
|
||||
if let current {
|
||||
return current.withUpdatedPrivacy(privacy)
|
||||
} else {
|
||||
return MediaEditorStoredState(privacy: privacy, textSettings: nil)
|
||||
}
|
||||
}).start()
|
||||
}
|
||||
|
||||
if mediaEditor.resultIsVideo {
|
||||
self.saveDraft(id: randomId)
|
||||
|
||||
var firstFrame: Signal<(UIImage?, UIImage?), NoError>
|
||||
let firstFrameTime: CMTime
|
||||
if let coverImageTimestamp = mediaEditor.values.coverImageTimestamp {
|
||||
firstFrameTime = CMTime(seconds: coverImageTimestamp, preferredTimescale: CMTimeScale(60))
|
||||
} else {
|
||||
firstFrameTime = CMTime(seconds: mediaEditor.values.videoTrimRange?.lowerBound ?? 0.0, preferredTimescale: CMTimeScale(60))
|
||||
}
|
||||
let videoResult: Signal<MediaResult.VideoResult, NoError>
|
||||
var videoIsMirrored = false
|
||||
let duration: Double
|
||||
switch subject {
|
||||
case let .empty(dimensions):
|
||||
let image = generateImage(dimensions.cgSize, opaque: false, scale: 1.0, rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: .zero, size: size))
|
||||
})!
|
||||
let tempImagePath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).jpg"
|
||||
if let data = image.jpegData(compressionQuality: 0.85) {
|
||||
try? data.write(to: URL(fileURLWithPath: tempImagePath))
|
||||
}
|
||||
videoResult = .single(.imageFile(path: tempImagePath))
|
||||
duration = 3.0
|
||||
|
||||
firstFrame = .single((image, nil))
|
||||
case let .image(image, _, _, _):
|
||||
let tempImagePath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).jpg"
|
||||
if let data = image.jpegData(compressionQuality: 0.85) {
|
||||
try? data.write(to: URL(fileURLWithPath: tempImagePath))
|
||||
}
|
||||
videoResult = .single(.imageFile(path: tempImagePath))
|
||||
duration = 5.0
|
||||
|
||||
firstFrame = .single((image, nil))
|
||||
case let .video(path, _, mirror, additionalPath, _, _, durationValue, _, _):
|
||||
videoIsMirrored = mirror
|
||||
videoResult = .single(.videoFile(path: path))
|
||||
if let videoTrimRange = mediaEditor.values.videoTrimRange {
|
||||
duration = videoTrimRange.upperBound - videoTrimRange.lowerBound
|
||||
} else {
|
||||
duration = durationValue
|
||||
}
|
||||
|
||||
var additionalPath = additionalPath
|
||||
if additionalPath == nil, let valuesAdditionalPath = mediaEditor.values.additionalVideoPath {
|
||||
additionalPath = valuesAdditionalPath
|
||||
}
|
||||
|
||||
firstFrame = Signal<(UIImage?, UIImage?), NoError> { subscriber in
|
||||
let avAsset = AVURLAsset(url: URL(fileURLWithPath: path))
|
||||
let avAssetGenerator = AVAssetImageGenerator(asset: avAsset)
|
||||
avAssetGenerator.appliesPreferredTrackTransform = true
|
||||
avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)], completionHandler: { _, cgImage, _, _, _ in
|
||||
if let cgImage {
|
||||
if let additionalPath {
|
||||
let avAsset = AVURLAsset(url: URL(fileURLWithPath: additionalPath))
|
||||
let avAssetGenerator = AVAssetImageGenerator(asset: avAsset)
|
||||
avAssetGenerator.appliesPreferredTrackTransform = true
|
||||
avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)], completionHandler: { _, additionalCGImage, _, _, _ in
|
||||
if let additionalCGImage {
|
||||
subscriber.putNext((UIImage(cgImage: cgImage), UIImage(cgImage: additionalCGImage)))
|
||||
subscriber.putCompletion()
|
||||
} else {
|
||||
subscriber.putNext((UIImage(cgImage: cgImage), nil))
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
subscriber.putNext((UIImage(cgImage: cgImage), nil))
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
}
|
||||
})
|
||||
return ActionDisposable {
|
||||
avAssetGenerator.cancelAllCGImageGeneration()
|
||||
}
|
||||
}
|
||||
case let .videoCollage(items):
|
||||
var maxDurationItem: (Double, Subject.VideoCollageItem)?
|
||||
for item in items {
|
||||
switch item.content {
|
||||
case .image:
|
||||
break
|
||||
case let .video(_, duration):
|
||||
if let (maxDuration, _) = maxDurationItem {
|
||||
if duration > maxDuration {
|
||||
maxDurationItem = (duration, item)
|
||||
}
|
||||
} else {
|
||||
maxDurationItem = (duration, item)
|
||||
}
|
||||
case let .asset(asset):
|
||||
if let (maxDuration, _) = maxDurationItem {
|
||||
if asset.duration > maxDuration {
|
||||
maxDurationItem = (asset.duration, item)
|
||||
}
|
||||
} else {
|
||||
maxDurationItem = (asset.duration, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
guard let (maxDuration, mainItem) = maxDurationItem else {
|
||||
fatalError()
|
||||
}
|
||||
switch mainItem.content {
|
||||
case let .video(path, _):
|
||||
videoResult = .single(.videoFile(path: path))
|
||||
case let .asset(asset):
|
||||
videoResult = .single(.asset(localIdentifier: asset.localIdentifier))
|
||||
default:
|
||||
fatalError()
|
||||
}
|
||||
let image = generateImage(storyDimensions, opaque: false, scale: 1.0, rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: .zero, size: size))
|
||||
})!
|
||||
firstFrame = .single((image, nil))
|
||||
if let videoTrimRange = mediaEditor.values.videoTrimRange {
|
||||
duration = videoTrimRange.upperBound - videoTrimRange.lowerBound
|
||||
} else {
|
||||
duration = min(maxDuration, storyMaxVideoDuration)
|
||||
}
|
||||
case let .asset(asset):
|
||||
videoResult = .single(.asset(localIdentifier: asset.localIdentifier))
|
||||
if asset.mediaType == .video {
|
||||
if let videoTrimRange = mediaEditor.values.videoTrimRange {
|
||||
duration = videoTrimRange.upperBound - videoTrimRange.lowerBound
|
||||
} else {
|
||||
duration = min(asset.duration, storyMaxVideoDuration)
|
||||
}
|
||||
} else {
|
||||
duration = 5.0
|
||||
}
|
||||
|
||||
var additionalPath: String?
|
||||
if let valuesAdditionalPath = mediaEditor.values.additionalVideoPath {
|
||||
additionalPath = valuesAdditionalPath
|
||||
}
|
||||
|
||||
firstFrame = Signal<(UIImage?, UIImage?), NoError> { subscriber in
|
||||
if asset.mediaType == .video {
|
||||
PHImageManager.default().requestAVAsset(forVideo: asset, options: nil) { avAsset, _, _ in
|
||||
if let avAsset {
|
||||
let avAssetGenerator = AVAssetImageGenerator(asset: avAsset)
|
||||
avAssetGenerator.appliesPreferredTrackTransform = true
|
||||
avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)], completionHandler: { _, cgImage, _, _, _ in
|
||||
if let cgImage {
|
||||
if let additionalPath {
|
||||
let avAsset = AVURLAsset(url: URL(fileURLWithPath: additionalPath))
|
||||
let avAssetGenerator = AVAssetImageGenerator(asset: avAsset)
|
||||
avAssetGenerator.appliesPreferredTrackTransform = true
|
||||
avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)], completionHandler: { _, additionalCGImage, _, _, _ in
|
||||
if let additionalCGImage {
|
||||
subscriber.putNext((UIImage(cgImage: cgImage), UIImage(cgImage: additionalCGImage)))
|
||||
subscriber.putCompletion()
|
||||
} else {
|
||||
subscriber.putNext((UIImage(cgImage: cgImage), nil))
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
subscriber.putNext((UIImage(cgImage: cgImage), nil))
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let options = PHImageRequestOptions()
|
||||
options.deliveryMode = .highQualityFormat
|
||||
PHImageManager.default().requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .default, options: options) { image, _ in
|
||||
if let image {
|
||||
if let additionalPath {
|
||||
let avAsset = AVURLAsset(url: URL(fileURLWithPath: additionalPath))
|
||||
let avAssetGenerator = AVAssetImageGenerator(asset: avAsset)
|
||||
avAssetGenerator.appliesPreferredTrackTransform = true
|
||||
avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)], completionHandler: { _, additionalCGImage, _, _, _ in
|
||||
if let additionalCGImage {
|
||||
subscriber.putNext((image, UIImage(cgImage: additionalCGImage)))
|
||||
subscriber.putCompletion()
|
||||
} else {
|
||||
subscriber.putNext((image, nil))
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
subscriber.putNext((image, nil))
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return EmptyDisposable
|
||||
}
|
||||
case let .draft(draft, _):
|
||||
let draftPath = draft.fullPath(engine: context.engine)
|
||||
if draft.isVideo {
|
||||
videoResult = .single(.videoFile(path: draftPath))
|
||||
if let videoTrimRange = mediaEditor.values.videoTrimRange {
|
||||
duration = videoTrimRange.upperBound - videoTrimRange.lowerBound
|
||||
} else {
|
||||
duration = min(draft.duration ?? 5.0, storyMaxVideoDuration)
|
||||
}
|
||||
firstFrame = Signal<(UIImage?, UIImage?), NoError> { subscriber in
|
||||
let avAsset = AVURLAsset(url: URL(fileURLWithPath: draftPath))
|
||||
let avAssetGenerator = AVAssetImageGenerator(asset: avAsset)
|
||||
avAssetGenerator.appliesPreferredTrackTransform = true
|
||||
avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)], completionHandler: { _, cgImage, _, _, _ in
|
||||
if let cgImage {
|
||||
subscriber.putNext((UIImage(cgImage: cgImage), nil))
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
})
|
||||
return ActionDisposable {
|
||||
avAssetGenerator.cancelAllCGImageGeneration()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
videoResult = .single(.imageFile(path: draftPath))
|
||||
duration = 5.0
|
||||
|
||||
if let image = UIImage(contentsOfFile: draftPath) {
|
||||
firstFrame = .single((image, nil))
|
||||
} else {
|
||||
firstFrame = .single((UIImage(), nil))
|
||||
}
|
||||
}
|
||||
case .message, .gift:
|
||||
let peerId: EnginePeer.Id
|
||||
if case let .message(messageIds) = subject {
|
||||
peerId = messageIds.first!.peerId
|
||||
} else {
|
||||
peerId = self.context.account.peerId
|
||||
}
|
||||
|
||||
let isNightTheme = mediaEditor.values.nightTheme
|
||||
let wallpaper = getChatWallpaperImage(context: self.context, peerId: peerId)
|
||||
|> map { _, image, nightImage -> UIImage? in
|
||||
if isNightTheme {
|
||||
return nightImage ?? image
|
||||
} else {
|
||||
return image
|
||||
}
|
||||
}
|
||||
|
||||
videoResult = wallpaper
|
||||
|> mapToSignal { image in
|
||||
if let image {
|
||||
let tempImagePath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).jpg"
|
||||
if let data = image.jpegData(compressionQuality: 0.85) {
|
||||
try? data.write(to: URL(fileURLWithPath: tempImagePath))
|
||||
}
|
||||
return .single(.imageFile(path: tempImagePath))
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
|
||||
firstFrame = wallpaper
|
||||
|> map { image in
|
||||
return (image, nil)
|
||||
}
|
||||
duration = 5.0
|
||||
case .sticker:
|
||||
let image = generateImage(storyDimensions, contextGenerator: { size, context in
|
||||
context.clear(CGRect(origin: .zero, size: size))
|
||||
}, opaque: false, scale: 1.0)
|
||||
let tempImagePath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).png"
|
||||
if let data = image?.pngData() {
|
||||
try? data.write(to: URL(fileURLWithPath: tempImagePath))
|
||||
}
|
||||
videoResult = .single(.imageFile(path: tempImagePath))
|
||||
duration = 3.0
|
||||
|
||||
firstFrame = .single((image, nil))
|
||||
case .assets:
|
||||
fatalError()
|
||||
}
|
||||
|
||||
let _ = combineLatest(queue: Queue.mainQueue(), firstFrame, videoResult)
|
||||
.start(next: { [weak self] images, videoResult in
|
||||
if let self {
|
||||
let (image, additionalImage) = images
|
||||
var currentImage = mediaEditor.resultImage
|
||||
if let image {
|
||||
mediaEditor.replaceSource(image, additionalImage: additionalImage, time: firstFrameTime, mirror: true)
|
||||
if let updatedImage = mediaEditor.getResultImage(mirror: videoIsMirrored) {
|
||||
currentImage = updatedImage
|
||||
}
|
||||
}
|
||||
|
||||
var inputImage: UIImage
|
||||
if let currentImage {
|
||||
inputImage = currentImage
|
||||
} else if let image {
|
||||
inputImage = image
|
||||
} else {
|
||||
inputImage = UIImage()
|
||||
}
|
||||
|
||||
var values = mediaEditor.values
|
||||
if case .avatarEditor = self.mode, values.videoTrimRange == nil && duration > avatarMaxVideoDuration {
|
||||
values = values.withUpdatedVideoTrimRange(0 ..< avatarMaxVideoDuration)
|
||||
}
|
||||
|
||||
makeEditorImageComposition(context: self.node.ciContext, postbox: self.context.account.postbox, inputImage: inputImage, dimensions: storyDimensions, values: values, time: firstFrameTime, textScale: 2.0, completion: { [weak self] coverImage in
|
||||
if let self {
|
||||
self.willComplete(coverImage, true, { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
Logger.shared.log("MediaEditor", "Completed with video \(videoResult)")
|
||||
self.completion([MediaEditorScreenImpl.Result(media: .video(video: videoResult, coverImage: coverImage, values: values, duration: duration, dimensions: values.resultDimensions), mediaAreas: mediaAreas, caption: caption, coverTimestamp: values.coverImageTimestamp, options: self.state.privacy, stickers: stickers, randomId: randomId)], { [weak self] finished in
|
||||
self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in
|
||||
self?.dismiss()
|
||||
Queue.mainQueue().justDispatch {
|
||||
finished()
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
if case let .draft(draft, id) = actualSubject, id == nil {
|
||||
removeStoryDraft(engine: self.context.engine, path: draft.path, delete: false)
|
||||
}
|
||||
} else if let image = mediaEditor.resultImage {
|
||||
self.saveDraft(id: randomId)
|
||||
|
||||
var values = mediaEditor.values
|
||||
var outputDimensions: CGSize?
|
||||
if case .avatarEditor = self.mode {
|
||||
outputDimensions = CGSize(width: 640.0, height: 640.0)
|
||||
values = values.withUpdatedQualityPreset(.profile)
|
||||
}
|
||||
makeEditorImageComposition(
|
||||
context: self.node.ciContext,
|
||||
postbox: self.context.account.postbox,
|
||||
inputImage: image,
|
||||
dimensions: storyDimensions,
|
||||
outputDimensions: outputDimensions,
|
||||
values: values,
|
||||
time: .zero,
|
||||
textScale: 2.0,
|
||||
completion: { [weak self] resultImage in
|
||||
if let self, let resultImage {
|
||||
self.willComplete(resultImage, false, { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
Logger.shared.log("MediaEditor", "Completed with image \(resultImage)")
|
||||
self.completion([MediaEditorScreenImpl.Result(media: .image(image: resultImage, dimensions: PixelDimensions(resultImage.size)), mediaAreas: mediaAreas, caption: caption, coverTimestamp: nil, options: self.state.privacy, stickers: stickers, randomId: randomId)], { [weak self] finished in
|
||||
self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in
|
||||
self?.dismiss()
|
||||
Queue.mainQueue().justDispatch {
|
||||
finished()
|
||||
}
|
||||
})
|
||||
})
|
||||
if case let .draft(draft, id) = actualSubject, id == nil {
|
||||
removeStoryDraft(engine: self.context.engine, path: draft.path, delete: true)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private func processMultipleItems(items: [EditingItem], isLongVideo: Bool) {
|
||||
guard !items.isEmpty else {
|
||||
return
|
||||
}
|
||||
|
||||
var items = items
|
||||
if let mediaEditor = self.node.mediaEditor, case let .asset(asset) = self.node.subject, let currentItemIndex = items.firstIndex(where: { $0.asset.localIdentifier == asset.localIdentifier }) {
|
||||
var updatedCurrentItem = items[currentItemIndex]
|
||||
updatedCurrentItem.caption = self.node.getCaption()
|
||||
updatedCurrentItem.values = mediaEditor.values
|
||||
items[currentItemIndex] = updatedCurrentItem
|
||||
}
|
||||
|
||||
let multipleResults = Atomic<[MediaEditorScreenImpl.Result]>(value: [])
|
||||
let totalItems = items.count
|
||||
|
||||
let dispatchGroup = DispatchGroup()
|
||||
|
||||
let privacy = self.state.privacy
|
||||
|
||||
if !(self.isEditingStory || self.isEditingStoryCover) {
|
||||
let _ = updateMediaEditorStoredStateInteractively(engine: self.context.engine, { current in
|
||||
if let current {
|
||||
return current.withUpdatedPrivacy(privacy)
|
||||
} else {
|
||||
return MediaEditorStoredState(privacy: privacy, textSettings: nil)
|
||||
}
|
||||
}).start()
|
||||
}
|
||||
|
||||
var order: [Int64] = []
|
||||
for (index, item) in items.enumerated() {
|
||||
guard item.isEnabled else {
|
||||
continue
|
||||
}
|
||||
|
||||
dispatchGroup.enter()
|
||||
|
||||
let randomId = Int64.random(in: .min ... .max)
|
||||
order.append(randomId)
|
||||
|
||||
if item.asset.mediaType == .video {
|
||||
processVideoItem(item: item, index: index, randomId: randomId, isLongVideo: isLongVideo) { result in
|
||||
let _ = multipleResults.modify { results in
|
||||
var updatedResults = results
|
||||
updatedResults.append(result)
|
||||
return updatedResults
|
||||
}
|
||||
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
} else if item.asset.mediaType == .image {
|
||||
processImageItem(item: item, index: index, randomId: randomId) { result in
|
||||
let _ = multipleResults.modify { results in
|
||||
var updatedResults = results
|
||||
updatedResults.append(result)
|
||||
return updatedResults
|
||||
}
|
||||
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
} else {
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
}
|
||||
|
||||
dispatchGroup.notify(queue: .main) {
|
||||
let results = multipleResults.with { $0 }
|
||||
if results.count == totalItems {
|
||||
var orderedResults: [MediaEditorScreenImpl.Result] = []
|
||||
for id in order {
|
||||
if let item = results.first(where: { $0.randomId == id }) {
|
||||
orderedResults.append(item)
|
||||
}
|
||||
}
|
||||
self.completion(results, { [weak self] finished in
|
||||
self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in
|
||||
self?.dismiss()
|
||||
Queue.mainQueue().justDispatch {
|
||||
finished()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func processVideoItem(item: EditingItem, index: Int, randomId: Int64, isLongVideo: Bool, completion: @escaping (MediaEditorScreenImpl.Result) -> Void) {
|
||||
let asset = item.asset
|
||||
|
||||
let itemMediaEditor = setupMediaEditorForItem(item: item)
|
||||
|
||||
var caption = item.caption
|
||||
caption = convertMarkdownToAttributes(caption)
|
||||
|
||||
var mediaAreas: [MediaArea] = []
|
||||
var stickers: [TelegramMediaFile] = []
|
||||
|
||||
if let entities = item.values?.entities {
|
||||
for entity in entities {
|
||||
if let mediaArea = entity.mediaArea {
|
||||
mediaAreas.append(mediaArea)
|
||||
}
|
||||
extractStickersFromEntity(entity, into: &stickers)
|
||||
}
|
||||
}
|
||||
|
||||
let firstFrameTime: CMTime
|
||||
if let coverImageTimestamp = item.values?.coverImageTimestamp, !isLongVideo || index == 0 {
|
||||
firstFrameTime = CMTime(seconds: coverImageTimestamp, preferredTimescale: CMTimeScale(60))
|
||||
} else {
|
||||
firstFrameTime = CMTime(seconds: item.values?.videoTrimRange?.lowerBound ?? 0.0, preferredTimescale: CMTimeScale(60))
|
||||
}
|
||||
|
||||
PHImageManager.default().requestAVAsset(forVideo: asset, options: nil) { [weak self] avAsset, _, _ in
|
||||
guard let avAsset else {
|
||||
DispatchQueue.main.async {
|
||||
if let self {
|
||||
completion(self.createEmptyResult(randomId: randomId))
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let duration: Double
|
||||
if let videoTrimRange = item.values?.videoTrimRange {
|
||||
duration = videoTrimRange.upperBound - videoTrimRange.lowerBound
|
||||
} else {
|
||||
duration = min(asset.duration, storyMaxVideoDuration)
|
||||
}
|
||||
|
||||
let avAssetGenerator = AVAssetImageGenerator(asset: avAsset)
|
||||
avAssetGenerator.appliesPreferredTrackTransform = true
|
||||
avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)]) { [weak self] _, cgImage, _, _, _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
if let cgImage {
|
||||
let image = UIImage(cgImage: cgImage)
|
||||
itemMediaEditor.replaceSource(image, additionalImage: nil, time: firstFrameTime, mirror: false)
|
||||
|
||||
if let resultImage = itemMediaEditor.resultImage {
|
||||
makeEditorImageComposition(
|
||||
context: self.node.ciContext,
|
||||
postbox: self.context.account.postbox,
|
||||
inputImage: resultImage,
|
||||
dimensions: storyDimensions,
|
||||
values: itemMediaEditor.values,
|
||||
time: firstFrameTime,
|
||||
textScale: 2.0
|
||||
) { coverImage in
|
||||
if let coverImage = coverImage {
|
||||
let result = MediaEditorScreenImpl.Result(
|
||||
media: .video(
|
||||
video: .asset(localIdentifier: asset.localIdentifier),
|
||||
coverImage: coverImage,
|
||||
values: itemMediaEditor.values,
|
||||
duration: duration,
|
||||
dimensions: itemMediaEditor.values.resultDimensions
|
||||
),
|
||||
mediaAreas: mediaAreas,
|
||||
caption: caption,
|
||||
coverTimestamp: itemMediaEditor.values.coverImageTimestamp,
|
||||
options: self.state.privacy,
|
||||
stickers: stickers,
|
||||
randomId: randomId
|
||||
)
|
||||
completion(result)
|
||||
} else {
|
||||
completion(self.createEmptyResult(randomId: randomId))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
completion(self.createEmptyResult(randomId: randomId))
|
||||
}
|
||||
} else {
|
||||
completion(self.createEmptyResult(randomId: randomId))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func processImageItem(item: EditingItem, index: Int, randomId: Int64, completion: @escaping (MediaEditorScreenImpl.Result) -> Void) {
|
||||
let asset = item.asset
|
||||
|
||||
let itemMediaEditor = setupMediaEditorForItem(item: item)
|
||||
|
||||
var caption = item.caption
|
||||
caption = convertMarkdownToAttributes(caption)
|
||||
|
||||
var mediaAreas: [MediaArea] = []
|
||||
var stickers: [TelegramMediaFile] = []
|
||||
|
||||
if let entities = item.values?.entities {
|
||||
for entity in entities {
|
||||
if let mediaArea = entity.mediaArea {
|
||||
mediaAreas.append(mediaArea)
|
||||
}
|
||||
extractStickersFromEntity(entity, into: &stickers)
|
||||
}
|
||||
}
|
||||
|
||||
let options = PHImageRequestOptions()
|
||||
options.deliveryMode = .highQualityFormat
|
||||
options.isNetworkAccessAllowed = true
|
||||
|
||||
PHImageManager.default().requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .default, options: options) { [weak self] image, _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
if let image {
|
||||
itemMediaEditor.replaceSource(image, additionalImage: nil, time: .zero, mirror: false)
|
||||
|
||||
if let resultImage = itemMediaEditor.resultImage {
|
||||
makeEditorImageComposition(
|
||||
context: self.node.ciContext,
|
||||
postbox: self.context.account.postbox,
|
||||
inputImage: resultImage,
|
||||
dimensions: storyDimensions,
|
||||
values: itemMediaEditor.values,
|
||||
time: .zero,
|
||||
textScale: 2.0
|
||||
) { resultImage in
|
||||
if let resultImage = resultImage {
|
||||
let result = MediaEditorScreenImpl.Result(
|
||||
media: .image(
|
||||
image: resultImage,
|
||||
dimensions: PixelDimensions(resultImage.size)
|
||||
),
|
||||
mediaAreas: mediaAreas,
|
||||
caption: caption,
|
||||
coverTimestamp: nil,
|
||||
options: self.state.privacy,
|
||||
stickers: stickers,
|
||||
randomId: randomId
|
||||
)
|
||||
completion(result)
|
||||
} else {
|
||||
completion(self.createEmptyResult(randomId: randomId))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
completion(self.createEmptyResult(randomId: randomId))
|
||||
}
|
||||
} else {
|
||||
completion(self.createEmptyResult(randomId: randomId))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func setupMediaEditorForItem(item: EditingItem) -> MediaEditor {
|
||||
var values = item.values
|
||||
if values?.videoTrimRange == nil {
|
||||
values = values?.withUpdatedVideoTrimRange(0 ..< storyMaxVideoDuration)
|
||||
}
|
||||
return MediaEditor(
|
||||
context: self.context,
|
||||
mode: .default,
|
||||
subject: .asset(item.asset),
|
||||
values: values,
|
||||
hasHistogram: false,
|
||||
isStandalone: true
|
||||
)
|
||||
}
|
||||
|
||||
private func extractStickersFromEntity(_ entity: CodableDrawingEntity, into stickers: inout [TelegramMediaFile]) {
|
||||
switch entity {
|
||||
case let .sticker(stickerEntity):
|
||||
if case let .file(file, fileType) = stickerEntity.content, case .sticker = fileType {
|
||||
stickers.append(file.media)
|
||||
}
|
||||
case let .text(textEntity):
|
||||
if let subEntities = textEntity.renderSubEntities {
|
||||
for entity in subEntities {
|
||||
if let stickerEntity = entity as? DrawingStickerEntity, case let .file(file, fileType) = stickerEntity.content, case .sticker = fileType {
|
||||
stickers.append(file.media)
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func createEmptyResult(randomId: Int64) -> MediaEditorScreenImpl.Result {
|
||||
let emptyImage = UIImage()
|
||||
return MediaEditorScreenImpl.Result(
|
||||
media: .image(
|
||||
image: emptyImage,
|
||||
dimensions: PixelDimensions(emptyImage.size)
|
||||
),
|
||||
mediaAreas: [],
|
||||
caption: NSAttributedString(),
|
||||
coverTimestamp: nil,
|
||||
options: self.state.privacy,
|
||||
stickers: [],
|
||||
randomId: randomId
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
func updateMediaEditorEntities() {
|
||||
guard let mediaEditor = self.node.mediaEditor else {
|
||||
return
|
||||
}
|
||||
let entities = self.node.entitiesView.entities.filter { !($0 is DrawingMediaEntity) }
|
||||
let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.node.entitiesView)
|
||||
mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities)
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -84,6 +84,7 @@ public final class MediaScrubberComponent: Component {
|
||||
let position: Double
|
||||
let minDuration: Double
|
||||
let maxDuration: Double
|
||||
let segmentDuration: Double?
|
||||
let isPlaying: Bool
|
||||
|
||||
let tracks: [Track]
|
||||
@ -112,6 +113,7 @@ public final class MediaScrubberComponent: Component {
|
||||
position: Double,
|
||||
minDuration: Double,
|
||||
maxDuration: Double,
|
||||
segmentDuration: Double? = nil,
|
||||
isPlaying: Bool,
|
||||
tracks: [Track],
|
||||
isCollage: Bool,
|
||||
@ -135,6 +137,7 @@ public final class MediaScrubberComponent: Component {
|
||||
self.position = position
|
||||
self.minDuration = minDuration
|
||||
self.maxDuration = maxDuration
|
||||
self.segmentDuration = segmentDuration
|
||||
self.isPlaying = isPlaying
|
||||
self.tracks = tracks
|
||||
self.isCollage = isCollage
|
||||
@ -171,6 +174,9 @@ public final class MediaScrubberComponent: Component {
|
||||
if lhs.maxDuration != rhs.maxDuration {
|
||||
return false
|
||||
}
|
||||
if lhs.segmentDuration != rhs.segmentDuration {
|
||||
return false
|
||||
}
|
||||
if lhs.isPlaying != rhs.isPlaying {
|
||||
return false
|
||||
}
|
||||
@ -624,6 +630,7 @@ public final class MediaScrubberComponent: Component {
|
||||
isSelected: isSelected,
|
||||
availableSize: availableSize,
|
||||
duration: self.duration,
|
||||
segmentDuration: lowestVideoId == track.id ? component.segmentDuration : nil,
|
||||
transition: trackTransition
|
||||
)
|
||||
trackLayout[id] = (CGRect(origin: CGPoint(x: 0.0, y: totalHeight), size: trackSize), trackTransition, animateTrackIn)
|
||||
@ -675,6 +682,7 @@ public final class MediaScrubberComponent: Component {
|
||||
isSelected: false,
|
||||
availableSize: availableSize,
|
||||
duration: self.duration,
|
||||
segmentDuration: nil,
|
||||
transition: trackTransition
|
||||
)
|
||||
trackTransition.setFrame(view: trackView, frame: CGRect(origin: .zero, size: trackSize))
|
||||
@ -955,6 +963,9 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega
|
||||
fileprivate let audioContentMaskView: UIImageView
|
||||
fileprivate let audioIconView: UIImageView
|
||||
fileprivate let audioTitle = ComponentView<Empty>()
|
||||
|
||||
fileprivate var segmentTitles: [Int32: ComponentView<Empty>] = [:]
|
||||
fileprivate var segmentLayers: [Int32: SimpleLayer] = [:]
|
||||
|
||||
fileprivate let videoTransparentFramesContainer = UIView()
|
||||
fileprivate var videoTransparentFrameLayers: [VideoFrameLayer] = []
|
||||
@ -1142,6 +1153,7 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega
|
||||
isSelected: Bool,
|
||||
availableSize: CGSize,
|
||||
duration: Double,
|
||||
segmentDuration: Double?,
|
||||
transition: ComponentTransition
|
||||
) -> CGSize {
|
||||
let previousParams = self.params
|
||||
@ -1477,6 +1489,86 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega
|
||||
transition.setFrame(view: self.vibrancyView, frame: CGRect(origin: .zero, size: containerFrame.size))
|
||||
transition.setFrame(view: self.vibrancyContainer, frame: CGRect(origin: .zero, size: containerFrame.size))
|
||||
|
||||
var segmentCount = 0
|
||||
var segmentOrigin: CGFloat = 0.0
|
||||
var segmentWidth: CGFloat = 0.0
|
||||
if let segmentDuration {
|
||||
if duration > segmentDuration {
|
||||
let fraction = segmentDuration / duration
|
||||
segmentCount = Int(ceil(duration / segmentDuration)) - 1
|
||||
segmentWidth = floorToScreenPixels(containerFrame.width * fraction)
|
||||
}
|
||||
if let trimRange = track.trimRange {
|
||||
if trimRange.lowerBound > 0.0 {
|
||||
let fraction = trimRange.lowerBound / duration
|
||||
segmentOrigin = floorToScreenPixels(containerFrame.width * fraction)
|
||||
}
|
||||
let actualSegmentCount = Int(ceil((trimRange.upperBound - trimRange.lowerBound) / segmentDuration)) - 1
|
||||
segmentCount = min(actualSegmentCount, segmentCount)
|
||||
}
|
||||
}
|
||||
|
||||
var validIds = Set<Int32>()
|
||||
var segmentFrame = CGRect(x: segmentOrigin + segmentWidth, y: 0.0, width: 1.0, height: containerFrame.size.height)
|
||||
for i in 0 ..< min(segmentCount, 2) {
|
||||
let id = Int32(i)
|
||||
validIds.insert(id)
|
||||
|
||||
let segmentLayer: SimpleLayer
|
||||
let segmentTitle: ComponentView<Empty>
|
||||
|
||||
var segmentTransition = transition
|
||||
if let currentLayer = self.segmentLayers[id], let currentTitle = self.segmentTitles[id] {
|
||||
segmentLayer = currentLayer
|
||||
segmentTitle = currentTitle
|
||||
} else {
|
||||
segmentTransition = .immediate
|
||||
segmentLayer = SimpleLayer()
|
||||
segmentLayer.backgroundColor = UIColor.white.cgColor
|
||||
segmentTitle = ComponentView<Empty>()
|
||||
|
||||
self.segmentLayers[id] = segmentLayer
|
||||
self.segmentTitles[id] = segmentTitle
|
||||
|
||||
self.containerView.layer.addSublayer(segmentLayer)
|
||||
}
|
||||
|
||||
transition.setFrame(layer: segmentLayer, frame: segmentFrame)
|
||||
|
||||
let segmentTitleSize = segmentTitle.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: "#\(i + 2)", font: Font.semibold(11.0), textColor: .white)),
|
||||
textShadowColor: UIColor(rgb: 0x000000, alpha: 0.4),
|
||||
textShadowBlur: 1.0
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: containerFrame.size
|
||||
)
|
||||
if let view = segmentTitle.view {
|
||||
if view.superview == nil {
|
||||
self.containerView.addSubview(view)
|
||||
}
|
||||
segmentTransition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: segmentFrame.maxX + 2.0, y: 2.0), size: segmentTitleSize))
|
||||
}
|
||||
segmentFrame.origin.x += segmentWidth
|
||||
}
|
||||
|
||||
var removeIds: [Int32] = []
|
||||
for (id, segmentLayer) in self.segmentLayers {
|
||||
if !validIds.contains(id) {
|
||||
removeIds.append(id)
|
||||
segmentLayer.removeFromSuperlayer()
|
||||
if let segmentTitle = self.segmentTitles[id] {
|
||||
segmentTitle.view?.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
for id in removeIds {
|
||||
self.segmentLayers.removeValue(forKey: id)
|
||||
self.segmentTitles.removeValue(forKey: id)
|
||||
}
|
||||
|
||||
return scrubberSize
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
12
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/PriceTag.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/PriceTag.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "price (2).pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/PriceTag.imageset/price (2).pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/PriceTag.imageset/price (2).pdf
vendored
Normal file
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Settings/Menu/AutoTranslate.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Settings/Menu/AutoTranslate.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "translation.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Settings/Menu/AutoTranslate.imageset/translation.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Settings/Menu/AutoTranslate.imageset/translation.pdf
vendored
Normal file
Binary file not shown.
@ -977,8 +977,8 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
|
||||
var setPresentationCall: ((PresentationCall?) -> Void)?
|
||||
let sharedContext = SharedAccountContextImpl(mainWindow: self.mainWindow, sharedContainerPath: legacyBasePath, basePath: rootPath, encryptionParameters: encryptionParameters, accountManager: accountManager, appLockContext: appLockContext, notificationController: nil, applicationBindings: applicationBindings, initialPresentationDataAndSettings: initialPresentationDataAndSettings, networkArguments: networkArguments, hasInAppPurchases: buildConfig.isAppStoreBuild && buildConfig.apiId == 1, rootPath: rootPath, legacyBasePath: legacyBasePath, apsNotificationToken: self.notificationTokenPromise.get() |> map(Optional.init), voipNotificationToken: self.voipTokenPromise.get() |> map(Optional.init), firebaseSecretStream: self.firebaseSecretStream.get(), setNotificationCall: { call in
|
||||
setPresentationCall?(call)
|
||||
}, navigateToChat: { accountId, peerId, messageId in
|
||||
self.openChatWhenReady(accountId: accountId, peerId: peerId, threadId: nil, messageId: messageId, storyId: nil)
|
||||
}, navigateToChat: { accountId, peerId, messageId, alwaysKeepMessageId in
|
||||
self.openChatWhenReady(accountId: accountId, peerId: peerId, threadId: nil, messageId: messageId, storyId: nil, alwaysKeepMessageId: alwaysKeepMessageId)
|
||||
}, displayUpgradeProgress: { progress in
|
||||
if let progress = progress {
|
||||
if self.dataImportSplash == nil {
|
||||
@ -2736,7 +2736,7 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
|
||||
}))
|
||||
}
|
||||
|
||||
private func openChatWhenReady(accountId: AccountRecordId?, peerId: PeerId, threadId: Int64?, messageId: MessageId? = nil, activateInput: Bool = false, storyId: StoryId?, openAppIfAny: Bool = false) {
|
||||
private func openChatWhenReady(accountId: AccountRecordId?, peerId: PeerId, threadId: Int64?, messageId: MessageId? = nil, activateInput: Bool = false, storyId: StoryId?, openAppIfAny: Bool = false, alwaysKeepMessageId: Bool = false) {
|
||||
let signal = self.sharedContextPromise.get()
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue
|
||||
@ -2755,7 +2755,7 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
|
||||
}
|
||||
self.openChatWhenReadyDisposable.set((signal
|
||||
|> deliverOnMainQueue).start(next: { context in
|
||||
context.openChatWithPeerId(peerId: peerId, threadId: threadId, messageId: messageId, activateInput: activateInput, storyId: storyId, openAppIfAny: openAppIfAny)
|
||||
context.openChatWithPeerId(peerId: peerId, threadId: threadId, messageId: messageId, activateInput: activateInput, storyId: storyId, openAppIfAny: openAppIfAny, alwaysKeepMessageId: alwaysKeepMessageId)
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -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))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -677,17 +677,22 @@ extension ChatControllerImpl {
|
||||
|
||||
let isHidden = self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.TranslationHidden(id: peerId))
|
||||
|> distinctUntilChanged
|
||||
|
||||
let hasAutoTranslate = self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.AutoTranslateEnabled(id: peerId))
|
||||
|> distinctUntilChanged
|
||||
|
||||
self.translationStateDisposable = (combineLatest(
|
||||
queue: .concurrentDefaultQueue(),
|
||||
isPremium,
|
||||
isHidden,
|
||||
hasAutoTranslate,
|
||||
ApplicationSpecificNotice.translationSuggestion(accountManager: self.context.sharedContext.accountManager)
|
||||
) |> mapToSignal { isPremium, isHidden, counterAndTimestamp -> Signal<ChatPresentationTranslationState?, NoError> in
|
||||
) |> mapToSignal { isPremium, isHidden, hasAutoTranslate, counterAndTimestamp -> Signal<ChatPresentationTranslationState?, NoError> in
|
||||
var maybeSuggestPremium = false
|
||||
if counterAndTimestamp.0 >= 3 {
|
||||
maybeSuggestPremium = true
|
||||
}
|
||||
if (isPremium || maybeSuggestPremium) && !isHidden {
|
||||
if (isPremium || maybeSuggestPremium || hasAutoTranslate) && !isHidden {
|
||||
return chatTranslationState(context: context, peerId: peerId)
|
||||
|> map { translationState -> ChatPresentationTranslationState? in
|
||||
if let translationState, !translationState.fromLang.isEmpty && (translationState.fromLang != baseLanguageCode || translationState.isEnabled) {
|
||||
|
@ -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()
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -39,7 +39,7 @@ public func makeTempContext(
|
||||
firebaseSecretStream: .never(),
|
||||
setNotificationCall: { _ in
|
||||
},
|
||||
navigateToChat: { _, _, _ in
|
||||
navigateToChat: { _, _, _, _ in
|
||||
}, displayUpgradeProgress: { _ in
|
||||
},
|
||||
appDelegate: nil
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -133,7 +133,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
}
|
||||
}
|
||||
|
||||
private let navigateToChatImpl: (AccountRecordId, PeerId, MessageId?) -> Void
|
||||
private let navigateToChatImpl: (AccountRecordId, PeerId, MessageId?, Bool) -> Void
|
||||
|
||||
private let apsNotificationToken: Signal<Data?, NoError>
|
||||
private let voipNotificationToken: Signal<Data?, NoError>
|
||||
@ -268,7 +268,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
|
||||
private let energyUsageAutomaticDisposable = MetaDisposable()
|
||||
|
||||
init(mainWindow: Window1?, sharedContainerPath: String, basePath: String, encryptionParameters: ValueBoxEncryptionParameters, accountManager: AccountManager<TelegramAccountManagerTypes>, appLockContext: AppLockContext, notificationController: NotificationContainerController?, applicationBindings: TelegramApplicationBindings, initialPresentationDataAndSettings: InitialPresentationDataAndSettings, networkArguments: NetworkInitializationArguments, hasInAppPurchases: Bool, rootPath: String, legacyBasePath: String?, apsNotificationToken: Signal<Data?, NoError>, voipNotificationToken: Signal<Data?, NoError>, firebaseSecretStream: Signal<[String: String], NoError>, setNotificationCall: @escaping (PresentationCall?) -> Void, navigateToChat: @escaping (AccountRecordId, PeerId, MessageId?) -> Void, displayUpgradeProgress: @escaping (Float?) -> Void = { _ in }, appDelegate: AppDelegate?) {
|
||||
init(mainWindow: Window1?, sharedContainerPath: String, basePath: String, encryptionParameters: ValueBoxEncryptionParameters, accountManager: AccountManager<TelegramAccountManagerTypes>, appLockContext: AppLockContext, notificationController: NotificationContainerController?, applicationBindings: TelegramApplicationBindings, initialPresentationDataAndSettings: InitialPresentationDataAndSettings, networkArguments: NetworkInitializationArguments, hasInAppPurchases: Bool, rootPath: String, legacyBasePath: String?, apsNotificationToken: Signal<Data?, NoError>, voipNotificationToken: Signal<Data?, NoError>, firebaseSecretStream: Signal<[String: String], NoError>, setNotificationCall: @escaping (PresentationCall?) -> Void, navigateToChat: @escaping (AccountRecordId, PeerId, MessageId?, Bool) -> Void, displayUpgradeProgress: @escaping (Float?) -> Void = { _ in }, appDelegate: AppDelegate?) {
|
||||
assert(Queue.mainQueue().isCurrent())
|
||||
|
||||
precondition(!testHasInstance)
|
||||
@ -1760,7 +1760,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
}
|
||||
|
||||
public func navigateToChat(accountId: AccountRecordId, peerId: PeerId, messageId: MessageId?) {
|
||||
self.navigateToChatImpl(accountId, peerId, messageId)
|
||||
self.navigateToChatImpl(accountId, peerId, messageId, true)
|
||||
}
|
||||
|
||||
public func messageFromPreloadedChatHistoryViewForLocation(id: MessageId, location: ChatHistoryLocationInput, context: AccountContext, chatLocation: ChatLocation, subject: ChatControllerSubject?, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>, tag: HistoryViewInputTag?) -> Signal<(MessageIndex?, Bool), NoError> {
|
||||
@ -2991,8 +2991,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
let _ = (combineLatest(
|
||||
queue: Queue.mainQueue(),
|
||||
controller.result,
|
||||
options.get())
|
||||
|> take(1)).startStandalone(next: { [weak controller] result, options in
|
||||
options.get() |> distinctUntilChanged
|
||||
)).startStandalone(next: { [weak controller] result, options in
|
||||
if let (peers, _, _, _, _, _) = result, let contactPeer = peers.first, case let .peer(peer, _, _) = contactPeer, let starsContext = context.starsContext {
|
||||
if case .starGiftTransfer = source {
|
||||
presentTransferAlertImpl?(EnginePeer(peer))
|
||||
@ -3462,9 +3462,9 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}, completion: { result, commit in
|
||||
completion(result, commit)
|
||||
} as (MediaEditorScreenImpl.Result, @escaping (@escaping () -> Void) -> Void) -> Void
|
||||
}, completion: { results, commit in
|
||||
completion(results.first!, commit)
|
||||
} as ([MediaEditorScreenImpl.Result], @escaping (@escaping () -> Void) -> Void) -> Void
|
||||
)
|
||||
editorController.cancelled = { _ in
|
||||
cancelled()
|
||||
@ -3526,13 +3526,13 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}, completion: { result, commit in
|
||||
if case let .sticker(file, emoji) = result.media {
|
||||
}, completion: { results, commit in
|
||||
if case let .sticker(file, emoji) = results.first?.media {
|
||||
completion(file, emoji, {
|
||||
commit({})
|
||||
})
|
||||
}
|
||||
} as (MediaEditorScreenImpl.Result, @escaping (@escaping () -> Void) -> Void) -> Void
|
||||
} as ([MediaEditorScreenImpl.Result], @escaping (@escaping () -> Void) -> Void) -> Void
|
||||
)
|
||||
editorController.cancelled = { _ in
|
||||
cancelled()
|
||||
@ -3551,7 +3551,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
}
|
||||
let editorController = MediaEditorScreenImpl(
|
||||
context: context,
|
||||
mode: .storyEditor,
|
||||
mode: .storyEditor(remainingCount: 1),
|
||||
subject: subject,
|
||||
customTarget: nil,
|
||||
initialCaption: text.flatMap { NSAttributedString(string: $0) },
|
||||
@ -3559,13 +3559,10 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
transitionIn: nil,
|
||||
transitionOut: { finished, isNew in
|
||||
return nil
|
||||
}, completion: { result, commit in
|
||||
completion(result, commit)
|
||||
} as (MediaEditorScreenImpl.Result, @escaping (@escaping () -> Void) -> Void) -> Void
|
||||
}, completion: { results, commit in
|
||||
completion(results.first!, commit)
|
||||
} as ([MediaEditorScreenImpl.Result], @escaping (@escaping () -> Void) -> Void) -> Void
|
||||
)
|
||||
// editorController.cancelled = { _ in
|
||||
// cancelled()
|
||||
// }
|
||||
return editorController
|
||||
}
|
||||
|
||||
@ -3726,13 +3723,16 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let controller = MediaEditorScreenImpl(
|
||||
context: context,
|
||||
mode: .storyEditor,
|
||||
mode: .storyEditor(remainingCount: 1),
|
||||
subject: editorSubject,
|
||||
transitionIn: nil,
|
||||
transitionOut: { _, _ in
|
||||
return nil
|
||||
},
|
||||
completion: { [weak parentController] result, commit in
|
||||
completion: { [weak parentController] results, commit in
|
||||
guard let result = results.first else {
|
||||
return
|
||||
}
|
||||
let targetPeerId: EnginePeer.Id
|
||||
let target: Stories.PendingTarget
|
||||
if let sendAsPeerId = result.options.sendAsPeerId {
|
||||
|
@ -346,7 +346,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
return nil
|
||||
}
|
||||
},
|
||||
completion: { result, resultTransition, dismissed in
|
||||
completion: { result, resultTransition, storyRemainingCount, dismissed in
|
||||
let subject: Signal<MediaEditorScreenImpl.Subject?, NoError> = result
|
||||
|> map { value -> MediaEditorScreenImpl.Subject? in
|
||||
func editorPIPPosition(_ position: CameraScreenImpl.PIPPosition) -> MediaEditorScreenImpl.PIPPosition {
|
||||
@ -422,7 +422,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
|
||||
let controller = MediaEditorScreenImpl(
|
||||
context: context,
|
||||
mode: .storyEditor,
|
||||
mode: .storyEditor(remainingCount: storyRemainingCount ?? 1),
|
||||
subject: subject,
|
||||
customTarget: mediaEditorCustomTarget,
|
||||
transitionIn: transitionIn,
|
||||
@ -444,7 +444,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}, completion: { [weak self] result, commit in
|
||||
}, completion: { [weak self] results, commit in
|
||||
guard let self else {
|
||||
dismissCameraImpl?()
|
||||
commit({})
|
||||
@ -453,7 +453,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
|
||||
if let customTarget, case .botPreview = customTarget {
|
||||
externalState.storyTarget = customTarget
|
||||
self.proceedWithStoryUpload(target: customTarget, results: [result], existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit)
|
||||
self.proceedWithStoryUpload(target: customTarget, results: results, existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit)
|
||||
|
||||
dismissCameraImpl?()
|
||||
return
|
||||
@ -464,7 +464,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
target = .peer(id)
|
||||
targetPeerId = id
|
||||
} else {
|
||||
if let sendAsPeerId = result.options.sendAsPeerId {
|
||||
if let sendAsPeerId = results.first?.options.sendAsPeerId {
|
||||
target = .peer(sendAsPeerId)
|
||||
targetPeerId = sendAsPeerId
|
||||
} else {
|
||||
@ -486,12 +486,12 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
externalState.isPeerArchived = channel.storiesHidden ?? false
|
||||
}
|
||||
|
||||
self.proceedWithStoryUpload(target: target, results: [result], existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit)
|
||||
self.proceedWithStoryUpload(target: target, results: results, existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit)
|
||||
|
||||
dismissCameraImpl?()
|
||||
})
|
||||
}
|
||||
} as (MediaEditorScreenImpl.Result, @escaping (@escaping () -> Void) -> Void) -> Void
|
||||
} as ([MediaEditorScreenImpl.Result], @escaping (@escaping () -> Void) -> Void) -> Void
|
||||
)
|
||||
controller.cancelled = { showDraftTooltip in
|
||||
if showDraftTooltip {
|
||||
|
@ -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) {
|
||||
|
@ -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"
|
||||
|
@ -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)),
|
||||
|
Loading…
x
Reference in New Issue
Block a user