Various improvements

This commit is contained in:
Ilya Laktyushin 2025-04-22 21:06:46 +04:00
parent 14064f94eb
commit 2962851527
34 changed files with 1086 additions and 508 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,8 +864,13 @@ 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
switch error {
case let .starGiftResellTooEarly(value):
return .fail(.starGiftResellTooEarly(value))
default:
return .fail(.generic)
}
}
|> mapToSignal { paymentForm in
if let paymentForm {
return _internal_sendStarsPaymentForm(account: account, formId: paymentForm.id, source: source)
@ -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,12 +1526,21 @@ private final class ProfileGiftsContextImpl {
}
}
func updateStarGiftResellPrice(reference: StarGiftReference, price: Int64?) {
self.actionDisposable.set(
_internal_updateStarGiftResalePrice(account: self.account, reference: reference, price: price).startStrict()
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
@ -1542,6 +1568,12 @@ private final class ProfileGiftsContextImpl {
}
self.pushState()
subscriber.putCompletion()
})
)
return disposable
}
}
func toggleStarGiftsNotifications(enabled: Bool) {
@ -1939,9 +1971,17 @@ public final class ProfileGiftsContext {
}
}
public func updateStarGiftResellPrice(reference: StarGiftReference, price: Int64?) {
public func updateStarGiftResellPrice(reference: StarGiftReference, price: Int64?) -> Signal<Never, UpdateStarGiftPriceError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
impl.updateStarGiftResellPrice(reference: reference, price: price)
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 {
|> mapToSignal { updates -> Signal<Void, UpdateStarGiftPriceError> in
account.stateManager.addUpdates(updates)
}
return .complete()
}
|> ignoreValues
@ -2497,6 +2535,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(
sorting: self.sorting,
@ -2585,6 +2683,34 @@ public final class ResaleGiftsContext {
}
}
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?
self.impl.syncWith { impl in

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -82,6 +82,7 @@ final class GiftSetupScreenComponent: Component {
private let navigationTitle = ComponentView<Empty>()
private let remainingCount = ComponentView<Empty>()
private let resaleSection = ComponentView<Empty>()
private let introContent = ComponentView<Empty>()
private let introSection = ComponentView<Empty>()
private let starsSection = ComponentView<Empty>()
@ -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>] = []

View File

@ -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
}
} else {
return nil
}
@ -253,15 +242,14 @@ final class GiftStoreScreenComponent: Component {
}
let giftController = GiftViewScreen(
context: component.context,
subject: .uniqueGift(uniqueGift, state.peerId)
subject: .uniqueGift(uniqueGift, state.peerId),
buyGift: { slug, peerId in
return self.state?.starGiftsContext.buyStarGift(slug: slug, peerId: peerId) ?? .complete()
},
updateResellStars: { price in
return self.state?.starGiftsContext.updateStarGiftResellPrice(slug: uniqueGift.slug, price: price) ?? .complete()
}
)
giftController.onBuySuccess = { [weak self] in
guard let self else {
return
}
self.removedStarGifts.insert(uniqueGift.slug)
self.state?.updated(transition: .spring(duration: 0.3))
}
mainController.push(giftController)
}
}
@ -507,6 +495,7 @@ final class GiftStoreScreenComponent: Component {
//TODO:localize
var items: [ContextMenuItem] = []
if modelAttributes.count >= 8 {
items.append(.custom(SearchContextItem(
context: component.context,
placeholder: "Search",
@ -516,6 +505,7 @@ final class GiftStoreScreenComponent: Component {
}
), false))
items.append(.separator)
}
items.append(.custom(GiftAttributeListContextItem(
context: component.context,
attributes: modelAttributes,
@ -597,6 +587,7 @@ final class GiftStoreScreenComponent: Component {
//TODO:localize
var items: [ContextMenuItem] = []
if backdropAttributes.count >= 8 {
items.append(.custom(SearchContextItem(
context: component.context,
placeholder: "Search",
@ -606,6 +597,7 @@ final class GiftStoreScreenComponent: Component {
}
), false))
items.append(.separator)
}
items.append(.custom(GiftAttributeListContextItem(
context: component.context,
attributes: backdropAttributes,
@ -687,6 +679,7 @@ final class GiftStoreScreenComponent: Component {
//TODO:localize
var items: [ContextMenuItem] = []
if patternAttributes.count >= 8 {
items.append(.custom(SearchContextItem(
context: component.context,
placeholder: "Search",
@ -696,6 +689,7 @@ final class GiftStoreScreenComponent: Component {
}
), false))
items.append(.separator)
}
items.append(.custom(GiftAttributeListContextItem(
context: component.context,
attributes: patternAttributes,

View File

@ -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,7 +3423,10 @@ public class GiftViewScreen: ViewControllerComponentContainer {
guard let self else {
return
}
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))))
@ -3434,7 +3435,6 @@ public class GiftViewScreen: ViewControllerComponentContainer {
default:
break
}
self.onBuySuccess()
let text = "\(giftTitle) is removed from sale."
let tooltipController = UndoOverlayController(
@ -3455,14 +3455,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
}
)
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()
}
})
}),
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
})
@ -3476,6 +3469,14 @@ public class GiftViewScreen: ViewControllerComponentContainer {
return
}
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
}
switch self.subject {
case let .profileGift(peerId, currentSubject):
self.subject = .profileGift(peerId, currentSubject.withGift(.unique(gift.withResellStars(price))))
@ -3508,14 +3509,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
}
)
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()
}
})
})
self.push(resellController)
}

