Various fixes

This commit is contained in:
Ilya Laktyushin 2023-07-26 18:08:45 +02:00
parent cbbe02a7a3
commit 3e4cf69560
11 changed files with 698 additions and 630 deletions

View File

@ -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.";

View File

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

View File

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

View 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
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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