Various improvements

This commit is contained in:
Ilya Laktyushin 2023-10-12 14:02:36 +04:00
parent 0f0b14833f
commit b80f2636d6
17 changed files with 669 additions and 66 deletions

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

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

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

@ -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, countriesIso2: nil, 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, countriesIso2: nil, 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

@ -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,48 @@ 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
}
let currentTime = CACurrentMediaTime()
if let previousTime = self.previousSeekTime, currentTime - previousTime < 0.1 {
return
}
self.previousSeekTime = currentTime
let initialSeekTimestamp: Double
if let current = self.initialSeekTimestamp {
initialSeekTimestamp = current
} else {
initialSeekTimestamp = visibleItemView.effectiveTimestamp
self.initialSeekTimestamp = initialSeekTimestamp
}
let timestamp: Double
if translation.x > 0.0 {
let fraction = translation.x / (self.bounds.width - initialLocation.x)
timestamp = initialSeekTimestamp + (visibleItemView.effectiveDuration - initialSeekTimestamp) * fraction * fraction
} else {
let fraction = translation.x / initialLocation.x
timestamp = initialSeekTimestamp + initialSeekTimestamp * fraction * fraction * -1.0
}
visibleItemView.seekTo(timestamp)
}
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,6 +397,25 @@ final class StoryItemContentComponent: Component {
}
}
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() {
guard let videoPlaybackStatus = self.videoPlaybackStatus else {
return
@ -510,6 +529,14 @@ final class StoryItemContentComponent: Component {
)
}
func seekTo(_ timestamp: Double) {
guard let videoNode = self.videoNode else {
return
}
videoNode.seek(timestamp)
self.updateVideoPlaybackProgress()
}
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
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,16 +4697,34 @@ 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.setFrame(view: navigationStripView, frame: CGRect(origin: CGPoint(x: navigationStripSideInset, y: navigationStripTopInset), size: CGSize(width: availableSize.width - navigationStripSideInset * 2.0, height: navigationStripSize.height)))
transition.setAlpha(view: navigationStripView, alpha: self.isEditingStory ? 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)
}
}
component.externalState.derivedMediaSize = contentFrame.size