mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-10-09 03:20:48 +00:00
Merge commit '4013fca50e5ac104f860728aaa715fdca50ae54a'
# Conflicts: # submodules/TelegramUI/Sources/ChatControllerNode.swift
This commit is contained in:
commit
7812c11601
@ -329,6 +329,9 @@ alternate_icon_folders = [
|
||||
"Premium",
|
||||
"PremiumBlack",
|
||||
"PremiumTurbo",
|
||||
"PremiumCoffee",
|
||||
"PremiumDuck",
|
||||
"PremiumSteam",
|
||||
]
|
||||
|
||||
[
|
||||
|
BIN
Telegram/Telegram-iOS/PremiumCoffee.alticon/PremiumCoffee@2x.png
Normal file
BIN
Telegram/Telegram-iOS/PremiumCoffee.alticon/PremiumCoffee@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
BIN
Telegram/Telegram-iOS/PremiumCoffee.alticon/PremiumCoffee@3x.png
Normal file
BIN
Telegram/Telegram-iOS/PremiumCoffee.alticon/PremiumCoffee@3x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 48 KiB |
BIN
Telegram/Telegram-iOS/PremiumDuck.alticon/PremiumDuck@2x.png
Normal file
BIN
Telegram/Telegram-iOS/PremiumDuck.alticon/PremiumDuck@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 19 KiB |
BIN
Telegram/Telegram-iOS/PremiumDuck.alticon/PremiumDuck@3x.png
Normal file
BIN
Telegram/Telegram-iOS/PremiumDuck.alticon/PremiumDuck@3x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
BIN
Telegram/Telegram-iOS/PremiumSteam.alticon/PremiumSteam@2x.png
Normal file
BIN
Telegram/Telegram-iOS/PremiumSteam.alticon/PremiumSteam@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
BIN
Telegram/Telegram-iOS/PremiumSteam.alticon/PremiumSteam@3x.png
Normal file
BIN
Telegram/Telegram-iOS/PremiumSteam.alticon/PremiumSteam@3x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 48 KiB |
@ -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";
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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) }
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -80,6 +80,7 @@ swift_library(
|
||||
"//submodules/Utils/VolumeButtons",
|
||||
"//submodules/TelegramNotices",
|
||||
"//submodules/DeviceAccess",
|
||||
"//submodules/TelegramUI/Components/Utils/RoundedRectWithTailPath",
|
||||
|
||||
],
|
||||
visibility = [
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 []
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user