mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 21:45:19 +00:00
Various improvements
This commit is contained in:
parent
14064f94eb
commit
2962851527
@ -14202,3 +14202,21 @@ 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";
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -171,6 +171,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
||||
dict[589338437] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionStartGroupCall($0) }
|
||||
dict[-1895328189] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionStopPoll($0) }
|
||||
dict[1693675004] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionToggleAntiSpam($0) }
|
||||
dict[-988285058] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionToggleAutotranslation($0) }
|
||||
dict[46949251] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionToggleForum($0) }
|
||||
dict[1456906823] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionToggleGroupCallSetting($0) }
|
||||
dict[460916654] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionToggleInvites($0) }
|
||||
@ -1461,6 +1462,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
||||
dict[276907596] = { return Api.storage.FileType.parse_fileWebp($0) }
|
||||
dict[1862033025] = { return Api.stories.AllStories.parse_allStories($0) }
|
||||
dict[291044926] = { return Api.stories.AllStories.parse_allStoriesNotModified($0) }
|
||||
dict[-1014513586] = { return Api.stories.CanSendStoryCount.parse_canSendStoryCount($0) }
|
||||
dict[-488736969] = { return Api.stories.FoundStories.parse_foundStories($0) }
|
||||
dict[-890861720] = { return Api.stories.PeerStories.parse_peerStories($0) }
|
||||
dict[1673780490] = { return Api.stories.Stories.parse_stories($0) }
|
||||
@ -2592,6 +2594,8 @@ public extension Api {
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.stories.AllStories:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.stories.CanSendStoryCount:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.stories.FoundStories:
|
||||
_1.serialize(buffer, boxed)
|
||||
case let _1 as Api.stories.PeerStories:
|
||||
|
@ -605,6 +605,7 @@ public extension Api {
|
||||
case channelAdminLogEventActionStartGroupCall(call: Api.InputGroupCall)
|
||||
case channelAdminLogEventActionStopPoll(message: Api.Message)
|
||||
case channelAdminLogEventActionToggleAntiSpam(newValue: Api.Bool)
|
||||
case channelAdminLogEventActionToggleAutotranslation(newValue: Api.Bool)
|
||||
case channelAdminLogEventActionToggleForum(newValue: Api.Bool)
|
||||
case channelAdminLogEventActionToggleGroupCallSetting(joinMuted: Api.Bool)
|
||||
case channelAdminLogEventActionToggleInvites(newValue: Api.Bool)
|
||||
@ -897,6 +898,12 @@ public extension Api {
|
||||
}
|
||||
newValue.serialize(buffer, true)
|
||||
break
|
||||
case .channelAdminLogEventActionToggleAutotranslation(let newValue):
|
||||
if boxed {
|
||||
buffer.appendInt32(-988285058)
|
||||
}
|
||||
newValue.serialize(buffer, true)
|
||||
break
|
||||
case .channelAdminLogEventActionToggleForum(let newValue):
|
||||
if boxed {
|
||||
buffer.appendInt32(46949251)
|
||||
@ -1039,6 +1046,8 @@ public extension Api {
|
||||
return ("channelAdminLogEventActionStopPoll", [("message", message as Any)])
|
||||
case .channelAdminLogEventActionToggleAntiSpam(let newValue):
|
||||
return ("channelAdminLogEventActionToggleAntiSpam", [("newValue", newValue as Any)])
|
||||
case .channelAdminLogEventActionToggleAutotranslation(let newValue):
|
||||
return ("channelAdminLogEventActionToggleAutotranslation", [("newValue", newValue as Any)])
|
||||
case .channelAdminLogEventActionToggleForum(let newValue):
|
||||
return ("channelAdminLogEventActionToggleForum", [("newValue", newValue as Any)])
|
||||
case .channelAdminLogEventActionToggleGroupCallSetting(let joinMuted):
|
||||
@ -1677,6 +1686,19 @@ public extension Api {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
public static func parse_channelAdminLogEventActionToggleAutotranslation(_ reader: BufferReader) -> ChannelAdminLogEventAction? {
|
||||
var _1: Api.Bool?
|
||||
if let signature = reader.readInt32() {
|
||||
_1 = Api.parse(reader, signature: signature) as? Api.Bool
|
||||
}
|
||||
let _c1 = _1 != nil
|
||||
if _c1 {
|
||||
return Api.ChannelAdminLogEventAction.channelAdminLogEventActionToggleAutotranslation(newValue: _1!)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
public static func parse_channelAdminLogEventActionToggleForum(_ reader: BufferReader) -> ChannelAdminLogEventAction? {
|
||||
var _1: Api.Bool?
|
||||
if let signature = reader.readInt32() {
|
||||
|
@ -760,6 +760,42 @@ public extension Api.stories {
|
||||
|
||||
}
|
||||
}
|
||||
public extension Api.stories {
|
||||
enum CanSendStoryCount: TypeConstructorDescription {
|
||||
case canSendStoryCount(countRemains: Int32)
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .canSendStoryCount(let countRemains):
|
||||
if boxed {
|
||||
buffer.appendInt32(-1014513586)
|
||||
}
|
||||
serializeInt32(countRemains, buffer: buffer, boxed: false)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||
switch self {
|
||||
case .canSendStoryCount(let countRemains):
|
||||
return ("canSendStoryCount", [("countRemains", countRemains as Any)])
|
||||
}
|
||||
}
|
||||
|
||||
public static func parse_canSendStoryCount(_ reader: BufferReader) -> CanSendStoryCount? {
|
||||
var _1: Int32?
|
||||
_1 = reader.readInt32()
|
||||
let _c1 = _1 != nil
|
||||
if _c1 {
|
||||
return Api.stories.CanSendStoryCount.canSendStoryCount(countRemains: _1!)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
public extension Api.stories {
|
||||
enum FoundStories: TypeConstructorDescription {
|
||||
case foundStories(flags: Int32, count: Int32, stories: [Api.FoundStory], nextOffset: String?, chats: [Api.Chat], users: [Api.User])
|
||||
@ -1560,55 +1596,3 @@ public extension Api.updates {
|
||||
|
||||
}
|
||||
}
|
||||
public extension Api.updates {
|
||||
enum State: TypeConstructorDescription {
|
||||
case state(pts: Int32, qts: Int32, date: Int32, seq: Int32, unreadCount: Int32)
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .state(let pts, let qts, let date, let seq, let unreadCount):
|
||||
if boxed {
|
||||
buffer.appendInt32(-1519637954)
|
||||
}
|
||||
serializeInt32(pts, buffer: buffer, boxed: false)
|
||||
serializeInt32(qts, buffer: buffer, boxed: false)
|
||||
serializeInt32(date, buffer: buffer, boxed: false)
|
||||
serializeInt32(seq, buffer: buffer, boxed: false)
|
||||
serializeInt32(unreadCount, buffer: buffer, boxed: false)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||
switch self {
|
||||
case .state(let pts, let qts, let date, let seq, let unreadCount):
|
||||
return ("state", [("pts", pts as Any), ("qts", qts as Any), ("date", date as Any), ("seq", seq as Any), ("unreadCount", unreadCount as Any)])
|
||||
}
|
||||
}
|
||||
|
||||
public static func parse_state(_ reader: BufferReader) -> State? {
|
||||
var _1: Int32?
|
||||
_1 = reader.readInt32()
|
||||
var _2: Int32?
|
||||
_2 = reader.readInt32()
|
||||
var _3: Int32?
|
||||
_3 = reader.readInt32()
|
||||
var _4: Int32?
|
||||
_4 = reader.readInt32()
|
||||
var _5: Int32?
|
||||
_5 = reader.readInt32()
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = _2 != nil
|
||||
let _c3 = _3 != nil
|
||||
let _c4 = _4 != nil
|
||||
let _c5 = _5 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 && _c5 {
|
||||
return Api.updates.State.state(pts: _1!, qts: _2!, date: _3!, seq: _4!, unreadCount: _5!)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,55 @@
|
||||
public extension Api.updates {
|
||||
enum State: TypeConstructorDescription {
|
||||
case state(pts: Int32, qts: Int32, date: Int32, seq: Int32, unreadCount: Int32)
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .state(let pts, let qts, let date, let seq, let unreadCount):
|
||||
if boxed {
|
||||
buffer.appendInt32(-1519637954)
|
||||
}
|
||||
serializeInt32(pts, buffer: buffer, boxed: false)
|
||||
serializeInt32(qts, buffer: buffer, boxed: false)
|
||||
serializeInt32(date, buffer: buffer, boxed: false)
|
||||
serializeInt32(seq, buffer: buffer, boxed: false)
|
||||
serializeInt32(unreadCount, buffer: buffer, boxed: false)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||
switch self {
|
||||
case .state(let pts, let qts, let date, let seq, let unreadCount):
|
||||
return ("state", [("pts", pts as Any), ("qts", qts as Any), ("date", date as Any), ("seq", seq as Any), ("unreadCount", unreadCount as Any)])
|
||||
}
|
||||
}
|
||||
|
||||
public static func parse_state(_ reader: BufferReader) -> State? {
|
||||
var _1: Int32?
|
||||
_1 = reader.readInt32()
|
||||
var _2: Int32?
|
||||
_2 = reader.readInt32()
|
||||
var _3: Int32?
|
||||
_3 = reader.readInt32()
|
||||
var _4: Int32?
|
||||
_4 = reader.readInt32()
|
||||
var _5: Int32?
|
||||
_5 = reader.readInt32()
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = _2 != nil
|
||||
let _c3 = _3 != nil
|
||||
let _c4 = _4 != nil
|
||||
let _c5 = _5 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 && _c5 {
|
||||
return Api.updates.State.state(pts: _1!, qts: _2!, date: _3!, seq: _4!, unreadCount: _5!)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
public extension Api.upload {
|
||||
enum CdnFile: TypeConstructorDescription {
|
||||
case cdnFile(bytes: Buffer)
|
||||
|
@ -3590,6 +3590,22 @@ public extension Api.functions.channels {
|
||||
})
|
||||
}
|
||||
}
|
||||
public extension Api.functions.channels {
|
||||
static func toggleAutotranslation(channel: Api.InputChannel, enabled: Api.Bool) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(377471137)
|
||||
channel.serialize(buffer, true)
|
||||
enabled.serialize(buffer, true)
|
||||
return (FunctionDescription(name: "channels.toggleAutotranslation", parameters: [("channel", String(describing: channel)), ("enabled", String(describing: enabled))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in
|
||||
let reader = BufferReader(buffer)
|
||||
var result: Api.Updates?
|
||||
if let signature = reader.readInt32() {
|
||||
result = Api.parse(reader, signature: signature) as? Api.Updates
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
}
|
||||
public extension Api.functions.channels {
|
||||
static func toggleForum(channel: Api.InputChannel, enabled: Api.Bool) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
|
||||
let buffer = Buffer()
|
||||
@ -11139,15 +11155,15 @@ public extension Api.functions.stories {
|
||||
}
|
||||
}
|
||||
public extension Api.functions.stories {
|
||||
static func canSendStory(peer: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
|
||||
static func canSendStory(peer: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.stories.CanSendStoryCount>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(-941629475)
|
||||
buffer.appendInt32(820732912)
|
||||
peer.serialize(buffer, true)
|
||||
return (FunctionDescription(name: "stories.canSendStory", parameters: [("peer", String(describing: peer))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
|
||||
return (FunctionDescription(name: "stories.canSendStory", parameters: [("peer", String(describing: peer))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.stories.CanSendStoryCount? in
|
||||
let reader = BufferReader(buffer)
|
||||
var result: Api.Bool?
|
||||
var result: Api.stories.CanSendStoryCount?
|
||||
if let signature = reader.readInt32() {
|
||||
result = Api.parse(reader, signature: signature) as? Api.Bool
|
||||
result = Api.parse(reader, signature: signature) as? Api.stories.CanSendStoryCount
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
@ -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
|
||||
|
@ -179,6 +179,7 @@ public enum BotPaymentFormRequestError {
|
||||
case alreadyActive
|
||||
case noPaymentNeeded
|
||||
case disallowedStarGift
|
||||
case starGiftResellTooEarly(Int32)
|
||||
}
|
||||
|
||||
extension BotPaymentInvoice {
|
||||
@ -482,6 +483,11 @@ func _internal_fetchBotPaymentForm(accountPeerId: PeerId, postbox: Postbox, netw
|
||||
return .fail(.noPaymentNeeded)
|
||||
} else if error.errorDescription == "USER_DISALLOWED_STARGIFTS" {
|
||||
return .fail(.disallowedStarGift)
|
||||
} else 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 .fail(.starGiftResellTooEarly(value))
|
||||
}
|
||||
}
|
||||
return .fail(.generic)
|
||||
}
|
||||
|
@ -847,8 +847,14 @@ public enum TransferStarGiftError {
|
||||
|
||||
public enum BuyStarGiftError {
|
||||
case generic
|
||||
case starGiftResellTooEarly(Int32)
|
||||
}
|
||||
|
||||
public enum UpdateStarGiftPriceError {
|
||||
case generic
|
||||
}
|
||||
|
||||
|
||||
public enum UpgradeStarGiftError {
|
||||
case generic
|
||||
}
|
||||
@ -858,7 +864,12 @@ func _internal_buyStarGift(account: Account, slug: String, peerId: EnginePeer.Id
|
||||
return _internal_fetchBotPaymentForm(accountPeerId: account.peerId, postbox: account.postbox, network: account.network, source: source, themeParams: nil)
|
||||
|> map(Optional.init)
|
||||
|> `catch` { error -> Signal<BotPaymentForm?, BuyStarGiftError> in
|
||||
return .fail(.generic)
|
||||
switch error {
|
||||
case let .starGiftResellTooEarly(value):
|
||||
return .fail(.starGiftResellTooEarly(value))
|
||||
default:
|
||||
return .fail(.generic)
|
||||
}
|
||||
}
|
||||
|> mapToSignal { paymentForm in
|
||||
if let paymentForm {
|
||||
@ -1487,7 +1498,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 +1526,54 @@ private final class ProfileGiftsContextImpl {
|
||||
}
|
||||
}
|
||||
|
||||
func updateStarGiftResellPrice(reference: StarGiftReference, price: Int64?) {
|
||||
self.actionDisposable.set(
|
||||
_internal_updateStarGiftResalePrice(account: self.account, reference: reference, price: price).startStrict()
|
||||
)
|
||||
|
||||
|
||||
if let index = self.gifts.firstIndex(where: { gift in
|
||||
if gift.reference == reference {
|
||||
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))
|
||||
self.gifts[index] = updatedGift
|
||||
func updateStarGiftResellPrice(reference: StarGiftReference, 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: reference,
|
||||
price: price
|
||||
)
|
||||
|> 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
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
if let index = self.filteredGifts.firstIndex(where: { gift in
|
||||
if gift.reference == reference {
|
||||
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()
|
||||
|
||||
subscriber.putCompletion()
|
||||
})
|
||||
)
|
||||
return disposable
|
||||
}
|
||||
|
||||
if let index = self.filteredGifts.firstIndex(where: { gift in
|
||||
if gift.reference == reference {
|
||||
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) {
|
||||
@ -1939,9 +1971,17 @@ public final class ProfileGiftsContext {
|
||||
}
|
||||
}
|
||||
|
||||
public func updateStarGiftResellPrice(reference: StarGiftReference, price: Int64?) {
|
||||
self.impl.with { impl in
|
||||
impl.updateStarGiftResellPrice(reference: reference, price: price)
|
||||
public func updateStarGiftResellPrice(reference: StarGiftReference, price: Int64?) -> Signal<Never, UpdateStarGiftPriceError> {
|
||||
return Signal { subscriber in
|
||||
let disposable = MetaDisposable()
|
||||
self.impl.with { impl in
|
||||
disposable.set(impl.updateStarGiftResellPrice(reference: reference, price: price).start(error: { error in
|
||||
subscriber.putError(error)
|
||||
}, completed: {
|
||||
subscriber.putCompletion()
|
||||
}))
|
||||
}
|
||||
return disposable
|
||||
}
|
||||
}
|
||||
|
||||
@ -2274,23 +2314,21 @@ func _internal_toggleStarGiftsNotifications(account: Account, peerId: EnginePeer
|
||||
}
|
||||
}
|
||||
|
||||
func _internal_updateStarGiftResalePrice(account: Account, reference: StarGiftReference, price: Int64?) -> Signal<Never, NoError> {
|
||||
func _internal_updateStarGiftResalePrice(account: Account, reference: StarGiftReference, price: Int64?) -> Signal<Never, UpdateStarGiftPriceError> {
|
||||
return account.postbox.transaction { transaction in
|
||||
return reference.apiStarGiftReference(transaction: transaction)
|
||||
}
|
||||
|> castError(UpdateStarGiftPriceError.self)
|
||||
|> mapToSignal { starGift in
|
||||
guard let starGift else {
|
||||
return .complete()
|
||||
}
|
||||
return account.network.request(Api.functions.payments.updateStarGiftPrice(stargift: starGift, resellStars: price ?? 0))
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
|
||||
return .single(nil)
|
||||
|> mapError { error -> UpdateStarGiftPriceError in
|
||||
return .generic
|
||||
}
|
||||
|> mapToSignal { updates -> Signal<Void, NoError> in
|
||||
if let updates {
|
||||
account.stateManager.addUpdates(updates)
|
||||
}
|
||||
|> mapToSignal { updates -> Signal<Void, UpdateStarGiftPriceError> in
|
||||
account.stateManager.addUpdates(updates)
|
||||
return .complete()
|
||||
}
|
||||
|> ignoreValues
|
||||
@ -2496,6 +2534,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(
|
||||
@ -2584,6 +2682,34 @@ 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 var currentState: ResaleGiftsContext.State? {
|
||||
var state: ResaleGiftsContext.State?
|
||||
|
@ -153,7 +153,7 @@ public extension TelegramEngine {
|
||||
return _internal_toggleStarGiftsNotifications(account: self.account, peerId: peerId, enabled: enabled)
|
||||
}
|
||||
|
||||
public func updateStarGiftResalePrice(reference: StarGiftReference, price: Int64?) -> Signal<Never, NoError> {
|
||||
public func updateStarGiftResalePrice(reference: StarGiftReference, price: Int64?) -> Signal<Never, UpdateStarGiftPriceError> {
|
||||
return _internal_updateStarGiftResalePrice(account: self.account, reference: reference, price: price)
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -3469,7 +3469,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 {
|
||||
let _ = remainingCount
|
||||
return
|
||||
}
|
||||
self.node.postingAvailable = false
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>()
|
||||
@ -787,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: "Available for Resale", 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>] = []
|
||||
|
@ -139,21 +139,10 @@ final class GiftStoreScreenComponent: Component {
|
||||
self.updateScrolling(interactive: true, transition: self.nextScrollTransition ?? .immediate)
|
||||
}
|
||||
|
||||
private var removedStarGifts = Set<String>()
|
||||
private var currentGifts: ([StarGift], Set<String>, Set<String>, Set<String>)?
|
||||
private var effectiveGifts: [StarGift]? {
|
||||
if let gifts = self.state?.starGiftsState?.gifts {
|
||||
if !self.removedStarGifts.isEmpty {
|
||||
return gifts.filter { gift in
|
||||
if case let .unique(uniqueGift) = gift {
|
||||
return !self.removedStarGifts.contains(uniqueGift.slug)
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return gifts
|
||||
}
|
||||
return gifts
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
@ -253,15 +242,14 @@ final class GiftStoreScreenComponent: Component {
|
||||
}
|
||||
let giftController = GiftViewScreen(
|
||||
context: component.context,
|
||||
subject: .uniqueGift(uniqueGift, state.peerId)
|
||||
)
|
||||
giftController.onBuySuccess = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
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()
|
||||
}
|
||||
self.removedStarGifts.insert(uniqueGift.slug)
|
||||
self.state?.updated(transition: .spring(duration: 0.3))
|
||||
}
|
||||
)
|
||||
mainController.push(giftController)
|
||||
}
|
||||
}
|
||||
@ -507,15 +495,17 @@ 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: "Search",
|
||||
value: "",
|
||||
valueChanged: { value in
|
||||
searchQueryPromise.set(value)
|
||||
}
|
||||
), false))
|
||||
items.append(.separator)
|
||||
}
|
||||
items.append(.custom(GiftAttributeListContextItem(
|
||||
context: component.context,
|
||||
attributes: modelAttributes,
|
||||
@ -597,15 +587,17 @@ 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: "Search",
|
||||
value: "",
|
||||
valueChanged: { value in
|
||||
searchQueryPromise.set(value)
|
||||
}
|
||||
), false))
|
||||
items.append(.separator)
|
||||
}
|
||||
items.append(.custom(GiftAttributeListContextItem(
|
||||
context: component.context,
|
||||
attributes: backdropAttributes,
|
||||
@ -687,15 +679,17 @@ 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: "Search",
|
||||
value: "",
|
||||
valueChanged: { value in
|
||||
searchQueryPromise.set(value)
|
||||
}
|
||||
), false))
|
||||
items.append(.separator)
|
||||
}
|
||||
items.append(.custom(GiftAttributeListContextItem(
|
||||
context: component.context,
|
||||
attributes: patternAttributes,
|
||||
|
@ -460,8 +460,6 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
guard let self, let controller = self.getController() as? GiftViewScreen else {
|
||||
return
|
||||
}
|
||||
controller.onBuySuccess()
|
||||
|
||||
self.inProgress = false
|
||||
|
||||
var animationFile: TelegramMediaFile?
|
||||
@ -2902,7 +2900,6 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
||||
let updateSubject = ActionSlot<GiftViewScreen.Subject>()
|
||||
|
||||
public var disposed: () -> Void = {}
|
||||
public var onBuySuccess: () -> Void = {}
|
||||
|
||||
fileprivate var showBalance = false {
|
||||
didSet {
|
||||
@ -2922,7 +2919,7 @@ 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
|
||||
) {
|
||||
@ -3413,6 +3410,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
||||
|
||||
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)
|
||||
|
||||
//TODO:localize
|
||||
if let resellStars = gift.resellStars, resellStars > 0, !update {
|
||||
@ -3425,44 +3423,39 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
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.onBuySuccess()
|
||||
|
||||
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 reference = arguments.reference ?? .slug(slug: gift.slug)
|
||||
let _ = (context.engine.payments.updateStarGiftResalePrice(reference: reference, price: nil)
|
||||
|> deliverOnMainQueue).startStandalone()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
)
|
||||
self.present(tooltipController, in: .window(.root))
|
||||
})
|
||||
}),
|
||||
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
|
||||
})
|
||||
@ -3476,46 +3469,47 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
||||
return
|
||||
}
|
||||
|
||||
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 = "\(giftTitle) is now for sale!"
|
||||
if update {
|
||||
text = "\(giftTitle) is relisted for \(presentationStringsFormattedNumber(Int32(price), presentationData.dateTimeFormat.groupingSeparator)) 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 reference = arguments.reference ?? .slug(slug: gift.slug)
|
||||
let _ = (context.engine.payments.updateStarGiftResalePrice(reference: reference, 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 = "\(giftTitle) is now for sale!"
|
||||
if update {
|
||||
text = "\(giftTitle) is relisted for \(presentationStringsFormattedNumber(Int32(price), presentationData.dateTimeFormat.groupingSeparator)) 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
|
||||
}
|
||||
)
|
||||
self.present(tooltipController, in: .window(.root))
|
||||
})
|
||||
})
|
||||
self.push(resellController)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -327,7 +327,7 @@ final class MediaEditorScreenComponent: Component {
|
||||
private let switchCameraButton = ComponentView<Empty>()
|
||||
|
||||
private let selectionButton = ComponentView<Empty>()
|
||||
private let selectionPanel = ComponentView<Empty>()
|
||||
private var selectionPanel: ComponentView<Empty>?
|
||||
|
||||
private let textCancelButton = ComponentView<Empty>()
|
||||
private let textDoneButton = ComponentView<Empty>()
|
||||
@ -577,6 +577,11 @@ final class MediaEditorScreenComponent: Component {
|
||||
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
view.layer.animateScale(from: 0.6, to: 1.0, duration: 0.2)
|
||||
}
|
||||
|
||||
if let view = self.selectionButton.view {
|
||||
view.layer.animateAlpha(from: 0.0, to: view.alpha, duration: 0.2)
|
||||
view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -589,14 +594,14 @@ final class MediaEditorScreenComponent: Component {
|
||||
transition.setScale(view: view, scale: 0.1)
|
||||
}
|
||||
|
||||
let buttons = [
|
||||
let toolbarButtons = [
|
||||
self.drawButton,
|
||||
self.textButton,
|
||||
self.stickerButton,
|
||||
self.toolsButton
|
||||
]
|
||||
|
||||
for button in buttons {
|
||||
for button in toolbarButtons {
|
||||
if let view = button.view {
|
||||
view.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: 64.0), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true)
|
||||
view.layer.animateAlpha(from: view.alpha, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||
@ -617,19 +622,17 @@ final class MediaEditorScreenComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
if let view = self.saveButton.view {
|
||||
transition.setAlpha(view: view, alpha: 0.0)
|
||||
transition.setScale(view: view, scale: 0.1)
|
||||
}
|
||||
let topButtons = [
|
||||
self.saveButton,
|
||||
self.muteButton,
|
||||
self.playbackButton
|
||||
]
|
||||
|
||||
if let view = self.muteButton.view {
|
||||
transition.setAlpha(view: view, alpha: 0.0)
|
||||
transition.setScale(view: view, scale: 0.1)
|
||||
}
|
||||
|
||||
if let view = self.playbackButton.view {
|
||||
transition.setAlpha(view: view, alpha: 0.0)
|
||||
transition.setScale(view: view, scale: 0.1)
|
||||
for button in topButtons {
|
||||
if let view = button.view {
|
||||
transition.setAlpha(view: view, alpha: 0.0)
|
||||
transition.setScale(view: view, scale: 0.1)
|
||||
}
|
||||
}
|
||||
|
||||
if let view = self.scrubber?.view {
|
||||
@ -638,35 +641,30 @@ final class MediaEditorScreenComponent: Component {
|
||||
view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2)
|
||||
}
|
||||
|
||||
if let view = self.undoButton.view {
|
||||
transition.setAlpha(view: view, alpha: 0.0)
|
||||
transition.setScale(view: view, scale: 0.1)
|
||||
}
|
||||
let stickerButtons = [
|
||||
self.undoButton,
|
||||
self.eraseButton,
|
||||
self.restoreButton,
|
||||
self.outlineButton,
|
||||
self.cutoutButton
|
||||
]
|
||||
|
||||
if let view = self.eraseButton.view {
|
||||
transition.setAlpha(view: view, alpha: 0.0)
|
||||
transition.setScale(view: view, scale: 0.1)
|
||||
}
|
||||
|
||||
if let view = self.restoreButton.view {
|
||||
transition.setAlpha(view: view, alpha: 0.0)
|
||||
transition.setScale(view: view, scale: 0.1)
|
||||
}
|
||||
|
||||
if let view = self.outlineButton.view {
|
||||
transition.setAlpha(view: view, alpha: 0.0)
|
||||
transition.setScale(view: view, scale: 0.1)
|
||||
}
|
||||
|
||||
if let view = self.cutoutButton.view {
|
||||
transition.setAlpha(view: view, alpha: 0.0)
|
||||
transition.setScale(view: view, scale: 0.1)
|
||||
for button in stickerButtons {
|
||||
if let view = button.view {
|
||||
transition.setAlpha(view: view, alpha: 0.0)
|
||||
transition.setScale(view: view, scale: 0.1)
|
||||
}
|
||||
}
|
||||
|
||||
if let view = self.textSize.view {
|
||||
transition.setAlpha(view: view, alpha: 0.0)
|
||||
transition.setScale(view: view, scale: 0.1)
|
||||
}
|
||||
|
||||
if let view = self.selectionButton.view {
|
||||
transition.setAlpha(view: view, alpha: 0.0)
|
||||
transition.setScale(view: view, scale: 0.1)
|
||||
}
|
||||
}
|
||||
|
||||
func animateOutToTool(inPlace: Bool, transition: ComponentTransition) {
|
||||
@ -2000,135 +1998,6 @@ final class MediaEditorScreenComponent: Component {
|
||||
transition.setScale(view: switchCameraButtonView, scale: isRecordingAdditionalVideo ? 1.0 : 0.01)
|
||||
transition.setAlpha(view: switchCameraButtonView, alpha: isRecordingAdditionalVideo ? 1.0 : 0.0)
|
||||
}
|
||||
|
||||
if controller.node.items.count > 1 {
|
||||
let selectionButtonSize = self.selectionButton.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(PlainButtonComponent(
|
||||
content: AnyComponent(
|
||||
SelectionPanelButtonContentComponent(
|
||||
count: Int32(controller.node.items.count(where: { $0.isEnabled })),
|
||||
isSelected: self.isSelectionPanelOpen,
|
||||
tag: nil
|
||||
)
|
||||
),
|
||||
effectAlignment: .center,
|
||||
action: { [weak self, weak controller] in
|
||||
if let self, let controller {
|
||||
self.isSelectionPanelOpen = !self.isSelectionPanelOpen
|
||||
if let mediaEditor = controller.node.mediaEditor {
|
||||
if self.isSelectionPanelOpen {
|
||||
mediaEditor.maybePauseVideo()
|
||||
} else {
|
||||
Queue.mainQueue().after(0.1) {
|
||||
mediaEditor.maybeUnpauseVideo()
|
||||
}
|
||||
}
|
||||
}
|
||||
self.state?.updated()
|
||||
|
||||
controller.hapticFeedback.impact(.light)
|
||||
}
|
||||
},
|
||||
animateAlpha: false
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 33.0, height: 33.0)
|
||||
)
|
||||
let selectionButtonFrame = CGRect(
|
||||
origin: CGPoint(x: availableSize.width - selectionButtonSize.width - 12.0, y: inputPanelFrame.minY - selectionButtonSize.height - 3.0),
|
||||
size: selectionButtonSize
|
||||
)
|
||||
if let selectionButtonView = self.selectionButton.view as? PlainButtonComponent.View {
|
||||
if selectionButtonView.superview == nil {
|
||||
self.addSubview(selectionButtonView)
|
||||
}
|
||||
transition.setPosition(view: selectionButtonView, position: selectionButtonFrame.center)
|
||||
transition.setBounds(view: selectionButtonView, bounds: CGRect(origin: .zero, size: selectionButtonFrame.size))
|
||||
transition.setScale(view: selectionButtonView, scale: displayTopButtons && !isRecordingAdditionalVideo ? 1.0 : 0.01)
|
||||
transition.setAlpha(view: selectionButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities && !isRecordingAdditionalVideo ? 1.0 : 0.0)
|
||||
|
||||
if self.isSelectionPanelOpen {
|
||||
let selectionPanelFrame = CGRect(
|
||||
origin: CGPoint(x: 12.0, y: inputPanelFrame.minY - selectionButtonSize.height - 3.0 - 130.0),
|
||||
size: CGSize(width: availableSize.width - 24.0, height: 120.0)
|
||||
)
|
||||
|
||||
var selectedItemId = ""
|
||||
if case let .asset(asset) = controller.node.subject {
|
||||
selectedItemId = asset.localIdentifier
|
||||
}
|
||||
|
||||
let _ = self.selectionPanel.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
SelectionPanelComponent(
|
||||
previewContainerView: controller.node.previewContentContainerView,
|
||||
frame: selectionPanelFrame,
|
||||
items: controller.node.items,
|
||||
selectedItemId: selectedItemId,
|
||||
itemTapped: { [weak self, weak controller] id in
|
||||
guard let self, let controller else {
|
||||
return
|
||||
}
|
||||
self.isSelectionPanelOpen = false
|
||||
self.state?.updated(transition: id == nil ? .spring(duration: 0.3) : .immediate)
|
||||
|
||||
if let id {
|
||||
controller.node.switchToItem(id)
|
||||
|
||||
controller.hapticFeedback.impact(.light)
|
||||
}
|
||||
},
|
||||
itemSelectionToggled: { [weak self, weak controller] id in
|
||||
guard let self, let controller else {
|
||||
return
|
||||
}
|
||||
if let itemIndex = controller.node.items.firstIndex(where: { $0.asset.localIdentifier == id }) {
|
||||
controller.node.items[itemIndex].isEnabled = !controller.node.items[itemIndex].isEnabled
|
||||
}
|
||||
self.state?.updated(transition: .spring(duration: 0.3))
|
||||
},
|
||||
itemReordered: { [weak self, weak controller] fromId, toId in
|
||||
guard let self, let controller else {
|
||||
return
|
||||
}
|
||||
guard let fromIndex = controller.node.items.firstIndex(where: { $0.asset.localIdentifier == fromId }), let toIndex = controller.node.items.firstIndex(where: { $0.asset.localIdentifier == toId }), toIndex < controller.node.items.count else {
|
||||
return
|
||||
}
|
||||
let fromItem = controller.node.items[fromIndex]
|
||||
let toItem = controller.node.items[toIndex]
|
||||
controller.node.items[fromIndex] = toItem
|
||||
controller.node.items[toIndex] = fromItem
|
||||
self.state?.updated(transition: .spring(duration: 0.3))
|
||||
|
||||
controller.hapticFeedback.tap()
|
||||
}
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
if let selectionPanelView = self.selectionPanel.view as? SelectionPanelComponent.View {
|
||||
if selectionPanelView.superview == nil {
|
||||
self.insertSubview(selectionPanelView, belowSubview: selectionButtonView)
|
||||
if let buttonView = selectionButtonView.contentView as? SelectionPanelButtonContentComponent.View {
|
||||
selectionPanelView.animateIn(from: buttonView)
|
||||
}
|
||||
}
|
||||
selectionPanelView.frame = CGRect(origin: .zero, size: availableSize)
|
||||
}
|
||||
} else if let selectionPanelView = self.selectionPanel.view as? SelectionPanelComponent.View {
|
||||
if !transition.animation.isImmediate, let buttonView = selectionButtonView.contentView as? SelectionPanelButtonContentComponent.View {
|
||||
selectionPanelView.animateOut(to: buttonView, completion: { [weak selectionPanelView] in
|
||||
selectionPanelView?.removeFromSuperview()
|
||||
})
|
||||
} else {
|
||||
selectionPanelView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
inputPanelSize = CGSize(width: 0.0, height: 12.0)
|
||||
}
|
||||
@ -2136,20 +2005,24 @@ final class MediaEditorScreenComponent: Component {
|
||||
if case .stickerEditor = controller.mode {
|
||||
|
||||
} else {
|
||||
var selectionButtonInset: CGFloat = 0.0
|
||||
|
||||
if let playerState = state.playerState {
|
||||
let scrubberInset: CGFloat = 9.0
|
||||
|
||||
let minDuration: Double
|
||||
let maxDuration: Double
|
||||
var segmentDuration: Double?
|
||||
if playerState.isAudioOnly {
|
||||
minDuration = 5.0
|
||||
maxDuration = 15.0
|
||||
} else {
|
||||
minDuration = 1.0
|
||||
if case .avatarEditor = controller.mode {
|
||||
maxDuration = 10.0
|
||||
maxDuration = 9.9
|
||||
} else {
|
||||
maxDuration = storyMaxVideoDuration
|
||||
maxDuration = storyMaxCombinedVideoDuration
|
||||
segmentDuration = storyMaxVideoDuration
|
||||
}
|
||||
}
|
||||
|
||||
@ -2224,6 +2097,7 @@ final class MediaEditorScreenComponent: Component {
|
||||
position: playerState.position,
|
||||
minDuration: minDuration,
|
||||
maxDuration: maxDuration,
|
||||
segmentDuration: segmentDuration,
|
||||
isPlaying: playerState.isPlaying,
|
||||
tracks: visibleTracks,
|
||||
isCollage: isCollage,
|
||||
@ -2363,6 +2237,7 @@ final class MediaEditorScreenComponent: Component {
|
||||
}
|
||||
|
||||
let scrubberFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - scrubberSize.width) / 2.0), y: availableSize.height - environment.safeInsets.bottom - scrubberSize.height + controlsBottomInset - inputPanelSize.height + 3.0 - scrubberBottomOffset), size: scrubberSize)
|
||||
selectionButtonInset = scrubberSize.height + 11.0
|
||||
if let scrubberView = scrubber.view {
|
||||
var animateIn = false
|
||||
if scrubberView.superview == nil {
|
||||
@ -2407,6 +2282,146 @@ final class MediaEditorScreenComponent: Component {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if controller.node.items.count > 1 {
|
||||
let selectionButtonSize = self.selectionButton.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(PlainButtonComponent(
|
||||
content: AnyComponent(
|
||||
SelectionPanelButtonContentComponent(
|
||||
count: Int32(controller.node.items.count(where: { $0.isEnabled })),
|
||||
isSelected: self.isSelectionPanelOpen,
|
||||
tag: nil
|
||||
)
|
||||
),
|
||||
effectAlignment: .center,
|
||||
action: { [weak self, weak controller] in
|
||||
if let self, let controller {
|
||||
self.isSelectionPanelOpen = !self.isSelectionPanelOpen
|
||||
if let mediaEditor = controller.node.mediaEditor {
|
||||
if self.isSelectionPanelOpen {
|
||||
mediaEditor.maybePauseVideo()
|
||||
} else {
|
||||
Queue.mainQueue().after(0.1) {
|
||||
mediaEditor.maybeUnpauseVideo()
|
||||
}
|
||||
}
|
||||
}
|
||||
self.state?.updated(transition: .spring(duration: 0.3))
|
||||
|
||||
controller.hapticFeedback.impact(.light)
|
||||
}
|
||||
},
|
||||
animateAlpha: false
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 33.0, height: 33.0)
|
||||
)
|
||||
let selectionButtonFrame = CGRect(
|
||||
origin: CGPoint(x: availableSize.width - selectionButtonSize.width - 12.0, y: availableSize.height - environment.safeInsets.bottom - selectionButtonSize.height + controlsBottomInset - inputPanelSize.height - 3.0 - selectionButtonInset),
|
||||
size: selectionButtonSize
|
||||
)
|
||||
if let selectionButtonView = self.selectionButton.view as? PlainButtonComponent.View {
|
||||
if selectionButtonView.superview == nil {
|
||||
self.addSubview(selectionButtonView)
|
||||
}
|
||||
transition.setPosition(view: selectionButtonView, position: selectionButtonFrame.center)
|
||||
transition.setBounds(view: selectionButtonView, bounds: CGRect(origin: .zero, size: selectionButtonFrame.size))
|
||||
transition.setScale(view: selectionButtonView, scale: displayTopButtons && !isRecordingAdditionalVideo ? 1.0 : 0.01)
|
||||
transition.setAlpha(view: selectionButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities && !isRecordingAdditionalVideo ? 1.0 : 0.0)
|
||||
|
||||
if self.isSelectionPanelOpen {
|
||||
let selectionPanelFrame = CGRect(
|
||||
origin: CGPoint(x: 12.0, y: selectionButtonFrame.minY - 130.0),
|
||||
size: CGSize(width: availableSize.width - 24.0, height: 120.0)
|
||||
)
|
||||
|
||||
var selectedItemId = ""
|
||||
if case let .asset(asset) = controller.node.subject {
|
||||
selectedItemId = asset.localIdentifier
|
||||
}
|
||||
|
||||
let selectionPanel: ComponentView<Empty>
|
||||
if let current = self.selectionPanel {
|
||||
selectionPanel = current
|
||||
} else {
|
||||
selectionPanel = ComponentView<Empty>()
|
||||
self.selectionPanel = selectionPanel
|
||||
}
|
||||
|
||||
let _ = selectionPanel.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
SelectionPanelComponent(
|
||||
previewContainerView: controller.node.previewContentContainerView,
|
||||
frame: selectionPanelFrame,
|
||||
items: controller.node.items,
|
||||
selectedItemId: selectedItemId,
|
||||
itemTapped: { [weak self, weak controller] id in
|
||||
guard let self, let controller else {
|
||||
return
|
||||
}
|
||||
self.isSelectionPanelOpen = false
|
||||
self.state?.updated(transition: id == nil ? .spring(duration: 0.3) : .immediate)
|
||||
|
||||
if let id {
|
||||
controller.node.switchToItem(id)
|
||||
|
||||
controller.hapticFeedback.impact(.light)
|
||||
}
|
||||
},
|
||||
itemSelectionToggled: { [weak self, weak controller] id in
|
||||
guard let self, let controller else {
|
||||
return
|
||||
}
|
||||
if let itemIndex = controller.node.items.firstIndex(where: { $0.asset.localIdentifier == id }) {
|
||||
controller.node.items[itemIndex].isEnabled = !controller.node.items[itemIndex].isEnabled
|
||||
}
|
||||
self.state?.updated(transition: .spring(duration: 0.3))
|
||||
},
|
||||
itemReordered: { [weak self, weak controller] fromId, toId in
|
||||
guard let self, let controller else {
|
||||
return
|
||||
}
|
||||
guard let fromIndex = controller.node.items.firstIndex(where: { $0.asset.localIdentifier == fromId }), let toIndex = controller.node.items.firstIndex(where: { $0.asset.localIdentifier == toId }), toIndex < controller.node.items.count else {
|
||||
return
|
||||
}
|
||||
let fromItem = controller.node.items[fromIndex]
|
||||
let toItem = controller.node.items[toIndex]
|
||||
controller.node.items[fromIndex] = toItem
|
||||
controller.node.items[toIndex] = fromItem
|
||||
self.state?.updated(transition: .spring(duration: 0.3))
|
||||
|
||||
controller.hapticFeedback.tap()
|
||||
}
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
if let selectionPanelView = selectionPanel.view as? SelectionPanelComponent.View {
|
||||
if selectionPanelView.superview == nil {
|
||||
self.insertSubview(selectionPanelView, belowSubview: selectionButtonView)
|
||||
if let buttonView = selectionButtonView.contentView as? SelectionPanelButtonContentComponent.View {
|
||||
selectionPanelView.animateIn(from: buttonView)
|
||||
}
|
||||
}
|
||||
selectionPanelView.frame = CGRect(origin: .zero, size: availableSize)
|
||||
}
|
||||
} else if let selectionPanel = self.selectionPanel {
|
||||
self.selectionPanel = nil
|
||||
if let selectionPanelView = selectionPanel.view as? SelectionPanelComponent.View {
|
||||
if !transition.animation.isImmediate, let buttonView = selectionButtonView.contentView as? SelectionPanelButtonContentComponent.View {
|
||||
selectionPanelView.animateOut(to: buttonView, completion: { [weak selectionPanelView] in
|
||||
selectionPanelView?.removeFromSuperview()
|
||||
})
|
||||
} else {
|
||||
selectionPanelView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if case .stickerEditor = controller.mode {
|
||||
@ -2821,6 +2836,8 @@ final class MediaEditorScreenComponent: Component {
|
||||
|
||||
let storyDimensions = CGSize(width: 1080.0, height: 1920.0)
|
||||
let storyMaxVideoDuration: Double = 60.0
|
||||
let storyMaxCombinedVideoCount: Int = 3
|
||||
let storyMaxCombinedVideoDuration: Double = storyMaxVideoDuration * Double(storyMaxCombinedVideoCount)
|
||||
|
||||
public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UIDropInteractionDelegate {
|
||||
public enum Mode {
|
||||
@ -3489,6 +3506,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
values: initialValues,
|
||||
hasHistogram: true
|
||||
)
|
||||
mediaEditor.maxDuration = storyMaxCombinedVideoDuration
|
||||
if case .avatarEditor = controller.mode {
|
||||
mediaEditor.setVideoIsMuted(true)
|
||||
} else if case let .coverEditor(dimensions) = controller.mode {
|
||||
@ -5075,7 +5093,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
var audioTrimRange: Range<Double>?
|
||||
var audioOffset: Double?
|
||||
|
||||
if let videoDuration = mediaEditor.originalDuration {
|
||||
if let videoDuration = mediaEditor.originalCappedDuration {
|
||||
if let videoStart = mediaEditor.values.videoTrimRange?.lowerBound {
|
||||
audioOffset = -videoStart
|
||||
} else if let _ = mediaEditor.values.additionalVideoPath, let videoStart = mediaEditor.values.additionalVideoTrimRange?.lowerBound {
|
||||
@ -6694,7 +6712,10 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
}
|
||||
self.postingAvailabilityDisposable = (self.postingAvailabilityPromise.get()
|
||||
|> deliverOnMainQueue).start(next: { [weak self] availability in
|
||||
guard let self, availability != .available else {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if case .available = availability {
|
||||
return
|
||||
}
|
||||
|
||||
@ -7341,36 +7362,21 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
return true
|
||||
}
|
||||
|
||||
private func completeWithMultipleResults(results: [MediaEditorScreenImpl.Result]) {
|
||||
// Send all results to completion handler
|
||||
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 processMultipleItems() {
|
||||
guard !self.node.items.isEmpty else {
|
||||
private func processMultipleItems(items: [EditingItem]) {
|
||||
guard !items.isEmpty else {
|
||||
return
|
||||
}
|
||||
|
||||
if let mediaEditor = self.node.mediaEditor, case let .asset(asset) = self.node.subject, let currentItemIndex = self.node.items.firstIndex(where: { $0.asset.localIdentifier == asset.localIdentifier }) {
|
||||
let entities = self.node.entitiesView.entities.filter { !($0 is DrawingMediaEntity) }
|
||||
let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.node.entitiesView)
|
||||
mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities)
|
||||
|
||||
var updatedCurrentItem = self.node.items[currentItemIndex]
|
||||
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
|
||||
self.node.items[currentItemIndex] = updatedCurrentItem
|
||||
items[currentItemIndex] = updatedCurrentItem
|
||||
}
|
||||
|
||||
let multipleResults = Atomic<[MediaEditorScreenImpl.Result]>(value: [])
|
||||
let totalItems = self.node.items.count
|
||||
let totalItems = items.count
|
||||
|
||||
let dispatchGroup = DispatchGroup()
|
||||
|
||||
@ -7387,7 +7393,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
}
|
||||
|
||||
var order: [Int64] = []
|
||||
for (index, item) in self.node.items.enumerated() {
|
||||
for (index, item) in items.enumerated() {
|
||||
guard item.isEnabled else {
|
||||
continue
|
||||
}
|
||||
@ -7431,7 +7437,14 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
orderedResults.append(item)
|
||||
}
|
||||
}
|
||||
self.completeWithMultipleResults(results: orderedResults)
|
||||
self.completion(results, { [weak self] finished in
|
||||
self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in
|
||||
self?.dismiss()
|
||||
Queue.mainQueue().justDispatch {
|
||||
finished()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -7452,13 +7465,10 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
if let mediaArea = entity.mediaArea {
|
||||
mediaAreas.append(mediaArea)
|
||||
}
|
||||
|
||||
// Extract stickers from entities
|
||||
extractStickersFromEntity(entity, into: &stickers)
|
||||
}
|
||||
}
|
||||
|
||||
// Process video
|
||||
let firstFrameTime: CMTime
|
||||
if let coverImageTimestamp = item.values?.coverImageTimestamp {
|
||||
firstFrameTime = CMTime(seconds: coverImageTimestamp, preferredTimescale: CMTimeScale(60))
|
||||
@ -7476,7 +7486,6 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate duration
|
||||
let duration: Double
|
||||
if let videoTrimRange = item.values?.videoTrimRange {
|
||||
duration = videoTrimRange.upperBound - videoTrimRange.lowerBound
|
||||
@ -7484,7 +7493,6 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
duration = min(asset.duration, storyMaxVideoDuration)
|
||||
}
|
||||
|
||||
// Generate thumbnail frame
|
||||
let avAssetGenerator = AVAssetImageGenerator(asset: avAsset)
|
||||
avAssetGenerator.appliesPreferredTrackTransform = true
|
||||
avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)]) { [weak self] _, cgImage, _, _, _ in
|
||||
@ -7541,14 +7549,11 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
private func processImageItem(item: EditingItem, index: Int, randomId: Int64, completion: @escaping (MediaEditorScreenImpl.Result) -> Void) {
|
||||
let asset = item.asset
|
||||
|
||||
// Setup temporary media editor for this item
|
||||
let itemMediaEditor = setupMediaEditorForItem(item: item)
|
||||
|
||||
// Get caption for this item
|
||||
var caption = item.caption
|
||||
caption = convertMarkdownToAttributes(caption)
|
||||
|
||||
// Media areas and stickers
|
||||
var mediaAreas: [MediaArea] = []
|
||||
var stickers: [TelegramMediaFile] = []
|
||||
|
||||
@ -7557,13 +7562,10 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
if let mediaArea = entity.mediaArea {
|
||||
mediaAreas.append(mediaArea)
|
||||
}
|
||||
|
||||
// Extract stickers from entities
|
||||
extractStickersFromEntity(entity, into: &stickers)
|
||||
}
|
||||
}
|
||||
|
||||
// Request full-size image
|
||||
let options = PHImageRequestOptions()
|
||||
options.deliveryMode = .highQualityFormat
|
||||
options.isNetworkAccessAllowed = true
|
||||
@ -7664,10 +7666,6 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
guard let mediaEditor = self.node.mediaEditor, let subject = self.node.subject, let actualSubject = self.node.actualSubject 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)
|
||||
|
||||
var caption = self.node.getCaption()
|
||||
caption = convertMarkdownToAttributes(caption)
|
||||
@ -7680,6 +7678,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
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 {
|
||||
@ -8108,6 +8107,15 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private 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)
|
||||
}
|
||||
|
||||
private var didComplete = false
|
||||
func requestStoryCompletion(animated: Bool) {
|
||||
@ -8117,7 +8125,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
|
||||
self.didComplete = true
|
||||
|
||||
self.dismissAllTooltips()
|
||||
self.updateMediaEditorEntities()
|
||||
|
||||
mediaEditor.stop()
|
||||
mediaEditor.invalidate()
|
||||
@ -8127,11 +8135,42 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
navigationController.updateRootContainerTransitionOffset(0.0, transition: .immediate)
|
||||
}
|
||||
|
||||
if self.node.items.count(where: { $0.isEnabled }) > 1 {
|
||||
self.processMultipleItems()
|
||||
var multipleItems: [EditingItem] = []
|
||||
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 _ in 0 ..< storyCount {
|
||||
let trimmedValues = values.withUpdatedVideoTrimRange(start ..< min(start + storyMaxVideoDuration, originalDuration))
|
||||
|
||||
var editingItem = EditingItem(asset: asset)
|
||||
editingItem.caption = self.node.getCaption()
|
||||
editingItem.values = trimmedValues
|
||||
multipleItems.append(editingItem)
|
||||
|
||||
start += storyMaxVideoDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if multipleItems.count > 1 {
|
||||
self.processMultipleItems(items: multipleItems)
|
||||
} else {
|
||||
self.processSingleItem()
|
||||
}
|
||||
|
||||
self.dismissAllTooltips()
|
||||
}
|
||||
|
||||
func requestStickerCompletion(animated: Bool) {
|
||||
@ -8157,10 +8196,8 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
navigationController.updateRootContainerTransitionOffset(0.0, transition: .immediate)
|
||||
}
|
||||
|
||||
let entities = self.node.entitiesView.entities.filter { !($0 is DrawingMediaEntity) }
|
||||
let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.node.entitiesView)
|
||||
mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities)
|
||||
|
||||
self.updateMediaEditorEntities()
|
||||
|
||||
if let image = mediaEditor.resultImage {
|
||||
let values = mediaEditor.values.withUpdatedQualityPreset(.sticker)
|
||||
makeEditorImageComposition(context: self.node.ciContext, postbox: self.context.account.postbox, inputImage: image, dimensions: storyDimensions, outputDimensions: CGSize(width: 512, height: 512), values: values, time: .zero, textScale: 2.0, completion: { [weak self] resultImage in
|
||||
@ -8181,11 +8218,9 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
if let navigationController = self.navigationController as? NavigationController {
|
||||
navigationController.updateRootContainerTransitionOffset(0.0, transition: .immediate)
|
||||
}
|
||||
|
||||
let entities = self.node.entitiesView.entities.filter { !($0 is DrawingMediaEntity) }
|
||||
let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.node.entitiesView)
|
||||
mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities)
|
||||
|
||||
|
||||
self.updateMediaEditorEntities()
|
||||
|
||||
if let image = mediaEditor.resultImage {
|
||||
let values = mediaEditor.values.withUpdatedCoverDimensions(dimensions)
|
||||
makeEditorImageComposition(context: self.node.ciContext, postbox: self.context.account.postbox, inputImage: image, dimensions: storyDimensions, outputDimensions: dimensions.aspectFitted(CGSize(width: 1080, height: 1080)), values: values, time: .zero, textScale: 2.0, completion: { [weak self] resultImage in
|
||||
@ -8786,12 +8821,8 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
return
|
||||
}
|
||||
|
||||
let context = self.context
|
||||
self.updateMediaEditorEntities()
|
||||
|
||||
let entities = self.node.entitiesView.entities.filter { !($0 is DrawingMediaEntity) }
|
||||
let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.node.entitiesView)
|
||||
mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities)
|
||||
|
||||
let isSticker = toStickerResource != nil
|
||||
if !isSticker {
|
||||
self.previousSavedValues = mediaEditor.values
|
||||
@ -8820,6 +8851,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
})
|
||||
}
|
||||
|
||||
let context = self.context
|
||||
if mediaEditor.resultIsVideo {
|
||||
if !isSticker {
|
||||
mediaEditor.maybePauseVideo()
|
||||
|
@ -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 ..< segmentCount {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -613,6 +613,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(
|
||||
@ -683,6 +685,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
|
||||
@ -752,6 +756,8 @@ private final class PeerInfoInteraction {
|
||||
self.openBirthdayContextMenu = openBirthdayContextMenu
|
||||
self.editingOpenAffiliateProgram = editingOpenAffiliateProgram
|
||||
self.editingOpenVerifyAccounts = editingOpenVerifyAccounts
|
||||
self.editingToggleAutoTranslate = editingToggleAutoTranslate
|
||||
self.displayAutoTranslateLocked = displayAutoTranslateLocked
|
||||
self.getController = getController
|
||||
}
|
||||
}
|
||||
@ -2154,6 +2160,7 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL
|
||||
let ItemBanned = 11
|
||||
let ItemRecentActions = 12
|
||||
let ItemAffiliatePrograms = 13
|
||||
let ItemPeerAutoTranslate = 14
|
||||
|
||||
let isCreator = channel.flags.contains(.isCreator)
|
||||
|
||||
@ -2268,6 +2275,18 @@ 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()
|
||||
}))
|
||||
|
||||
var isLocked = true
|
||||
if let approximateBoostLevel = channel.approximateBoostLevel, approximateBoostLevel >= 3 {
|
||||
isLocked = false
|
||||
}
|
||||
items[.peerSettings]!.append(PeerInfoScreenSwitchItem(id: ItemPeerAutoTranslate, text: presentationData.strings.Channel_Info_AutoTranslate, value: false, icon: UIImage(bundleImageName: "Settings/Menu/AutoTranslate"), isLocked: isLocked, toggled: { value in
|
||||
if isLocked {
|
||||
interaction.displayAutoTranslateLocked()
|
||||
} else {
|
||||
interaction.editingToggleAutoTranslate(value)
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
var canEditMembers = false
|
||||
@ -3194,6 +3213,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
|
||||
@ -9127,6 +9156,28 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
}
|
||||
}
|
||||
|
||||
private func toggleAutoTranslate(isEnabled: Bool) {
|
||||
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -608,9 +608,9 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
|
||||
},
|
||||
updateResellStars: { [weak self] price in
|
||||
guard let self, let reference = product.reference else {
|
||||
return
|
||||
return .never()
|
||||
}
|
||||
self.profileGifts.updateStarGiftResellPrice(reference: reference, price: price)
|
||||
return self.profileGifts.updateStarGiftResellPrice(reference: reference, price: price)
|
||||
},
|
||||
togglePinnedToTop: { [weak self] pinnedToTop in
|
||||
guard let self else {
|
||||
|
@ -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 {
|
||||
@ -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 {
|
||||
|
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))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -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> {
|
||||
|
Loading…
x
Reference in New Issue
Block a user