mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Various fixes
This commit is contained in:
parent
cbbe02a7a3
commit
3e4cf69560
@ -9555,6 +9555,7 @@ Sorry for the inconvenience.";
|
||||
"Story.PrivacyTooltipContacts" = "This story is shown to all your contacts.";
|
||||
"Story.PrivacyTooltipCloseFriends" = "This story is shown to your close friends.";
|
||||
"Story.PrivacyTooltipSelectedContacts" = "This story is shown to selected contacts.";
|
||||
"Story.PrivacyTooltipSelectedContactsCount" = "This story is now shown to %@ contacts.";
|
||||
"Story.PrivacyTooltipNobody" = "This story is shown only to you.";
|
||||
"Story.PrivacyTooltipEveryone" = "This story is shown to everyone.";
|
||||
|
||||
@ -9732,3 +9733,5 @@ Sorry for the inconvenience.";
|
||||
"ChatList.StoryFeedTooltipUsers" = "Tap above to view stories from %@";
|
||||
|
||||
"Story.TooltipPrivacyCloseFriends2" = "You are seeing this story because **%@** added you to their list of Close Friends.";
|
||||
|
||||
"Story.Editor.VideoTooShort" = "A video must be at least 1 second long.";
|
||||
|
@ -310,8 +310,8 @@ private final class VideoRecorderImpl {
|
||||
self.queue.async {
|
||||
var stopTime = CMTime(seconds: CACurrentMediaTime(), preferredTimescale: CMTimeScale(NSEC_PER_SEC))
|
||||
if self.recordingStartSampleTime.isValid {
|
||||
if (stopTime - self.recordingStartSampleTime).seconds < 1.0 {
|
||||
stopTime = self.recordingStartSampleTime + CMTime(seconds: 1.0, preferredTimescale: self.recordingStartSampleTime.timescale)
|
||||
if (stopTime - self.recordingStartSampleTime).seconds < 1.5 {
|
||||
stopTime = self.recordingStartSampleTime + CMTime(seconds: 1.5, preferredTimescale: self.recordingStartSampleTime.timescale)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1064,15 +1064,16 @@ private class DrawingTextLayoutManager: NSLayoutManager {
|
||||
|
||||
self.radius = cur.height * 0.18
|
||||
|
||||
let t1 = ((cur.minX - last.minX < 2.0 * self.radius) && (cur.minX > last.minX)) || ((cur.maxX - last.maxX > -2.0 * self.radius) && (cur.maxX < last.maxX))
|
||||
let t2 = ((last.minX - cur.minX < 2.0 * self.radius) && (last.minX > cur.minX)) || ((last.maxX - cur.maxX > -2.0 * self.radius) && (last.maxX < cur.maxX))
|
||||
let doubleRadius = self.radius * 2.5
|
||||
|
||||
let t1 = ((cur.minX - last.minX < doubleRadius) && (cur.minX > last.minX)) || ((cur.maxX - last.maxX > -doubleRadius) && (cur.maxX < last.maxX))
|
||||
let t2 = ((last.minX - cur.minX < doubleRadius) && (last.minX > cur.minX)) || ((last.maxX - cur.maxX > -doubleRadius) && (last.maxX < cur.maxX))
|
||||
|
||||
if t2 {
|
||||
let newRect = CGRect(origin: CGPoint(x: cur.minX, y: last.minY), size: CGSize(width: cur.width, height: last.height))
|
||||
self.rectArray[index - 1] = newRect
|
||||
self.processRectIndex(index - 1)
|
||||
}
|
||||
if t1 {
|
||||
} else if t1 {
|
||||
let newRect = CGRect(origin: CGPoint(x: last.minX, y: cur.minY), size: CGSize(width: last.width, height: cur.height))
|
||||
self.rectArray[index] = newRect
|
||||
self.processRectIndex(index + 1)
|
||||
@ -1126,7 +1127,7 @@ private class DrawingTextLayoutManager: NSLayoutManager {
|
||||
path.append(UIBezierPath(roundedRect: cur, cornerRadius: self.radius))
|
||||
if i == 0 {
|
||||
last = cur
|
||||
} else if i > 0 && abs(last.maxY - cur.minY) < 10.0 {
|
||||
} else if i > 0 && abs(last.maxY - cur.minY) < 15.0 {
|
||||
let a = cur.origin
|
||||
let b = CGPoint(x: cur.maxX, y: cur.minY)
|
||||
let c = CGPoint(x: last.minX, y: last.maxY)
|
||||
|
585
submodules/PremiumUI/Sources/LimitsPageComponent.swift
Normal file
585
submodules/PremiumUI/Sources/LimitsPageComponent.swift
Normal file
@ -0,0 +1,585 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import AccountContext
|
||||
import MultilineTextComponent
|
||||
import BlurredBackgroundComponent
|
||||
import Markdown
|
||||
import TelegramPresentationData
|
||||
|
||||
private final class LimitComponent: CombinedComponent {
|
||||
let title: String
|
||||
let titleColor: UIColor
|
||||
let text: String
|
||||
let textColor: UIColor
|
||||
let accentColor: UIColor
|
||||
let inactiveColor: UIColor
|
||||
let inactiveTextColor: UIColor
|
||||
let inactiveTitle: String
|
||||
let inactiveValue: String
|
||||
let activeColor: UIColor
|
||||
let activeTextColor: UIColor
|
||||
let activeTitle: String
|
||||
let activeValue: String
|
||||
|
||||
public init(
|
||||
title: String,
|
||||
titleColor: UIColor,
|
||||
text: String,
|
||||
textColor: UIColor,
|
||||
accentColor: UIColor,
|
||||
inactiveColor: UIColor,
|
||||
inactiveTextColor: UIColor,
|
||||
inactiveTitle: String,
|
||||
inactiveValue: String,
|
||||
activeColor: UIColor,
|
||||
activeTextColor: UIColor,
|
||||
activeTitle: String,
|
||||
activeValue: String
|
||||
) {
|
||||
self.title = title
|
||||
self.titleColor = titleColor
|
||||
self.text = text
|
||||
self.textColor = textColor
|
||||
self.accentColor = accentColor
|
||||
self.inactiveColor = inactiveColor
|
||||
self.inactiveTextColor = inactiveTextColor
|
||||
self.inactiveTitle = inactiveTitle
|
||||
self.inactiveValue = inactiveValue
|
||||
self.activeColor = activeColor
|
||||
self.activeTextColor = activeTextColor
|
||||
self.activeTitle = activeTitle
|
||||
self.activeValue = activeValue
|
||||
}
|
||||
|
||||
static func ==(lhs: LimitComponent, rhs: LimitComponent) -> Bool {
|
||||
if lhs.title != rhs.title {
|
||||
return false
|
||||
}
|
||||
if lhs.titleColor != rhs.titleColor {
|
||||
return false
|
||||
}
|
||||
if lhs.text != rhs.text {
|
||||
return false
|
||||
}
|
||||
if lhs.textColor != rhs.textColor {
|
||||
return false
|
||||
}
|
||||
if lhs.accentColor != rhs.accentColor {
|
||||
return false
|
||||
}
|
||||
if lhs.inactiveColor != rhs.inactiveColor {
|
||||
return false
|
||||
}
|
||||
if lhs.inactiveTextColor != rhs.inactiveTextColor {
|
||||
return false
|
||||
}
|
||||
if lhs.inactiveTitle != rhs.inactiveTitle {
|
||||
return false
|
||||
}
|
||||
if lhs.inactiveValue != rhs.inactiveValue {
|
||||
return false
|
||||
}
|
||||
if lhs.activeColor != rhs.activeColor {
|
||||
return false
|
||||
}
|
||||
if lhs.activeTextColor != rhs.activeTextColor {
|
||||
return false
|
||||
}
|
||||
if lhs.activeTitle != rhs.activeTitle {
|
||||
return false
|
||||
}
|
||||
if lhs.activeValue != rhs.activeValue {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
static var body: Body {
|
||||
let title = Child(MultilineTextComponent.self)
|
||||
let text = Child(MultilineTextComponent.self)
|
||||
let limit = Child(PremiumLimitDisplayComponent.self)
|
||||
|
||||
return { context in
|
||||
let component = context.component
|
||||
|
||||
let sideInset: CGFloat = 16.0
|
||||
let textSideInset: CGFloat = sideInset + 8.0
|
||||
let spacing: CGFloat = 4.0
|
||||
|
||||
let textTopInset: CGFloat = 9.0
|
||||
|
||||
let title = title.update(
|
||||
component: MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: component.title,
|
||||
font: Font.regular(17.0),
|
||||
textColor: component.titleColor,
|
||||
paragraphAlignment: .natural
|
||||
)),
|
||||
horizontalAlignment: .center,
|
||||
maximumNumberOfLines: 1
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude),
|
||||
transition: .immediate
|
||||
)
|
||||
|
||||
let textFont = Font.regular(13.0)
|
||||
let boldTextFont = Font.semibold(13.0)
|
||||
let textColor = component.textColor
|
||||
let markdownAttributes = MarkdownAttributes(
|
||||
body: MarkdownAttributeSet(font: textFont, textColor: textColor),
|
||||
bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor),
|
||||
link: MarkdownAttributeSet(font: textFont, textColor: component.accentColor),
|
||||
linkAttribute: { _ in
|
||||
return nil
|
||||
}
|
||||
)
|
||||
|
||||
let text = text.update(
|
||||
component: MultilineTextComponent(
|
||||
text: .markdown(text: component.text, attributes: markdownAttributes),
|
||||
horizontalAlignment: .natural,
|
||||
maximumNumberOfLines: 0,
|
||||
lineSpacing: 0.0
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height),
|
||||
transition: .immediate
|
||||
)
|
||||
|
||||
let limit = limit.update(
|
||||
component: PremiumLimitDisplayComponent(
|
||||
inactiveColor: component.inactiveColor,
|
||||
activeColors: [component.activeColor],
|
||||
inactiveTitle: component.inactiveTitle,
|
||||
inactiveValue: component.inactiveValue,
|
||||
inactiveTitleColor: component.inactiveTextColor,
|
||||
activeTitle: component.activeTitle,
|
||||
activeValue: component.activeValue,
|
||||
activeTitleColor: component.activeTextColor,
|
||||
badgeIconName: "",
|
||||
badgeText: nil,
|
||||
badgePosition: 0.0,
|
||||
badgeGraphPosition: 0.5,
|
||||
isPremiumDisabled: false
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height),
|
||||
transition: .immediate
|
||||
)
|
||||
|
||||
context.add(title
|
||||
.position(CGPoint(x: textSideInset + title.size.width / 2.0, y: textTopInset + title.size.height / 2.0))
|
||||
)
|
||||
|
||||
context.add(text
|
||||
.position(CGPoint(x: textSideInset + text.size.width / 2.0, y: textTopInset + title.size.height + spacing + text.size.height / 2.0))
|
||||
)
|
||||
|
||||
context.add(limit
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: textTopInset + title.size.height + spacing + text.size.height - 20.0))
|
||||
)
|
||||
|
||||
return CGSize(width: context.availableSize.width, height: textTopInset + title.size.height + text.size.height + 56.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum Limit: CaseIterable {
|
||||
case groups
|
||||
case pins
|
||||
case publicLinks
|
||||
case savedGifs
|
||||
case favedStickers
|
||||
case about
|
||||
case captions
|
||||
case folders
|
||||
case chatsPerFolder
|
||||
case account
|
||||
|
||||
func title(strings: PresentationStrings) -> String {
|
||||
switch self {
|
||||
case .groups:
|
||||
return strings.Premium_Limits_GroupsAndChannels
|
||||
case .pins:
|
||||
return strings.Premium_Limits_PinnedChats
|
||||
case .publicLinks:
|
||||
return strings.Premium_Limits_PublicLinks
|
||||
case .savedGifs:
|
||||
return strings.Premium_Limits_SavedGifs
|
||||
case .favedStickers:
|
||||
return strings.Premium_Limits_FavedStickers
|
||||
case .about:
|
||||
return strings.Premium_Limits_Bio
|
||||
case .captions:
|
||||
return strings.Premium_Limits_Captions
|
||||
case .folders:
|
||||
return strings.Premium_Limits_Folders
|
||||
case .chatsPerFolder:
|
||||
return strings.Premium_Limits_ChatsPerFolder
|
||||
case .account:
|
||||
return strings.Premium_Limits_Accounts
|
||||
}
|
||||
}
|
||||
|
||||
func text(strings: PresentationStrings) -> String {
|
||||
switch self {
|
||||
case .groups:
|
||||
return strings.Premium_Limits_GroupsAndChannelsInfo
|
||||
case .pins:
|
||||
return strings.Premium_Limits_PinnedChatsInfo
|
||||
case .publicLinks:
|
||||
return strings.Premium_Limits_PublicLinksInfo
|
||||
case .savedGifs:
|
||||
return strings.Premium_Limits_SavedGifsInfo
|
||||
case .favedStickers:
|
||||
return strings.Premium_Limits_FavedStickersInfo
|
||||
case .about:
|
||||
return strings.Premium_Limits_BioInfo
|
||||
case .captions:
|
||||
return strings.Premium_Limits_CaptionsInfo
|
||||
case .folders:
|
||||
return strings.Premium_Limits_FoldersInfo
|
||||
case .chatsPerFolder:
|
||||
return strings.Premium_Limits_ChatsPerFolderInfo
|
||||
case .account:
|
||||
return strings.Premium_Limits_AccountsInfo
|
||||
}
|
||||
}
|
||||
|
||||
func limit(_ configuration: EngineConfiguration.UserLimits, isPremium: Bool) -> String {
|
||||
let value: Int32
|
||||
switch self {
|
||||
case .groups:
|
||||
value = configuration.maxChannelsCount
|
||||
case .pins:
|
||||
value = configuration.maxPinnedChatCount
|
||||
case .publicLinks:
|
||||
value = configuration.maxPublicLinksCount
|
||||
case .savedGifs:
|
||||
value = configuration.maxSavedGifCount
|
||||
case .favedStickers:
|
||||
value = configuration.maxFavedStickerCount
|
||||
case .about:
|
||||
value = configuration.maxAboutLength
|
||||
case .captions:
|
||||
value = configuration.maxCaptionLength
|
||||
case .folders:
|
||||
value = configuration.maxFoldersCount
|
||||
case .chatsPerFolder:
|
||||
value = configuration.maxFolderChatsCount
|
||||
case .account:
|
||||
value = isPremium ? 4 : 3
|
||||
}
|
||||
return "\(value)"
|
||||
}
|
||||
}
|
||||
|
||||
private final class LimitsListComponent: CombinedComponent {
|
||||
typealias EnvironmentType = (Empty, ScrollChildEnvironment)
|
||||
|
||||
let context: AccountContext
|
||||
let topInset: CGFloat
|
||||
let bottomInset: CGFloat
|
||||
|
||||
init(context: AccountContext, topInset: CGFloat, bottomInset: CGFloat) {
|
||||
self.context = context
|
||||
self.topInset = topInset
|
||||
self.bottomInset = bottomInset
|
||||
}
|
||||
|
||||
static func ==(lhs: LimitsListComponent, rhs: LimitsListComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.topInset != rhs.topInset {
|
||||
return false
|
||||
}
|
||||
if lhs.bottomInset != rhs.bottomInset {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class State: ComponentState {
|
||||
private let context: AccountContext
|
||||
|
||||
private var disposable: Disposable?
|
||||
var limits: EngineConfiguration.UserLimits = .defaultValue
|
||||
var premiumLimits: EngineConfiguration.UserLimits = .defaultValue
|
||||
|
||||
init(context: AccountContext) {
|
||||
self.context = context
|
||||
|
||||
super.init()
|
||||
|
||||
self.disposable = (context.engine.data.get(
|
||||
TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false),
|
||||
TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true)
|
||||
)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] limits, premiumLimits in
|
||||
if let strongSelf = self {
|
||||
strongSelf.limits = limits
|
||||
strongSelf.premiumLimits = premiumLimits
|
||||
strongSelf.updated(transition: .immediate)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.disposable?.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
func makeState() -> State {
|
||||
return State(context: self.context)
|
||||
}
|
||||
|
||||
static var body: Body {
|
||||
let list = Child(List<Empty>.self)
|
||||
|
||||
return { context in
|
||||
let state = context.state
|
||||
let theme = context.component.context.sharedContext.currentPresentationData.with { $0 }.theme
|
||||
let strings = context.component.context.sharedContext.currentPresentationData.with { $0 }.strings
|
||||
|
||||
let colors = [
|
||||
UIColor(rgb: 0x5ba0ff),
|
||||
UIColor(rgb: 0x798aff),
|
||||
UIColor(rgb: 0x9377ff),
|
||||
UIColor(rgb: 0xac64f3),
|
||||
UIColor(rgb: 0xc456ae),
|
||||
UIColor(rgb: 0xcf579a),
|
||||
UIColor(rgb: 0xdb5887),
|
||||
UIColor(rgb: 0xdb496f),
|
||||
UIColor(rgb: 0xe95d44),
|
||||
UIColor(rgb: 0xf2822a)
|
||||
]
|
||||
|
||||
let items: [AnyComponentWithIdentity<Empty>] = Limit.allCases.enumerated().map { index, value in
|
||||
AnyComponentWithIdentity(
|
||||
id: value, component: AnyComponent(
|
||||
LimitComponent(
|
||||
title: value.title(strings: strings),
|
||||
titleColor: theme.list.itemPrimaryTextColor,
|
||||
text: value.text(strings: strings),
|
||||
textColor: theme.list.itemSecondaryTextColor,
|
||||
accentColor: theme.list.itemAccentColor,
|
||||
inactiveColor: theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.5),
|
||||
inactiveTextColor: theme.list.itemPrimaryTextColor,
|
||||
inactiveTitle: strings.Premium_Free,
|
||||
inactiveValue: value.limit(state.limits, isPremium: false),
|
||||
activeColor: colors[index],
|
||||
activeTextColor: .white,
|
||||
activeTitle: strings.Premium_Premium,
|
||||
activeValue: value.limit(state.premiumLimits, isPremium: true)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
let list = list.update(
|
||||
component: List(items),
|
||||
availableSize: CGSize(width: context.availableSize.width, height: 10000.0),
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
let contentHeight = context.component.topInset + list.size.height + context.component.bottomInset
|
||||
context.add(list
|
||||
.position(CGPoint(x: list.size.width / 2.0, y: context.component.topInset + list.size.height / 2.0))
|
||||
)
|
||||
|
||||
return CGSize(width: context.availableSize.width, height: contentHeight)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
final class LimitsPageComponent: CombinedComponent {
|
||||
typealias EnvironmentType = DemoPageEnvironment
|
||||
|
||||
let context: AccountContext
|
||||
let bottomInset: CGFloat
|
||||
let updatedBottomAlpha: (CGFloat) -> Void
|
||||
let updatedDismissOffset: (CGFloat) -> Void
|
||||
let updatedIsDisplaying: (Bool) -> Void
|
||||
|
||||
init(context: AccountContext, bottomInset: CGFloat, updatedBottomAlpha: @escaping (CGFloat) -> Void, updatedDismissOffset: @escaping (CGFloat) -> Void, updatedIsDisplaying: @escaping (Bool) -> Void) {
|
||||
self.context = context
|
||||
self.bottomInset = bottomInset
|
||||
self.updatedBottomAlpha = updatedBottomAlpha
|
||||
self.updatedDismissOffset = updatedDismissOffset
|
||||
self.updatedIsDisplaying = updatedIsDisplaying
|
||||
}
|
||||
|
||||
static func ==(lhs: LimitsPageComponent, rhs: LimitsPageComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.bottomInset != rhs.bottomInset {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class State: ComponentState {
|
||||
let updateBottomAlpha: (CGFloat) -> Void
|
||||
let updateDismissOffset: (CGFloat) -> Void
|
||||
let updatedIsDisplaying: (Bool) -> Void
|
||||
|
||||
var resetScroll: ActionSlot<Void>?
|
||||
|
||||
var topContentOffset: CGFloat = 0.0
|
||||
var bottomContentOffset: CGFloat = 100.0 {
|
||||
didSet {
|
||||
self.updateAlpha()
|
||||
}
|
||||
}
|
||||
|
||||
var position: CGFloat? {
|
||||
didSet {
|
||||
self.updateAlpha()
|
||||
}
|
||||
}
|
||||
|
||||
var isDisplaying = false {
|
||||
didSet {
|
||||
if oldValue != self.isDisplaying {
|
||||
self.updatedIsDisplaying(self.isDisplaying)
|
||||
|
||||
if !self.isDisplaying {
|
||||
self.resetScroll?.invoke(Void())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(updateBottomAlpha: @escaping (CGFloat) -> Void, updateDismissOffset: @escaping (CGFloat) -> Void, updateIsDisplaying: @escaping (Bool) -> Void) {
|
||||
self.updateBottomAlpha = updateBottomAlpha
|
||||
self.updateDismissOffset = updateDismissOffset
|
||||
self.updatedIsDisplaying = updateIsDisplaying
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
func updateAlpha() {
|
||||
let dismissPosition = min(1.0, abs(self.position ?? 0.0) / 1.3333)
|
||||
let position = min(1.0, abs(self.position ?? 0.0))
|
||||
self.updateDismissOffset(dismissPosition)
|
||||
|
||||
let verticalPosition = 1.0 - min(30.0, self.bottomContentOffset) / 30.0
|
||||
|
||||
let backgroundAlpha: CGFloat = max(position, verticalPosition)
|
||||
self.updateBottomAlpha(backgroundAlpha)
|
||||
}
|
||||
}
|
||||
|
||||
func makeState() -> State {
|
||||
return State(updateBottomAlpha: self.updatedBottomAlpha, updateDismissOffset: self.updatedDismissOffset, updateIsDisplaying: self.updatedIsDisplaying)
|
||||
}
|
||||
|
||||
static var body: Body {
|
||||
let background = Child(Rectangle.self)
|
||||
let scroll = Child(ScrollComponent<Empty>.self)
|
||||
let topPanel = Child(BlurredBackgroundComponent.self)
|
||||
let topSeparator = Child(Rectangle.self)
|
||||
let title = Child(MultilineTextComponent.self)
|
||||
|
||||
let resetScroll = ActionSlot<Void>()
|
||||
|
||||
return { context in
|
||||
let state = context.state
|
||||
|
||||
let environment = context.environment[DemoPageEnvironment.self].value
|
||||
state.resetScroll = resetScroll
|
||||
state.position = environment.position
|
||||
state.isDisplaying = environment.isDisplaying
|
||||
|
||||
let theme = context.component.context.sharedContext.currentPresentationData.with { $0 }.theme
|
||||
let strings = context.component.context.sharedContext.currentPresentationData.with { $0 }.strings
|
||||
|
||||
let topInset: CGFloat = 56.0
|
||||
|
||||
let scroll = scroll.update(
|
||||
component: ScrollComponent<Empty>(
|
||||
content: AnyComponent(
|
||||
LimitsListComponent(
|
||||
context: context.component.context,
|
||||
topInset: topInset,
|
||||
bottomInset: context.component.bottomInset
|
||||
)
|
||||
),
|
||||
contentInsets: UIEdgeInsets(top: topInset, left: 0.0, bottom: 0.0, right: 0.0),
|
||||
contentOffsetUpdated: { [weak state] topContentOffset, bottomContentOffset in
|
||||
state?.topContentOffset = topContentOffset
|
||||
state?.bottomContentOffset = bottomContentOffset
|
||||
Queue.mainQueue().justDispatch {
|
||||
state?.updated(transition: .immediate)
|
||||
}
|
||||
},
|
||||
contentOffsetWillCommit: { _ in },
|
||||
resetScroll: resetScroll
|
||||
),
|
||||
availableSize: context.availableSize,
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
let background = background.update(
|
||||
component: Rectangle(color: theme.list.plainBackgroundColor),
|
||||
availableSize: scroll.size,
|
||||
transition: context.transition
|
||||
)
|
||||
context.add(background
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: background.size.height / 2.0))
|
||||
)
|
||||
|
||||
context.add(scroll
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: scroll.size.height / 2.0))
|
||||
)
|
||||
|
||||
let topPanel = topPanel.update(
|
||||
component: BlurredBackgroundComponent(
|
||||
color: theme.rootController.navigationBar.blurredBackgroundColor
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width, height: topInset),
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
let topSeparator = topSeparator.update(
|
||||
component: Rectangle(
|
||||
color: theme.rootController.navigationBar.separatorColor
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width, height: UIScreenPixel),
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
let title = title.update(
|
||||
component: MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: strings.Premium_DoubledLimits, font: Font.semibold(17.0), textColor: theme.rootController.navigationBar.primaryTextColor)),
|
||||
horizontalAlignment: .center,
|
||||
truncationType: .end,
|
||||
maximumNumberOfLines: 1
|
||||
),
|
||||
availableSize: context.availableSize,
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
let topPanelAlpha: CGFloat = min(30.0, state.topContentOffset) / 30.0
|
||||
context.add(topPanel
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: topPanel.size.height / 2.0))
|
||||
.opacity(topPanelAlpha)
|
||||
)
|
||||
context.add(topSeparator
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: topPanel.size.height))
|
||||
.opacity(topPanelAlpha)
|
||||
)
|
||||
context.add(title
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: topPanel.size.height / 2.0))
|
||||
)
|
||||
|
||||
return scroll.size
|
||||
}
|
||||
}
|
||||
}
|
@ -17,579 +17,6 @@ import Markdown
|
||||
import SolidRoundedButtonNode
|
||||
import BlurredBackgroundComponent
|
||||
|
||||
private final class LimitComponent: CombinedComponent {
|
||||
let title: String
|
||||
let titleColor: UIColor
|
||||
let text: String
|
||||
let textColor: UIColor
|
||||
let accentColor: UIColor
|
||||
let inactiveColor: UIColor
|
||||
let inactiveTextColor: UIColor
|
||||
let inactiveTitle: String
|
||||
let inactiveValue: String
|
||||
let activeColor: UIColor
|
||||
let activeTextColor: UIColor
|
||||
let activeTitle: String
|
||||
let activeValue: String
|
||||
|
||||
public init(
|
||||
title: String,
|
||||
titleColor: UIColor,
|
||||
text: String,
|
||||
textColor: UIColor,
|
||||
accentColor: UIColor,
|
||||
inactiveColor: UIColor,
|
||||
inactiveTextColor: UIColor,
|
||||
inactiveTitle: String,
|
||||
inactiveValue: String,
|
||||
activeColor: UIColor,
|
||||
activeTextColor: UIColor,
|
||||
activeTitle: String,
|
||||
activeValue: String
|
||||
) {
|
||||
self.title = title
|
||||
self.titleColor = titleColor
|
||||
self.text = text
|
||||
self.textColor = textColor
|
||||
self.accentColor = accentColor
|
||||
self.inactiveColor = inactiveColor
|
||||
self.inactiveTextColor = inactiveTextColor
|
||||
self.inactiveTitle = inactiveTitle
|
||||
self.inactiveValue = inactiveValue
|
||||
self.activeColor = activeColor
|
||||
self.activeTextColor = activeTextColor
|
||||
self.activeTitle = activeTitle
|
||||
self.activeValue = activeValue
|
||||
}
|
||||
|
||||
static func ==(lhs: LimitComponent, rhs: LimitComponent) -> Bool {
|
||||
if lhs.title != rhs.title {
|
||||
return false
|
||||
}
|
||||
if lhs.titleColor != rhs.titleColor {
|
||||
return false
|
||||
}
|
||||
if lhs.text != rhs.text {
|
||||
return false
|
||||
}
|
||||
if lhs.textColor != rhs.textColor {
|
||||
return false
|
||||
}
|
||||
if lhs.accentColor != rhs.accentColor {
|
||||
return false
|
||||
}
|
||||
if lhs.inactiveColor != rhs.inactiveColor {
|
||||
return false
|
||||
}
|
||||
if lhs.inactiveTextColor != rhs.inactiveTextColor {
|
||||
return false
|
||||
}
|
||||
if lhs.inactiveTitle != rhs.inactiveTitle {
|
||||
return false
|
||||
}
|
||||
if lhs.inactiveValue != rhs.inactiveValue {
|
||||
return false
|
||||
}
|
||||
if lhs.activeColor != rhs.activeColor {
|
||||
return false
|
||||
}
|
||||
if lhs.activeTextColor != rhs.activeTextColor {
|
||||
return false
|
||||
}
|
||||
if lhs.activeTitle != rhs.activeTitle {
|
||||
return false
|
||||
}
|
||||
if lhs.activeValue != rhs.activeValue {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
static var body: Body {
|
||||
let title = Child(MultilineTextComponent.self)
|
||||
let text = Child(MultilineTextComponent.self)
|
||||
let limit = Child(PremiumLimitDisplayComponent.self)
|
||||
|
||||
return { context in
|
||||
let component = context.component
|
||||
|
||||
let sideInset: CGFloat = 16.0
|
||||
let textSideInset: CGFloat = sideInset + 8.0
|
||||
let spacing: CGFloat = 4.0
|
||||
|
||||
let textTopInset: CGFloat = 9.0
|
||||
|
||||
let title = title.update(
|
||||
component: MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: component.title,
|
||||
font: Font.regular(17.0),
|
||||
textColor: component.titleColor,
|
||||
paragraphAlignment: .natural
|
||||
)),
|
||||
horizontalAlignment: .center,
|
||||
maximumNumberOfLines: 1
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude),
|
||||
transition: .immediate
|
||||
)
|
||||
|
||||
let textFont = Font.regular(13.0)
|
||||
let boldTextFont = Font.semibold(13.0)
|
||||
let textColor = component.textColor
|
||||
let markdownAttributes = MarkdownAttributes(
|
||||
body: MarkdownAttributeSet(font: textFont, textColor: textColor),
|
||||
bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor),
|
||||
link: MarkdownAttributeSet(font: textFont, textColor: component.accentColor),
|
||||
linkAttribute: { _ in
|
||||
return nil
|
||||
}
|
||||
)
|
||||
|
||||
let text = text.update(
|
||||
component: MultilineTextComponent(
|
||||
text: .markdown(text: component.text, attributes: markdownAttributes),
|
||||
horizontalAlignment: .natural,
|
||||
maximumNumberOfLines: 0,
|
||||
lineSpacing: 0.0
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height),
|
||||
transition: .immediate
|
||||
)
|
||||
|
||||
let limit = limit.update(
|
||||
component: PremiumLimitDisplayComponent(
|
||||
inactiveColor: component.inactiveColor,
|
||||
activeColors: [component.activeColor],
|
||||
inactiveTitle: component.inactiveTitle,
|
||||
inactiveValue: component.inactiveValue,
|
||||
inactiveTitleColor: component.inactiveTextColor,
|
||||
activeTitle: component.activeTitle,
|
||||
activeValue: component.activeValue,
|
||||
activeTitleColor: component.activeTextColor,
|
||||
badgeIconName: "",
|
||||
badgeText: nil,
|
||||
badgePosition: 0.0,
|
||||
badgeGraphPosition: 0.0,
|
||||
isPremiumDisabled: false
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height),
|
||||
transition: .immediate
|
||||
)
|
||||
|
||||
context.add(title
|
||||
.position(CGPoint(x: textSideInset + title.size.width / 2.0, y: textTopInset + title.size.height / 2.0))
|
||||
)
|
||||
|
||||
context.add(text
|
||||
.position(CGPoint(x: textSideInset + text.size.width / 2.0, y: textTopInset + title.size.height + spacing + text.size.height / 2.0))
|
||||
)
|
||||
|
||||
context.add(limit
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: textTopInset + title.size.height + spacing + text.size.height - 20.0))
|
||||
)
|
||||
|
||||
return CGSize(width: context.availableSize.width, height: textTopInset + title.size.height + text.size.height + 56.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum Limit: CaseIterable {
|
||||
case groups
|
||||
case pins
|
||||
case publicLinks
|
||||
case savedGifs
|
||||
case favedStickers
|
||||
case about
|
||||
case captions
|
||||
case folders
|
||||
case chatsPerFolder
|
||||
case account
|
||||
|
||||
func title(strings: PresentationStrings) -> String {
|
||||
switch self {
|
||||
case .groups:
|
||||
return strings.Premium_Limits_GroupsAndChannels
|
||||
case .pins:
|
||||
return strings.Premium_Limits_PinnedChats
|
||||
case .publicLinks:
|
||||
return strings.Premium_Limits_PublicLinks
|
||||
case .savedGifs:
|
||||
return strings.Premium_Limits_SavedGifs
|
||||
case .favedStickers:
|
||||
return strings.Premium_Limits_FavedStickers
|
||||
case .about:
|
||||
return strings.Premium_Limits_Bio
|
||||
case .captions:
|
||||
return strings.Premium_Limits_Captions
|
||||
case .folders:
|
||||
return strings.Premium_Limits_Folders
|
||||
case .chatsPerFolder:
|
||||
return strings.Premium_Limits_ChatsPerFolder
|
||||
case .account:
|
||||
return strings.Premium_Limits_Accounts
|
||||
}
|
||||
}
|
||||
|
||||
func text(strings: PresentationStrings) -> String {
|
||||
switch self {
|
||||
case .groups:
|
||||
return strings.Premium_Limits_GroupsAndChannelsInfo
|
||||
case .pins:
|
||||
return strings.Premium_Limits_PinnedChatsInfo
|
||||
case .publicLinks:
|
||||
return strings.Premium_Limits_PublicLinksInfo
|
||||
case .savedGifs:
|
||||
return strings.Premium_Limits_SavedGifsInfo
|
||||
case .favedStickers:
|
||||
return strings.Premium_Limits_FavedStickersInfo
|
||||
case .about:
|
||||
return strings.Premium_Limits_BioInfo
|
||||
case .captions:
|
||||
return strings.Premium_Limits_CaptionsInfo
|
||||
case .folders:
|
||||
return strings.Premium_Limits_FoldersInfo
|
||||
case .chatsPerFolder:
|
||||
return strings.Premium_Limits_ChatsPerFolderInfo
|
||||
case .account:
|
||||
return strings.Premium_Limits_AccountsInfo
|
||||
}
|
||||
}
|
||||
|
||||
func limit(_ configuration: EngineConfiguration.UserLimits, isPremium: Bool) -> String {
|
||||
let value: Int32
|
||||
switch self {
|
||||
case .groups:
|
||||
value = configuration.maxChannelsCount
|
||||
case .pins:
|
||||
value = configuration.maxPinnedChatCount
|
||||
case .publicLinks:
|
||||
value = configuration.maxPublicLinksCount
|
||||
case .savedGifs:
|
||||
value = configuration.maxSavedGifCount
|
||||
case .favedStickers:
|
||||
value = configuration.maxFavedStickerCount
|
||||
case .about:
|
||||
value = configuration.maxAboutLength
|
||||
case .captions:
|
||||
value = configuration.maxCaptionLength
|
||||
case .folders:
|
||||
value = configuration.maxFoldersCount
|
||||
case .chatsPerFolder:
|
||||
value = configuration.maxFolderChatsCount
|
||||
case .account:
|
||||
value = isPremium ? 4 : 3
|
||||
}
|
||||
return "\(value)"
|
||||
}
|
||||
}
|
||||
|
||||
private final class LimitsListComponent: CombinedComponent {
|
||||
typealias EnvironmentType = (Empty, ScrollChildEnvironment)
|
||||
|
||||
let context: AccountContext
|
||||
let topInset: CGFloat
|
||||
let bottomInset: CGFloat
|
||||
|
||||
init(context: AccountContext, topInset: CGFloat, bottomInset: CGFloat) {
|
||||
self.context = context
|
||||
self.topInset = topInset
|
||||
self.bottomInset = bottomInset
|
||||
}
|
||||
|
||||
static func ==(lhs: LimitsListComponent, rhs: LimitsListComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.topInset != rhs.topInset {
|
||||
return false
|
||||
}
|
||||
if lhs.bottomInset != rhs.bottomInset {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class State: ComponentState {
|
||||
private let context: AccountContext
|
||||
|
||||
private var disposable: Disposable?
|
||||
var limits: EngineConfiguration.UserLimits = .defaultValue
|
||||
var premiumLimits: EngineConfiguration.UserLimits = .defaultValue
|
||||
|
||||
init(context: AccountContext) {
|
||||
self.context = context
|
||||
|
||||
super.init()
|
||||
|
||||
self.disposable = (context.engine.data.get(
|
||||
TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false),
|
||||
TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true)
|
||||
)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] limits, premiumLimits in
|
||||
if let strongSelf = self {
|
||||
strongSelf.limits = limits
|
||||
strongSelf.premiumLimits = premiumLimits
|
||||
strongSelf.updated(transition: .immediate)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.disposable?.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
func makeState() -> State {
|
||||
return State(context: self.context)
|
||||
}
|
||||
|
||||
static var body: Body {
|
||||
let list = Child(List<Empty>.self)
|
||||
|
||||
return { context in
|
||||
let state = context.state
|
||||
let theme = context.component.context.sharedContext.currentPresentationData.with { $0 }.theme
|
||||
let strings = context.component.context.sharedContext.currentPresentationData.with { $0 }.strings
|
||||
|
||||
let colors = [
|
||||
UIColor(rgb: 0x5ba0ff),
|
||||
UIColor(rgb: 0x798aff),
|
||||
UIColor(rgb: 0x9377ff),
|
||||
UIColor(rgb: 0xac64f3),
|
||||
UIColor(rgb: 0xc456ae),
|
||||
UIColor(rgb: 0xcf579a),
|
||||
UIColor(rgb: 0xdb5887),
|
||||
UIColor(rgb: 0xdb496f),
|
||||
UIColor(rgb: 0xe95d44),
|
||||
UIColor(rgb: 0xf2822a)
|
||||
]
|
||||
|
||||
let items: [AnyComponentWithIdentity<Empty>] = Limit.allCases.enumerated().map { index, value in
|
||||
AnyComponentWithIdentity(
|
||||
id: value, component: AnyComponent(
|
||||
LimitComponent(
|
||||
title: value.title(strings: strings),
|
||||
titleColor: theme.list.itemPrimaryTextColor,
|
||||
text: value.text(strings: strings),
|
||||
textColor: theme.list.itemSecondaryTextColor,
|
||||
accentColor: theme.list.itemAccentColor,
|
||||
inactiveColor: theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.5),
|
||||
inactiveTextColor: theme.list.itemPrimaryTextColor,
|
||||
inactiveTitle: strings.Premium_Free,
|
||||
inactiveValue: value.limit(state.limits, isPremium: false),
|
||||
activeColor: colors[index],
|
||||
activeTextColor: .white,
|
||||
activeTitle: strings.Premium_Premium,
|
||||
activeValue: value.limit(state.premiumLimits, isPremium: true)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
let list = list.update(
|
||||
component: List(items),
|
||||
availableSize: CGSize(width: context.availableSize.width, height: 10000.0),
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
let contentHeight = context.component.topInset + list.size.height + context.component.bottomInset
|
||||
context.add(list
|
||||
.position(CGPoint(x: list.size.width / 2.0, y: context.component.topInset + list.size.height / 2.0))
|
||||
)
|
||||
|
||||
return CGSize(width: context.availableSize.width, height: contentHeight)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class LimitsPageComponent: CombinedComponent {
|
||||
typealias EnvironmentType = DemoPageEnvironment
|
||||
|
||||
let context: AccountContext
|
||||
let bottomInset: CGFloat
|
||||
let updatedBottomAlpha: (CGFloat) -> Void
|
||||
let updatedDismissOffset: (CGFloat) -> Void
|
||||
let updatedIsDisplaying: (Bool) -> Void
|
||||
|
||||
init(context: AccountContext, bottomInset: CGFloat, updatedBottomAlpha: @escaping (CGFloat) -> Void, updatedDismissOffset: @escaping (CGFloat) -> Void, updatedIsDisplaying: @escaping (Bool) -> Void) {
|
||||
self.context = context
|
||||
self.bottomInset = bottomInset
|
||||
self.updatedBottomAlpha = updatedBottomAlpha
|
||||
self.updatedDismissOffset = updatedDismissOffset
|
||||
self.updatedIsDisplaying = updatedIsDisplaying
|
||||
}
|
||||
|
||||
static func ==(lhs: LimitsPageComponent, rhs: LimitsPageComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.bottomInset != rhs.bottomInset {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class State: ComponentState {
|
||||
let updateBottomAlpha: (CGFloat) -> Void
|
||||
let updateDismissOffset: (CGFloat) -> Void
|
||||
let updatedIsDisplaying: (Bool) -> Void
|
||||
|
||||
var resetScroll: ActionSlot<Void>?
|
||||
|
||||
var topContentOffset: CGFloat = 0.0
|
||||
var bottomContentOffset: CGFloat = 100.0 {
|
||||
didSet {
|
||||
self.updateAlpha()
|
||||
}
|
||||
}
|
||||
|
||||
var position: CGFloat? {
|
||||
didSet {
|
||||
self.updateAlpha()
|
||||
}
|
||||
}
|
||||
|
||||
var isDisplaying = false {
|
||||
didSet {
|
||||
if oldValue != self.isDisplaying {
|
||||
self.updatedIsDisplaying(self.isDisplaying)
|
||||
|
||||
if !self.isDisplaying {
|
||||
self.resetScroll?.invoke(Void())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(updateBottomAlpha: @escaping (CGFloat) -> Void, updateDismissOffset: @escaping (CGFloat) -> Void, updateIsDisplaying: @escaping (Bool) -> Void) {
|
||||
self.updateBottomAlpha = updateBottomAlpha
|
||||
self.updateDismissOffset = updateDismissOffset
|
||||
self.updatedIsDisplaying = updateIsDisplaying
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
func updateAlpha() {
|
||||
let dismissPosition = min(1.0, abs(self.position ?? 0.0) / 1.3333)
|
||||
let position = min(1.0, abs(self.position ?? 0.0))
|
||||
self.updateDismissOffset(dismissPosition)
|
||||
|
||||
let verticalPosition = 1.0 - min(30.0, self.bottomContentOffset) / 30.0
|
||||
|
||||
let backgroundAlpha: CGFloat = max(position, verticalPosition)
|
||||
self.updateBottomAlpha(backgroundAlpha)
|
||||
}
|
||||
}
|
||||
|
||||
func makeState() -> State {
|
||||
return State(updateBottomAlpha: self.updatedBottomAlpha, updateDismissOffset: self.updatedDismissOffset, updateIsDisplaying: self.updatedIsDisplaying)
|
||||
}
|
||||
|
||||
static var body: Body {
|
||||
let background = Child(Rectangle.self)
|
||||
let scroll = Child(ScrollComponent<Empty>.self)
|
||||
let topPanel = Child(BlurredBackgroundComponent.self)
|
||||
let topSeparator = Child(Rectangle.self)
|
||||
let title = Child(MultilineTextComponent.self)
|
||||
|
||||
let resetScroll = ActionSlot<Void>()
|
||||
|
||||
return { context in
|
||||
let state = context.state
|
||||
|
||||
let environment = context.environment[DemoPageEnvironment.self].value
|
||||
state.resetScroll = resetScroll
|
||||
state.position = environment.position
|
||||
state.isDisplaying = environment.isDisplaying
|
||||
|
||||
let theme = context.component.context.sharedContext.currentPresentationData.with { $0 }.theme
|
||||
let strings = context.component.context.sharedContext.currentPresentationData.with { $0 }.strings
|
||||
|
||||
let topInset: CGFloat = 56.0
|
||||
|
||||
let scroll = scroll.update(
|
||||
component: ScrollComponent<Empty>(
|
||||
content: AnyComponent(
|
||||
LimitsListComponent(
|
||||
context: context.component.context,
|
||||
topInset: topInset,
|
||||
bottomInset: context.component.bottomInset
|
||||
)
|
||||
),
|
||||
contentInsets: UIEdgeInsets(top: topInset, left: 0.0, bottom: 0.0, right: 0.0),
|
||||
contentOffsetUpdated: { [weak state] topContentOffset, bottomContentOffset in
|
||||
state?.topContentOffset = topContentOffset
|
||||
state?.bottomContentOffset = bottomContentOffset
|
||||
Queue.mainQueue().justDispatch {
|
||||
state?.updated(transition: .immediate)
|
||||
}
|
||||
},
|
||||
contentOffsetWillCommit: { _ in },
|
||||
resetScroll: resetScroll
|
||||
),
|
||||
availableSize: context.availableSize,
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
let background = background.update(
|
||||
component: Rectangle(color: theme.list.plainBackgroundColor),
|
||||
availableSize: scroll.size,
|
||||
transition: context.transition
|
||||
)
|
||||
context.add(background
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: background.size.height / 2.0))
|
||||
)
|
||||
|
||||
context.add(scroll
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: scroll.size.height / 2.0))
|
||||
)
|
||||
|
||||
let topPanel = topPanel.update(
|
||||
component: BlurredBackgroundComponent(
|
||||
color: theme.rootController.navigationBar.blurredBackgroundColor
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width, height: topInset),
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
let topSeparator = topSeparator.update(
|
||||
component: Rectangle(
|
||||
color: theme.rootController.navigationBar.separatorColor
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width, height: UIScreenPixel),
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
let title = title.update(
|
||||
component: MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: strings.Premium_DoubledLimits, font: Font.semibold(17.0), textColor: theme.rootController.navigationBar.primaryTextColor)),
|
||||
horizontalAlignment: .center,
|
||||
truncationType: .end,
|
||||
maximumNumberOfLines: 1
|
||||
),
|
||||
availableSize: context.availableSize,
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
let topPanelAlpha: CGFloat = min(30.0, state.topContentOffset) / 30.0
|
||||
context.add(topPanel
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: topPanel.size.height / 2.0))
|
||||
.opacity(topPanelAlpha)
|
||||
)
|
||||
context.add(topSeparator
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: topPanel.size.height))
|
||||
.opacity(topPanelAlpha)
|
||||
)
|
||||
context.add(title
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: topPanel.size.height / 2.0))
|
||||
)
|
||||
|
||||
return scroll.size
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class PremiumLimitsListScreen: ViewController {
|
||||
final class Node: ViewControllerTracingNode, UIScrollViewDelegate, UIGestureRecognizerDelegate {
|
||||
private var presentationData: PresentationData
|
||||
|
@ -284,7 +284,6 @@ final class PendingStoryManager {
|
||||
self.currentPendingItemContext = pendingItemContext
|
||||
|
||||
let stableId = firstItem.stableId
|
||||
Logger.shared.log("PendingStoryManager", "setting up item context for: \(firstItem.stableId) randomId: \(firstItem.randomId)")
|
||||
pendingItemContext.disposable = (_internal_uploadStoryImpl(postbox: self.postbox, network: self.network, accountPeerId: self.accountPeerId, stateManager: self.stateManager, messageMediaPreuploadManager: self.messageMediaPreuploadManager, revalidationContext: self.revalidationContext, auxiliaryMethods: self.auxiliaryMethods, stableId: stableId, media: firstItem.media, text: firstItem.text, entities: firstItem.entities, embeddedStickers: firstItem.embeddedStickers, pin: firstItem.pin, privacy: firstItem.privacy, isForwardingDisabled: firstItem.isForwardingDisabled, period: Int(firstItem.period), randomId: firstItem.randomId)
|
||||
|> deliverOn(self.queue)).start(next: { [weak self] event in
|
||||
guard let `self` = self else {
|
||||
|
@ -804,7 +804,6 @@ func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text:
|
||||
period: Int32(period),
|
||||
randomId: randomId
|
||||
))
|
||||
Logger.shared.log("UploadStory", "Appended new pending item stableId: \(stableId) randomId: \(randomId)")
|
||||
transaction.setLocalStoryState(state: CodableEntry(currentState))
|
||||
}).start()
|
||||
}
|
||||
@ -848,7 +847,6 @@ private func _internal_putPendingStoryIdMapping(accountPeerId: PeerId, stableId:
|
||||
}
|
||||
|
||||
func _internal_uploadStoryImpl(postbox: Postbox, network: Network, accountPeerId: PeerId, stateManager: AccountStateManager, messageMediaPreuploadManager: MessageMediaPreuploadManager, revalidationContext: MediaReferenceRevalidationContext, auxiliaryMethods: AccountAuxiliaryMethods, stableId: Int32, media: Media, text: String, entities: [MessageTextEntity], embeddedStickers: [TelegramMediaFile], pin: Bool, privacy: EngineStoryPrivacy, isForwardingDisabled: Bool, period: Int, randomId: Int64) -> Signal<StoryUploadResult, NoError> {
|
||||
Logger.shared.log("UploadStory", "uploadStoryImpl for stableId: \(stableId) randomId: \(randomId)")
|
||||
let passFetchProgress = media is TelegramMediaFile
|
||||
let (contentSignal, originalMedia) = uploadedStoryContent(postbox: postbox, network: network, media: media, embeddedStickers: embeddedStickers, accountPeerId: accountPeerId, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: revalidationContext, auxiliaryMethods: auxiliaryMethods, passFetchProgress: passFetchProgress)
|
||||
return contentSignal
|
||||
|
@ -2485,7 +2485,22 @@ public class CameraScreen: ViewController {
|
||||
transitionOut: transitionOut
|
||||
)
|
||||
if let asset = result as? PHAsset {
|
||||
self.completion(.single(.asset(asset)), resultTransition, dismissed)
|
||||
if asset.mediaType == .video && asset.duration < 1.0 {
|
||||
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
||||
let alertController = textAlertController(
|
||||
context: self.context,
|
||||
forceTheme: defaultDarkColorPresentationTheme,
|
||||
title: nil,
|
||||
text: presentationData.strings.Story_Editor_VideoTooShort,
|
||||
actions: [
|
||||
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})
|
||||
],
|
||||
actionLayout: .vertical
|
||||
)
|
||||
self.present(alertController, in: .window(.root))
|
||||
} else {
|
||||
self.completion(.single(.asset(asset)), resultTransition, dismissed)
|
||||
}
|
||||
} else if let draft = result as? MediaEditorDraft {
|
||||
self.completion(.single(.draft(draft)), resultTransition, dismissed)
|
||||
}
|
||||
|
@ -566,25 +566,33 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
return
|
||||
}
|
||||
|
||||
let countLimit = 200
|
||||
var groupTooLarge = false
|
||||
if case let .legacyGroup(group) = peer, group.participantCount > 200 {
|
||||
if case let .legacyGroup(group) = peer, group.participantCount > countLimit {
|
||||
groupTooLarge = true
|
||||
} else if let stateValue = self.effectiveStateValue, let count = stateValue.participants[peer.id], count > 200 {
|
||||
} else if let stateValue = self.effectiveStateValue, let count = stateValue.participants[peer.id], count > countLimit {
|
||||
groupTooLarge = true
|
||||
}
|
||||
|
||||
if groupTooLarge {
|
||||
let showCountLimitAlert = { [weak controller, weak environment, weak component] in
|
||||
guard let controller, let environment, let component else {
|
||||
return
|
||||
}
|
||||
let alertController = textAlertController(
|
||||
context: component.context,
|
||||
forceTheme: defaultDarkColorPresentationTheme,
|
||||
title: environment.strings.Story_Privacy_GroupTooLarge,
|
||||
text: environment.strings.Story_Privacy_GroupParticipantsLimit,
|
||||
actions: [
|
||||
TextAlertAction(type: .defaultAction, title: "OK", action: {})
|
||||
TextAlertAction(type: .defaultAction, title: environment.strings.Common_OK, action: {})
|
||||
],
|
||||
actionLayout: .vertical
|
||||
)
|
||||
controller.present(alertController, in: .window(.root))
|
||||
}
|
||||
|
||||
if groupTooLarge {
|
||||
showCountLimitAlert()
|
||||
return
|
||||
}
|
||||
|
||||
@ -595,6 +603,39 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
self.selectedGroups.append(peer.id)
|
||||
append = true
|
||||
}
|
||||
|
||||
let processPeers: ([EnginePeer]) -> Void = { [weak self] peers in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
var peerIds = Set<EnginePeer.Id>()
|
||||
for peer in peers {
|
||||
self.peersMap[peer.id] = peer
|
||||
peerIds.insert(peer.id)
|
||||
}
|
||||
var existingPeerIds = Set<EnginePeer.Id>()
|
||||
for peerId in self.selectedPeers {
|
||||
existingPeerIds.insert(peerId)
|
||||
}
|
||||
if append {
|
||||
if peers.count > countLimit {
|
||||
showCountLimitAlert()
|
||||
return
|
||||
}
|
||||
for peer in peers {
|
||||
if !existingPeerIds.contains(peer.id) {
|
||||
self.selectedPeers.append(peer.id)
|
||||
existingPeerIds.insert(peer.id)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.selectedPeers = self.selectedPeers.filter { !peerIds.contains($0) }
|
||||
}
|
||||
let transition = Transition(animation: .curve(duration: 0.35, curve: .spring))
|
||||
self.state?.updated(transition: transition)
|
||||
}
|
||||
|
||||
let context = component.context
|
||||
if peer.id.namespace == Namespaces.Peer.CloudGroup {
|
||||
let _ = (context.engine.data.subscribe(
|
||||
@ -620,22 +661,8 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
}
|
||||
}
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peers in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
var peerIds = Set<EnginePeer.Id>()
|
||||
for peer in peers {
|
||||
self.peersMap[peer.id] = peer
|
||||
peerIds.insert(peer.id)
|
||||
}
|
||||
if append {
|
||||
self.selectedPeers.append(contentsOf: peers.map { $0.id })
|
||||
} else {
|
||||
self.selectedPeers = self.selectedPeers.filter { !peerIds.contains($0) }
|
||||
}
|
||||
let transition = Transition(animation: .curve(duration: 0.35, curve: .spring))
|
||||
self.state?.updated(transition: transition)
|
||||
|> deliverOnMainQueue).start(next: { peers in
|
||||
processPeers(peers)
|
||||
})
|
||||
} else if peer.id.namespace == Namespaces.Peer.CloudChannel {
|
||||
let participants: Signal<[EnginePeer], NoError> = Signal { subscriber in
|
||||
@ -651,25 +678,11 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
})
|
||||
return disposable
|
||||
}
|
||||
|> take(1)
|
||||
|
||||
let _ = (participants
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peers in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
var peerIds = Set<EnginePeer.Id>()
|
||||
for peer in peers {
|
||||
self.peersMap[peer.id] = peer
|
||||
peerIds.insert(peer.id)
|
||||
}
|
||||
if append {
|
||||
self.selectedPeers.append(contentsOf: peers.map { $0.id })
|
||||
} else {
|
||||
self.selectedPeers = self.selectedPeers.filter { !peerIds.contains($0) }
|
||||
}
|
||||
let transition = Transition(animation: .curve(duration: 0.35, curve: .spring))
|
||||
self.state?.updated(transition: transition)
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { peers in
|
||||
processPeers(peers)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -2007,7 +2020,7 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
||||
context.engine.data.get(TelegramEngine.EngineData.Item.Contacts.List(includePresences: true))
|
||||
)
|
||||
|> mapToSignal { chatList, contacts -> Signal<(EngineChatList, EngineContactList, [EnginePeer.Id: Optional<Int>]), NoError> in
|
||||
return context.engine.data.get(
|
||||
return context.engine.data.subscribe(
|
||||
EngineDataMap(chatList.items.map(\.renderedPeer.peerId).map(TelegramEngine.EngineData.Item.Peer.ParticipantCount.init))
|
||||
)
|
||||
|> map { participantCountMap -> (EngineChatList, EngineContactList, [EnginePeer.Id: Optional<Int>]) in
|
||||
@ -2079,6 +2092,12 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
||||
self.stateValue = state
|
||||
self.stateSubject.set(.single(state))
|
||||
|
||||
for peer in state.peers {
|
||||
if case let .channel(channel) = peer, participants[channel.id] == nil {
|
||||
let _ = context.engine.peers.fetchAndUpdateCachedPeerData(peerId: channel.id).start()
|
||||
}
|
||||
}
|
||||
|
||||
self.readySubject.set(true)
|
||||
})
|
||||
case let .contacts(base):
|
||||
@ -2139,7 +2158,7 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
||||
self.readySubject.set(true)
|
||||
})
|
||||
case let .search(query, onlyContacts):
|
||||
let signal: Signal<[EngineRenderedPeer], NoError>
|
||||
let signal: Signal<([EngineRenderedPeer], [EnginePeer.Id: Optional<Int>]), NoError>
|
||||
if onlyContacts {
|
||||
signal = combineLatest(
|
||||
context.engine.contacts.searchLocalPeers(query: query),
|
||||
@ -2147,17 +2166,32 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
||||
)
|
||||
|> map { peers, contacts in
|
||||
let contactIds = Set(contacts.0.map { $0.id })
|
||||
return peers.filter { contactIds.contains($0.peerId) }
|
||||
return (peers.filter { contactIds.contains($0.peerId) }, [:])
|
||||
}
|
||||
} else {
|
||||
signal = context.engine.contacts.searchLocalPeers(query: query)
|
||||
|> mapToSignal { peers in
|
||||
return context.engine.data.subscribe(
|
||||
EngineDataMap(peers.map(\.peerId).map(TelegramEngine.EngineData.Item.Peer.ParticipantCount.init))
|
||||
)
|
||||
|> map { participantCountMap -> ([EngineRenderedPeer], [EnginePeer.Id: Optional<Int>]) in
|
||||
return (peers, participantCountMap)
|
||||
}
|
||||
}
|
||||
}
|
||||
self.stateDisposable = (signal
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peers in
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peers, participantCounts in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
var participants: [EnginePeer.Id: Int] = [:]
|
||||
for (key, value) in participantCounts {
|
||||
if let value {
|
||||
participants[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
let state = State(
|
||||
peers: peers.compactMap { $0.peer }.filter { peer in
|
||||
if case let .user(user) = peer {
|
||||
@ -2181,12 +2215,18 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
||||
}
|
||||
},
|
||||
presences: [:],
|
||||
participants: [:],
|
||||
participants: participants,
|
||||
closeFriendsPeers: []
|
||||
)
|
||||
self.stateValue = state
|
||||
self.stateSubject.set(.single(state))
|
||||
|
||||
for peer in state.peers {
|
||||
if case let .channel(channel) = peer, participants[channel.id] == nil {
|
||||
let _ = context.engine.peers.fetchAndUpdateCachedPeerData(peerId: channel.id).start()
|
||||
}
|
||||
}
|
||||
|
||||
self.readySubject.set(true)
|
||||
})
|
||||
}
|
||||
|
@ -191,8 +191,8 @@ public final class StoryContentContextImpl: StoryContentContext {
|
||||
isPublic: item.privacy.base == .everyone,
|
||||
isPending: true,
|
||||
isCloseFriends: item.privacy.base == .closeFriends,
|
||||
isContacts: item.privacy.base == .contacts && item.privacy.additionallyIncludePeers.isEmpty,
|
||||
isSelectedContacts: item.privacy.base == .contacts && !item.privacy.additionallyIncludePeers.isEmpty,
|
||||
isContacts: item.privacy.base == .contacts,
|
||||
isSelectedContacts: item.privacy.base == .nobody,
|
||||
isForwardingDisabled: false,
|
||||
isEdited: false
|
||||
))
|
||||
|
@ -3233,7 +3233,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
text = component.strings.Story_PrivacyTooltipCloseFriends
|
||||
} else if privacy.base == .nobody {
|
||||
if !privacy.additionallyIncludePeers.isEmpty {
|
||||
text = component.strings.Story_PrivacyTooltipSelectedContacts
|
||||
text = component.strings.Story_PrivacyTooltipSelectedContactsCount("\(privacy.additionallyIncludePeers.count)").string
|
||||
} else {
|
||||
text = component.strings.Story_PrivacyTooltipNobody
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user