Merge commit '4013fca50e5ac104f860728aaa715fdca50ae54a'

# Conflicts:
#	submodules/TelegramUI/Sources/ChatControllerNode.swift
This commit is contained in:
Ali 2023-10-13 15:26:00 +04:00
commit 7812c11601
37 changed files with 784 additions and 102 deletions

View File

@ -329,6 +329,9 @@ alternate_icon_folders = [
"Premium",
"PremiumBlack",
"PremiumTurbo",
"PremiumCoffee",
"PremiumDuck",
"PremiumSteam",
]
[

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View File

@ -10104,3 +10104,9 @@ Sorry for the inconvenience.";
"GiftLink.LinkSharedToSavedMessages" = "Gift link forwarded to **Saved Messages**";
"ChatContextMenu.TextSelectionTip2" = "Hold on a word, then move cursor to select more| text to copy or quote.";
"Appearance.AppIconCoffee" = "Coffee";
"Appearance.AppIconDuck" = "Duck";
"Appearance.AppIconSteam" = "Steam";
"Notification.GiftLink" = "You received a gift";

View File

@ -41,7 +41,8 @@ public final class HStack<ChildEnvironment: Equatable>: CombinedComponent {
size.width += child.size.width
size.height = max(size.height, child.size.height)
}
size.width += context.component.spacing * CGFloat(updatedChildren.count - 1)
var nextX = 0.0
for child in updatedChildren {
context.add(child

View File

@ -46,9 +46,10 @@ public final class Image: Component {
func update(component: Image, availableSize: CGSize, environment: Environment<Empty>, transition: Transition) -> CGSize {
self.image = component.image
self.tintColor = component.tintColor
self.contentMode = component.contentMode
transition.setTintColor(view: self, color: component.tintColor ?? .white)
return component.size ?? availableSize
}
}

View File

@ -573,6 +573,7 @@ private final class PendingInAppPurchaseState: Codable {
case peers
case boostPeer
case additionalPeerIds
case countries
case onlyNewSubscribers
case randomId
case untilDate
@ -592,7 +593,7 @@ private final class PendingInAppPurchaseState: Codable {
case restore
case gift(peerId: EnginePeer.Id)
case giftCode(peerIds: [EnginePeer.Id], boostPeer: EnginePeer.Id?)
case giveaway(boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], onlyNewSubscribers: Bool, randomId: Int64, untilDate: Int32)
case giveaway(boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, randomId: Int64, untilDate: Int32)
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
@ -618,6 +619,7 @@ private final class PendingInAppPurchaseState: Codable {
self = .giveaway(
boostPeer: EnginePeer.Id(try container.decode(Int64.self, forKey: .boostPeer)),
additionalPeerIds: try container.decode([Int64].self, forKey: .randomId).map { EnginePeer.Id($0) },
countries: try container.decodeIfPresent([String].self, forKey: .countries) ?? [],
onlyNewSubscribers: try container.decode(Bool.self, forKey: .onlyNewSubscribers),
randomId: try container.decode(Int64.self, forKey: .randomId),
untilDate: try container.decode(Int32.self, forKey: .untilDate)
@ -644,10 +646,11 @@ private final class PendingInAppPurchaseState: Codable {
try container.encode(PurposeType.giftCode.rawValue, forKey: .type)
try container.encode(peerIds.map { $0.toInt64() }, forKey: .peers)
try container.encodeIfPresent(boostPeer?.toInt64(), forKey: .boostPeer)
case let .giveaway(boostPeer, additionalPeerIds, onlyNewSubscribers, randomId, untilDate):
case let .giveaway(boostPeer, additionalPeerIds, countries, onlyNewSubscribers, randomId, untilDate):
try container.encode(PurposeType.giveaway.rawValue, forKey: .type)
try container.encode(boostPeer.toInt64(), forKey: .boostPeer)
try container.encode(additionalPeerIds.map { $0.toInt64() }, forKey: .additionalPeerIds)
try container.encode(countries, forKey: .countries)
try container.encode(onlyNewSubscribers, forKey: .onlyNewSubscribers)
try container.encode(randomId, forKey: .randomId)
try container.encode(untilDate, forKey: .untilDate)
@ -666,8 +669,8 @@ private final class PendingInAppPurchaseState: Codable {
self = .gift(peerId: peerId)
case let .giftCode(peerIds, boostPeer, _, _):
self = .giftCode(peerIds: peerIds, boostPeer: boostPeer)
case let .giveaway(boostPeer, additionalPeerIds, onlyNewSubscribers, randomId, untilDate, _, _):
self = .giveaway(boostPeer: boostPeer, additionalPeerIds: additionalPeerIds, onlyNewSubscribers: onlyNewSubscribers, randomId: randomId, untilDate: untilDate)
case let .giveaway(boostPeer, additionalPeerIds, countries, onlyNewSubscribers, randomId, untilDate, _, _):
self = .giveaway(boostPeer: boostPeer, additionalPeerIds: additionalPeerIds, countries: countries, onlyNewSubscribers: onlyNewSubscribers, randomId: randomId, untilDate: untilDate)
}
}
@ -684,8 +687,8 @@ private final class PendingInAppPurchaseState: Codable {
return .gift(peerId: peerId, currency: currency, amount: amount)
case let .giftCode(peerIds, boostPeer):
return .giftCode(peerIds: peerIds, boostPeer: boostPeer, currency: currency, amount: amount)
case let .giveaway(boostPeer, additionalPeerIds, onlyNewSubscribers, randomId, untilDate):
return .giveaway(boostPeer: boostPeer, additionalPeerIds: additionalPeerIds, onlyNewSubscribers: onlyNewSubscribers, randomId: randomId, untilDate: untilDate, currency: currency, amount: amount)
case let .giveaway(boostPeer, additionalPeerIds, countries, onlyNewSubscribers, randomId, untilDate):
return .giveaway(boostPeer: boostPeer, additionalPeerIds: additionalPeerIds, countries: countries, onlyNewSubscribers: onlyNewSubscribers, randomId: randomId, untilDate: untilDate, currency: currency, amount: amount)
}
}
}

View File

@ -72,6 +72,12 @@ final class AppIconsDemoComponent: Component {
image = UIImage(bundleImageName: "Premium/Icons/Black")
case "PremiumTurbo":
image = UIImage(bundleImageName: "Premium/Icons/Turbo")
case "PremiumDuck":
image = UIImage(bundleImageName: "Premium/Icons/Duck")
case "PremiumCoffee":
image = UIImage(bundleImageName: "Premium/Icons/Coffee")
case "PremiumSteam":
image = UIImage(bundleImageName: "Premium/Icons/Steam")
default:
image = nil
}

View File

@ -598,6 +598,7 @@ private struct CreateGiveawayControllerState: Equatable {
var channels: [EnginePeer.Id]
var peers: [EnginePeer.Id]
var selectedMonths: Int32?
var countries: [String]
var onlyNewEligible: Bool
var time: Int32
var pickingTimeLimit = false
@ -620,7 +621,7 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
}
let expiryTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + 86400 * 5
let initialState: CreateGiveawayControllerState = CreateGiveawayControllerState(mode: .giveaway, subscriptions: initialSubscriptions, channels: [], peers: [], onlyNewEligible: false, time: expiryTime)
let initialState: CreateGiveawayControllerState = CreateGiveawayControllerState(mode: .giveaway, subscriptions: initialSubscriptions, channels: [], peers: [], countries: [], onlyNewEligible: false, time: expiryTime)
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
let stateValue = Atomic(value: initialState)
@ -790,11 +791,14 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
let (currency, amount) = selectedProduct.storeProduct.priceCurrencyAndAmount
let purpose: AppStoreTransactionPurpose
let quantity: Int32
switch state.mode {
case .giveaway:
purpose = .giveaway(boostPeer: peerId, additionalPeerIds: state.channels.filter { $0 != peerId }, onlyNewSubscribers: state.onlyNewEligible, randomId: Int64.random(in: .min ..< .max), untilDate: state.time, currency: currency, amount: amount)
purpose = .giveaway(boostPeer: peerId, additionalPeerIds: state.channels.filter { $0 != peerId }, countries: state.countries, onlyNewSubscribers: state.onlyNewEligible, randomId: Int64.random(in: .min ..< .max), untilDate: state.time, currency: currency, amount: amount)
quantity = selectedProduct.giftOption.storeQuantity
case .gift:
purpose = .giftCode(peerIds: state.peers, boostPeer: peerId, currency: currency, amount: amount)
quantity = Int32(state.peers.count)
}
updateState { state in
@ -808,7 +812,7 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
let _ = (context.engine.payments.canPurchasePremium(purpose: purpose)
|> deliverOnMainQueue).startStandalone(next: { [weak controller] available in
if available, let inAppPurchaseManager = context.inAppPurchaseManager {
let _ = (inAppPurchaseManager.buyProduct(selectedProduct.storeProduct, quantity: selectedProduct.giftOption.storeQuantity, purpose: purpose)
let _ = (inAppPurchaseManager.buyProduct(selectedProduct.storeProduct, quantity: quantity, purpose: purpose)
|> deliverOnMainQueue).startStandalone(next: { [weak controller] status in
if case .purchased = status {
if let controller, let navigationController = controller.navigationController as? NavigationController {
@ -890,7 +894,7 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
}
})
case let .prepaid(prepaidGiveaway):
let _ = (context.engine.payments.launchPrepaidGiveaway(peerId: peerId, id: prepaidGiveaway.id, additionalPeerIds: state.channels.filter { $0 != peerId }, onlyNewSubscribers: state.onlyNewEligible, randomId: Int64.random(in: .min ..< .max), untilDate: state.time)
let _ = (context.engine.payments.launchPrepaidGiveaway(peerId: peerId, id: prepaidGiveaway.id, additionalPeerIds: state.channels.filter { $0 != peerId }, countries: state.countries, onlyNewSubscribers: state.onlyNewEligible, randomId: Int64.random(in: .min ..< .max), untilDate: state.time)
|> deliverOnMainQueue).startStandalone(completed: {
if let controller, let navigationController = controller.navigationController as? NavigationController {
var controllers = navigationController.viewControllers

View File

@ -365,6 +365,12 @@ class ThemeSettingsAppIconItemNode: ListViewItemNode, ItemListItemNode {
name = item.strings.Appearance_AppIconBlack
case "PremiumTurbo":
name = item.strings.Appearance_AppIconTurbo
case "PremiumDuck":
name = item.strings.Appearance_AppIconDuck
case "PremiumCoffee":
name = item.strings.Appearance_AppIconCoffee
case "PremiumSteam":
name = item.strings.Appearance_AppIconSteam
default:
name = icon.name
}

View File

@ -407,7 +407,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[1251549527] = { return Api.InputStickeredMedia.parse_inputStickeredMediaPhoto($0) }
dict[1634697192] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentGiftPremium($0) }
dict[-1551868097] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentPremiumGiftCode($0) }
dict[-381016791] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentPremiumGiveaway($0) }
dict[2090038758] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentPremiumGiveaway($0) }
dict[-1502273946] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentPremiumSubscription($0) }
dict[1012306921] = { return Api.InputTheme.parse_inputTheme($0) }
dict[-175567375] = { return Api.InputTheme.parse_inputThemeSlug($0) }
@ -535,7 +535,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-38694904] = { return Api.MessageMedia.parse_messageMediaGame($0) }
dict[1457575028] = { return Api.MessageMedia.parse_messageMediaGeo($0) }
dict[-1186937242] = { return Api.MessageMedia.parse_messageMediaGeoLive($0) }
dict[1116825468] = { return Api.MessageMedia.parse_messageMediaGiveaway($0) }
dict[1478887012] = { return Api.MessageMedia.parse_messageMediaGiveaway($0) }
dict[-156940077] = { return Api.MessageMedia.parse_messageMediaInvoice($0) }
dict[1766936791] = { return Api.MessageMedia.parse_messageMediaPhoto($0) }
dict[1272375192] = { return Api.MessageMedia.parse_messageMediaPoll($0) }

View File

@ -600,7 +600,7 @@ public extension Api {
indirect enum InputStorePaymentPurpose: TypeConstructorDescription {
case inputStorePaymentGiftPremium(userId: Api.InputUser, currency: String, amount: Int64)
case inputStorePaymentPremiumGiftCode(flags: Int32, users: [Api.InputUser], boostPeer: Api.InputPeer?, currency: String, amount: Int64)
case inputStorePaymentPremiumGiveaway(flags: Int32, boostPeer: Api.InputPeer, additionalPeers: [Api.InputPeer]?, randomId: Int64, untilDate: Int32, currency: String, amount: Int64)
case inputStorePaymentPremiumGiveaway(flags: Int32, boostPeer: Api.InputPeer, additionalPeers: [Api.InputPeer]?, countriesIso2: [String]?, randomId: Int64, untilDate: Int32, currency: String, amount: Int64)
case inputStorePaymentPremiumSubscription(flags: Int32)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
@ -627,9 +627,9 @@ public extension Api {
serializeString(currency, buffer: buffer, boxed: false)
serializeInt64(amount, buffer: buffer, boxed: false)
break
case .inputStorePaymentPremiumGiveaway(let flags, let boostPeer, let additionalPeers, let randomId, let untilDate, let currency, let amount):
case .inputStorePaymentPremiumGiveaway(let flags, let boostPeer, let additionalPeers, let countriesIso2, let randomId, let untilDate, let currency, let amount):
if boxed {
buffer.appendInt32(-381016791)
buffer.appendInt32(2090038758)
}
serializeInt32(flags, buffer: buffer, boxed: false)
boostPeer.serialize(buffer, true)
@ -638,6 +638,11 @@ public extension Api {
for item in additionalPeers! {
item.serialize(buffer, true)
}}
if Int(flags) & Int(1 << 2) != 0 {buffer.appendInt32(481674261)
buffer.appendInt32(Int32(countriesIso2!.count))
for item in countriesIso2! {
serializeString(item, buffer: buffer, boxed: false)
}}
serializeInt64(randomId, buffer: buffer, boxed: false)
serializeInt32(untilDate, buffer: buffer, boxed: false)
serializeString(currency, buffer: buffer, boxed: false)
@ -658,8 +663,8 @@ public extension Api {
return ("inputStorePaymentGiftPremium", [("userId", userId as Any), ("currency", currency as Any), ("amount", amount as Any)])
case .inputStorePaymentPremiumGiftCode(let flags, let users, let boostPeer, let currency, let amount):
return ("inputStorePaymentPremiumGiftCode", [("flags", flags as Any), ("users", users as Any), ("boostPeer", boostPeer as Any), ("currency", currency as Any), ("amount", amount as Any)])
case .inputStorePaymentPremiumGiveaway(let flags, let boostPeer, let additionalPeers, let randomId, let untilDate, let currency, let amount):
return ("inputStorePaymentPremiumGiveaway", [("flags", flags as Any), ("boostPeer", boostPeer as Any), ("additionalPeers", additionalPeers as Any), ("randomId", randomId as Any), ("untilDate", untilDate as Any), ("currency", currency as Any), ("amount", amount as Any)])
case .inputStorePaymentPremiumGiveaway(let flags, let boostPeer, let additionalPeers, let countriesIso2, let randomId, let untilDate, let currency, let amount):
return ("inputStorePaymentPremiumGiveaway", [("flags", flags as Any), ("boostPeer", boostPeer as Any), ("additionalPeers", additionalPeers as Any), ("countriesIso2", countriesIso2 as Any), ("randomId", randomId as Any), ("untilDate", untilDate as Any), ("currency", currency as Any), ("amount", amount as Any)])
case .inputStorePaymentPremiumSubscription(let flags):
return ("inputStorePaymentPremiumSubscription", [("flags", flags as Any)])
}
@ -722,23 +727,28 @@ public extension Api {
if Int(_1!) & Int(1 << 1) != 0 {if let _ = reader.readInt32() {
_3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputPeer.self)
} }
var _4: Int64?
_4 = reader.readInt64()
var _5: Int32?
_5 = reader.readInt32()
var _6: String?
_6 = parseString(reader)
var _7: Int64?
_7 = reader.readInt64()
var _4: [String]?
if Int(_1!) & Int(1 << 2) != 0 {if let _ = reader.readInt32() {
_4 = Api.parseVector(reader, elementSignature: -1255641564, elementType: String.self)
} }
var _5: Int64?
_5 = reader.readInt64()
var _6: Int32?
_6 = reader.readInt32()
var _7: String?
_7 = parseString(reader)
var _8: Int64?
_8 = reader.readInt64()
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil
let _c4 = _4 != nil
let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil
let _c5 = _5 != nil
let _c6 = _6 != nil
let _c7 = _7 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 {
return Api.InputStorePaymentPurpose.inputStorePaymentPremiumGiveaway(flags: _1!, boostPeer: _2!, additionalPeers: _3, randomId: _4!, untilDate: _5!, currency: _6!, amount: _7!)
let _c8 = _8 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 {
return Api.InputStorePaymentPurpose.inputStorePaymentPremiumGiveaway(flags: _1!, boostPeer: _2!, additionalPeers: _3, countriesIso2: _4, randomId: _5!, untilDate: _6!, currency: _7!, amount: _8!)
}
else {
return nil

View File

@ -741,7 +741,7 @@ public extension Api {
case messageMediaGame(game: Api.Game)
case messageMediaGeo(geo: Api.GeoPoint)
case messageMediaGeoLive(flags: Int32, geo: Api.GeoPoint, heading: Int32?, period: Int32, proximityNotificationRadius: Int32?)
case messageMediaGiveaway(flags: Int32, channels: [Int64], quantity: Int32, months: Int32, untilDate: Int32)
case messageMediaGiveaway(flags: Int32, channels: [Int64], countriesIso2: [String]?, quantity: Int32, months: Int32, untilDate: Int32)
case messageMediaInvoice(flags: Int32, title: String, description: String, photo: Api.WebDocument?, receiptMsgId: Int32?, currency: String, totalAmount: Int64, startParam: String, extendedMedia: Api.MessageExtendedMedia?)
case messageMediaPhoto(flags: Int32, photo: Api.Photo?, ttlSeconds: Int32?)
case messageMediaPoll(poll: Api.Poll, results: Api.PollResults)
@ -806,9 +806,9 @@ public extension Api {
serializeInt32(period, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 1) != 0 {serializeInt32(proximityNotificationRadius!, buffer: buffer, boxed: false)}
break
case .messageMediaGiveaway(let flags, let channels, let quantity, let months, let untilDate):
case .messageMediaGiveaway(let flags, let channels, let countriesIso2, let quantity, let months, let untilDate):
if boxed {
buffer.appendInt32(1116825468)
buffer.appendInt32(1478887012)
}
serializeInt32(flags, buffer: buffer, boxed: false)
buffer.appendInt32(481674261)
@ -816,6 +816,11 @@ public extension Api {
for item in channels {
serializeInt64(item, buffer: buffer, boxed: false)
}
if Int(flags) & Int(1 << 1) != 0 {buffer.appendInt32(481674261)
buffer.appendInt32(Int32(countriesIso2!.count))
for item in countriesIso2! {
serializeString(item, buffer: buffer, boxed: false)
}}
serializeInt32(quantity, buffer: buffer, boxed: false)
serializeInt32(months, buffer: buffer, boxed: false)
serializeInt32(untilDate, buffer: buffer, boxed: false)
@ -900,8 +905,8 @@ public extension Api {
return ("messageMediaGeo", [("geo", geo as Any)])
case .messageMediaGeoLive(let flags, let geo, let heading, let period, let proximityNotificationRadius):
return ("messageMediaGeoLive", [("flags", flags as Any), ("geo", geo as Any), ("heading", heading as Any), ("period", period as Any), ("proximityNotificationRadius", proximityNotificationRadius as Any)])
case .messageMediaGiveaway(let flags, let channels, let quantity, let months, let untilDate):
return ("messageMediaGiveaway", [("flags", flags as Any), ("channels", channels as Any), ("quantity", quantity as Any), ("months", months as Any), ("untilDate", untilDate as Any)])
case .messageMediaGiveaway(let flags, let channels, let countriesIso2, let quantity, let months, let untilDate):
return ("messageMediaGiveaway", [("flags", flags as Any), ("channels", channels as Any), ("countriesIso2", countriesIso2 as Any), ("quantity", quantity as Any), ("months", months as Any), ("untilDate", untilDate as Any)])
case .messageMediaInvoice(let flags, let title, let description, let photo, let receiptMsgId, let currency, let totalAmount, let startParam, let extendedMedia):
return ("messageMediaInvoice", [("flags", flags as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("receiptMsgId", receiptMsgId as Any), ("currency", currency as Any), ("totalAmount", totalAmount as Any), ("startParam", startParam as Any), ("extendedMedia", extendedMedia as Any)])
case .messageMediaPhoto(let flags, let photo, let ttlSeconds):
@ -1041,19 +1046,24 @@ public extension Api {
if let _ = reader.readInt32() {
_2 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self)
}
var _3: Int32?
_3 = reader.readInt32()
var _3: [String]?
if Int(_1!) & Int(1 << 1) != 0 {if let _ = reader.readInt32() {
_3 = Api.parseVector(reader, elementSignature: -1255641564, elementType: String.self)
} }
var _4: Int32?
_4 = reader.readInt32()
var _5: Int32?
_5 = reader.readInt32()
var _6: Int32?
_6 = reader.readInt32()
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil
let _c4 = _4 != nil
let _c5 = _5 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 {
return Api.MessageMedia.messageMediaGiveaway(flags: _1!, channels: _2!, quantity: _3!, months: _4!, untilDate: _5!)
let _c6 = _6 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 {
return Api.MessageMedia.messageMediaGiveaway(flags: _1!, channels: _2!, countriesIso2: _3, quantity: _4!, months: _5!, untilDate: _6!)
}
else {
return nil

View File

@ -401,12 +401,12 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI
case let .messageMediaStory(flags, peerId, id, _):
let isMention = (flags & (1 << 1)) != 0
return (TelegramMediaStory(storyId: StoryId(peerId: peerId.peerId, id: id), isMention: isMention), nil, nil, nil)
case let .messageMediaGiveaway(apiFlags, channels, quantity, months, untilDate):
case let .messageMediaGiveaway(apiFlags, channels, countries, quantity, months, untilDate):
var flags: TelegramMediaGiveaway.Flags = []
if (apiFlags & (1 << 0)) != 0 {
flags.insert(.onlyNewSubscribers)
}
return (TelegramMediaGiveaway(flags: flags, channelPeerIds: channels.map { PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value($0)) }, quantity: quantity, months: months, untilDate: untilDate), nil, nil, nil)
return (TelegramMediaGiveaway(flags: flags, channelPeerIds: channels.map { PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value($0)) }, countries: countries ?? [], quantity: quantity, months: months, untilDate: untilDate), nil, nil, nil)
}
}

View File

@ -23,6 +23,7 @@ public struct UserLimitsConfiguration: Equatable {
public let maxStoriesMonthlyCount: Int32
public let maxStoriesSuggestedReactions: Int32
public let maxGiveawayChannelsCount: Int32
public let maxGiveawayCountriesCount: Int32
public static var defaultValue: UserLimitsConfiguration {
return UserLimitsConfiguration(
@ -46,7 +47,8 @@ public struct UserLimitsConfiguration: Equatable {
maxStoriesWeeklyCount: 7,
maxStoriesMonthlyCount: 30,
maxStoriesSuggestedReactions: 1,
maxGiveawayChannelsCount: 10
maxGiveawayChannelsCount: 10,
maxGiveawayCountriesCount: 10
)
}
@ -71,7 +73,8 @@ public struct UserLimitsConfiguration: Equatable {
maxStoriesWeeklyCount: Int32,
maxStoriesMonthlyCount: Int32,
maxStoriesSuggestedReactions: Int32,
maxGiveawayChannelsCount: Int32
maxGiveawayChannelsCount: Int32,
maxGiveawayCountriesCount: Int32
) {
self.maxPinnedChatCount = maxPinnedChatCount
self.maxArchivedPinnedChatCount = maxArchivedPinnedChatCount
@ -94,6 +97,7 @@ public struct UserLimitsConfiguration: Equatable {
self.maxStoriesMonthlyCount = maxStoriesMonthlyCount
self.maxStoriesSuggestedReactions = maxStoriesSuggestedReactions
self.maxGiveawayChannelsCount = maxGiveawayChannelsCount
self.maxGiveawayCountriesCount = maxGiveawayCountriesCount
}
}
@ -139,5 +143,6 @@ extension UserLimitsConfiguration {
self.maxStoriesMonthlyCount = getValue("stories_sent_monthly_limit", orElse: defaultValue.maxStoriesMonthlyCount)
self.maxStoriesSuggestedReactions = getValue("stories_suggested_reactions_limit", orElse: defaultValue.maxStoriesMonthlyCount)
self.maxGiveawayChannelsCount = getGeneralValue("giveaway_add_peers_max", orElse: defaultValue.maxGiveawayChannelsCount)
self.maxGiveawayCountriesCount = getGeneralValue("giveaway_countries_max", orElse: defaultValue.maxGiveawayCountriesCount)
}
}

View File

@ -20,13 +20,15 @@ public final class TelegramMediaGiveaway: Media, Equatable {
public let flags: Flags
public let channelPeerIds: [PeerId]
public let countries: [String]
public let quantity: Int32
public let months: Int32
public let untilDate: Int32
public init(flags: Flags, channelPeerIds: [PeerId], quantity: Int32, months: Int32, untilDate: Int32) {
public init(flags: Flags, channelPeerIds: [PeerId], countries: [String], quantity: Int32, months: Int32, untilDate: Int32) {
self.flags = flags
self.channelPeerIds = channelPeerIds
self.countries = countries
self.quantity = quantity
self.months = months
self.untilDate = untilDate
@ -35,6 +37,7 @@ public final class TelegramMediaGiveaway: Media, Equatable {
public init(decoder: PostboxDecoder) {
self.flags = Flags(rawValue: decoder.decodeInt32ForKey("flg", orElse: 0))
self.channelPeerIds = decoder.decodeInt64ArrayForKey("cns").map { PeerId($0) }
self.countries = decoder.decodeStringArrayForKey("cnt")
self.quantity = decoder.decodeInt32ForKey("qty", orElse: 0)
self.months = decoder.decodeInt32ForKey("mts", orElse: 0)
self.untilDate = decoder.decodeInt32ForKey("unt", orElse: 0)
@ -43,6 +46,7 @@ public final class TelegramMediaGiveaway: Media, Equatable {
public func encode(_ encoder: PostboxEncoder) {
encoder.encodeInt32(self.flags.rawValue, forKey: "flg")
encoder.encodeInt64Array(self.channelPeerIds.map { $0.toInt64() }, forKey: "cns")
encoder.encodeStringArray(self.countries, forKey: "cnt")
encoder.encodeInt32(self.quantity, forKey: "qty")
encoder.encodeInt32(self.months, forKey: "mts")
encoder.encodeInt32(self.untilDate, forKey: "unt")
@ -62,6 +66,9 @@ public final class TelegramMediaGiveaway: Media, Equatable {
if self.channelPeerIds != other.channelPeerIds {
return false
}
if self.countries != other.countries {
return false
}
if self.quantity != other.quantity {
return false
}

View File

@ -57,6 +57,7 @@ public enum EngineConfiguration {
public let maxStoriesMonthlyCount: Int32
public let maxStoriesSuggestedReactions: Int32
public let maxGiveawayChannelsCount: Int32
public let maxGiveawayCountriesCount: Int32
public static var defaultValue: UserLimits {
return UserLimits(UserLimitsConfiguration.defaultValue)
@ -83,7 +84,8 @@ public enum EngineConfiguration {
maxStoriesWeeklyCount: Int32,
maxStoriesMonthlyCount: Int32,
maxStoriesSuggestedReactions: Int32,
maxGiveawayChannelsCount: Int32
maxGiveawayChannelsCount: Int32,
maxGiveawayCountriesCount: Int32
) {
self.maxPinnedChatCount = maxPinnedChatCount
self.maxArchivedPinnedChatCount = maxArchivedPinnedChatCount
@ -106,6 +108,7 @@ public enum EngineConfiguration {
self.maxStoriesMonthlyCount = maxStoriesMonthlyCount
self.maxStoriesSuggestedReactions = maxStoriesSuggestedReactions
self.maxGiveawayChannelsCount = maxGiveawayChannelsCount
self.maxGiveawayCountriesCount = maxGiveawayCountriesCount
}
}
}
@ -163,7 +166,8 @@ public extension EngineConfiguration.UserLimits {
maxStoriesWeeklyCount: userLimitsConfiguration.maxStoriesWeeklyCount,
maxStoriesMonthlyCount: userLimitsConfiguration.maxStoriesMonthlyCount,
maxStoriesSuggestedReactions: userLimitsConfiguration.maxStoriesSuggestedReactions,
maxGiveawayChannelsCount: userLimitsConfiguration.maxGiveawayChannelsCount
maxGiveawayChannelsCount: userLimitsConfiguration.maxGiveawayChannelsCount,
maxGiveawayCountriesCount: userLimitsConfiguration.maxGiveawayCountriesCount
)
}
}

View File

@ -16,7 +16,7 @@ public enum AppStoreTransactionPurpose {
case restore
case gift(peerId: EnginePeer.Id, currency: String, amount: Int64)
case giftCode(peerIds: [EnginePeer.Id], boostPeer: EnginePeer.Id?, currency: String, amount: Int64)
case giveaway(boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], onlyNewSubscribers: Bool, randomId: Int64, untilDate: Int32, currency: String, amount: Int64)
case giveaway(boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, randomId: Int64, untilDate: Int32, currency: String, amount: Int64)
}
private func apiInputStorePaymentPurpose(account: Account, purpose: AppStoreTransactionPurpose) -> Signal<Api.InputStorePaymentPurpose, NoError> {
@ -59,7 +59,7 @@ private func apiInputStorePaymentPurpose(account: Account, purpose: AppStoreTran
return .inputStorePaymentPremiumGiftCode(flags: flags, users: apiInputUsers, boostPeer: apiBoostPeer, currency: currency, amount: amount)
}
case let .giveaway(boostPeerId, additionalPeerIds, onlyNewSubscribers, randomId, untilDate, currency, amount):
case let .giveaway(boostPeerId, additionalPeerIds, countries, onlyNewSubscribers, randomId, untilDate, currency, amount):
return account.postbox.transaction { transaction -> Signal<Api.InputStorePaymentPurpose, NoError> in
guard let peer = transaction.getPeer(boostPeerId), let apiBoostPeer = apiInputPeer(peer) else {
return .complete()
@ -77,7 +77,10 @@ private func apiInputStorePaymentPurpose(account: Account, purpose: AppStoreTran
}
}
}
return .single(.inputStorePaymentPremiumGiveaway(flags: flags, boostPeer: apiBoostPeer, additionalPeers: additionalPeers, randomId: randomId, untilDate: untilDate, currency: currency, amount: amount))
if !countries.isEmpty {
flags |= (1 << 2)
}
return .single(.inputStorePaymentPremiumGiveaway(flags: flags, boostPeer: apiBoostPeer, additionalPeers: additionalPeers, countriesIso2: countries, randomId: randomId, untilDate: untilDate, currency: currency, amount: amount))
}
|> switchToLatest
}

View File

@ -184,7 +184,7 @@ func _internal_applyPremiumGiftCode(account: Account, slug: String) -> Signal<Ne
}
}
func _internal_launchPrepaidGiveaway(account: Account, peerId: EnginePeer.Id, id: Int64, additionalPeerIds: [EnginePeer.Id], onlyNewSubscribers: Bool, randomId: Int64, untilDate: Int32) -> Signal<Never, NoError> {
func _internal_launchPrepaidGiveaway(account: Account, peerId: EnginePeer.Id, id: Int64, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, randomId: Int64, untilDate: Int32) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> Signal<Never, NoError> in
var flags: Int32 = 0
if onlyNewSubscribers {
@ -206,10 +206,14 @@ func _internal_launchPrepaidGiveaway(account: Account, peerId: EnginePeer.Id, id
}
}
if !countries.isEmpty {
flags |= (1 << 2)
}
guard let inputPeer = inputPeer else {
return .complete()
}
return account.network.request(Api.functions.payments.launchPrepaidGiveaway(peer: inputPeer, giveawayId: id, purpose: .inputStorePaymentPremiumGiveaway(flags: flags, boostPeer: inputPeer, additionalPeers: additionalPeers, randomId: randomId, untilDate: untilDate, currency: "", amount: 0)))
return account.network.request(Api.functions.payments.launchPrepaidGiveaway(peer: inputPeer, giveawayId: id, purpose: .inputStorePaymentPremiumGiveaway(flags: flags, boostPeer: inputPeer, additionalPeers: additionalPeers, countriesIso2: countries, randomId: randomId, untilDate: untilDate, currency: "", amount: 0)))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
return .single(nil)

View File

@ -62,8 +62,8 @@ public extension TelegramEngine {
return _internal_getPremiumGiveawayInfo(account: self.account, peerId: peerId, messageId: messageId)
}
public func launchPrepaidGiveaway(peerId: EnginePeer.Id, id: Int64, additionalPeerIds: [EnginePeer.Id], onlyNewSubscribers: Bool, randomId: Int64, untilDate: Int32) -> Signal<Never, NoError> {
return _internal_launchPrepaidGiveaway(account: self.account, peerId: peerId, id: id, additionalPeerIds: additionalPeerIds, onlyNewSubscribers: onlyNewSubscribers, randomId: randomId, untilDate: untilDate)
public func launchPrepaidGiveaway(peerId: EnginePeer.Id, id: Int64, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, randomId: Int64, untilDate: Int32) -> Signal<Never, NoError> {
return _internal_launchPrepaidGiveaway(account: self.account, peerId: peerId, id: id, additionalPeerIds: additionalPeerIds, countries: countries, onlyNewSubscribers: onlyNewSubscribers, randomId: randomId, untilDate: untilDate)
}
}
}

View File

@ -26,6 +26,7 @@ public enum MessageContentKindKey {
case dice
case invoice
case story
case giveaway
}
public enum MessageContentKind: Equatable {
@ -48,6 +49,7 @@ public enum MessageContentKind: Equatable {
case dice(String)
case invoice(String)
case story
case giveaway
public func isSemanticallyEqual(to other: MessageContentKind) -> Bool {
switch self {
@ -165,6 +167,12 @@ public enum MessageContentKind: Equatable {
} else {
return false
}
case .giveaway:
if case .giveaway = other {
return true
} else {
return false
}
}
}
@ -208,6 +216,8 @@ public enum MessageContentKind: Equatable {
return .invoice
case .story:
return .story
case .giveaway:
return .giveaway
}
}
}
@ -397,6 +407,8 @@ public func stringForMediaKind(_ kind: MessageContentKind, strings: Presentation
return (NSAttributedString(string: text), true)
case .story:
return (NSAttributedString(string: strings.Message_Story), true)
case .giveaway:
return (NSAttributedString(string: strings.Message_Giveaway), true)
}
}

View File

@ -899,7 +899,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes])
}
case .giftCode:
attributedString = NSAttributedString(string: "Gift Link", font: titleFont, textColor: primaryTextColor)
attributedString = NSAttributedString(string: strings.Notification_GiftLink, font: titleFont, textColor: primaryTextColor)
case .unknown:
attributedString = nil
}

View File

@ -54,6 +54,7 @@ public final class CameraButton: Component {
}
public final class View: UIButton, ComponentTaggedView {
private let containerView = UIView()
public var contentView: ComponentHostView<Empty>
private var component: CameraButton?
@ -75,12 +76,14 @@ public final class CameraButton: Component {
} else {
scale = 1.0
}
transition.setScale(view: self, scale: scale)
transition.setScale(view: self.containerView, scale: scale)
}
private var longTapGestureRecognizer: UILongPressGestureRecognizer?
public override init(frame: CGRect) {
self.containerView.isUserInteractionEnabled = false
self.contentView = ComponentHostView<Empty>()
self.contentView.isUserInteractionEnabled = false
self.contentView.layer.allowsGroupOpacity = true
@ -89,7 +92,8 @@ public final class CameraButton: Component {
self.isExclusiveTouch = true
self.addSubview(self.contentView)
self.addSubview(self.containerView)
self.containerView.addSubview(self.contentView)
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
@ -145,12 +149,12 @@ public final class CameraButton: Component {
self.contentView = ComponentHostView<Empty>()
self.contentView.isUserInteractionEnabled = false
self.contentView.layer.allowsGroupOpacity = true
self.addSubview(self.contentView)
self.containerView.addSubview(self.contentView)
if transition.animation.isImmediate {
previousContentView.removeFromSuperview()
} else {
self.addSubview(previousContentView)
self.containerView.addSubview(previousContentView)
previousContentView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousContentView] _ in
previousContentView?.removeFromSuperview()
})
@ -175,7 +179,11 @@ public final class CameraButton: Component {
self.isEnabled = component.isEnabled
self.longTapGestureRecognizer?.isEnabled = component.longTapAction != nil
self.contentView.frame = CGRect(origin: CGPoint(x: floor((size.width - contentSize.width) / 2.0), y: floor((size.height - contentSize.height) / 2.0)), size: contentSize)
self.contentView.bounds = CGRect(origin: .zero, size: contentSize)
self.contentView.center = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
self.containerView.bounds = CGRect(origin: .zero, size: size)
self.containerView.center = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
return size
}

View File

@ -80,6 +80,7 @@ swift_library(
"//submodules/Utils/VolumeButtons",
"//submodules/TelegramNotices",
"//submodules/DeviceAccess",
"//submodules/TelegramUI/Components/Utils/RoundedRectWithTailPath",
],
visibility = [

View File

@ -30,16 +30,27 @@ enum CameraMode: Equatable {
case video
}
private struct CameraState: Equatable {
struct CameraState: Equatable {
enum Recording: Equatable {
case none
case holding
case handsFree
}
enum FlashTint {
enum FlashTint: Equatable {
case white
case yellow
case blue
var color: UIColor {
switch self {
case .white:
return .white
case .yellow:
return UIColor(rgb: 0xffed8c)
case .blue:
return UIColor(rgb: 0x8cdfff)
}
}
}
let mode: CameraMode
@ -225,6 +236,8 @@ private final class CameraScreenComponent: CombinedComponent {
var swipeHint: CaptureControlsComponent.SwipeHint = .none
var isTransitioning = false
var displayingFlashTint = false
private let hapticFeedback = HapticFeedback()
init(
@ -395,7 +408,7 @@ private final class CameraScreenComponent: CombinedComponent {
camera.setFlashMode(.on)
}
}
func toggleFlashMode() {
guard let controller = self.getController(), let camera = controller.camera else {
return
@ -415,6 +428,27 @@ private final class CameraScreenComponent: CombinedComponent {
self.hapticFeedback.impact(.light)
}
func updateFlashTint(_ tint: CameraState.FlashTint?) {
guard let controller = self.getController(), let camera = controller.camera else {
return
}
if let tint {
controller.updateCameraState({ $0.updatedFlashTint(tint) }, transition: .easeInOut(duration: 0.2))
} else {
camera.setFlashMode(.off)
}
}
func presentFlashTint() {
guard let controller = self.getController(), let camera = controller.camera else {
return
}
camera.setFlashMode(.on)
self.displayingFlashTint = true
self.updated(transition: .immediate)
}
private var lastFlipTimestamp: Double?
func togglePosition(_ action: ActionSlot<Void>) {
guard let controller = self.getController(), let camera = controller.camera else {
@ -622,6 +656,7 @@ private final class CameraScreenComponent: CombinedComponent {
let dualButton = Child(CameraButton.self)
let modeControl = Child(ModeComponent.self)
let hintLabel = Child(HintLabelComponent.self)
let flashTintControl = Child(FlashTintControlComponent.self)
let timeBackground = Child(RoundedRectangle.self)
let timeLabel = Child(MultilineTextComponent.self)
@ -713,19 +748,11 @@ private final class CameraScreenComponent: CombinedComponent {
// )
}
let displayFrontFlash = component.cameraState.recording != .none || component.cameraState.mode == .video || state.displayingFlashTint
var controlsTintColor: UIColor = .white
if case .front = component.cameraState.position, case .on = component.cameraState.flashMode {
let flashTintColor: UIColor
switch component.cameraState.flashTint {
case .white:
flashTintColor = .white
case .yellow:
flashTintColor = UIColor(rgb: 0xffed8c)
case .blue:
flashTintColor = UIColor(rgb: 0x8cdfff)
}
if case .front = component.cameraState.position, case .on = component.cameraState.flashMode, displayFrontFlash {
let frontFlash = frontFlash.update(
component: Image(image: state.image(.flashImage), tintColor: flashTintColor),
component: Image(image: state.image(.flashImage), tintColor: component.cameraState.flashTint.color),
availableSize: availableSize,
transition: .easeInOut(duration: 0.2)
)
@ -849,6 +876,7 @@ private final class CameraScreenComponent: CombinedComponent {
.position(captureControlsPosition)
)
var flashButtonPosition: CGPoint?
let topControlInset: CGFloat = 20.0
if case .none = component.cameraState.recording, !state.isTransitioning {
let cancelButton = cancelButton.update(
@ -928,13 +956,21 @@ private final class CameraScreenComponent: CombinedComponent {
if let state {
state.toggleFlashMode()
}
},
longTapAction: { [weak state] in
if let state {
state.presentFlashTint()
}
}
).tagged(flashButtonTag),
availableSize: CGSize(width: 40.0, height: 40.0),
transition: .immediate
)
let position = CGPoint(x: isTablet ? availableSize.width - smallPanelWidth / 2.0 : availableSize.width - topControlInset - flashButton.size.width / 2.0 - 5.0, y: max(environment.statusBarHeight + 5.0, environment.safeInsets.top + topControlInset) + flashButton.size.height / 2.0)
flashButtonPosition = position
context.add(flashButton
.position(CGPoint(x: isTablet ? availableSize.width - smallPanelWidth / 2.0 : availableSize.width - topControlInset - flashButton.size.width / 2.0 - 5.0, y: max(environment.statusBarHeight + 5.0, environment.safeInsets.top + topControlInset) + flashButton.size.height / 2.0))
.position(position)
.appear(.default(scale: true))
.disappear(.default(scale: true))
)
@ -1114,6 +1150,28 @@ private final class CameraScreenComponent: CombinedComponent {
.disappear(.default(alpha: true))
)
}
if let flashButtonPosition, state.displayingFlashTint {
let flashTintControl = flashTintControl.update(
component: FlashTintControlComponent(
position: flashButtonPosition.offsetBy(dx: 0.0, dy: 27.0),
tint: component.cameraState.flashTint,
update: { [weak state] tint in
state?.updateFlashTint(tint)
},
dismiss: { [weak state] in
state?.displayingFlashTint = false
state?.updated(transition: .easeInOut(duration: 0.2))
}
),
availableSize: availableSize,
transition: context.transition
)
context.add(flashTintControl
.position(CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0))
.disappear(.default(alpha: true))
)
}
return availableSize
}
}
@ -2446,6 +2504,12 @@ public class CameraScreen: ViewController {
if isTablet && isFirstTime {
self.animateIn()
}
if self.cameraState.flashMode == .on && (self.cameraState.recording != .none || self.cameraState.mode == .video) {
self.controller?.statusBarStyle = .Black
} else {
self.controller?.statusBarStyle = .White
}
}
}
@ -2743,13 +2807,12 @@ public class CameraScreen: ViewController {
self.node.camera?.stopCapture(invalidate: true)
self.isDismissed = true
if animated {
self.ignoreStatusBar = true
if let layout = self.validLayout, layout.metrics.isTablet {
self.statusBar.updateStatusBarStyle(.Ignore, animated: true)
self.node.animateOut(completion: {
self.dismiss(animated: false)
})
} else {
self.statusBar.updateStatusBarStyle(.Ignore, animated: true)
if !interactive {
if let navigationController = self.navigationController as? NavigationController {
navigationController.updateRootContainerTransitionOffset(self.node.frame.width, transition: .immediate)
@ -2810,16 +2873,41 @@ public class CameraScreen: ViewController {
}
}
private var statusBarStyle: StatusBarStyle = .White {
didSet {
if self.statusBarStyle != oldValue {
self.updateStatusBarAppearance()
}
}
}
private var ignoreStatusBar = false {
didSet {
if self.ignoreStatusBar != oldValue {
self.updateStatusBarAppearance()
}
}
}
private func updateStatusBarAppearance() {
let effectiveStatusBarStyle: StatusBarStyle
if !self.ignoreStatusBar {
effectiveStatusBarStyle = self.statusBarStyle
} else {
effectiveStatusBarStyle = .Ignore
}
self.statusBar.updateStatusBarStyle(effectiveStatusBarStyle, animated: true)
}
public func completeWithTransitionProgress(_ transitionFraction: CGFloat, velocity: CGFloat, dismissing: Bool) {
if let layout = self.validLayout, layout.metrics.isTablet {
return
}
if dismissing {
if transitionFraction < 0.7 || velocity < -1000.0 {
self.statusBar.updateStatusBarStyle(.Ignore, animated: true)
self.ignoreStatusBar = true
self.requestDismiss(animated: true, interactive: true)
} else {
self.statusBar.updateStatusBarStyle(.White, animated: true)
self.ignoreStatusBar = false
self.updateTransitionProgress(1.0, transition: .animated(duration: 0.4, curve: .spring), completion: { [weak self] in
if let self, let navigationController = self.navigationController as? NavigationController {
navigationController.updateRootContainerTransitionOffset(0.0, transition: .immediate)
@ -2828,7 +2916,8 @@ public class CameraScreen: ViewController {
}
} else {
if transitionFraction > 0.33 || velocity > 1000.0 {
self.statusBar.updateStatusBarStyle(.White, animated: true)
self.ignoreStatusBar = false
self.updateStatusBarAppearance()
self.updateTransitionProgress(1.0, transition: .animated(duration: 0.4, curve: .spring), completion: { [weak self] in
if let self, let navigationController = self.navigationController as? NavigationController {
navigationController.updateRootContainerTransitionOffset(0.0, transition: .immediate)
@ -2837,7 +2926,8 @@ public class CameraScreen: ViewController {
}
})
} else {
self.statusBar.updateStatusBarStyle(.Ignore, animated: true)
self.ignoreStatusBar = true
self.updateStatusBarAppearance()
self.requestDismiss(animated: true, interactive: true)
}
}

View File

@ -0,0 +1,300 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import RoundedRectWithTailPath
private final class FlashColorComponent: Component {
let tint: CameraState.FlashTint?
let isSelected: Bool
let action: () -> Void
init(
tint: CameraState.FlashTint?,
isSelected: Bool,
action: @escaping () -> Void
) {
self.tint = tint
self.isSelected = isSelected
self.action = action
}
static func == (lhs: FlashColorComponent, rhs: FlashColorComponent) -> Bool {
return lhs.tint == rhs.tint && lhs.isSelected == rhs.isSelected
}
final class View: UIButton {
private var component: FlashColorComponent?
private var contentView: UIView
private let circleLayer: SimpleShapeLayer
private var ringLayer: CALayer?
private var iconLayer: CALayer?
private var currentIsHighlighted: Bool = false {
didSet {
if self.currentIsHighlighted != oldValue {
self.contentView.alpha = self.currentIsHighlighted ? 0.6 : 1.0
}
}
}
override init(frame: CGRect) {
self.contentView = UIView(frame: CGRect(origin: .zero, size: frame.size))
self.contentView.isUserInteractionEnabled = false
self.circleLayer = SimpleShapeLayer()
super.init(frame: frame)
self.addSubview(self.contentView)
self.contentView.layer.addSublayer(self.circleLayer)
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func pressed() {
self.component?.action()
}
override public func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
self.currentIsHighlighted = true
return super.beginTracking(touch, with: event)
}
override public func endTracking(_ touch: UITouch?, with event: UIEvent?) {
self.currentIsHighlighted = false
super.endTracking(touch, with: event)
}
override public func cancelTracking(with event: UIEvent?) {
self.currentIsHighlighted = false
super.cancelTracking(with: event)
}
func update(component: FlashColorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
self.component = component
let contentSize = CGSize(width: 24.0, height: 24.0)
self.contentView.frame = CGRect(origin: .zero, size: contentSize)
let bounds = CGRect(origin: .zero, size: contentSize)
self.layer.allowsGroupOpacity = true
self.contentView.layer.allowsGroupOpacity = true
self.circleLayer.frame = bounds
if self.ringLayer == nil {
let ringLayer = SimpleLayer()
ringLayer.backgroundColor = UIColor.clear.cgColor
ringLayer.cornerRadius = contentSize.width / 2.0
ringLayer.borderWidth = 1.0 + UIScreenPixel
ringLayer.frame = CGRect(origin: .zero, size: contentSize)
self.contentView.layer.insertSublayer(ringLayer, at: 0)
self.ringLayer = ringLayer
}
if component.isSelected {
transition.setShapeLayerPath(layer: self.circleLayer, path: CGPath(ellipseIn: bounds.insetBy(dx: 3.0 - UIScreenPixel, dy: 3.0 - UIScreenPixel), transform: nil))
} else {
transition.setShapeLayerPath(layer: self.circleLayer, path: CGPath(ellipseIn: bounds, transform: nil))
}
if let color = component.tint?.color {
self.circleLayer.fillColor = color.cgColor
self.ringLayer?.borderColor = color.cgColor
} else {
if self.iconLayer == nil {
let iconLayer = SimpleLayer()
iconLayer.contents = UIImage(bundleImageName: "Camera/FlashOffIcon")?.cgImage
iconLayer.contentsGravity = .resizeAspect
iconLayer.frame = bounds.insetBy(dx: -4.0, dy: -4.0)
self.contentView.layer.addSublayer(iconLayer)
self.iconLayer = iconLayer
}
self.circleLayer.fillColor = UIColor(rgb: 0xffffff, alpha: 0.1).cgColor
self.ringLayer?.borderColor = UIColor.clear.cgColor
}
return contentSize
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
final class FlashTintControlComponent: Component {
let position: CGPoint
let tint: CameraState.FlashTint
let update: (CameraState.FlashTint?) -> Void
let dismiss: () -> Void
init(
position: CGPoint,
tint: CameraState.FlashTint,
update: @escaping (CameraState.FlashTint?) -> Void,
dismiss: @escaping () -> Void
) {
self.position = position
self.tint = tint
self.update = update
self.dismiss = dismiss
}
static func == (lhs: FlashTintControlComponent, rhs: FlashTintControlComponent) -> Bool {
return lhs.position == rhs.position && lhs.tint == rhs.tint
}
final class View: UIButton {
private var component: FlashTintControlComponent?
private let dismissView = UIView()
private let containerView = UIView()
private let effectView: UIVisualEffectView
private let maskLayer = CAShapeLayer()
private let swatches = ComponentView<Empty>()
override init(frame: CGRect) {
self.effectView = UIVisualEffectView(effect: UIBlurEffect(style: .dark))
super.init(frame: frame)
self.containerView.layer.anchorPoint = CGPoint(x: 0.8, y: 0.0)
self.addSubview(self.dismissView)
self.addSubview(self.containerView)
self.containerView.addSubview(self.effectView)
self.dismissView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dismissTapped)))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func dismissTapped() {
self.component?.dismiss()
}
func update(component: FlashTintControlComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
let isFirstTime = self.component == nil
self.component = component
let size = CGSize(width: 160.0, height: 40.0)
if isFirstTime {
self.maskLayer.path = generateRoundedRectWithTailPath(rectSize: size, cornerRadius: 10.0, tailSize: CGSize(width: 18, height: 7.0), tailRadius: 1.0, tailPosition: 0.8, transformTail: false).cgPath
self.maskLayer.frame = CGRect(origin: .zero, size: CGSize(width: size.width, height: size.height + 7.0))
self.effectView.layer.mask = self.maskLayer
}
let swatchesSize = self.swatches.update(
transition: transition,
component: AnyComponent(
HStack(
[
AnyComponentWithIdentity(
id: "off",
component: AnyComponent(
FlashColorComponent(
tint: nil,
isSelected: false,
action: {
component.update(nil)
component.dismiss()
}
)
)
),
AnyComponentWithIdentity(
id: "white",
component: AnyComponent(
FlashColorComponent(
tint: .white,
isSelected: component.tint == .white,
action: {
component.update(.white)
}
)
)
),
AnyComponentWithIdentity(
id: "yellow",
component: AnyComponent(
FlashColorComponent(
tint: .yellow,
isSelected: component.tint == .yellow,
action: {
component.update(.yellow)
}
)
)
),
AnyComponentWithIdentity(
id: "blue",
component: AnyComponent(
FlashColorComponent(
tint: .blue,
isSelected: component.tint == .blue,
action: {
component.update(.blue)
}
)
)
)
],
spacing: 16.0
)
),
environment: {},
containerSize: availableSize
)
if let view = self.swatches.view {
if view.superview == nil {
self.containerView.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - swatchesSize.width) / 2.0), y: floorToScreenPixels((size.height - swatchesSize.height) / 2.0)), size: swatchesSize)
}
self.dismissView.frame = CGRect(origin: .zero, size: availableSize)
self.containerView.bounds = CGRect(origin: .zero, size: size)
self.containerView.center = component.position
self.effectView.frame = CGRect(origin: CGPoint(x: 0.0, y: -7.0), size: CGSize(width: size.width, height: size.height + 7.0))
if isFirstTime {
self.containerView.layer.animateScale(from: 0.0, to: 1.0, duration: 0.35, timingFunction: kCAMediaTimingFunctionSpring)
self.containerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
return availableSize
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if !self.containerView.frame.contains(point) {
return self.dismissView
}
return super.hitTest(point, with: event)
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -27,10 +27,12 @@ final class MediaNavigationStripComponent: Component {
let index: Int
let count: Int
let isSeeking: Bool
init(index: Int, count: Int) {
init(index: Int, count: Int, isSeeking: Bool) {
self.index = index
self.count = count
self.isSeeking = isSeeking
}
static func ==(lhs: MediaNavigationStripComponent, rhs: MediaNavigationStripComponent) -> Bool {
@ -40,6 +42,9 @@ final class MediaNavigationStripComponent: Component {
if lhs.count != rhs.count {
return false
}
if lhs.isSeeking != rhs.isSeeking {
return false
}
return true
}
@ -155,12 +160,18 @@ final class MediaNavigationStripComponent: Component {
return
}
guard !self.isTransitioning else {
return
}
let itemFrame = itemLayer.bounds
transition.setFrame(layer: itemLayer.foregroundLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: value * itemFrame.width, height: itemFrame.height)))
itemLayer.updateIsBuffering(size: itemFrame.size, isBuffering: isBuffering)
}
private var isTransitioning = false
func update(component: MediaNavigationStripComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
let previousComponent = self.component
self.component = component
let environment = environment[EnvironmentType.self].value
@ -169,6 +180,10 @@ final class MediaNavigationStripComponent: Component {
let itemHeight: CGFloat = 2.0
let minItemWidth: CGFloat = 2.0
var size = CGSize(width: availableSize.width, height: itemHeight)
var didSetCompletion = false
var validIndices: [Int] = []
if component.count != 0 {
let idealItemWidth: CGFloat = (availableSize.width - CGFloat(component.count - 1) * spacing) / CGFloat(component.count)
@ -205,8 +220,12 @@ final class MediaNavigationStripComponent: Component {
if i >= component.count {
continue
}
let itemFrame = CGRect(origin: CGPoint(x: -globalOffset + CGFloat(i) * (itemWidth + spacing), y: 0.0), size: CGSize(width: itemWidth, height: itemHeight))
if itemFrame.maxY < 0.0 || itemFrame.minY >= availableSize.width {
var itemFrame = CGRect(origin: CGPoint(x: -globalOffset + CGFloat(i) * (itemWidth + spacing), y: 0.0), size: CGSize(width: itemWidth, height: itemHeight))
if component.isSeeking {
itemFrame = CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: 6.0))
size.height = itemFrame.height
}
if itemFrame.maxX < 0.0 || itemFrame.minX >= availableSize.width {
continue
}
@ -219,11 +238,21 @@ final class MediaNavigationStripComponent: Component {
itemLayer = ItemLayer()
self.layer.addSublayer(itemLayer)
self.visibleItems[i] = itemLayer
itemLayer.cornerRadius = itemHeight * 0.5
}
transition.setFrame(layer: itemLayer, frame: itemFrame)
transition.setCornerRadius(layer: itemLayer, cornerRadius: itemFrame.height * 0.5)
transition.setCornerRadius(layer: itemLayer.foregroundLayer, cornerRadius: itemFrame.height * 0.5, completion: transition.animation.isImmediate || didSetCompletion ? nil : { [weak self] _ in
if let self {
self.isTransitioning = false
}
})
if !transition.animation.isImmediate && component.isSeeking != previousComponent?.isSeeking {
self.isTransitioning = true
didSetCompletion = true
}
itemLayer.backgroundColor = UIColor(white: 1.0, alpha: 0.5).cgColor
itemLayer.foregroundLayer.backgroundColor = UIColor(white: 1.0, alpha: 1.0).cgColor
@ -239,6 +268,9 @@ final class MediaNavigationStripComponent: Component {
}
transition.setFrame(layer: itemLayer.foregroundLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: itemProgress * itemFrame.width, height: itemFrame.height)))
transition.setAlpha(layer: itemLayer, alpha: !component.isSeeking || i == component.index ? 1.0 : 0.0)
itemLayer.updateIsBuffering(size: itemFrame.size, isBuffering: itemIsBuffering)
}
}

View File

@ -92,6 +92,8 @@ private final class MuteMonitor {
private final class StoryLongPressRecognizer: UILongPressGestureRecognizer {
var shouldBegin: ((UITouch) -> Bool)?
var updateIsTracking: ((CGPoint?) -> Void)?
var updatePanMove: ((CGPoint, CGPoint) -> Void)?
var updatePanEnded: (() -> Void)?
override var state: UIGestureRecognizer.State {
didSet {
@ -110,6 +112,8 @@ private final class StoryLongPressRecognizer: UILongPressGestureRecognizer {
private var isTracking: Bool = false
private var isValidated: Bool = false
private var initialLocation: CGPoint?
override func reset() {
super.reset()
@ -134,10 +138,33 @@ private final class StoryLongPressRecognizer: UILongPressGestureRecognizer {
if !self.isTracking {
self.isTracking = true
self.updateIsTracking?(touches.first?.location(in: self.view))
self.initialLocation = touches.first?.location(in: self.view)
self.updateIsTracking?(initialLocation)
}
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
if self.isValidated {
super.touchesMoved(touches, with: event)
if let location = touches.first?.location(in: self.view), let initialLocation = self.initialLocation {
self.updatePanMove?(initialLocation, CGPoint(x: location.x - initialLocation.x, y: location.y - initialLocation.y))
}
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesEnded(touches, with: event)
self.updatePanEnded?()
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesCancelled(touches, with: event)
self.updatePanEnded?()
}
}
private final class StoryPinchGesture: UIPinchGestureRecognizer {
@ -397,6 +424,9 @@ private final class StoryContainerScreenComponent: Component {
private var isDisplayingInteractionGuide: Bool = false
private var displayInteractionGuideDisposable: Disposable?
private var previousSeekTime: Double?
private var initialSeekTimestamp: Double?
override init(frame: CGRect) {
self.backgroundLayer = SimpleLayer()
self.backgroundLayer.backgroundColor = UIColor.black.cgColor
@ -468,6 +498,52 @@ private final class StoryContainerScreenComponent: Component {
}
}
}
longPressRecognizer.updatePanMove = { [weak self] initialLocation, translation in
guard let self else {
return
}
guard let stateValue = self.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id], let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View else {
return
}
guard let visibleItemView = itemSetComponentView.visibleItems[slice.item.storyItem.id]?.view.view as? StoryItemContentComponent.View else {
return
}
var apply = true
let currentTime = CACurrentMediaTime()
if let previousTime = self.previousSeekTime, currentTime - previousTime < 0.1 {
apply = false
}
if apply {
self.previousSeekTime = currentTime
}
let initialSeekTimestamp: Double
if let current = self.initialSeekTimestamp {
initialSeekTimestamp = current
} else {
initialSeekTimestamp = visibleItemView.effectiveTimestamp
self.initialSeekTimestamp = initialSeekTimestamp
}
let duration = visibleItemView.effectiveDuration
let timestamp: Double
if translation.x > 0.0 {
let fraction = translation.x / (self.bounds.width / 2.0)
timestamp = initialSeekTimestamp + duration * fraction
} else {
let fraction = translation.x / (self.bounds.width / 2.0)
timestamp = initialSeekTimestamp + duration * fraction
}
visibleItemView.seekTo(max(0.0, min(duration, timestamp)), apply: apply)
}
longPressRecognizer.updatePanEnded = { [weak self] in
guard let self else {
return
}
self.initialSeekTimestamp = nil
self.previousSeekTime = nil
}
longPressRecognizer.shouldBegin = { [weak self] touch in
guard let self else {
return false

View File

@ -397,7 +397,26 @@ final class StoryItemContentComponent: Component {
}
}
private func updateVideoPlaybackProgress() {
var effectiveTimestamp: Double {
guard let videoPlaybackStatus = self.videoPlaybackStatus else {
return 0.0
}
return videoPlaybackStatus.timestamp
}
var effectiveDuration: Double {
let effectiveDuration: Double
if let videoPlaybackStatus, videoPlaybackStatus.duration > 0.0 {
effectiveDuration = videoPlaybackStatus.duration
} else if case let .file(file) = self.currentMessageMedia, let duration = file.duration {
effectiveDuration = Double(max(1, duration))
} else {
effectiveDuration = 1.0
}
return effectiveDuration
}
private func updateVideoPlaybackProgress(_ scrubbingTimestamp: Double? = nil) {
guard let videoPlaybackStatus = self.videoPlaybackStatus else {
return
}
@ -478,6 +497,13 @@ final class StoryItemContentComponent: Component {
}
}
if let scrubbingTimestamp {
currentProgress = CGFloat(scrubbingTimestamp / effectiveDuration)
if currentProgress.isNaN || !currentProgress.isFinite {
currentProgress = 0.0
}
}
let clippedProgress = max(0.0, min(1.0, currentProgress))
self.environment?.presentationProgressUpdated(clippedProgress, isBuffering, false)
}
@ -510,6 +536,16 @@ final class StoryItemContentComponent: Component {
)
}
func seekTo(_ timestamp: Double, apply: Bool) {
guard let videoNode = self.videoNode else {
return
}
if apply {
videoNode.seek(timestamp)
}
self.updateVideoPlaybackProgress(timestamp)
}
func update(component: StoryItemContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<StoryContentItem.Environment>, transition: Transition) -> CGSize {
let previousItem = self.component?.item

View File

@ -403,6 +403,7 @@ public final class StoryItemSetContainerComponent: Component {
let itemsContainerView: UIView
let controlsContainerView: UIView
let controlsClippingView: UIView
let controlsNavigationClippingView: UIView
let topContentGradientView: UIImageView
let bottomContentGradientLayer: SimpleGradientLayer
let contentDimView: UIView
@ -411,6 +412,7 @@ public final class StoryItemSetContainerComponent: Component {
let closeButtonIconView: UIImageView
let navigationStrip = ComponentView<MediaNavigationStripComponent.EnvironmentType>()
let seekLabel = ComponentView<Empty>()
var centerInfoItem: InfoItem?
var leftInfoItem: InfoItem?
@ -508,6 +510,12 @@ public final class StoryItemSetContainerComponent: Component {
self.controlsClippingView.layer.cornerCurve = .continuous
}
self.controlsNavigationClippingView = SparseContainerView()
self.controlsNavigationClippingView.clipsToBounds = true
if #available(iOS 13.0, *) {
self.controlsNavigationClippingView.layer.cornerCurve = .continuous
}
self.topContentGradientView = UIImageView()
if let image = StoryItemSetContainerComponent.shadowImage {
self.topContentGradientView.image = image.stretchableImage(withLeftCapWidth: 0, topCapHeight: Int(image.size.height - 1.0))
@ -539,11 +547,12 @@ public final class StoryItemSetContainerComponent: Component {
self.itemsContainerView.addGestureRecognizer(self.scroller.panGestureRecognizer)
self.componentContainerView.addSubview(self.itemsContainerView)
self.componentContainerView.addSubview(self.controlsNavigationClippingView)
self.componentContainerView.addSubview(self.controlsClippingView)
self.componentContainerView.addSubview(self.controlsContainerView)
self.controlsClippingView.addSubview(self.contentDimView)
self.controlsClippingView.addSubview(self.topContentGradientView)
self.controlsNavigationClippingView.addSubview(self.topContentGradientView)
self.layer.addSublayer(self.bottomContentGradientLayer)
self.componentContainerView.addSubview(self.viewListsContainer)
@ -2054,6 +2063,9 @@ public final class StoryItemSetContainerComponent: Component {
duration: 0.3
)
self.controlsNavigationClippingView.layer.animatePosition(from: sourceLocalFrame.center, to: self.controlsNavigationClippingView.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
self.controlsNavigationClippingView.layer.animateBounds(from: CGRect(origin: CGPoint(x: innerSourceLocalFrame.minX, y: innerSourceLocalFrame.minY), size: sourceLocalFrame.size), to: self.controlsNavigationClippingView.bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
if let component = self.component, let visibleItemView = self.visibleItems[component.slice.item.storyItem.id]?.view.view {
let innerScale = innerSourceLocalFrame.width / visibleItemView.bounds.width
let innerFromFrame = CGRect(origin: CGPoint(x: innerSourceLocalFrame.minX, y: innerSourceLocalFrame.minY), size: CGSize(width: innerSourceLocalFrame.width, height: visibleItemView.bounds.height * innerScale))
@ -2264,6 +2276,7 @@ public final class StoryItemSetContainerComponent: Component {
unclippedContainerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
self.controlsContainerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
self.controlsClippingView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
self.controlsNavigationClippingView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
for transitionViewImpl in transitionViewsImpl {
transition.setFrame(view: transitionViewImpl, frame: sourceLocalFrame)
@ -2318,6 +2331,9 @@ public final class StoryItemSetContainerComponent: Component {
removeOnCompletion: false
)
self.controlsNavigationClippingView.layer.animatePosition(from: self.controlsNavigationClippingView.center, to: sourceLocalFrame.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
self.controlsNavigationClippingView.layer.animateBounds(from: self.controlsNavigationClippingView.bounds, to: CGRect(origin: CGPoint(x: innerSourceLocalFrame.minX, y: innerSourceLocalFrame.minY), size: sourceLocalFrame.size), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
self.overlayContainerView.clipsToBounds = true
let overlayToFrame = sourceLocalFrame
let overlayToBounds = CGRect(origin: CGPoint(x: overlayToFrame.minX, y: overlayToFrame.minY), size: overlayToFrame.size)
@ -2382,6 +2398,7 @@ public final class StoryItemSetContainerComponent: Component {
unclippedContainerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
self.controlsContainerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
self.controlsClippingView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
self.controlsNavigationClippingView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
for transitionViewImpl in transitionViewsImpl {
transition.setFrame(view: transitionViewImpl, frame: sourceLocalFrame)
@ -2523,6 +2540,8 @@ public final class StoryItemSetContainerComponent: Component {
#endif*/
}
let previousComponent = self.component
var isFirstItem = false
var itemChanged = false
var resetInputContents: MessageInputPanelComponent.SendMessageInput?
@ -3603,6 +3622,9 @@ public final class StoryItemSetContainerComponent: Component {
transition.setPosition(view: self.controlsClippingView, position: contentFrame.center)
transition.setBounds(view: self.controlsClippingView, bounds: CGRect(origin: CGPoint(), size: contentFrame.size))
transition.setPosition(view: self.controlsNavigationClippingView, position: contentFrame.center)
transition.setBounds(view: self.controlsNavigationClippingView, bounds: CGRect(origin: CGPoint(), size: contentFrame.size))
var transform = CATransform3DMakeScale(contentVisualScale, contentVisualScale, 1.0)
if let pinchState = component.pinchState {
let pinchOffset = CGPoint(
@ -3619,6 +3641,7 @@ public final class StoryItemSetContainerComponent: Component {
}
transition.setTransform(view: self.controlsContainerView, transform: transform)
transition.setTransform(view: self.controlsClippingView, transform: transform)
transition.setTransform(view: self.controlsNavigationClippingView, transform: transform)
transition.setCornerRadius(layer: self.controlsClippingView.layer, cornerRadius: 12.0 * (1.0 / contentVisualScale))
@ -3870,6 +3893,7 @@ public final class StoryItemSetContainerComponent: Component {
let controlsContainerAlpha = (component.hideUI || self.isEditingStory || self.viewListDisplayState != .hidden) ? 0.0 : 1.0
transition.setAlpha(view: self.controlsContainerView, alpha: controlsContainerAlpha)
transition.setAlpha(view: self.controlsClippingView, alpha: controlsContainerAlpha)
transition.setAlpha(view: self.controlsNavigationClippingView, alpha: self.isEditingStory || self.viewListDisplayState != .hidden ? 0.0 : 1.0)
let focusedItem: StoryContentItem? = component.slice.item
@ -4584,7 +4608,7 @@ public final class StoryItemSetContainerComponent: Component {
//transition.setAlpha(layer: self.bottomContentGradientLayer, alpha: inputPanelIsOverlay ? 1.0 : 0.0)
transition.setAlpha(layer: self.bottomContentGradientLayer, alpha: 0.0)
var topGradientAlpha: CGFloat = (component.hideUI || self.viewListDisplayState != .hidden || self.isEditingStory) ? 0.0 : 1.0
var topGradientAlpha: CGFloat = (self.viewListDisplayState != .hidden || self.isEditingStory) ? 0.0 : 1.0
var normalDimAlpha: CGFloat = 0.0
var forceDimAnimation = false
if let captionItem = self.captionItem {
@ -4644,10 +4668,9 @@ public final class StoryItemSetContainerComponent: Component {
let startTime9 = CFAbsoluteTimeGetCurrent()
let navigationStripSideInset: CGFloat = 8.0
let navigationStripTopInset: CGFloat = 8.0
if let focusedItem, let visibleItem = self.visibleItems[focusedItem.storyItem.id], let index = focusedItem.position {
let navigationStripSideInset: CGFloat = 8.0
let navigationStripTopInset: CGFloat = 8.0
var index = max(0, min(index, component.slice.totalCount - 1))
var count = component.slice.totalCount
if let dayCounters = focusedItem.dayCounters {
@ -4655,11 +4678,18 @@ public final class StoryItemSetContainerComponent: Component {
count = dayCounters.totalCount
}
let _ = self.navigationStrip.update(
transition: transition,
let isSeeking = component.isProgressPaused && component.hideUI && isVideo
var navigationStripTransition = transition
if let previousComponent, (previousComponent.isProgressPaused && component.hideUI) != isSeeking {
navigationStripTransition = .easeInOut(duration: 0.3)
}
let navigationStripSize = self.navigationStrip.update(
transition: navigationStripTransition,
component: AnyComponent(MediaNavigationStripComponent(
index: index,
count: count
count: count,
isSeeking: isSeeking
)),
environment: {
MediaNavigationStripComponent.EnvironmentType(
@ -4667,15 +4697,35 @@ public final class StoryItemSetContainerComponent: Component {
currentIsBuffering: visibleItem.isBuffering
)
},
containerSize: CGSize(width: availableSize.width - navigationStripSideInset * 2.0, height: 2.0)
containerSize: CGSize(width: availableSize.width - navigationStripSideInset * 2.0, height: 6.0)
)
if let navigationStripView = self.navigationStrip.view {
if navigationStripView.superview == nil {
navigationStripView.isUserInteractionEnabled = false
self.controlsClippingView.addSubview(navigationStripView)
self.controlsNavigationClippingView.addSubview(navigationStripView)
}
transition.setFrame(view: navigationStripView, frame: CGRect(origin: CGPoint(x: navigationStripSideInset, y: navigationStripTopInset), size: CGSize(width: availableSize.width - navigationStripSideInset * 2.0, height: 2.0)))
transition.setAlpha(view: navigationStripView, alpha: self.isEditingStory ? 0.0 : 1.0)
transition.setFrame(view: navigationStripView, frame: CGRect(origin: CGPoint(x: navigationStripSideInset, y: navigationStripTopInset), size: CGSize(width: availableSize.width - navigationStripSideInset * 2.0, height: navigationStripSize.height)))
let hideUI = component.hideUI && !isVideo
transition.setAlpha(view: navigationStripView, alpha: self.isEditingStory || hideUI ? 0.0 : 1.0)
}
let seekLabelSize = self.seekLabel.update(
transition: .immediate,
component: AnyComponent(Text(text: "Slide left or right to seek", font: Font.semibold(14.0), color: .white)),
environment: {},
containerSize: availableSize
)
if let seekLabelView = self.seekLabel.view {
if seekLabelView.superview == nil {
seekLabelView.alpha = 0.0
seekLabelView.isUserInteractionEnabled = false
self.controlsNavigationClippingView.addSubview(seekLabelView)
}
seekLabelView.bounds = CGRect(origin: .zero, size: seekLabelSize)
navigationStripTransition.setPosition(view: seekLabelView, position: CGPoint(x: availableSize.width / 2.0, y: navigationStripTopInset + 22.0 + 6.0 - (!isSeeking ? 12.0 : 0.0)))
navigationStripTransition.setAlpha(view: seekLabelView, alpha: isSeeking ? 1.0 : 0.0)
navigationStripTransition.setScale(view: seekLabelView, scale: isSeeking ? 1.0 : 0.02)
}
}

View File

@ -799,6 +799,10 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
icons.append(PresentationAppIcon(name: "PremiumBlack", imageName: "PremiumBlack", isPremium: true))
icons.append(PresentationAppIcon(name: "PremiumTurbo", imageName: "PremiumTurbo", isPremium: true))
icons.append(PresentationAppIcon(name: "PremiumDuck", imageName: "PremiumDuck", isPremium: true))
icons.append(PresentationAppIcon(name: "PremiumCoffee", imageName: "PremiumCoffee", isPremium: true))
icons.append(PresentationAppIcon(name: "PremiumSteam", imageName: "PremiumSteam", isPremium: true))
return icons
} else {
return []

View File

@ -430,7 +430,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
if attribute is ReplyThreadMessageAttribute {
return false
}
if attribute is ViewCountMessageAttribute{
if attribute is ViewCountMessageAttribute {
return false
}
if attribute is ForwardCountMessageAttribute {

View File

@ -5485,7 +5485,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}
} else if let channel = peer as? TelegramChannel {
if let cachedData = strongSelf.data?.cachedData as? CachedChannelData {
if channel.hasPermission(.editStories) {
if case .broadcast = channel.info, channel.hasPermission(.editStories) {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.PeerInfo_Channel_ArchivedStories, icon: { theme in
generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Archive"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in