This commit is contained in:
Ali 2023-08-10 00:13:20 +03:00
parent e0c9a3075d
commit 458a153bad
14 changed files with 501 additions and 66 deletions

View File

@ -0,0 +1,57 @@
import Foundation
import UIKit
public final class HStack<ChildEnvironment: Equatable>: CombinedComponent {
public typealias EnvironmentType = ChildEnvironment
private let items: [AnyComponentWithIdentity<ChildEnvironment>]
private let spacing: CGFloat
public init(_ items: [AnyComponentWithIdentity<ChildEnvironment>], spacing: CGFloat) {
self.items = items
self.spacing = spacing
}
public static func ==(lhs: HStack<ChildEnvironment>, rhs: HStack<ChildEnvironment>) -> Bool {
if lhs.items != rhs.items {
return false
}
if lhs.spacing != rhs.spacing {
return false
}
return true
}
public static var body: Body {
let children = ChildMap(environment: ChildEnvironment.self, keyedBy: AnyHashable.self)
return { context in
let updatedChildren = context.component.items.map { item in
return children[item.id].update(
component: item.component, environment: {
context.environment[ChildEnvironment.self]
},
availableSize: context.availableSize,
transition: context.transition
)
}
var size = CGSize(width: 0.0, height: 0.0)
for child in updatedChildren {
size.width += child.size.width
size.height = max(size.height, child.size.height)
}
var nextX = 0.0
for child in updatedChildren {
context.add(child
.position(child.size.centered(in: CGRect(origin: CGPoint(x: nextX, y: floor((size.height - child.size.height) * 0.5)), size: child.size)).center)
)
nextX += child.size.width
nextX += context.component.spacing
}
return size
}
}
}

View File