View File

@ -353,6 +353,8 @@ public final class MediaEditor {
}
}
public var maxDuration: Double = 60.0
public var duration: Double? {
if let stickerEntity = self.stickerEntity {
return stickerEntity.totalDuration
@ -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
}

View File

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

View File

@ -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 {
let topButtons = [
self.saveButton,
self.muteButton,
self.playbackButton
]
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.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)
}
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 {
let stickerButtons = [
self.undoButton,
self.eraseButton,
self.restoreButton,
self.outlineButton,
self.cutoutButton
]
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.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)
}
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
@ -7665,10 +7667,6 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
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 {
@ -8109,6 +8108,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) {
guard let mediaEditor = self.node.mediaEditor, !self.didComplete else {
@ -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,9 +8196,7 @@ 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)
@ -8182,9 +8219,7 @@ 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.withUpdatedCoverDimensions(dimensions)
@ -8786,11 +8821,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
return
}
let context = self.context
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()
let isSticker = toStickerResource != nil
if !isSticker {
@ -8820,6 +8851,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
})
}
let context = self.context
if mediaEditor.resultIsVideo {
if !isSticker {
mediaEditor.maybePauseVideo()

View File

@ -84,6 +84,7 @@ public final class MediaScrubberComponent: Component {
let position: Double
let minDuration: Double
let maxDuration: Double
let segmentDuration: Double?
let isPlaying: Bool
let tracks: [Track]
@ -112,6 +113,7 @@ public final class MediaScrubberComponent: Component {
position: Double,
minDuration: Double,
maxDuration: Double,
segmentDuration: Double? = nil,
isPlaying: Bool,
tracks: [Track],
isCollage: Bool,
@ -135,6 +137,7 @@ public final class MediaScrubberComponent: Component {
self.position = position
self.minDuration = minDuration
self.maxDuration = maxDuration
self.segmentDuration = segmentDuration
self.isPlaying = isPlaying
self.tracks = tracks
self.isCollage = isCollage
@ -171,6 +174,9 @@ public final class MediaScrubberComponent: Component {
if lhs.maxDuration != rhs.maxDuration {
return false
}
if lhs.segmentDuration != rhs.segmentDuration {
return false
}
if lhs.isPlaying != rhs.isPlaying {
return false
}
@ -624,6 +630,7 @@ public final class MediaScrubberComponent: Component {
isSelected: isSelected,
availableSize: availableSize,
duration: self.duration,
segmentDuration: lowestVideoId == track.id ? component.segmentDuration : nil,
transition: trackTransition
)
trackLayout[id] = (CGRect(origin: CGPoint(x: 0.0, y: totalHeight), size: trackSize), trackTransition, animateTrackIn)
@ -675,6 +682,7 @@ public final class MediaScrubberComponent: Component {
isSelected: false,
availableSize: availableSize,
duration: self.duration,
segmentDuration: nil,
transition: trackTransition
)
trackTransition.setFrame(view: trackView, frame: CGRect(origin: .zero, size: trackSize))
@ -956,6 +964,9 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega
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] = []
fileprivate let videoOpaqueFramesContainer = UIView()
@ -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
}
}

View File

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

View File

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

View File

@ -173,6 +173,8 @@ private final class StarsTransactionSheetContent: CombinedComponent {
let spaceRegex = try? NSRegularExpression(pattern: "\\[(.*?)\\]", options: [])
let giftCompositionExternalState = GiftCompositionComponent.ExternalState()
return { context in
let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
let controller = environment.controller
@ -366,8 +368,14 @@ private final class StarsTransactionSheetContent: CombinedComponent {
}
case let .transaction(transaction, parentPeer):
if let starGift = transaction.starGift {
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)"
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))
)
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 {
@ -1317,7 +1359,17 @@ private final class StarsTransactionSheetContent: CombinedComponent {
state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: linkColor)!, theme)
}
let textColor = countOnTop && !isSubscriber ? theme.list.itemPrimaryTextColor : textColor
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 markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
})
@ -1362,7 +1414,13 @@ private final class StarsTransactionSheetContent: CombinedComponent {
context.add(description
.position(CGPoint(x: context.availableSize.width / 2.0, y: descriptionOrigin + description.size.height / 2.0))
)
originY += description.size.height + 10.0
originY += description.size.height
if case .unique = giftAnimationSubject {
originY += 6.0
} else {
originY += 10.0
}
}
let amountSpacing: CGFloat = countBackgroundColor != nil ? 4.0 : 1.0

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -133,7 +133,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
}
}
private let navigateToChatImpl: (AccountRecordId, PeerId, MessageId?) -> Void
private let navigateToChatImpl: (AccountRecordId, PeerId, MessageId?, Bool) -> Void
private let apsNotificationToken: Signal<Data?, NoError>
private let voipNotificationToken: Signal<Data?, NoError>
@ -268,7 +268,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
private let energyUsageAutomaticDisposable = MetaDisposable()
init(mainWindow: Window1?, sharedContainerPath: String, basePath: String, encryptionParameters: ValueBoxEncryptionParameters, accountManager: AccountManager<TelegramAccountManagerTypes>, appLockContext: AppLockContext, notificationController: NotificationContainerController?, applicationBindings: TelegramApplicationBindings, initialPresentationDataAndSettings: InitialPresentationDataAndSettings, networkArguments: NetworkInitializationArguments, hasInAppPurchases: Bool, rootPath: String, legacyBasePath: String?, apsNotificationToken: Signal<Data?, NoError>, voipNotificationToken: Signal<Data?, NoError>, firebaseSecretStream: Signal<[String: String], NoError>, setNotificationCall: @escaping (PresentationCall?) -> Void, navigateToChat: @escaping (AccountRecordId, PeerId, MessageId?) -> Void, displayUpgradeProgress: @escaping (Float?) -> Void = { _ in }, appDelegate: AppDelegate?) {
init(mainWindow: Window1?, sharedContainerPath: String, basePath: String, encryptionParameters: ValueBoxEncryptionParameters, accountManager: AccountManager<TelegramAccountManagerTypes>, appLockContext: AppLockContext, notificationController: NotificationContainerController?, applicationBindings: TelegramApplicationBindings, initialPresentationDataAndSettings: InitialPresentationDataAndSettings, networkArguments: NetworkInitializationArguments, hasInAppPurchases: Bool, rootPath: String, legacyBasePath: String?, apsNotificationToken: Signal<Data?, NoError>, voipNotificationToken: Signal<Data?, NoError>, firebaseSecretStream: Signal<[String: String], NoError>, setNotificationCall: @escaping (PresentationCall?) -> Void, navigateToChat: @escaping (AccountRecordId, PeerId, MessageId?, Bool) -> Void, displayUpgradeProgress: @escaping (Float?) -> Void = { _ in }, appDelegate: AppDelegate?) {
assert(Queue.mainQueue().isCurrent())
precondition(!testHasInstance)
@ -1760,7 +1760,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
}
public func navigateToChat(accountId: AccountRecordId, peerId: PeerId, messageId: MessageId?) {
self.navigateToChatImpl(accountId, peerId, messageId)
self.navigateToChatImpl(accountId, peerId, messageId, true)
}
public func messageFromPreloadedChatHistoryViewForLocation(id: MessageId, location: ChatHistoryLocationInput, context: AccountContext, chatLocation: ChatLocation, subject: ChatControllerSubject?, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>, tag: HistoryViewInputTag?) -> Signal<(MessageIndex?, Bool), NoError> {