@ -578,7 +578,7 @@ private func privacyAndSecurityControllerEntries(
entries.append(.voiceMessagePrivacy(presentationData.theme, presentationData.strings.Privacy_VoiceMessages, stringForSelectiveSettings(strings: presentationData.strings, settings: privacySettings.voiceMessages), !isPremium))
}
entries.append(.selectivePrivacyInfo(presentationData.theme, presentationData.strings.PrivacyLastSeenSettings_GroupsAndChannelsHelp))
//entries.append(.selectivePrivacyInfo(presentationData.theme, presentationData.strings.PrivacyLastSeenSettings_GroupsAndChannelsHelp))
} else {
entries.append(.phoneNumberPrivacy(presentationData.theme, presentationData.strings.PrivacySettings_PhoneNumber, presentationData.strings.Channel_NotificationLoading))
entries.append(.lastSeenPrivacy(presentationData.theme, presentationData.strings.PrivacySettings_LastSeen, presentationData.strings.Channel_NotificationLoading))
@ -591,7 +591,7 @@ private func privacyAndSecurityControllerEntries(
entries.append(.voiceMessagePrivacy(presentationData.theme, presentationData.strings.Privacy_VoiceMessages, presentationData.strings.Channel_NotificationLoading, !isPremium))
}
entries.append(.selectivePrivacyInfo(presentationData.theme, presentationData.strings.PrivacyLastSeenSettings_GroupsAndChannelsHelp))
//entries.append(.selectivePrivacyInfo(presentationData.theme, presentationData.strings.PrivacyLastSeenSettings_GroupsAndChannelsHelp))
}
if canAutoarchive {

View File

@ -82,17 +82,20 @@ public final class LottieComponent: Component {
public let color: UIColor?
public let startingPosition: StartingPosition
public let size: CGSize?
public let loop: Bool
public init(
content: Content,
color: UIColor? = nil,
startingPosition: StartingPosition = .end,
size: CGSize? = nil
size: CGSize? = nil,
loop: Bool = false
) {
self.content = content
self.color = color
self.startingPosition = startingPosition
self.size = size
self.loop = loop
}
public static func ==(lhs: LottieComponent, rhs: LottieComponent) -> Bool {
@ -108,6 +111,9 @@ public final class LottieComponent: Component {
if lhs.size != rhs.size {
return false
}
if lhs.loop != rhs.loop {
return false
}
return true
}
@ -116,6 +122,8 @@ public final class LottieComponent: Component {
private var component: LottieComponent?
private var scheduledPlayOnce: Bool = false
private var isPlaying: Bool = false
private var playOnceCompletion: (() -> Void)?
private var animationInstance: LottieInstance?
private var animationFrameRange: Range<Int>?
@ -196,6 +204,7 @@ public final class LottieComponent: Component {
}
self.scheduledPlayOnce = false
self.isPlaying = true
if self.currentFrame != animationFrameRange.lowerBound {
self.currentFrame = animationFrameRange.lowerBound
@ -284,11 +293,19 @@ public final class LottieComponent: Component {
advanceFrameCount = 4
}
self.currentFrame += advanceFrameCount
if self.currentFrame >= animationFrameRange.upperBound - 1 {
if let component = self.component, component.loop {
self.currentFrame = animationFrameRange.lowerBound
}
}
if self.currentFrame >= animationFrameRange.upperBound - 1 {
self.currentFrame = animationFrameRange.upperBound - 1
self.updateImage()
self.displayLink?.invalidate()
self.displayLink = nil
self.isPlaying = false
if let playOnceCompletion = self.playOnceCompletion {
self.playOnceCompletion = nil
@ -362,6 +379,10 @@ public final class LottieComponent: Component {
transition.setTintColor(view: self, color: color)
}
if component.loop && !self.isPlaying {
self.playOnce()
}
return size
}
}

View File

@ -75,6 +75,9 @@ public final class NavigationSearchComponent: Component {
private let searchIconView: UIImageView
private let placeholderText = ComponentView<Empty>()
private let clearButton: HighlightableButton
private let clearIconView: UIImageView
private var button: ComponentView<Empty>?
private var textField: UITextField?
@ -85,12 +88,21 @@ public final class NavigationSearchComponent: Component {
self.searchIconView = UIImageView(image: UIImage(bundleImageName: "Components/Search Bar/Loupe")?.withRenderingMode(.alwaysTemplate))
self.clearButton = HighlightableButton()
self.clearIconView = UIImageView(image: UIImage(bundleImageName: "Components/Search Bar/Clear")?.withRenderingMode(.alwaysTemplate))
super.init(frame: frame)
self.addSubview(self.backgroundView)
self.addSubview(self.searchIconView)
self.addSubview(self.clearButton)
self.clearButton.addSubview(self.clearIconView)
self.clearButton.isHidden = true
self.backgroundView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.backgroundTapGesture(_:))))
self.clearButton.addTarget(self, action: #selector(self.clearPressed), for: .touchUpInside)
}
required init?(coder: NSCoder) {
@ -109,6 +121,7 @@ public final class NavigationSearchComponent: Component {
textField.addTarget(self, action: #selector(self.textChanged), for: .editingChanged)
self.addSubview(textField)
textField.keyboardAppearance = .dark
textField.returnKeyType = .done
}
self.textField?.becomeFirstResponder()
@ -124,14 +137,26 @@ public final class NavigationSearchComponent: Component {
}
}
public func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
return false
}
@objc private func textChanged() {
self.updateText(updateComponent: true)
}
@objc private func clearPressed() {
self.textField?.text = ""
self.updateText(updateComponent: true)
}
@objc private func updateText(updateComponent: Bool) {
let isEmpty = self.textField?.text?.isEmpty ?? true
self.placeholderText.view?.isHidden = !isEmpty
self.clearButton.isHidden = isEmpty
if updateComponent, let component = self.component {
component.updateQuery(self.textField?.text ?? "")
}
@ -212,6 +237,7 @@ public final class NavigationSearchComponent: Component {
}
if previousComponent?.colors.inactiveForeground != component.colors.inactiveForeground {
self.searchIconView.tintColor = component.colors.inactiveForeground
self.clearIconView.tintColor = component.colors.inactiveForeground
}
let placeholderSize = self.placeholderText.update(
@ -241,6 +267,15 @@ public final class NavigationSearchComponent: Component {
let searchIconFrame = CGRect(origin: CGPoint(x: placeholderTextFrame.minX - searchIconSpacing - searchIconSize.width, y: backgroundFrame.minY + floor((fieldHeight - searchIconSize.height) * 0.5)), size: searchIconSize)
transition.setFrame(view: self.searchIconView, frame: searchIconFrame)
if let image = self.clearIconView.image {
let clearSize = image.size
let clearFrame = CGRect(origin: CGPoint(x: backgroundFrame.maxX - 6.0 - clearSize.width, y: backgroundFrame.minY + floor((backgroundFrame.height - clearSize.height) / 2.0)), size: clearSize)
let clearButtonFrame = CGRect(origin: CGPoint(x: clearFrame.minX - 4.0, y: backgroundFrame.minY), size: CGSize(width: clearFrame.width + 8.0, height: backgroundFrame.height))
transition.setFrame(view: self.clearButton, frame: clearButtonFrame)
transition.setFrame(view: self.clearIconView, frame: clearFrame.offsetBy(dx: -clearButtonFrame.minX, dy: -clearButtonFrame.minY))
}
if let textField = self.textField {
var textFieldTransition = transition
var animateIn = false
@ -255,7 +290,7 @@ public final class NavigationSearchComponent: Component {
}
let textLeftInset: CGFloat = fieldSideInset + searchIconSize.width + searchIconSpacing
let textRightInset: CGFloat = 8.0
let textRightInset: CGFloat = 8.0 + 30.0
textFieldTransition.setFrame(view: textField, frame: CGRect(origin: CGPoint(x: placeholderTextFrame.minX, y: backgroundFrame.minY - 1.0), size: CGSize(width: backgroundFrame.width - textLeftInset - textRightInset, height: backgroundFrame.height)))
if animateIn {

View File

@ -287,7 +287,7 @@ final class StorageUsageScreenComponent: Component {
case .misc:
return UIColor(rgb: 0xFF9500)
case .stories:
return UIColor(rgb: 0x3478F6)
return UIColor(rgb: 0xFF2D55)
}
}

View File

@ -888,7 +888,10 @@ private final class StoryContainerScreenComponent: Component {
}
controller.forEachController { controller in
if let controller = controller as? UndoOverlayController {
if let tag = controller.tag as? String, tag == "no_auto_dismiss" {
} else {
controller.dismissWithCommitAction()
}
} else if let controller = controller as? TooltipScreen {
controller.dismiss()
}

View File

@ -348,6 +348,7 @@ public final class StoryItemSetContainerComponent: Component {
weak var scrollView: UIScrollView?
var startContentOffsetY: CGFloat = 0.0
var accumulatedOffset: CGFloat = 0.0
var dismissedTooltips: Bool = false
init(fraction: CGFloat, scrollView: UIScrollView?) {
self.fraction = fraction
@ -998,6 +999,11 @@ public final class StoryItemSetContainerComponent: Component {
}
if let verticalPanState = self.verticalPanState {
if abs(verticalPanState.fraction) >= 0.1 && !verticalPanState.dismissedTooltips {
verticalPanState.dismissedTooltips = true
self.dismissAllTooltips()
}
if let scrollView = verticalPanState.scrollView {
let relativeTranslationY = recognizer.translation(in: self).y - verticalPanState.startContentOffsetY
let overflowY = scrollView.contentOffset.y - relativeTranslationY
@ -1061,6 +1067,7 @@ public final class StoryItemSetContainerComponent: Component {
} else if verticalPanState.fraction >= 0.05 && velocity.y >= -80.0 {
self.viewListDisplayState = .half
}
self.dismissAllTooltips()
}
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
@ -1073,6 +1080,7 @@ public final class StoryItemSetContainerComponent: Component {
if component.slice.peer.id == component.context.account.peerId {
self.viewListDisplayState = self.targetViewListDisplayStateIsFull ? .full : .half
self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring)))
self.dismissAllTooltips()
} else {
self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring)))
@ -1511,6 +1519,8 @@ public final class StoryItemSetContainerComponent: Component {
self.preparingToDisplayViewList = false
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
self.dismissAllTooltips()
}
},
deleteAction: { [weak self] in
@ -1627,6 +1637,9 @@ public final class StoryItemSetContainerComponent: Component {
if component.slice.peer.id == component.context.account.peerId {
self.viewListDisplayState = .half
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
self.dismissAllTooltips()
return true
} else {
var canReply = true
@ -2127,9 +2140,6 @@ public final class StoryItemSetContainerComponent: Component {
}
func maybeDisplayReactionTooltip() {
if "".isEmpty {
return
}
guard let component = self.component else {
return
}
@ -2207,9 +2217,12 @@ public final class StoryItemSetContainerComponent: Component {
}
if let tooltipScreen = self.sendMessageContext.tooltipScreen {
if let tooltipScreen = tooltipScreen as? UndoOverlayController, let tag = tooltipScreen.tag as? String, tag == "no_auto_dismiss" {
} else {
tooltipScreen.dismiss()
}
}
}
var itemsTransition = transition
var resetScrollingOffsetWithItemTransition = false
if let animateNextNavigationId = self.animateNextNavigationId, animateNextNavigationId == component.slice.item.storyItem.id {
@ -2736,6 +2749,11 @@ public final class StoryItemSetContainerComponent: Component {
var safeInsets = component.safeInsets
safeInsets.bottom = max(safeInsets.bottom, component.inputHeight)
var hasPremium = false
if case let .user(user) = component.slice.peer {
hasPremium = user.isPremium
}
viewList.view.parentState = state
let viewListSize = viewList.view.update(
transition: viewListTransition.withUserData(PeerListItemComponent.TransitionHint(
@ -2751,6 +2769,7 @@ public final class StoryItemSetContainerComponent: Component {
peerId: component.slice.peer.id,
safeInsets: safeInsets,
storyItem: item.storyItem,
hasPremium: hasPremium,
effectiveHeight: viewListHeight,
minHeight: midViewListHeight,
availableReactions: component.availableReactions,
@ -3007,7 +3026,12 @@ public final class StoryItemSetContainerComponent: Component {
}
self.openPeerStories(peer: peer, avatarNode: avatarNode)
},
openPremiumIntro: {},
openPremiumIntro: { [weak self] in
guard let self else {
return
}
self.presentStoriesUpgradeScreen()
},
setIsSearchActive: { [weak self] value in
guard let self else {
return
@ -4136,7 +4160,7 @@ public final class StoryItemSetContainerComponent: Component {
component.externalState.derivedMediaSize = contentFrame.size
if component.slice.peer.id == component.context.account.peerId {
component.externalState.derivedBottomInset = availableSize.height - contentFrame.maxY
component.externalState.derivedBottomInset = availableSize.height - itemsContainerFrame.maxY
} else {
component.externalState.derivedBottomInset = availableSize.height - min(inputPanelFrame.minY, contentFrame.maxY)
}
@ -4674,6 +4698,15 @@ public final class StoryItemSetContainerComponent: Component {
), nil)
}
private func presentStealthModeUpgradeScreen() {
self.sendMessageContext.presentStealthModeUpgrade(view: self, action: { [weak self] in
guard let self else {
return
}
self.presentStoriesUpgradeScreen()
})
}
private func presentStoriesUpgradeScreen() {
guard let component = self.component else {
return
@ -5095,7 +5128,7 @@ public final class StoryItemSetContainerComponent: Component {
if accountUser.isPremium {
self.sendMessageContext.requestStealthMode(view: self)
} else {
self.presentStoriesUpgradeScreen()
self.presentStealthModeUpgradeScreen()
}
})))
}
@ -5327,7 +5360,7 @@ public final class StoryItemSetContainerComponent: Component {
if accountUser.isPremium {
self.sendMessageContext.requestStealthMode(view: self)
} else {
self.presentStoriesUpgradeScreen()
self.presentStealthModeUpgradeScreen()
}
})))

View File

@ -3004,6 +3004,7 @@ final class StoryItemSetContainerSendMessage {
return false
}
)
tooltipScreen.tag = "no_auto_dismiss"
weak var tooltipScreenValue: UndoOverlayController? = tooltipScreen
self.currentTooltipUpdateTimer?.invalidate()
self.currentTooltipUpdateTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { [weak self, weak view] _ in
@ -3046,7 +3047,7 @@ final class StoryItemSetContainerSendMessage {
let sheet = StoryStealthModeSheetScreen(
context: component.context,
cooldownUntilTimestamp: config.stealthModeState.actualizedNow().cooldownUntilTimestamp,
mode: .control(cooldownUntilTimestamp: config.stealthModeState.actualizedNow().cooldownUntilTimestamp),
backwardDuration: pastPeriod,
forwardDuration: futurePeriod,
buttonAction: { [weak self, weak view] in
@ -3095,6 +3096,52 @@ final class StoryItemSetContainerSendMessage {
})
}
func presentStealthModeUpgrade(view: StoryItemSetContainerComponent.View, action: @escaping () -> Void) {
guard let component = view.component else {
return
}
let _ = (component.context.engine.data.get(
TelegramEngine.EngineData.Item.Configuration.StoryConfigurationState(),
TelegramEngine.EngineData.Item.Configuration.App()
)
|> deliverOnMainQueue).start(next: { [weak self, weak view] config, appConfig in
guard let self, let view, let component = view.component, let controller = component.controller() else {
return
}
let pastPeriod: Int32
let futurePeriod: Int32
if let data = appConfig.data, let futurePeriodF = data["stories_stealth_future_period"] as? Double, let pastPeriodF = data["stories_stealth_past_period"] as? Double {
futurePeriod = Int32(futurePeriodF)
pastPeriod = Int32(pastPeriodF)
} else {
pastPeriod = 5 * 60
futurePeriod = 25 * 60
}
let sheet = StoryStealthModeSheetScreen(
context: component.context,
mode: .upgrade,
backwardDuration: pastPeriod,
forwardDuration: futurePeriod,
buttonAction: {
action()
}
)
sheet.wasDismissed = { [weak self, weak view] in
guard let self, let view else {
return
}
self.actionSheet = nil
view.updateIsProgressPaused()
}
self.actionSheet = sheet
view.updateIsProgressPaused()
controller.push(sheet)
})
}
func activateMediaArea(view: StoryItemSetContainerComponent.View, mediaArea: MediaArea) {
guard let component = view.component, let controller = component.controller() else {
return

View File

@ -59,6 +59,7 @@ final class StoryItemSetViewListComponent: Component {
let peerId: EnginePeer.Id
let safeInsets: UIEdgeInsets
let storyItem: EngineStoryItem
let hasPremium: Bool
let effectiveHeight: CGFloat
let minHeight: CGFloat
let availableReactions: StoryAvailableReactions?
@ -82,6 +83,7 @@ final class StoryItemSetViewListComponent: Component {
peerId: EnginePeer.Id,
safeInsets: UIEdgeInsets,
storyItem: EngineStoryItem,
hasPremium: Bool,
effectiveHeight: CGFloat,
minHeight: CGFloat,
availableReactions: StoryAvailableReactions?,
@ -104,6 +106,7 @@ final class StoryItemSetViewListComponent: Component {
self.peerId = peerId
self.safeInsets = safeInsets
self.storyItem = storyItem
self.hasPremium = hasPremium
self.effectiveHeight = effectiveHeight
self.minHeight = minHeight
self.availableReactions = availableReactions
@ -136,6 +139,9 @@ final class StoryItemSetViewListComponent: Component {
if lhs.storyItem != rhs.storyItem {
return false
}
if lhs.hasPremium != rhs.hasPremium {
return false
}
if lhs.effectiveHeight != rhs.effectiveHeight {
return false
}
@ -248,6 +254,7 @@ final class StoryItemSetViewListComponent: Component {
var emptyIcon: ComponentView<Empty>?
var emptyText: ComponentView<Empty>?
var emptyButton: ComponentView<Empty>?
let scrollView: UIScrollView
var itemLayout: ItemLayout?
@ -267,6 +274,8 @@ final class StoryItemSetViewListComponent: Component {
var contentLoaded: Bool = false
var contentLoadedUpdated: ((Bool) -> Void)?
var dismissInput: (() -> Void)?
init(configuration: ContentConfigurationKey) {
self.configuration = configuration
@ -317,6 +326,8 @@ final class StoryItemSetViewListComponent: Component {
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
cancelContextGestures(view: scrollView)
self.dismissInput?()
}
private func updateScrolling(transition: Transition) {
@ -759,6 +770,8 @@ final class StoryItemSetViewListComponent: Component {
self.updateScrolling(transition: transition)
if let viewListState = self.viewListState, viewListState.loadMoreToken == nil, viewListState.items.isEmpty, viewListState.totalCount == 0 {
self.scrollView.isUserInteractionEnabled = false
var emptyTransition = transition
let emptyIcon: ComponentView<Empty>
@ -778,6 +791,25 @@ final class StoryItemSetViewListComponent: Component {
self.emptyText = emptyText
}
var emptyButtonTransition = transition
let emptyButton: ComponentView<Empty>?
if !component.hasPremium, let views = component.storyItem.views, views.seenCount != 0 {
if let current = self.emptyButton {
emptyButton = current
} else {
emptyButtonTransition = emptyButtonTransition.withAnimation(.none)
emptyButton = ComponentView()
self.emptyButton = emptyButton
}
} else {
if let emptyButton = self.emptyButton {
self.emptyButton = nil
emptyButton.view?.removeFromSuperview()
}
emptyButton = nil
}
let emptyIconSize = emptyIcon.update(
transition: emptyTransition,
component: AnyComponent(LottieComponent(
@ -794,12 +826,17 @@ final class StoryItemSetViewListComponent: Component {
let body = MarkdownAttributeSet(font: Font.regular(fontSize), textColor: component.theme.list.itemSecondaryTextColor)
let bold = MarkdownAttributeSet(font: Font.semibold(fontSize), textColor: component.theme.list.itemSecondaryTextColor)
let link = MarkdownAttributeSet(font: Font.semibold(fontSize), textColor: component.theme.list.itemAccentColor)
let attributes = MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { _ in nil })
let attributes = MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { _ in return ("URL", "") })
let text: String
if self.configuration.listMode == .everyone && (self.query == nil || self.query == "") {
if component.storyItem.expirationTimestamp <= Int32(Date().timeIntervalSince1970) {
if emptyButton == nil {
text = component.strings.Story_Views_ViewsExpired
} else {
//TODO:localize
text = "List of viewers isn't available after 24 hours of story expiration.\n\nTo unlock viewers' lists for expired and saved stories, subscribe to [Telegram Premium]()."
}
} else {
text = component.strings.Story_Views_NoViews
}
@ -811,7 +848,12 @@ final class StoryItemSetViewListComponent: Component {
text = "None of your contacts viewed this story."
} else {
if component.storyItem.expirationTimestamp <= Int32(Date().timeIntervalSince1970) {
if emptyButton == nil {
text = component.strings.Story_Views_ViewsExpired
} else {
//TODO:localize
text = "List of viewers isn't available after 24 hours of story expiration.\n\nTo unlock viewers' lists for expired and saved stories, subscribe to [Telegram Premium]()."
}
} else {
text = component.strings.Story_Views_NoViews
}
@ -822,15 +864,71 @@ final class StoryItemSetViewListComponent: Component {
component: AnyComponent(BalancedTextComponent(
text: .markdown(text: text, attributes: attributes),
horizontalAlignment: .center,
maximumNumberOfLines: 0
maximumNumberOfLines: 0,
highlightColor: component.theme.list.itemAccentColor.withMultipliedAlpha(0.5),
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] {
return NSAttributedString.Key(rawValue: "URL")
} else {
return nil
}
},
tapAction: { [weak self] _, _ in
guard let self, let component = self.component else {
return
}
component.openPremiumIntro()
}
)),
environment: {},
containerSize: CGSize(width: min(220.0, availableSize.width - 16.0 * 2.0), height: 1000.0)
containerSize: CGSize(width: min(320.0, availableSize.width - 16.0 * 2.0), height: 1000.0)
)
let emptyContentSpacing: CGFloat = 20.0
let emptyContentHeight = emptyIconSize.height + emptyContentSpacing + textSize.height
var emptyContentY = navigationMinY + floor((availableSize.height - navigationMinY - emptyContentHeight) * 0.5)
var emptyContentHeight = emptyIconSize.height + emptyContentSpacing + textSize.height
var emptyButtonSize: CGSize?
if let emptyButton {
//TODO:localize
emptyButtonSize = emptyButton.update(
transition: emptyButtonTransition,
component: AnyComponent(ButtonComponent(
background: ButtonComponent.Background(
color: component.theme.list.itemCheckColors.fillColor,
foreground: component.theme.list.itemCheckColors.foregroundColor,
pressedColor: component.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
),
content: AnyComponentWithIdentity(
id: AnyHashable(0),
component: AnyComponent(ButtonTextContentComponent(
text: "Learn More",
badge: 0,
textColor: component.theme.list.itemCheckColors.foregroundColor,
badgeBackground: component.theme.list.itemCheckColors.foregroundColor,
badgeForeground: component.theme.list.itemCheckColors.fillColor
))
),
isEnabled: true,
displaysProgress: false,
action: { [weak self] in
guard let self, let component = self.component else {
return
}
component.openPremiumIntro()
}
)),
environment: {},
containerSize: CGSize(width: min(availableSize.width, 180.0), height: 50.0)
)
}
let emptyButtonSpacing: CGFloat = 32.0
if let emptyButtonSize {
emptyContentHeight += emptyButtonSpacing
emptyContentHeight += emptyButtonSize.height
}
var emptyContentY = navigationMinY + floor((availableSize.height - component.safeInsets.bottom - navigationMinY - emptyContentHeight) * 0.5)
if let emptyIconView = emptyIcon.view as? LottieComponent.View {
if emptyIconView.superview == nil {
@ -868,7 +966,17 @@ final class StoryItemSetViewListComponent: Component {
emptyTransition.setFrame(view: emptyTextView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - textSize.width) * 0.5), y: emptyContentY), size: textSize))
emptyContentY += textSize.height + emptyContentSpacing * 2.0
}
if let emptyButtonSize, let emptyButton, let emptyButtonView = emptyButton.view {
if emptyButtonView.superview == nil {
self.insertSubview(emptyButtonView, belowSubview: self.scrollView)
}
emptyTransition.setFrame(view: emptyButtonView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - emptyButtonSize.width) * 0.5), y: emptyContentY), size: emptyButtonSize))
emptyContentY += emptyButtonSize.height + emptyButtonSpacing
}
} else {
self.scrollView.isUserInteractionEnabled = true
if let emptyIcon = self.emptyIcon {
self.emptyIcon = nil
emptyIcon.view?.removeFromSuperview()
@ -877,6 +985,10 @@ final class StoryItemSetViewListComponent: Component {
self.emptyText = nil
emptyText.view?.removeFromSuperview()
}
if let emptyButton = self.emptyButton {
self.emptyButton = nil
emptyButton.view?.removeFromSuperview()
}
}
}
}
@ -1084,9 +1196,22 @@ final class StoryItemSetViewListComponent: Component {
environment: {},
containerSize: CGSize(width: availableSize.width - 10.0 * 2.0, height: 50.0)
)
//TODO:localize
let titleText: String
if let views = component.storyItem.views, views.seenCount != 0 {
if component.storyItem.expirationTimestamp <= Int32(Date().timeIntervalSince1970) {
titleText = component.strings.Story_Footer_Views(Int32(views.seenCount))
} else {
titleText = "All Viewers"
}
} else {
titleText = "No Views"
}
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(Text(text: "All Viewers", font: Font.semibold(17.0), color: .white)),
component: AnyComponent(Text(text: titleText, font: Font.semibold(17.0), color: .white)),
environment: {},
containerSize: CGSize(width: 260.0, height: 100.0)
)
@ -1265,6 +1390,9 @@ final class StoryItemSetViewListComponent: Component {
navigationHeight: navigationHeight,
transition: contentViewTransition
)
if currentContentView.contentLoaded {
currentContentView.isHidden = false
}
}
if !self.currentSearchQuery.isEmpty {
@ -1275,6 +1403,12 @@ final class StoryItemSetViewListComponent: Component {
currentSearchContentView = ContentView(configuration: currentConfiguration)
self.currentSearchContentView = currentSearchContentView
currentSearchContentView.isHidden = true
currentSearchContentView.dismissInput = { [weak self] in
guard let self else {
return
}
self.navigationSearch.view?.endEditing(true)
}
}
var contentViewTransition = transition

View File

@ -284,8 +284,6 @@ public final class StoryFooterPanelComponent: Component {
avatarsAlpha = pow(1.0 - component.expandFraction, 1.0)
baseViewCountAlpha = 1.0
}
//TODO:upload
let _ = baseViewCountAlpha
var peers: [EnginePeer] = []
@ -306,7 +304,9 @@ public final class StoryFooterPanelComponent: Component {
//TODO:localize
var regularSegments: [AnimatedCountLabelView.Segment] = []
if viewCount != 0 {
regularSegments.append(.number(viewCount, NSAttributedString(string: "\(viewCount)", font: Font.regular(15.0), textColor: .white)))
}
let viewPart: String
if viewCount == 0 {
@ -399,11 +399,13 @@ public final class StoryFooterPanelComponent: Component {
contentWidth += (avatarsSize.width + avatarViewsSpacing) * (1.0 - component.expandFraction)
if let image = self.viewsIconView.image {
if viewCount != 0 {
contentWidth += (image.size.width + viewsIconSpacing) * component.expandFraction
}
}
if viewCount == 0 {
contentWidth += viewStatsTextLayout.size.width * component.expandFraction
contentWidth += viewStatsTextLayout.size.width * (1.0 - component.expandFraction)
} else {
contentWidth += viewStatsTextLayout.size.width
}
@ -430,7 +432,11 @@ public final class StoryFooterPanelComponent: Component {
let viewsIconFrame = CGRect(origin: CGPoint(x: contentX, y: floor((size.height - image.size.height) * 0.5)), size: image.size)
transition.setPosition(view: self.viewsIconView, position: viewsIconFrame.center)
transition.setBounds(view: self.viewsIconView, bounds: CGRect(origin: CGPoint(), size: viewsIconFrame.size))
if viewCount == 0 {
transition.setAlpha(view: self.viewsIconView, alpha: 0.0)
} else {
transition.setAlpha(view: self.viewsIconView, alpha: component.expandFraction)
}
transition.setScale(view: self.viewsIconView, scale: CGFloat(1.0).interpolate(to: CGFloat(0.1), amount: 1.0 - component.expandFraction))
}

View File

@ -15,17 +15,23 @@ public final class StoryStealthModeInfoContentComponent: Component {
public let strings: PresentationStrings
public let backwardDuration: Int32
public let forwardDuration: Int32
public let mode: StoryStealthModeSheetScreen.Mode
public let dismiss: () -> Void
public init(
theme: PresentationTheme,
strings: PresentationStrings,
backwardDuration: Int32,
forwardDuration: Int32
forwardDuration: Int32,
mode: StoryStealthModeSheetScreen.Mode,
dismiss: @escaping () -> Void
) {
self.theme = theme
self.strings = strings
self.backwardDuration = backwardDuration
self.forwardDuration = forwardDuration
self.mode = mode
self.dismiss = dismiss
}
public static func ==(lhs: StoryStealthModeInfoContentComponent, rhs: StoryStealthModeInfoContentComponent) -> Bool {
@ -41,6 +47,9 @@ public final class StoryStealthModeInfoContentComponent: Component {
if lhs.forwardDuration != rhs.forwardDuration {
return false
}
if lhs.mode != rhs.mode {
return false
}
return true
}
@ -155,7 +164,13 @@ public final class StoryStealthModeInfoContentComponent: Component {
contentHeight += 15.0
//TODO:localize
let text: String = "Turn Stealth Mode on to hide the fact that you viewed peoples' stories from them."
let text: String
switch component.mode {
case .control:
text = "Turn Stealth Mode on to hide the fact that you viewed peoples' stories from them."
case .upgrade:
text = "Subscribe to Telegram Premium to hide the fact that you viewed peoples' stories from them."
}
let mainText = NSMutableAttributedString()
mainText.append(parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(
body: MarkdownAttributeSet(

View File

@ -15,25 +15,28 @@ import TelegramStringFormatting
private final class StoryStealthModeSheetContentComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let cooldownUntilTimestamp: Int32?
let mode: StoryStealthModeSheetScreen.Mode
let backwardDuration: Int32
let forwardDuration: Int32
let action: () -> Void
let dismiss: () -> Void
init(
cooldownUntilTimestamp: Int32?,
mode: StoryStealthModeSheetScreen.Mode,
backwardDuration: Int32,
forwardDuration: Int32,
action: @escaping () -> Void,
dismiss: @escaping () -> Void
) {
self.cooldownUntilTimestamp = cooldownUntilTimestamp
self.mode = mode
self.backwardDuration = backwardDuration
self.forwardDuration = forwardDuration
self.action = action
self.dismiss = dismiss
}
static func ==(lhs: StoryStealthModeSheetContentComponent, rhs: StoryStealthModeSheetContentComponent) -> Bool {
if lhs.cooldownUntilTimestamp != rhs.cooldownUntilTimestamp {
if lhs.mode != rhs.mode {
return false
}
if lhs.backwardDuration != rhs.backwardDuration {
@ -50,6 +53,8 @@ private final class StoryStealthModeSheetContentComponent: Component {
private let content = ComponentView<Empty>()
private let button = ComponentView<Empty>()
private var cancelButton: ComponentView<Empty>?
private var component: StoryStealthModeSheetContentComponent?
private weak var state: EmptyComponentState?
@ -95,10 +100,13 @@ private final class StoryStealthModeSheetContentComponent: Component {
self.state = state
var remainingCooldownSeconds: Int32 = 0
if let cooldownUntilTimestamp = component.cooldownUntilTimestamp {
if case let .control(cooldownUntilTimestamp) = component.mode {
if let cooldownUntilTimestamp {
remainingCooldownSeconds = cooldownUntilTimestamp - Int32(Date().timeIntervalSince1970)
remainingCooldownSeconds = max(0, remainingCooldownSeconds)
}
}
if remainingCooldownSeconds > 0 {
if self.timer == nil {
self.timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { [weak self] _ in
@ -170,6 +178,39 @@ private final class StoryStealthModeSheetContentComponent: Component {
}
}
if case .upgrade = component.mode {
let cancelButton: ComponentView<Empty>
if let current = self.cancelButton {
cancelButton = current
} else {
cancelButton = ComponentView()
self.cancelButton = cancelButton
}
let cancelButtonSize = cancelButton.update(
transition: transition,
component: AnyComponent(Button(
content: AnyComponent(Text(text: environment.strings.Common_Cancel, font: Font.regular(17.0), color: environment.theme.list.itemAccentColor)),
action: { [weak self] in
guard let self, let component = self.component else {
return
}
component.dismiss()
}
).minSize(CGSize(width: 8.0, height: 44.0))),
environment: {},
containerSize: CGSize(width: 200.0, height: 100.0)
)
if let cancelButtonView = cancelButton.view {
if cancelButtonView.superview == nil {
self.addSubview(cancelButtonView)
}
transition.setFrame(view: cancelButtonView, frame: CGRect(origin: CGPoint(x: 16.0, y: 6.0), size: cancelButtonSize))
}
} else if let cancelButton = self.cancelButton {
self.cancelButton = nil
cancelButton.view?.removeFromSuperview()
}
var contentHeight: CGFloat = 0.0
contentHeight += 32.0
@ -179,7 +220,14 @@ private final class StoryStealthModeSheetContentComponent: Component {
theme: environment.theme,
strings: environment.strings,
backwardDuration: component.backwardDuration,
forwardDuration: component.forwardDuration
forwardDuration: component.forwardDuration,
mode: component.mode,
dismiss: { [weak self] in
guard let self, let component = self.component else {
return
}
component.dismiss()
}
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height)
@ -195,11 +243,30 @@ private final class StoryStealthModeSheetContentComponent: Component {
//TODO:localize
let buttonText: String
let content: AnyComponentWithIdentity<Empty>
switch component.mode {
case .control:
if remainingCooldownSeconds <= 0 {
buttonText = "Enable Stealth Mode"
} else {
buttonText = "Available in \(stringForDuration(remainingCooldownSeconds))"
}
content = AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(Text(text: buttonText, font: Font.semibold(17.0), color: environment.theme.list.itemCheckColors.foregroundColor)))
case .upgrade:
buttonText = "Unlock Stealth Mode"
content = AnyComponentWithIdentity(id: AnyHashable(1 as Int), component: AnyComponent(
HStack([
AnyComponentWithIdentity(id: AnyHashable(1 as Int), component: AnyComponent(Text(text: buttonText, font: Font.semibold(17.0), color: environment.theme.list.itemCheckColors.foregroundColor))),
AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(LottieComponent(
content: LottieComponent.AppBundleContent(name: "premium_unlock"),
color: environment.theme.list.itemCheckColors.foregroundColor,
startingPosition: .begin,
size: CGSize(width: 30.0, height: 30.0),
loop: true
)))
], spacing: 4.0)
))
}
let buttonSize = self.button.update(
transition: transition,
component: AnyComponent(ButtonComponent(
@ -208,9 +275,7 @@ private final class StoryStealthModeSheetContentComponent: Component {
foreground: environment.theme.list.itemCheckColors.foregroundColor,
pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8)
),
content: AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(
Text(text: buttonText, font: Font.semibold(17.0), color: environment.theme.list.itemCheckColors.foregroundColor)
)),
content: content,
isEnabled: remainingCooldownSeconds <= 0,
allowActionWhenDisabled: true,
displaysProgress: false,
@ -219,8 +284,10 @@ private final class StoryStealthModeSheetContentComponent: Component {
return
}
switch component.mode {
case let .control(cooldownUntilTimestamp):
var remainingCooldownSeconds: Int32 = 0
if let cooldownUntilTimestamp = component.cooldownUntilTimestamp {
if let cooldownUntilTimestamp {
remainingCooldownSeconds = cooldownUntilTimestamp - Int32(Date().timeIntervalSince1970)
remainingCooldownSeconds = max(0, remainingCooldownSeconds)
}
@ -228,7 +295,10 @@ private final class StoryStealthModeSheetContentComponent: Component {
if remainingCooldownSeconds > 0 {
self.displayCooldown()
} else {
component.dismiss()
component.action()
}
case .upgrade:
component.action()
}
}
)),
@ -267,20 +337,20 @@ private final class StoryStealthModeSheetScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let cooldownUntilTimestamp: Int32?
let mode: StoryStealthModeSheetScreen.Mode
let backwardDuration: Int32
let forwardDuration: Int32
let buttonAction: (() -> Void)?
init(
context: AccountContext,
cooldownUntilTimestamp: Int32?,
mode: StoryStealthModeSheetScreen.Mode,
backwardDuration: Int32,
forwardDuration: Int32,
buttonAction: (() -> Void)?
) {
self.context = context
self.cooldownUntilTimestamp = cooldownUntilTimestamp
self.mode = mode
self.backwardDuration = backwardDuration
self.forwardDuration = forwardDuration
self.buttonAction = buttonAction
@ -290,7 +360,7 @@ private final class StoryStealthModeSheetScreenComponent: Component {
if lhs.context !== rhs.context {
return false
}
if lhs.cooldownUntilTimestamp != rhs.cooldownUntilTimestamp {
if lhs.mode != rhs.mode {
return false
}
if lhs.backwardDuration != rhs.backwardDuration {
@ -343,10 +413,10 @@ private final class StoryStealthModeSheetScreenComponent: Component {
transition: transition,
component: AnyComponent(SheetComponent(
content: AnyComponent(StoryStealthModeSheetContentComponent(
cooldownUntilTimestamp: component.cooldownUntilTimestamp,
mode: component.mode,
backwardDuration: component.backwardDuration,
forwardDuration: component.forwardDuration,
dismiss: { [weak self] in
action: { [weak self] in
guard let self else {
return
}
@ -360,9 +430,16 @@ private final class StoryStealthModeSheetScreenComponent: Component {
}
self.component?.buttonAction?()
})
},
dismiss: {
self.sheetAnimateOut.invoke(Action { _ in
if let controller = environment.controller() {
controller.dismiss(completion: nil)
}
})
}
)),
backgroundColor: .color(environment.theme.list.plainBackgroundColor),
backgroundColor: .color(environment.theme.overallDarkAppearance ? environment.theme.list.itemBlocksBackgroundColor : environment.theme.list.blocksBackgroundColor),
animateOut: self.sheetAnimateOut
)),
environment: {
@ -392,16 +469,21 @@ private final class StoryStealthModeSheetScreenComponent: Component {
}
public class StoryStealthModeSheetScreen: ViewControllerComponentContainer {
public enum Mode: Equatable {
case control(cooldownUntilTimestamp: Int32?)
case upgrade
}
public init(
context: AccountContext,
cooldownUntilTimestamp: Int32?,
mode: Mode,
backwardDuration: Int32,
forwardDuration: Int32,
buttonAction: (() -> Void)? = nil
) {
super.init(context: context, component: StoryStealthModeSheetScreenComponent(
context: context,
cooldownUntilTimestamp: cooldownUntilTimestamp,
mode: mode,
backwardDuration: backwardDuration,
forwardDuration: forwardDuration,
buttonAction: buttonAction

View File

@ -75,6 +75,8 @@ public final class UndoOverlayController: ViewController {
public var keepOnParentDismissal = false
public var tag: Any?
public init(presentationData: PresentationData, content: UndoOverlayContent, elevatedLayout: Bool, position: Position = .bottom, animateInAsReplacement: Bool = false, blurred: Bool = false, action: @escaping (UndoOverlayAction) -> Bool) {
self.presentationData = presentationData
self.content = content

View File

@ -1,5 +1,5 @@
{
"app": "9.6.7",
"app": "10.0.0",
"bazel": "6.1.1",
"xcode": "14.2"
}