Merge commit '131736b966a308538181018840678ce4a92320bf'

# Conflicts:
#	submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift
#	submodules/PremiumUI/Sources/PremiumIntroScreen.swift
This commit is contained in:
Isaac 2024-02-27 16:21:51 +04:00
commit 1a04361cb7
78 changed files with 2577 additions and 1215 deletions

View File

@ -11218,6 +11218,7 @@ Sorry for the inconvenience.";
"GroupBoost.AdditionalFeatures" = "Additional Features";
"GroupBoost.AdditionalFeaturesText" = "By gaining **boosts**, your group reaches higher levels and unlocks more features.";
"ChannelBoost.AdditionalFeaturesText" = "By gaining **boosts**, your channel reaches higher levels and unlocks more features.";
"Stats.Boosts.Group.NoBoostersYet" = "No users currently boost your group";
"Stats.Boosts.Group.BoostersInfo" = "Your group is currently boosted by these members.";

View File

@ -941,7 +941,7 @@ public protocol SharedAccountContext: AnyObject {
func makeHashtagSearchController(context: AccountContext, peer: EnginePeer?, query: String, all: Bool) -> ViewController
func makeMyStoriesController(context: AccountContext, isArchive: Bool) -> ViewController
func makeArchiveSettingsController(context: AccountContext) -> ViewController
func makeFilterSettingsController(context: AccountContext, modal: Bool, dismissed: (() -> Void)?) -> ViewController
func makeFilterSettingsController(context: AccountContext, modal: Bool, scrollToTags: Bool, dismissed: (() -> Void)?) -> ViewController
func makeBusinessSetupScreen(context: AccountContext) -> ViewController
func makeChatbotSetupScreen(context: AccountContext, initialData: ChatbotSetupScreenInitialData) -> ViewController
func makeChatbotSetupScreenInitialData(context: AccountContext) -> Signal<ChatbotSetupScreenInitialData, NoError>

View File

@ -70,6 +70,14 @@ public enum PremiumDemoSubject {
case messageTags
case lastSeen
case messagePrivacy
case folderTags
case businessLocation
case businessHours
case businessGreetingMessage
case businessQuickReplies
case businessAwayMessage
case businessChatBots
}
public enum PremiumLimitSubject {

View File

@ -5617,7 +5617,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
private func openFilterSettings() {
self.chatListDisplayNode.mainContainerNode.updateEnableAdjacentFilterLoading(false)
if let navigationController = self.context.sharedContext.mainWindow?.viewController as? NavigationController {
let controller = self.context.sharedContext.makeFilterSettingsController(context: self.context, modal: true, dismissed: { [weak self] in
let controller = self.context.sharedContext.makeFilterSettingsController(context: self.context, modal: true, scrollToTags: false, dismissed: { [weak self] in
self?.chatListDisplayNode.mainContainerNode.updateEnableAdjacentFilterLoading(true)
})
navigationController.pushViewController(controller)

View File

@ -43,6 +43,19 @@ private enum ChatListFilterPresetListSection: Int32 {
case tags
}
public enum ChatListFilterPresetListEntryTag: ItemListItemTag {
case displayTags
public func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? ChatListFilterPresetListEntryTag, self == other {
return true
} else {
return false
}
}
}
private func stringForUserCount(_ peers: [EnginePeer.Id: SelectivePrivacyPeer], strings: PresentationStrings) -> String {
if peers.isEmpty {
return strings.PrivacyLastSeenSettings_EmpryUsersPlaceholder
@ -208,7 +221,7 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry {
}
}, activatedWhileDisabled: {
arguments.updateDisplayTagsLocked()
})
}, tag: ChatListFilterPresetListEntryTag.displayTags)
case .displayTagsFooter:
//TODO:localize
return ItemListTextItem(presentationData: presentationData, text: .plain("Display folder names for each chat in the chat list."), sectionId: self.section)
@ -306,7 +319,7 @@ public enum ChatListFilterPresetListControllerMode {
case modal
}
public func chatListFilterPresetListController(context: AccountContext, mode: ChatListFilterPresetListControllerMode, dismissed: (() -> Void)? = nil) -> ViewController {
public func chatListFilterPresetListController(context: AccountContext, mode: ChatListFilterPresetListControllerMode, scrollToTags: Bool = false, dismissed: (() -> Void)? = nil) -> ViewController {
let initialState = ChatListFilterPresetListControllerState()
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
let stateValue = Atomic(value: initialState)
@ -657,7 +670,8 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.ChatListFolderSettings_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: chatListFilterPresetListControllerEntries(presentationData: presentationData, state: state, filters: filtersWithCountsValue, updatedFilterOrder: updatedFilterOrderValue, suggestedFilters: suggestedFilters, displayTags: displayTags, isPremium: isPremium, limits: limits, premiumLimits: premiumLimits), style: .blocks, animateChanges: true)
let entries = chatListFilterPresetListControllerEntries(presentationData: presentationData, state: state, filters: filtersWithCountsValue, updatedFilterOrder: updatedFilterOrderValue, suggestedFilters: suggestedFilters, displayTags: displayTags, isPremium: isPremium, limits: limits, premiumLimits: premiumLimits)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, initialScrollToItem: scrollToTags ? ListViewScrollToItem(index: entries.count - 1, position: .center(.bottom), animated: true, curve: .Spring(duration: 0.4), directionHint: .Down) : nil, animateChanges: true)
return (controllerState, (listState, arguments))
}

View File

@ -60,6 +60,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
private let context: AccountContext
private let size: CGSize
private let hasBin: Bool
private let isStickerEditor: Bool
weak var drawingView: DrawingView?
public weak var selectionContainerView: DrawingSelectionContainerView?
@ -95,16 +96,20 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
private let yAxisView = UIView()
private let angleLayer = SimpleShapeLayer()
private let bin = ComponentView<Empty>()
private let stickerOverlayLayer = SimpleShapeLayer()
private let stickerFrameLayer = SimpleShapeLayer()
public var onInteractionUpdated: (Bool) -> Void = { _ in }
public var edgePreviewUpdated: (Bool) -> Void = { _ in }
private let hapticFeedback = HapticFeedback()
public init(context: AccountContext, size: CGSize, hasBin: Bool = false) {
public init(context: AccountContext, size: CGSize, hasBin: Bool = false, isStickerEditor: Bool = false) {
self.context = context
self.size = size
self.hasBin = hasBin
self.isStickerEditor = isStickerEditor
super.init(frame: CGRect(origin: .zero, size: size))
@ -140,6 +145,13 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
self.angleLayer.opacity = 0.0
self.angleLayer.lineDashPattern = [12, 12] as [NSNumber]
self.stickerOverlayLayer.fillColor = UIColor(rgb: 0x000000, alpha: 0.6).cgColor
self.stickerFrameLayer.fillColor = UIColor.clear.cgColor
self.stickerFrameLayer.strokeColor = UIColor(rgb: 0xffffff, alpha: 0.55).cgColor
self.stickerFrameLayer.lineDashPattern = [24, 24] as [NSNumber]
self.stickerFrameLayer.lineCap = .round
self.addSubview(self.topEdgeView)
self.addSubview(self.leftEdgeView)
self.addSubview(self.rightEdgeView)
@ -148,12 +160,25 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
self.addSubview(self.xAxisView)
self.addSubview(self.yAxisView)
self.layer.addSublayer(self.angleLayer)
if isStickerEditor {
self.layer.addSublayer(self.stickerOverlayLayer)
self.layer.addSublayer(self.stickerFrameLayer)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func addSubview(_ view: UIView) {
super.addSubview(view)
if self.stickerOverlayLayer.superlayer != nil, view is DrawingEntityView {
self.layer.addSublayer(self.stickerOverlayLayer)
self.layer.addSublayer(self.stickerFrameLayer)
}
}
public override func layoutSubviews() {
super.layoutSubviews()
@ -189,6 +214,25 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
self.angleLayer.path = anglePath
self.angleLayer.lineWidth = width
self.angleLayer.bounds = CGRect(origin: .zero, size: CGSize(width: 3000.0, height: width))
let frameWidth = floor(self.bounds.width * 0.97)
let frameRect = CGRect(origin: CGPoint(x: floor((self.bounds.width - frameWidth) / 2.0), y: floor((self.bounds.height - frameWidth) / 2.0)), size: CGSize(width: frameWidth, height: frameWidth))
self.stickerOverlayLayer.frame = self.bounds
let overlayOuterRect = UIBezierPath(rect: self.bounds)
let overlayInnerRect = UIBezierPath(cgPath: CGPath(roundedRect: frameRect, cornerWidth: frameWidth / 8.0, cornerHeight: frameWidth / 8.0, transform: nil))
let overlayLineWidth: CGFloat = 2.0 * 2.2
overlayOuterRect.append(overlayInnerRect)
overlayOuterRect.usesEvenOddFillRule = true
self.stickerOverlayLayer.path = overlayOuterRect.cgPath
self.stickerOverlayLayer.fillRule = .evenOdd
self.stickerFrameLayer.frame = self.bounds
self.stickerFrameLayer.lineWidth = overlayLineWidth
self.stickerFrameLayer.path = CGPath(roundedRect: frameRect.insetBy(dx: -overlayLineWidth / 2.0, dy: -overlayLineWidth / 2.0), cornerWidth: frameWidth / 8.0 * 1.02, cornerHeight: frameWidth / 8.0 * 1.02, transform: nil)
}
public var entities: [DrawingEntity] {
@ -841,7 +885,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
} else if self.autoSelectEntities, gestureRecognizer.numberOfTouches == 1, let viewToSelect = self.entity(at: location) {
self.selectEntity(viewToSelect.entity, animate: false)
self.onInteractionUpdated(true)
} else if gestureRecognizer.numberOfTouches == 2, let mediaEntityView = self.subviews.first(where: { $0 is DrawingEntityMediaView }) as? DrawingEntityMediaView {
} else if gestureRecognizer.numberOfTouches == 2 || self.isStickerEditor, let mediaEntityView = self.subviews.first(where: { $0 is DrawingEntityMediaView }) as? DrawingEntityMediaView {
mediaEntityView.handlePan(gestureRecognizer)
}
}

View File

@ -192,6 +192,7 @@ private final class StickerSelectionComponent: Component {
insertText: { _ in
},
backwardsDeleteText: {},
openStickerEditor: {},
presentController: { [weak self] c, a in
if let self, let controller = self.component?.getController() {
controller.present(c, in: .window(.root), with: a)

View File

@ -889,11 +889,15 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM
}
})
if case let .share(_, selfPeer, _) = self.mode {
switch self.mode {
case let .share(_, selfPeer, _):
if let selfPeer {
self.headerNode.mapNode.userLocationAnnotation = LocationPinAnnotation(context: context, theme: self.presentationData.theme, peer: selfPeer)
}
self.headerNode.mapNode.hasPickerAnnotation = true
case .pick:
self.headerNode.mapNode.userLocationAnnotation = LocationPinAnnotation(context: context, theme: self.presentationData.theme, location: TelegramMediaMap(coordinate: CLLocationCoordinate2DMake(0, 0)), queryId: nil, resultId: nil, forcedSelection: true)
self.headerNode.mapNode.hasPickerAnnotation = true
}
self.listNode.updateFloatingHeaderOffset = { [weak self] offset, listTransition in

View File

@ -155,6 +155,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
case wallpaper
case story
case addImage
case createSticker
}
case assets(PHAssetCollection?, AssetsMode)
@ -273,7 +274,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
self.presentationData = controller.presentationData
var assetType: PHAssetMediaType?
if case let .assets(_, mode) = controller.subject, [.wallpaper, .addImage].contains(mode) {
if case let .assets(_, mode) = controller.subject, [.wallpaper, .addImage, .createSticker].contains(mode) {
assetType = .image
}
let mediaAssetsContext = MediaAssetsContext(assetType: assetType)
@ -432,7 +433,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
self.gridNode.scrollView.alwaysBounceVertical = true
self.gridNode.scrollView.showsVerticalScrollIndicator = false
if case let .assets(_, mode) = controller.subject, [.wallpaper, .story, .addImage].contains(mode) {
if case let .assets(_, mode) = controller.subject, [.wallpaper, .story, .addImage, .createSticker].contains(mode) {
} else {
let selectionGesture = MediaPickerGridSelectionGesture<TGMediaSelectableItem>()
@ -1566,7 +1567,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
self.titleView.title = collection.localizedTitle ?? presentationData.strings.Attachment_Gallery
} else {
switch mode {
case .default:
case .default, .createSticker:
self.titleView.title = presentationData.strings.MediaPicker_Recents
self.titleView.isEnabled = true
case .story:
@ -2258,15 +2259,15 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
return self.controllerNode.defaultTransitionView()
}
fileprivate func transitionView(for identifier: String, snapshot: Bool, hideSource: Bool = false) -> UIView? {
public func transitionView(for identifier: String, snapshot: Bool, hideSource: Bool = false) -> UIView? {
return self.controllerNode.transitionView(for: identifier, snapshot: snapshot, hideSource: hideSource)
}
fileprivate func transitionImage(for identifier: String) -> UIImage? {
public func transitionImage(for identifier: String) -> UIImage? {
return self.controllerNode.transitionImage(for: identifier)
}
func updateHiddenMediaId(_ id: String?) {
public func updateHiddenMediaId(_ id: String?) {
self.controllerNode.hiddenMediaId.set(.single(id))
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,61 @@
import Foundation
import UIKit
import SceneKit
import Display
import AppBundle
private let sceneVersion: Int = 1
final class BadgeBusinessView: UIView, PhoneDemoDecorationView {
private let sceneView: SCNView
private var leftParticles: SCNNode?
private var rightParticles: SCNNode?
override init(frame: CGRect) {
self.sceneView = SCNView(frame: CGRect(origin: .zero, size: frame.size))
self.sceneView.backgroundColor = .clear
if let scene = loadCompressedScene(name: "business", version: sceneVersion) {
self.sceneView.scene = scene
}
self.sceneView.isUserInteractionEnabled = false
self.sceneView.preferredFramesPerSecond = 60
super.init(frame: frame)
self.alpha = 0.0
self.addSubview(self.sceneView)
self.leftParticles = self.sceneView.scene?.rootNode.childNode(withName: "leftParticles", recursively: false)
self.rightParticles = self.sceneView.scene?.rootNode.childNode(withName: "rightParticles", recursively: false)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setVisible(_ visible: Bool) {
if visible, let leftParticles = self.leftParticles, let rightParticles = self.rightParticles, leftParticles.parent == nil {
self.sceneView.scene?.rootNode.addChildNode(leftParticles)
self.sceneView.scene?.rootNode.addChildNode(rightParticles)
}
let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear)
transition.updateAlpha(layer: self.layer, alpha: visible ? 0.5 : 0.0, completion: { [weak self] finished in
if let strongSelf = self, finished && !visible && strongSelf.leftParticles?.parent != nil {
strongSelf.leftParticles?.removeFromParentNode()
strongSelf.rightParticles?.removeFromParentNode()
}
})
}
func resetAnimation() {
}
override func layoutSubviews() {
super.layoutSubviews()
self.sceneView.frame = CGRect(origin: .zero, size: frame.size)
}
}

View File

@ -13,8 +13,8 @@ final class BadgeStarsView: UIView, PhoneDemoDecorationView {
override init(frame: CGRect) {
self.sceneView = SCNView(frame: CGRect(origin: .zero, size: frame.size))
self.sceneView.backgroundColor = .clear
if let url = getAppBundle().url(forResource: "badge", withExtension: "scn") {
self.sceneView.scene = try? SCNScene(url: url, options: nil)
if let scene = loadCompressedScene(name: "badge", version: 1) {
self.sceneView.scene = scene
}
self.sceneView.isUserInteractionEnabled = false
self.sceneView.preferredFramesPerSecond = 60
@ -67,8 +67,8 @@ final class EmojiStarsView: UIView, PhoneDemoDecorationView {
override init(frame: CGRect) {
self.sceneView = SCNView(frame: CGRect(origin: .zero, size: frame.size))
self.sceneView.backgroundColor = .clear
if let url = getAppBundle().url(forResource: "emoji", withExtension: "scn") {
self.sceneView.scene = try? SCNScene(url: url, options: nil)
if let scene = loadCompressedScene(name: "emoji", version: 1) {
self.sceneView.scene = scene
}
self.sceneView.isUserInteractionEnabled = false
self.sceneView.preferredFramesPerSecond = 60
@ -121,8 +121,8 @@ final class TagStarsView: UIView, PhoneDemoDecorationView {
override init(frame: CGRect) {
self.sceneView = SCNView(frame: CGRect(origin: .zero, size: frame.size))
self.sceneView.backgroundColor = .clear
if let url = getAppBundle().url(forResource: "tag", withExtension: "scn") {
self.sceneView.scene = try? SCNScene(url: url, options: nil)
if let scene = loadCompressedScene(name: "tag", version: 1) {
self.sceneView.scene = scene
}
self.sceneView.isUserInteractionEnabled = false
self.sceneView.preferredFramesPerSecond = 60

View File

@ -12,7 +12,7 @@ import TelegramCore
import MultilineTextComponent
import TelegramPresentationData
private let sceneVersion: Int = 3
private let sceneVersion: Int = 1
public final class BoostHeaderBackgroundComponent: Component {
let isVisible: Bool
@ -58,7 +58,7 @@ public final class BoostHeaderBackgroundComponent: Component {
private func setup() {
guard let url = getAppBundle().url(forResource: "boost", withExtension: "scn"), let scene = try? SCNScene(url: url, options: nil) else {
guard let scene = loadCompressedScene(name: "boost", version: sceneVersion) else {
return
}

View File

@ -13,8 +13,6 @@ import AnimationCache
import MultiAnimationRenderer
import EmojiStatusComponent
private let sceneVersion: Int = 3
class EmojiHeaderComponent: Component {
let context: AccountContext
let animationCache: AnimationCache

View File

@ -5,6 +5,8 @@ import Display
import AppBundle
import LegacyComponents
private let sceneVersion: Int = 1
final class FasterStarsView: UIView, PhoneDemoDecorationView {
private let sceneView: SCNView
@ -13,8 +15,8 @@ final class FasterStarsView: UIView, PhoneDemoDecorationView {
override init(frame: CGRect) {
self.sceneView = SCNView(frame: CGRect(origin: .zero, size: frame.size))
self.sceneView.backgroundColor = .clear
if let url = getAppBundle().url(forResource: "lightspeed", withExtension: "scn") {
self.sceneView.scene = try? SCNScene(url: url, options: nil)
if let scene = loadCompressedScene(name: "lightspeed", version: sceneVersion) {
self.sceneView.scene = scene
}
self.sceneView.isUserInteractionEnabled = false
self.sceneView.preferredFramesPerSecond = 60

View File

@ -14,7 +14,7 @@ import MergedAvatarsNode
import MultilineTextComponent
import TelegramPresentationData
private let sceneVersion: Int = 3
private let sceneVersion: Int = 1
final class GiftAvatarComponent: Component {
let context: AccountContext
@ -106,7 +106,7 @@ final class GiftAvatarComponent: Component {
}
private func setup() {
guard let url = getAppBundle().url(forResource: "gift", withExtension: "scn"), let scene = try? SCNScene(url: url, options: nil) else {
guard let scene = loadCompressedScene(name: "gift", version: sceneVersion) else {
return
}

View File

@ -371,6 +371,7 @@ final class PhoneDemoComponent: Component {
case emoji
case hello
case tag
case business
}
enum Model {
@ -547,6 +548,13 @@ final class PhoneDemoComponent: Component {
self.decorationView = starsView
self.decorationContainerView.addSubview(starsView)
}
case .business:
if let _ = self.decorationView as? BadgeBusinessView {
} else {
let starsView = BadgeBusinessView(frame: self.decorationContainerView.bounds)
self.decorationView = starsView
self.decorationContainerView.addSubview(starsView)
}
}
self.phoneView.setup(context: component.context, videoFile: component.videoFile, position: component.position)

View File

@ -698,7 +698,7 @@ private final class SheetContent: CombinedComponent {
isCurrent = mode == .current
}
case .features:
textString = strings.GroupBoost_AdditionalFeaturesText
textString = isGroup ? strings.GroupBoost_AdditionalFeaturesText : strings.ChannelBoost_AdditionalFeaturesText
}
let defaultTitle = strings.ChannelBoost_Level("\(level)").string

View File

@ -8,7 +8,7 @@ import GZip
import AppBundle
import LegacyComponents
private let sceneVersion: Int = 1
private let sceneVersion: Int = 2
private func deg2rad(_ number: Float) -> Float {
return number * .pi / 180
@ -223,24 +223,7 @@ class PremiumCoinComponent: Component {
}
private func setup() {
let resourceUrl: URL
if let url = getAppBundle().url(forResource: "coin", withExtension: "scn") {
resourceUrl = url
} else {
let fileName = "coin_\(sceneVersion).scn"
let tmpUrl = URL(fileURLWithPath: NSTemporaryDirectory() + fileName)
if !FileManager.default.fileExists(atPath: tmpUrl.path) {
guard let url = getAppBundle().url(forResource: "coin", withExtension: ""),
let compressedData = try? Data(contentsOf: url),
let decompressedData = TGGUnzipData(compressedData, 8 * 1024 * 1024) else {
return
}
try? decompressedData.write(to: tmpUrl)
}
resourceUrl = tmpUrl
}
guard let scene = try? SCNScene(url: resourceUrl, options: nil) else {
guard let scene = loadCompressedScene(name: "coin", version: sceneVersion) else {
return
}
@ -316,8 +299,8 @@ class PremiumCoinComponent: Component {
return
}
let fromScale: Float = 0.85
let toScale: Float = 0.9
let fromScale: Float = 0.9
let toScale: Float = 1.0
let animation = CABasicAnimation(keyPath: "scale")
animation.duration = 2.0

View File

@ -1177,7 +1177,7 @@ private final class DemoSheetContent: CombinedComponent {
text = strings.Premium_LastSeenInfo
case .messagePrivacy:
text = strings.Premium_MessagePrivacyInfo
case .doubleLimits, .stories, .business:
default:
text = ""
}
@ -1392,6 +1392,14 @@ public class PremiumDemoScreen: ViewControllerComponentContainer {
case lastSeen
case messagePrivacy
case business
case folderTags
case businessLocation
case businessHours
case businessGreetingMessage
case businessQuickReplies
case businessAwayMessage
case businessChatBots
}
public enum Source: Equatable {

View File

@ -533,6 +533,8 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent {
demoSubject = .messagePrivacy
case .business:
demoSubject = .business
default:
demoSubject = .doubleLimits
}
let buttonText: String

View File

@ -439,6 +439,15 @@ public enum PremiumPerk: CaseIterable {
case lastSeen
case messagePrivacy
case business
case folderTags
case businessLocation
case businessHours
case businessGreetingMessage
case businessQuickReplies
case businessAwayMessage
case businessChatBots
public static var allCases: [PremiumPerk] {
return [
@ -520,6 +529,8 @@ public enum PremiumPerk: CaseIterable {
return "message_privacy"
case .business:
return "business"
default:
return ""
}
}
@ -567,6 +578,8 @@ public enum PremiumPerk: CaseIterable {
return strings.Premium_MessagePrivacy
case .business:
return strings.Premium_Business
default:
return ""
}
}
@ -614,6 +627,8 @@ public enum PremiumPerk: CaseIterable {
return strings.Premium_MessagePrivacyInfo
case .business:
return strings.Premium_BusinessInfo
default:
return ""
}
}
@ -661,6 +676,8 @@ public enum PremiumPerk: CaseIterable {
return "Premium/Perk/MessagePrivacy"
case .business:
return "Premium/Perk/Business"
default:
return ""
}
}
}
@ -1948,75 +1965,77 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
))),
action: { [weak state] _ in
var demoSubject: PremiumDemoScreen.Subject
switch perk {
case .doubleLimits:
demoSubject = .doubleLimits
case .moreUpload:
demoSubject = .moreUpload
case .fasterDownload:
demoSubject = .fasterDownload
case .voiceToText:
demoSubject = .voiceToText
case .noAds:
demoSubject = .noAds
case .uniqueReactions:
demoSubject = .uniqueReactions
case .premiumStickers:
demoSubject = .premiumStickers
case .advancedChatManagement:
demoSubject = .advancedChatManagement
case .profileBadge:
demoSubject = .profileBadge
case .animatedUserpics:
demoSubject = .animatedUserpics
case .appIcons:
demoSubject = .appIcons
case .animatedEmoji:
demoSubject = .animatedEmoji
case .emojiStatus:
demoSubject = .emojiStatus
case .translation:
demoSubject = .translation
case .stories:
demoSubject = .stories
case .colors:
demoSubject = .colors
let _ = ApplicationSpecificNotice.setDismissedPremiumColorsBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()
case .wallpapers:
demoSubject = .wallpapers
let _ = ApplicationSpecificNotice.setDismissedPremiumWallpapersBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()
case .messageTags:
demoSubject = .messageTags
let _ = ApplicationSpecificNotice.setDismissedMessageTagsBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()
case .lastSeen:
demoSubject = .lastSeen
let _ = ApplicationSpecificNotice.setDismissedLastSeenBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()
case .messagePrivacy:
demoSubject = .messagePrivacy
let _ = ApplicationSpecificNotice.setDismissedMessagePrivacyBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()
case .business:
demoSubject = .business
switch perk {
case .doubleLimits:
demoSubject = .doubleLimits
case .moreUpload:
demoSubject = .moreUpload
case .fasterDownload:
demoSubject = .fasterDownload
case .voiceToText:
demoSubject = .voiceToText
case .noAds:
demoSubject = .noAds
case .uniqueReactions:
demoSubject = .uniqueReactions
case .premiumStickers:
demoSubject = .premiumStickers
case .advancedChatManagement:
demoSubject = .advancedChatManagement
case .profileBadge:
demoSubject = .profileBadge
case .animatedUserpics:
demoSubject = .animatedUserpics
case .appIcons:
demoSubject = .appIcons
case .animatedEmoji:
demoSubject = .animatedEmoji
case .emojiStatus:
demoSubject = .emojiStatus
case .translation:
demoSubject = .translation
case .stories:
demoSubject = .stories
case .colors:
demoSubject = .colors
let _ = ApplicationSpecificNotice.setDismissedPremiumColorsBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()
case .wallpapers:
demoSubject = .wallpapers
let _ = ApplicationSpecificNotice.setDismissedPremiumWallpapersBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()
case .messageTags:
demoSubject = .messageTags
let _ = ApplicationSpecificNotice.setDismissedMessageTagsBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()
case .lastSeen:
demoSubject = .lastSeen
let _ = ApplicationSpecificNotice.setDismissedLastSeenBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()
case .messagePrivacy:
demoSubject = .messagePrivacy
let _ = ApplicationSpecificNotice.setDismissedMessagePrivacyBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()
case .business:
demoSubject = .business
default:
demoSubject = .doubleLimits
}
let isPremium = state?.isPremium == true
var dismissImpl: (() -> Void)?
let controller = PremiumLimitsListScreen(context: accountContext, subject: demoSubject, source: .intro(state?.price), order: state?.configuration.perks, buttonText: isPremium ? strings.Common_OK : (state?.isAnnual == true ? strings.Premium_SubscribeForAnnual(state?.price ?? "").string : strings.Premium_SubscribeFor(state?.price ?? "").string), isPremium: isPremium, forceDark: forceDark)
controller.action = { [weak state] in
dismissImpl?()
if state?.isPremium == false {
buy()
}
let isPremium = state?.isPremium == true
var dismissImpl: (() -> Void)?
let controller = PremiumLimitsListScreen(context: accountContext, subject: demoSubject, source: .intro(state?.price), order: state?.configuration.perks, buttonText: isPremium ? strings.Common_OK : (state?.isAnnual == true ? strings.Premium_SubscribeForAnnual(state?.price ?? "").string : strings.Premium_SubscribeFor(state?.price ?? "").string), isPremium: isPremium, forceDark: forceDark)
controller.action = { [weak state] in
dismissImpl?()
if state?.isPremium == false {
buy()
}
}
controller.disposed = {
updateIsFocused(false)
}
present(controller)
dismissImpl = { [weak controller] in
controller?.dismiss(animated: true, completion: nil)
}
updateIsFocused(true)
addAppLogEvent(postbox: accountContext.account.postbox, type: "premium.promo_screen_tap", data: ["item": perk.identifier])
}
controller.disposed = {
updateIsFocused(false)
}
present(controller)
dismissImpl = { [weak controller] in
controller?.dismiss(animated: true, completion: nil)
}
updateIsFocused(true)
addAppLogEvent(postbox: accountContext.account.postbox, type: "premium.promo_screen_tap", data: ["item": perk.identifier])
}
))))
i += 1
@ -2100,64 +2119,92 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
foregroundColor: .white,
iconName: perk.iconName
))),
action: { _ in
switch perk {
case .location:
let _ = (accountContext.engine.data.get(
TelegramEngine.EngineData.Item.Peer.BusinessLocation(id: accountContext.account.peerId)
)
|> deliverOnMainQueue).start(next: { [weak accountContext] businessLocation in
guard let accountContext else {
return
action: { [weak state] _ in
let isPremium = state?.isPremium == true
if isPremium {
switch perk {
case .location:
let _ = (accountContext.engine.data.get(
TelegramEngine.EngineData.Item.Peer.BusinessLocation(id: accountContext.account.peerId)
)
|> deliverOnMainQueue).start(next: { [weak accountContext] businessLocation in
guard let accountContext else {
return
}
push(accountContext.sharedContext.makeBusinessLocationSetupScreen(context: accountContext, initialValue: businessLocation, completion: { _ in }))
})
case .hours:
let _ = (accountContext.engine.data.get(
TelegramEngine.EngineData.Item.Peer.BusinessHours(id: accountContext.account.peerId)
)
|> deliverOnMainQueue).start(next: { [weak accountContext] businessHours in
guard let accountContext else {
return
}
push(accountContext.sharedContext.makeBusinessHoursSetupScreen(context: accountContext, initialValue: businessHours, completion: { _ in }))
})
case .quickReplies:
let _ = (accountContext.sharedContext.makeQuickReplySetupScreenInitialData(context: accountContext)
|> take(1)
|> deliverOnMainQueue).start(next: { [weak accountContext] initialData in
guard let accountContext else {
return
}
push(accountContext.sharedContext.makeQuickReplySetupScreen(context: accountContext, initialData: initialData))
})
case .greetings:
let _ = (accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreenInitialData(context: accountContext)
|> take(1)
|> deliverOnMainQueue).start(next: { [weak accountContext] initialData in
guard let accountContext else {
return
}
push(accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreen(context: accountContext, initialData: initialData, isAwayMode: false))
})
case .awayMessages:
let _ = (accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreenInitialData(context: accountContext)
|> take(1)
|> deliverOnMainQueue).start(next: { [weak accountContext] initialData in
guard let accountContext else {
return
}
push(accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreen(context: accountContext, initialData: initialData, isAwayMode: true))
})
case .chatbots:
push(accountContext.sharedContext.makeChatbotSetupScreen(context: accountContext))
}
} else {
var demoSubject: PremiumDemoScreen.Subject
switch perk {
case .location:
demoSubject = .businessLocation
case .hours:
demoSubject = .businessHours
case .quickReplies:
demoSubject = .businessQuickReplies
case .greetings:
demoSubject = .businessGreetingMessage
case .awayMessages:
demoSubject = .businessAwayMessage
case .chatbots:
demoSubject = .businessChatBots
}
var dismissImpl: (() -> Void)?
let controller = PremiumLimitsListScreen(context: accountContext, subject: demoSubject, source: .intro(state?.price), order: [.businessLocation, .businessHours, .businessQuickReplies, .businessGreetingMessage, .businessAwayMessage, .businessChatBots], buttonText: isPremium ? strings.Common_OK : (state?.isAnnual == true ? strings.Premium_SubscribeForAnnual(state?.price ?? "").string : strings.Premium_SubscribeFor(state?.price ?? "").string), isPremium: isPremium, forceDark: forceDark)
controller.action = { [weak state] in
dismissImpl?()
if state?.isPremium == false {
buy()
}
push(accountContext.sharedContext.makeBusinessLocationSetupScreen(context: accountContext, initialValue: businessLocation, completion: { _ in }))
})
case .hours:
let _ = (accountContext.engine.data.get(
TelegramEngine.EngineData.Item.Peer.BusinessHours(id: accountContext.account.peerId)
)
|> deliverOnMainQueue).start(next: { [weak accountContext] businessHours in
guard let accountContext else {
return
}
push(accountContext.sharedContext.makeBusinessHoursSetupScreen(context: accountContext, initialValue: businessHours, completion: { _ in }))
})
case .quickReplies:
let _ = (accountContext.sharedContext.makeQuickReplySetupScreenInitialData(context: accountContext)
|> take(1)
|> deliverOnMainQueue).start(next: { [weak accountContext] initialData in
guard let accountContext else {
return
}
push(accountContext.sharedContext.makeQuickReplySetupScreen(context: accountContext, initialData: initialData))
})
case .greetings:
let _ = (accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreenInitialData(context: accountContext)
|> take(1)
|> deliverOnMainQueue).start(next: { [weak accountContext] initialData in
guard let accountContext else {
return
}
push(accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreen(context: accountContext, initialData: initialData, isAwayMode: false))
})
case .awayMessages:
let _ = (accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreenInitialData(context: accountContext)
|> take(1)
|> deliverOnMainQueue).start(next: { [weak accountContext] initialData in
guard let accountContext else {
return
}
push(accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreen(context: accountContext, initialData: initialData, isAwayMode: true))
})
case .chatbots:
let _ = (accountContext.sharedContext.makeChatbotSetupScreenInitialData(context: accountContext)
|> take(1)
|> deliverOnMainQueue).start(next: { [weak accountContext] initialData in
guard let accountContext else {
return
}
push(accountContext.sharedContext.makeChatbotSetupScreen(context: accountContext, initialData: initialData))
})
}
controller.disposed = {
updateIsFocused(false)
}
present(controller)
dismissImpl = { [weak controller] in
controller?.dismiss(animated: true, completion: nil)
}
updateIsFocused(true)
}
}
))))
@ -2259,7 +2306,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
iconName: "Premium/BusinessPerk/Tag"
))),
action: { _ in
push(accountContext.sharedContext.makeFilterSettingsController(context: accountContext, modal: false, dismissed: nil))
push(accountContext.sharedContext.makeFilterSettingsController(context: accountContext, modal: false, scrollToTags: true, dismissed: nil))
}
))))

View File

@ -833,6 +833,129 @@ public class PremiumLimitsListScreen: ViewController {
)
)
)
availableItems[.businessLocation] = DemoPagerComponent.Item(
AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.businessLocation,
component: AnyComponent(
PageComponent(
content: AnyComponent(PhoneDemoComponent(
context: context,
position: .top,
model: .island,
videoFile: configuration.videos["business_location"],
decoration: .business
)),
title: strings.Business_Location,
text: strings.Business_LocationInfo,
textColor: textColor
)
)
)
)
availableItems[.businessHours] = DemoPagerComponent.Item(
AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.businessHours,
component: AnyComponent(
PageComponent(
content: AnyComponent(PhoneDemoComponent(
context: context,
position: .top,
model: .island,
videoFile: configuration.videos["business_hours"],
decoration: .business
)),
title: strings.Business_OpeningHours,
text: strings.Business_OpeningHoursInfo,
textColor: textColor
)
)
)
)
availableItems[.businessQuickReplies] = DemoPagerComponent.Item(
AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.businessQuickReplies,
component: AnyComponent(
PageComponent(
content: AnyComponent(PhoneDemoComponent(
context: context,
position: .top,
model: .island,
videoFile: configuration.videos["greeting_message"],
decoration: .business
)),
title: strings.Business_QuickReplies,
text: strings.Business_QuickRepliesInfo,
textColor: textColor
)
)
)
)
availableItems[.businessGreetingMessage] = DemoPagerComponent.Item(
AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.businessGreetingMessage,
component: AnyComponent(
PageComponent(
content: AnyComponent(PhoneDemoComponent(
context: context,
position: .top,
model: .island,
videoFile: configuration.videos["greeting_message"],
decoration: .business
)),
title: strings.Business_GreetingMessages,
text: strings.Business_GreetingMessagesInfo,
textColor: textColor
)
)
)
)
availableItems[.businessAwayMessage] = DemoPagerComponent.Item(
AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.businessAwayMessage,
component: AnyComponent(
PageComponent(
content: AnyComponent(PhoneDemoComponent(
context: context,
position: .top,
model: .island,
videoFile: configuration.videos["away_message"],
decoration: .business
)),
title: strings.Business_AwayMessages,
text: strings.Business_AwayMessagesInfo,
textColor: textColor
)
)
)
)
availableItems[.businessChatBots] = DemoPagerComponent.Item(
AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.businessChatBots,
component: AnyComponent(
PageComponent(
content: AnyComponent(PhoneDemoComponent(
context: context,
position: .top,
model: .island,
videoFile: configuration.videos["business_bots"],
decoration: .business
)),
title: strings.Business_Chatbots,
text: strings.Business_ChatbotsInfo,
textColor: textColor
)
)
)
)
if let order = controller.order {
var items: [DemoPagerComponent.Item] = order.compactMap { availableItems[$0] }

View File

@ -8,7 +8,7 @@ import GZip
import AppBundle
import LegacyComponents
private let sceneVersion: Int = 6
private let sceneVersion: Int = 7
private func deg2rad(_ number: Float) -> Float {
return number * .pi / 180
@ -45,7 +45,31 @@ private func generateDiffuseTexture() -> UIImage {
})!
}
class PremiumStarComponent: Component {
func loadCompressedScene(name: String, version: Int) -> SCNScene? {
let resourceUrl: URL
if let url = getAppBundle().url(forResource: name, withExtension: "scn") {
resourceUrl = url
} else {
let fileName = "\(name)_\(version).scn"
let tmpUrl = URL(fileURLWithPath: NSTemporaryDirectory() + fileName)
if !FileManager.default.fileExists(atPath: tmpUrl.path) {
guard let url = getAppBundle().url(forResource: name, withExtension: ""),
let compressedData = try? Data(contentsOf: url),
let decompressedData = TGGUnzipData(compressedData, 8 * 1024 * 1024) else {
return nil
}
try? decompressedData.write(to: tmpUrl)
}
resourceUrl = tmpUrl
}
guard let scene = try? SCNScene(url: resourceUrl, options: nil) else {
return nil
}
return scene
}
final class PremiumStarComponent: Component {
let isIntro: Bool
let isVisible: Bool
let hasIdleAnimations: Bool
@ -251,24 +275,7 @@ class PremiumStarComponent: Component {
}
private func setup() {
let resourceUrl: URL
if let url = getAppBundle().url(forResource: "star", withExtension: "scn") {
resourceUrl = url
} else {
let fileName = "star_\(sceneVersion).scn"
let tmpUrl = URL(fileURLWithPath: NSTemporaryDirectory() + fileName)
if !FileManager.default.fileExists(atPath: tmpUrl.path) {
guard let url = getAppBundle().url(forResource: "star", withExtension: ""),
let compressedData = try? Data(contentsOf: url),
let decompressedData = TGGUnzipData(compressedData, 8 * 1024 * 1024) else {
return
}
try? decompressedData.write(to: tmpUrl)
}
resourceUrl = tmpUrl
}
guard let scene = try? SCNScene(url: resourceUrl, options: nil) else {
guard let scene = loadCompressedScene(name: "star", version: sceneVersion) else {
return
}

View File

@ -5,6 +5,8 @@ import Display
import AppBundle
import SwiftSignalKit
private let sceneVersion: Int = 1
final class SwirlStarsView: UIView, PhoneDemoDecorationView {
private let sceneView: SCNView
@ -13,8 +15,8 @@ final class SwirlStarsView: UIView, PhoneDemoDecorationView {
override init(frame: CGRect) {
self.sceneView = SCNView(frame: CGRect(origin: .zero, size: frame.size))
self.sceneView.backgroundColor = .clear
if let url = getAppBundle().url(forResource: "swirl", withExtension: "scn") {
self.sceneView.scene = try? SCNScene(url: url, options: nil)
if let scene = loadCompressedScene(name: "swirl", version: sceneVersion) {
self.sceneView.scene = scene
}
self.sceneView.isUserInteractionEnabled = false
self.sceneView.preferredFramesPerSecond = 60

View File

@ -572,6 +572,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
}, openPremiumStatusInfo: { _, _, _, _ in
}, openRecommendedChannelContextMenu: { _, _, _ in
}, openGroupBoostInfo: { _, _ in
}, openStickerEditor: {
}, requestMessageUpdate: { _, _ in
}, cancelInteractiveKeyboardGestures: {
}, dismissTextInput: {

View File

@ -232,6 +232,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
public let openPremiumStatusInfo: (EnginePeer.Id, UIView, Int64?, PeerNameColor) -> Void
public let openRecommendedChannelContextMenu: (EnginePeer, UIView, ContextGesture?) -> Void
public let openGroupBoostInfo: (EnginePeer.Id?, Int) -> Void
public let openStickerEditor: () -> Void
public let requestMessageUpdate: (MessageId, Bool) -> Void
public let cancelInteractiveKeyboardGestures: () -> Void
@ -355,6 +356,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
openPremiumStatusInfo: @escaping (EnginePeer.Id, UIView, Int64?, PeerNameColor) -> Void,
openRecommendedChannelContextMenu: @escaping (EnginePeer, UIView, ContextGesture?) -> Void,
openGroupBoostInfo: @escaping (EnginePeer.Id?, Int) -> Void,
openStickerEditor: @escaping () -> Void,
requestMessageUpdate: @escaping (MessageId, Bool) -> Void,
cancelInteractiveKeyboardGestures: @escaping () -> Void,
dismissTextInput: @escaping () -> Void,
@ -458,6 +460,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
self.openPremiumStatusInfo = openPremiumStatusInfo
self.openRecommendedChannelContextMenu = openRecommendedChannelContextMenu
self.openGroupBoostInfo = openGroupBoostInfo
self.openStickerEditor = openStickerEditor
self.requestMessageUpdate = requestMessageUpdate
self.cancelInteractiveKeyboardGestures = cancelInteractiveKeyboardGestures

View File

@ -56,6 +56,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
let dismissTextInput: () -> Void
let insertText: (NSAttributedString) -> Void
let backwardsDeleteText: () -> Void
let openStickerEditor: () -> Void
let presentController: (ViewController, Any?) -> Void
let presentGlobalOverlayController: (ViewController, Any?) -> Void
let getNavigationController: () -> NavigationController?
@ -72,6 +73,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
dismissTextInput: @escaping () -> Void,
insertText: @escaping (NSAttributedString) -> Void,
backwardsDeleteText: @escaping () -> Void,
openStickerEditor: @escaping () -> Void,
presentController: @escaping (ViewController, Any?) -> Void,
presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void,
getNavigationController: @escaping () -> NavigationController?,
@ -86,6 +88,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
self.dismissTextInput = dismissTextInput
self.insertText = insertText
self.backwardsDeleteText = backwardsDeleteText
self.openStickerEditor = openStickerEditor
self.presentController = presentController
self.presentGlobalOverlayController = presentGlobalOverlayController
self.getNavigationController = getNavigationController
@ -106,6 +109,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
self.dismissTextInput = chatControllerInteraction.dismissTextInput
self.insertText = panelInteraction.insertText
self.backwardsDeleteText = panelInteraction.backwardsDeleteText
self.openStickerEditor = chatControllerInteraction.openStickerEditor
self.presentController = chatControllerInteraction.presentController
self.presentGlobalOverlayController = chatControllerInteraction.presentGlobalOverlayController
self.getNavigationController = chatControllerInteraction.navigationController
@ -1140,6 +1144,9 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
return
}
guard let file = item.itemFile else {
if groupId == AnyHashable("recent"), case .icon(.add) = item.content {
interaction.openStickerEditor()
}
return
}

View File

@ -16,6 +16,8 @@ public final class EmptyStateIndicatorComponent: Component {
public let text: String
public let actionTitle: String?
public let action: () -> Void
public let additionalActionTitle: String?
public let additionalAction: () -> Void
public init(
context: AccountContext,
@ -24,7 +26,9 @@ public final class EmptyStateIndicatorComponent: Component {
title: String,
text: String,
actionTitle: String?,
action: @escaping () -> Void
action: @escaping () -> Void,
additionalActionTitle: String?,
additionalAction: @escaping () -> Void
) {
self.context = context
self.theme = theme
@ -33,6 +37,8 @@ public final class EmptyStateIndicatorComponent: Component {
self.text = text
self.actionTitle = actionTitle
self.action = action
self.additionalActionTitle = additionalActionTitle
self.additionalAction = additionalAction
}
public static func ==(lhs: EmptyStateIndicatorComponent, rhs: EmptyStateIndicatorComponent) -> Bool {
@ -54,6 +60,9 @@ public final class EmptyStateIndicatorComponent: Component {
if lhs.actionTitle != rhs.actionTitle {
return false
}
if lhs.additionalActionTitle != rhs.additionalActionTitle {
return false
}
return true
}
@ -65,6 +74,7 @@ public final class EmptyStateIndicatorComponent: Component {
private let title = ComponentView<Empty>()
private let text = ComponentView<Empty>()
private var button: ComponentView<Empty>?
private var additionalButton: ComponentView<Empty>?
override public init(frame: CGRect) {
super.init(frame: frame)
@ -139,7 +149,7 @@ public final class EmptyStateIndicatorComponent: Component {
}
)),
environment: {},
containerSize: CGSize(width: 240.0, height: 50.0)
containerSize: CGSize(width: 260.0, height: 50.0)
)
} else {
if let button = self.button {
@ -148,14 +158,52 @@ public final class EmptyStateIndicatorComponent: Component {
}
}
var additionalButtonSize: CGSize?
if let additionalActionTitle = component.additionalActionTitle {
let additionalButton: ComponentView<Empty>
if let current = self.additionalButton {
additionalButton = current
} else {
additionalButton = ComponentView()
self.additionalButton = additionalButton
}
additionalButtonSize = additionalButton.update(
transition: transition,
component: AnyComponent(Button(
content: AnyComponent(Text(
text: additionalActionTitle, font:
Font.regular(17.0),
color: component.theme.list.itemAccentColor)
),
action: { [weak self] in
guard let self, let component = self.component else {
return
}
component.additionalAction()
}
)),
environment: {},
containerSize: CGSize(width: 262.0, height: 50.0)
)
} else {
if let additionalButton = self.additionalButton {
self.additionalButton = nil
additionalButton.view?.removeFromSuperview()
}
}
let animationSpacing: CGFloat = 11.0
let titleSpacing: CGFloat = 17.0
let buttonSpacing: CGFloat = 17.0
let buttonSpacing: CGFloat = 21.0
var totalHeight: CGFloat = animationSize.height + animationSpacing + titleSize.height + titleSpacing + textSize.height
if let buttonSize {
totalHeight += buttonSpacing + buttonSize.height
}
if let additionalButtonSize {
totalHeight += buttonSpacing + additionalButtonSize.height
}
var contentY = floor((availableSize.height - totalHeight) * 0.5)
@ -185,7 +233,14 @@ public final class EmptyStateIndicatorComponent: Component {
self.addSubview(buttonView)
}
transition.setFrame(view: buttonView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - buttonSize.width) * 0.5), y: contentY), size: buttonSize))
contentY += buttonSize.height
contentY += buttonSize.height + buttonSpacing
}
if let additionalButtonSize, let additionalButtonView = self.additionalButton?.view {
if additionalButtonView.superview == nil {
self.addSubview(additionalButtonView)
}
transition.setFrame(view: additionalButtonView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - additionalButtonSize.width) * 0.5), y: contentY), size: additionalButtonSize))
contentY += additionalButtonSize.height
}
return availableSize

View File

@ -2519,6 +2519,7 @@ public final class EmojiPagerContentComponent: Component {
case premiumStar
case topic(String, Int32)
case stop
case add
}
case animation(EntityKeyboardAnimationData)
@ -3559,6 +3560,15 @@ public final class EmojiPagerContentComponent: Component {
let imageSize = image.size.aspectFitted(CGSize(width: size.width - 6.0, height: size.height - 6.0))
image.draw(in: CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: floor((size.height - imageSize.height) / 2.0)), size: imageSize))
}
case .add:
context.setFillColor(UIColor.black.withAlphaComponent(0.08).cgColor)
context.fillEllipse(in: CGRect(origin: .zero, size: size).insetBy(dx: 8.0, dy: 8.0))
context.setFillColor(UIColor.black.withAlphaComponent(0.16).cgColor)
let plusSize = CGSize(width: 4.5, height: 31.5)
context.addPath(UIBezierPath(roundedRect: CGRect(x: floorToScreenPixels((size.width - plusSize.width) / 2.0), y: floorToScreenPixels((size.height - plusSize.height) / 2.0), width: plusSize.width, height: plusSize.height), cornerRadius: plusSize.width / 2.0).cgPath)
context.addPath(UIBezierPath(roundedRect: CGRect(x: floorToScreenPixels((size.width - plusSize.height) / 2.0), y: floorToScreenPixels((size.height - plusSize.width) / 2.0), width: plusSize.height, height: plusSize.width), cornerRadius: plusSize.width / 2.0).cgPath)
context.fillPath()
}
UIGraphicsPopContext()

View File

@ -1784,6 +1784,7 @@ public extension EmojiPagerContentComponent {
}
if let recentStickers = recentStickers {
let groupId = "recent"
for item in recentStickers.items {
guard let item = item.contents.get(RecentMediaItem.self) else {
continue
@ -1807,7 +1808,6 @@ public extension EmojiPagerContentComponent {
tintMode: tintMode
)
let groupId = "recent"
if let groupIndex = itemGroupIndexById[groupId] {
itemGroups[groupIndex].items.append(resultItem)
} else {

View File

@ -182,7 +182,11 @@ public final class ListActionItemComponent: Component {
let contentRightInset: CGFloat
switch component.accessory {
case .none:
contentRightInset = 16.0
if let _ = component.icon {
contentRightInset = 42.0
} else {
contentRightInset = 16.0
}
case .arrow:
contentRightInset = 30.0
case .toggle:
@ -193,7 +197,7 @@ public final class ListActionItemComponent: Component {
contentHeight += component.contentInsets.top
if component.leftIcon != nil {
contentLeftInset += 46.0
contentLeftInset += 52.0
}
let titleSize = self.title.update(
@ -243,7 +247,7 @@ public final class ListActionItemComponent: Component {
var iconOffset: CGFloat = 0.0
if case .none = component.accessory {
iconOffset = 6.0
iconOffset = 26.0
}
let iconFrame = CGRect(origin: CGPoint(x: availableSize.width - contentRightInset - iconSize.width + iconOffset, y: floor((contentHeight - iconSize.height) * 0.5)), size: iconSize)

View File

@ -25,8 +25,8 @@ vertex RasterizerData defaultVertexShader(uint vertexID [[vertex_id]],
fragment half4 defaultFragmentShader(RasterizerData in [[stage_in]],
texture2d<half, access::sample> texture [[texture(0)]]) {
constexpr sampler samplr(filter::linear, mag_filter::linear, min_filter::linear);
half3 color = texture.sample(samplr, in.texCoord).rgb;
return half4(color, 1.0);
half4 color = texture.sample(samplr, in.texCoord);
return color;
}
fragment half histogramPrepareFragmentShader(RasterizerData in [[stage_in]],
@ -39,12 +39,12 @@ fragment half histogramPrepareFragmentShader(RasterizerData in [[stage_in]],
}
typedef struct {
float3 topColor;
float3 bottomColor;
float4 topColor;
float4 bottomColor;
} GradientColors;
fragment half4 gradientFragmentShader(RasterizerData in [[stage_in]],
constant GradientColors& colors [[buffer(0)]]) {
return half4(half3(mix(colors.topColor, colors.bottomColor, in.texCoord.y)), 1.0);
return half4(half3(mix(colors.topColor.rgb, colors.bottomColor.rgb, in.texCoord.y)), 1.0);
}

View File

@ -8,7 +8,7 @@ import VideoToolbox
private let queue = Queue()
public func cutoutStickerImage(from image: UIImage) -> Signal<UIImage?, NoError> {
public func cutoutStickerImage(from image: UIImage, onlyCheck: Bool = false) -> Signal<UIImage?, NoError> {
if #available(iOS 17.0, *) {
guard let cgImage = image.cgImage else {
return .single(nil)
@ -23,21 +23,26 @@ public func cutoutStickerImage(from image: UIImage) -> Signal<UIImage?, NoError>
subscriber.putCompletion()
return
}
let instances = instances(atPoint: nil, inObservation: result)
if let mask = try? result.generateScaledMaskForImage(forInstances: instances, from: handler) {
let filter = CIFilter.blendWithMask()
filter.inputImage = inputImage
filter.backgroundImage = CIImage(color: .clear)
filter.maskImage = CIImage(cvPixelBuffer: mask)
if let output = filter.outputImage, let cgImage = ciContext.createCGImage(output, from: inputImage.extent) {
let image = UIImage(cgImage: cgImage)
subscriber.putNext(image)
subscriber.putCompletion()
return
if onlyCheck {
subscriber.putNext(UIImage())
subscriber.putCompletion()
} else {
let instances = instances(atPoint: nil, inObservation: result)
if let mask = try? result.generateScaledMaskForImage(forInstances: instances, from: handler) {
let filter = CIFilter.blendWithMask()
filter.inputImage = inputImage
filter.backgroundImage = CIImage(color: .clear)
filter.maskImage = CIImage(cvPixelBuffer: mask)
if let output = filter.outputImage, let cgImage = ciContext.createCGImage(output, from: inputImage.extent) {
let image = UIImage(cgImage: cgImage)
subscriber.putNext(image)
subscriber.putCompletion()
return
}
}
subscriber.putNext(nil)
subscriber.putCompletion()
}
subscriber.putNext(nil)
subscriber.putCompletion()
}
try? handler.perform([request])
return ActionDisposable {

View File

@ -94,6 +94,11 @@ public final class MediaEditor {
}
}
public enum Mode {
case `default`
case sticker
}
public enum Subject {
case image(UIImage, PixelDimensions)
case video(String, UIImage?, Bool, String?, PixelDimensions, Double)
@ -116,6 +121,7 @@ public final class MediaEditor {
}
private let context: AccountContext
private let mode: Mode
private let subject: Subject
private let clock = CMClockGetHostTimeClock()
@ -182,6 +188,9 @@ public final class MediaEditor {
}
}
public private(set) var canCutout: Bool = false
public var canCutoutUpdated: (Bool) -> Void = { _ in }
private var textureCache: CVMetalTextureCache!
public var hasPortraitMask: Bool {
@ -391,8 +400,9 @@ public final class MediaEditor {
}
}
public init(context: AccountContext, subject: Subject, values: MediaEditorValues? = nil, hasHistogram: Bool = false) {
public init(context: AccountContext, mode: Mode, subject: Subject, values: MediaEditorValues? = nil, hasHistogram: Bool = false) {
self.context = context
self.mode = mode
self.subject = subject
if let values {
self.values = values
@ -668,6 +678,19 @@ public final class MediaEditor {
} else {
textureSource.setMainInput(.image(image))
}
if case .sticker = self.mode {
let _ = (cutoutStickerImage(from: image, onlyCheck: true)
|> deliverOnMainQueue).start(next: { [weak self] result in
guard let self, result != nil else {
return
}
self.canCutout = true
self.canCutoutUpdated(true)
})
}
}
if let player, let playerItem = player.currentItem, !textureSourceResult.playerIsReference {
textureSource.setMainInput(.video(playerItem))
@ -677,7 +700,12 @@ public final class MediaEditor {
}
self.renderer.textureSource = textureSource
self.setGradientColors(textureSourceResult.gradientColors)
switch self.mode {
case .default:
self.setGradientColors(textureSourceResult.gradientColors)
case .sticker:
self.setGradientColors(GradientColors(top: .clear, bottom: .clear))
}
if let _ = textureSourceResult.player {
self.updateRenderChain()
@ -1615,7 +1643,7 @@ public final class MediaEditor {
public func setGradientColors(_ gradientColors: GradientColors) {
self.gradientColorsPromise.set(.single(gradientColors))
self.updateValues(mode: .skipRendering) { values in
self.updateValues(mode: self.sourceIsVideo ? .skipRendering : .generic) { values in
return values.withUpdatedGradientColors(gradientColors: gradientColors.array)
}
}

View File

@ -178,7 +178,7 @@ final class MediaEditorComposer {
}
}
public func makeEditorImageComposition(context: CIContext, postbox: Postbox, inputImage: UIImage, dimensions: CGSize, values: MediaEditorValues, time: CMTime, textScale: CGFloat, completion: @escaping (UIImage?) -> Void) {
public func makeEditorImageComposition(context: CIContext, postbox: Postbox, inputImage: UIImage, dimensions: CGSize, outputDimensions: CGSize? = nil, values: MediaEditorValues, time: CMTime, textScale: CGFloat, completion: @escaping (UIImage?) -> Void) {
let colorSpace = CGColorSpaceCreateDeviceRGB()
let inputImage = CIImage(image: inputImage, options: [.colorSpace: colorSpace])!
var drawingImage: CIImage?
@ -192,7 +192,7 @@ public func makeEditorImageComposition(context: CIContext, postbox: Postbox, inp
entities.append(contentsOf: composerEntitiesForDrawingEntity(postbox: postbox, textScale: textScale, entity: entity.entity, colorSpace: colorSpace))
}
makeEditorImageFrameComposition(context: context, inputImage: inputImage, drawingImage: drawingImage, dimensions: dimensions, outputDimensions: dimensions, values: values, entities: entities, time: time, textScale: textScale, completion: { ciImage in
makeEditorImageFrameComposition(context: context, inputImage: inputImage, drawingImage: drawingImage, dimensions: dimensions, outputDimensions: outputDimensions ?? dimensions, values: values, entities: entities, time: time, textScale: textScale, completion: { ciImage in
if let ciImage {
if let cgImage = context.createCGImage(ciImage, from: CGRect(origin: .zero, size: ciImage.extent.size)) {
Queue.mainQueue().async {
@ -206,20 +206,19 @@ public func makeEditorImageComposition(context: CIContext, postbox: Postbox, inp
}
private func makeEditorImageFrameComposition(context: CIContext, inputImage: CIImage, drawingImage: CIImage?, dimensions: CGSize, outputDimensions: CGSize, values: MediaEditorValues, entities: [MediaEditorComposerEntity], time: CMTime, textScale: CGFloat = 1.0, completion: @escaping (CIImage?) -> Void) {
var resultImage = CIImage(color: .black).cropped(to: CGRect(origin: .zero, size: dimensions)).transformed(by: CGAffineTransform(translationX: -dimensions.width / 2.0, y: -dimensions.height / 2.0))
var isClear = false
if let gradientColor = values.gradientColors?.first, gradientColor.alpha.isZero {
isClear = true
}
var resultImage = CIImage(color: isClear ? .clear : .black).cropped(to: CGRect(origin: .zero, size: dimensions)).transformed(by: CGAffineTransform(translationX: -dimensions.width / 2.0, y: -dimensions.height / 2.0))
var mediaImage = inputImage.samplingLinear().transformed(by: CGAffineTransform(translationX: -inputImage.extent.midX, y: -inputImage.extent.midY))
var initialScale: CGFloat
if mediaImage.extent.height > mediaImage.extent.width && values.isStory {
initialScale = max(dimensions.width / mediaImage.extent.width, dimensions.height / mediaImage.extent.height)
} else {
initialScale = dimensions.width / mediaImage.extent.width
}
if values.isStory {
resultImage = mediaImage.samplingLinear().composited(over: resultImage)
} else {
let initialScale = dimensions.width / mediaImage.extent.width
var horizontalScale = initialScale
if values.cropMirroring {
horizontalScale *= -1.0

View File

@ -634,6 +634,10 @@ public final class MediaEditorValues: Codable, Equatable {
return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: offset, cropRect: self.cropRect, cropScale: scale, cropRotation: rotation, cropMirroring: mirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset)
}
public func withUpdatedCropRect(cropRect: CGRect, rotation: CGFloat, mirroring: Bool) -> MediaEditorValues {
return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: .zero, cropRect: cropRect, cropScale: 1.0, cropRotation: rotation, cropMirroring: mirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset)
}
func withUpdatedGradientColors(gradientColors: [UIColor]) -> MediaEditorValues {
return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset)
}
@ -716,6 +720,10 @@ public final class MediaEditorValues: Codable, Equatable {
return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, entities: entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset)
}
public func withUpdatedQualityPreset(_ qualityPreset: MediaQualityPreset?) -> MediaEditorValues {
return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: qualityPreset)
}
public var resultDimensions: PixelDimensions {
if self.videoIsFullHd {
return PixelDimensions(width: 1080, height: 1920)

View File

@ -165,6 +165,7 @@ final class OutputRenderPass: DefaultRenderPass {
renderPassDescriptor.colorAttachments[0].texture = (drawable as? CAMetalDrawable)?.texture
renderPassDescriptor.colorAttachments[0].loadAction = .clear
renderPassDescriptor.colorAttachments[0].storeAction = .store
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0)
let drawableSize = renderTarget.drawableSize

View File

@ -128,15 +128,6 @@ final class UniversalTextureSource: TextureSource {
self.update()
}
}
//
// private func setupDisplayLink(frameRate: Int) {
// self.displayLink?.invalidate()
// self.displayLink = nil
//
// if self.playerItemOutput != nil {
// }
// }
}
private protocol InputContext {

View File

@ -233,7 +233,7 @@ final class VideoFinishPass: RenderPass {
}
private let canvasSize = CGSize(width: 1080.0, height: 1920.0)
private var gradientColors = GradientColors(topColor: simd_float3(0.0, 0.0, 0.0), bottomColor: simd_float3(0.0, 0.0, 0.0))
private var gradientColors = GradientColors(topColor: simd_float4(0.0, 0.0, 0.0, 0.0), bottomColor: simd_float4(0.0, 0.0, 0.0, 0.0))
func update(values: MediaEditorValues, videoDuration: Double?, additionalVideoDuration: Double?) {
let position = CGPoint(
x: canvasSize.width / 2.0 + values.cropOffset.x,
@ -241,6 +241,7 @@ final class VideoFinishPass: RenderPass {
)
self.isStory = values.isStory
self.isSticker = values.gradientColors?.first?.alpha == 0.0
self.mainPosition = VideoFinishPass.VideoPosition(position: position, size: self.mainPosition.size, scale: values.cropScale, rotation: values.cropRotation, baseScale: self.mainPosition.baseScale)
if let position = values.additionalVideoPosition, let scale = values.additionalVideoScale, let rotation = values.additionalVideoRotation {
@ -262,12 +263,12 @@ final class VideoFinishPass: RenderPass {
}
if let gradientColors = values.gradientColors, let top = gradientColors.first, let bottom = gradientColors.last {
let (topRed, topGreen, topBlue, _) = top.components
let (bottomRed, bottomGreen, bottomBlue, _) = bottom.components
let (topRed, topGreen, topBlue, topAlpha) = top.components
let (bottomRed, bottomGreen, bottomBlue, bottomAlpha) = bottom.components
self.gradientColors = GradientColors(
topColor: simd_float3(Float(topRed), Float(topGreen), Float(topBlue)),
bottomColor: simd_float3(Float(bottomRed), Float(bottomGreen), Float(bottomBlue))
topColor: simd_float4(Float(topRed), Float(topGreen), Float(topBlue), Float(topAlpha)),
bottomColor: simd_float4(Float(bottomRed), Float(bottomGreen), Float(bottomBlue), Float(bottomAlpha))
)
}
}
@ -289,6 +290,7 @@ final class VideoFinishPass: RenderPass {
)
private var isStory = true
private var isSticker = true
private var videoPositionChanges: [VideoPositionChange] = []
private var videoDuration: Double?
private var additionalVideoDuration: Double?
@ -482,10 +484,18 @@ final class VideoFinishPass: RenderPass {
}
let baseScale: CGFloat
if input.height > input.width {
baseScale = max(canvasSize.width / CGFloat(input.width), canvasSize.height / CGFloat(input.height))
if !self.isSticker {
if input.height > input.width {
baseScale = max(canvasSize.width / CGFloat(input.width), canvasSize.height / CGFloat(input.height))
} else {
baseScale = canvasSize.width / CGFloat(input.width)
}
} else {
baseScale = canvasSize.width / CGFloat(input.width)
if input.height > input.width {
baseScale = canvasSize.width / CGFloat(input.width)
} else {
baseScale = canvasSize.width / CGFloat(input.height)
}
}
self.mainPosition = self.mainPosition.with(size: CGSize(width: input.width, height: input.height), baseScale: baseScale)
@ -508,9 +518,13 @@ final class VideoFinishPass: RenderPass {
let renderPassDescriptor = MTLRenderPassDescriptor()
renderPassDescriptor.colorAttachments[0].texture = self.cachedTexture!
renderPassDescriptor.colorAttachments[0].loadAction = .dontCare
if self.gradientColors.topColor.w > 0.0 {
renderPassDescriptor.colorAttachments[0].loadAction = .dontCare
} else {
renderPassDescriptor.colorAttachments[0].loadAction = .clear
}
renderPassDescriptor.colorAttachments[0].storeAction = .store
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 1.0)
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0)
guard let renderCommandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else {
return input
}
@ -521,12 +535,14 @@ final class VideoFinishPass: RenderPass {
znear: -1.0, zfar: 1.0)
)
renderCommandEncoder.setRenderPipelineState(self.gradientPipelineState!)
self.encodeGradient(
using: renderCommandEncoder,
containerSize: containerSize,
device: device
)
if self.gradientColors.topColor.w > 0.0 {
renderCommandEncoder.setRenderPipelineState(self.gradientPipelineState!)
self.encodeGradient(
using: renderCommandEncoder,
containerSize: containerSize,
device: device
)
}
renderCommandEncoder.setRenderPipelineState(self.mainPipelineState!)
@ -578,8 +594,8 @@ final class VideoFinishPass: RenderPass {
}
struct GradientColors {
var topColor: simd_float3
var bottomColor: simd_float3
var topColor: simd_float4
var bottomColor: simd_float4
}
func encodeGradient(

View File

@ -50,6 +50,7 @@ swift_library(
"//submodules/TelegramUI/Components/Stories/ForwardInfoPanelComponent",
"//submodules/TelegramUI/Components/ContextReferenceButtonComponent",
"//submodules/TelegramUI/Components/MediaScrubberComponent",
"//submodules/Components/BlurredBackgroundComponent",
],
visibility = [
"//visibility:public",

View File

@ -0,0 +1,363 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ComponentFlow
import SwiftSignalKit
import ViewControllerComponent
import ComponentDisplayAdapters
import TelegramPresentationData
import AccountContext
import TelegramCore
import MultilineTextComponent
import DrawingUI
import MediaEditor
import Photos
import LottieAnimationComponent
import MessageInputPanelComponent
private final class MediaCutoutScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let mediaEditor: MediaEditor
init(
context: AccountContext,
mediaEditor: MediaEditor
) {
self.context = context
self.mediaEditor = mediaEditor
}
static func ==(lhs: MediaCutoutScreenComponent, rhs: MediaCutoutScreenComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
return true
}
public final class View: UIView {
private let buttonsContainerView = UIView()
private let buttonsBackgroundView = UIView()
private let cancelButton = ComponentView<Empty>()
private let label = ComponentView<Empty>()
private let doneButton = ComponentView<Empty>()
private var component: MediaCutoutScreenComponent?
private weak var state: State?
private var environment: ViewControllerComponentContainer.Environment?
override init(frame: CGRect) {
self.buttonsContainerView.clipsToBounds = true
super.init(frame: frame)
self.backgroundColor = .clear
self.addSubview(self.buttonsContainerView)
self.buttonsContainerView.addSubview(self.buttonsBackgroundView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func animateInFromEditor() {
self.buttonsBackgroundView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.label.view?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
private var animatingOut = false
func animateOutToEditor(completion: @escaping () -> Void) {
self.animatingOut = true
self.cancelButton.view?.isHidden = true
self.buttonsBackgroundView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
completion()
})
self.label.view?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
self.state?.updated()
}
func update(component: MediaCutoutScreenComponent, availableSize: CGSize, state: State, environment: Environment<ViewControllerComponentContainer.Environment>, transition: Transition) -> CGSize {
let environment = environment[ViewControllerComponentContainer.Environment.self].value
self.environment = environment
self.component = component
self.state = state
// let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let isTablet: Bool
if case .regular = environment.metrics.widthClass {
isTablet = true
} else {
isTablet = false
}
// let mediaEditor = (environment.controller() as? MediaCutoutScreen)?.mediaEditor
let buttonSideInset: CGFloat
let buttonBottomInset: CGFloat = 8.0
var controlsBottomInset: CGFloat = 0.0
let previewSize: CGSize
var topInset: CGFloat = environment.statusBarHeight + 5.0
if isTablet {
let previewHeight = availableSize.height - topInset - 75.0
previewSize = CGSize(width: floorToScreenPixels(previewHeight / 1.77778), height: previewHeight)
buttonSideInset = 30.0
} else {
previewSize = CGSize(width: availableSize.width, height: floorToScreenPixels(availableSize.width * 1.77778))
buttonSideInset = 10.0
if availableSize.height < previewSize.height + 30.0 {
topInset = 0.0
controlsBottomInset = -75.0
} else {
self.buttonsBackgroundView.backgroundColor = .clear
}
}
// var previewContainerFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - previewSize.width) / 2.0), y: environment.safeInsets.top), size: CGSize(width: previewSize.width, height: availableSize.height - environment.safeInsets.top - environment.safeInsets.bottom + controlsBottomInset))
let buttonsContainerFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - environment.safeInsets.bottom + controlsBottomInset), size: CGSize(width: availableSize.width, height: environment.safeInsets.bottom - controlsBottomInset))
let cancelButtonSize = self.cancelButton.update(
transition: transition,
component: AnyComponent(Button(
content: AnyComponent(
LottieAnimationComponent(
animation: LottieAnimationComponent.AnimationItem(
name: "media_backToCancel",
mode: .animating(loop: false),
range: self.animatingOut ? (0.5, 1.0) : (0.0, 0.5)
),
colors: ["__allcolors__": .white],
size: CGSize(width: 33.0, height: 33.0)
)
),
action: {
guard let controller = environment.controller() as? MediaCutoutScreen else {
return
}
controller.requestDismiss(reset: true, animated: true)
}
)),
environment: {},
containerSize: CGSize(width: 44.0, height: 44.0)
)
let cancelButtonFrame = CGRect(
origin: CGPoint(x: buttonSideInset, y: buttonBottomInset),
size: cancelButtonSize
)
if let cancelButtonView = self.cancelButton.view {
if cancelButtonView.superview == nil {
self.buttonsContainerView.addSubview(cancelButtonView)
}
transition.setFrame(view: cancelButtonView, frame: cancelButtonFrame)
}
let labelSize = self.label.update(
transition: transition,
component: AnyComponent(Text(text: "Tap an object to cut it out", font: Font.regular(17.0), color: .white)),
environment: {},
containerSize: CGSize(width: availableSize.width - 88.0, height: 44.0)
)
let labelFrame = CGRect(
origin: CGPoint(x: floorToScreenPixels((availableSize.width - labelSize.width) / 2.0), y: buttonBottomInset + 4.0),
size: labelSize
)
if let labelView = self.label.view {
if labelView.superview == nil {
self.buttonsContainerView.addSubview(labelView)
}
transition.setFrame(view: labelView, frame: labelFrame)
}
transition.setFrame(view: self.buttonsContainerView, frame: buttonsContainerFrame)
transition.setFrame(view: self.buttonsBackgroundView, frame: CGRect(origin: .zero, size: buttonsContainerFrame.size))
return availableSize
}
}
func makeView() -> View {
return View()
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
public final class MediaCutoutScreen: ViewController {
fileprivate final class Node: ViewControllerTracingNode, UIGestureRecognizerDelegate {
private weak var controller: MediaCutoutScreen?
private let context: AccountContext
fileprivate let componentHost: ComponentView<ViewControllerComponentContainer.Environment>
private var presentationData: PresentationData
private var validLayout: ContainerViewLayout?
init(controller: MediaCutoutScreen) {
self.controller = controller
self.context = controller.context
self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
self.componentHost = ComponentView<ViewControllerComponentContainer.Environment>()
super.init()
self.backgroundColor = .clear
}
override func didLoad() {
super.didLoad()
self.view.disablesInteractiveModalDismiss = true
self.view.disablesInteractiveKeyboardGestureRecognizer = true
}
@objc func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
func animateInFromEditor() {
if let view = self.componentHost.view as? MediaCutoutScreenComponent.View {
view.animateInFromEditor()
}
}
func animateOutToEditor(completion: @escaping () -> Void) {
if let mediaEditor = self.controller?.mediaEditor {
mediaEditor.play()
}
if let view = self.componentHost.view as? MediaCutoutScreenComponent.View {
view.animateOutToEditor(completion: completion)
}
}
func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, animateOut: Bool = false, transition: Transition) {
guard let controller = self.controller else {
return
}
let isFirstTime = self.validLayout == nil
self.validLayout = layout
let isTablet = layout.metrics.isTablet
let previewSize: CGSize
let topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 5.0
if isTablet {
let previewHeight = layout.size.height - topInset - 75.0
previewSize = CGSize(width: floorToScreenPixels(previewHeight / 1.77778), height: previewHeight)
} else {
previewSize = CGSize(width: layout.size.width, height: floorToScreenPixels(layout.size.width * 1.77778))
}
let bottomInset = layout.size.height - previewSize.height - topInset
let environment = ViewControllerComponentContainer.Environment(
statusBarHeight: layout.statusBarHeight ?? 0.0,
navigationHeight: 0.0,
safeInsets: UIEdgeInsets(
top: topInset,
left: layout.safeInsets.left,
bottom: bottomInset,
right: layout.safeInsets.right
),
inputHeight: layout.inputHeight ?? 0.0,
metrics: layout.metrics,
deviceMetrics: layout.deviceMetrics,
orientation: nil,
isVisible: true,
theme: self.presentationData.theme,
strings: self.presentationData.strings,
dateTimeFormat: self.presentationData.dateTimeFormat,
controller: { [weak self] in
return self?.controller
}
)
let componentSize = self.componentHost.update(
transition: transition,
component: AnyComponent(
MediaCutoutScreenComponent(
context: self.context,
mediaEditor: controller.mediaEditor
)
),
environment: {
environment
},
forceUpdate: forceUpdate || animateOut,
containerSize: layout.size
)
if let componentView = self.componentHost.view {
if componentView.superview == nil {
self.view.insertSubview(componentView, at: 3)
componentView.clipsToBounds = true
}
let componentFrame = CGRect(origin: .zero, size: componentSize)
transition.setFrame(view: componentView, frame: CGRect(origin: componentFrame.origin, size: CGSize(width: componentFrame.width, height: componentFrame.height)))
}
if isFirstTime {
self.animateInFromEditor()
}
}
}
fileprivate var node: Node {
return self.displayNode as! Node
}
fileprivate let context: AccountContext
fileprivate let mediaEditor: MediaEditor
public var dismissed: () -> Void = {}
private var initialValues: MediaEditorValues
public init(context: AccountContext, mediaEditor: MediaEditor) {
self.context = context
self.mediaEditor = mediaEditor
self.initialValues = mediaEditor.values.makeCopy()
super.init(navigationBarPresentationData: nil)
self.navigationPresentation = .flatModal
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
self.statusBar.statusBarStyle = .White
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func loadDisplayNode() {
self.displayNode = Node(controller: self)
super.displayNodeDidLoad()
}
func requestDismiss(reset: Bool, animated: Bool) {
if reset {
self.mediaEditor.values = self.initialValues
}
self.dismissed()
self.node.animateOutToEditor(completion: {
self.dismiss()
})
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
(self.displayNode as! Node).containerLayoutUpdated(layout: layout, transition: Transition(transition))
}
}

View File

@ -3074,6 +3074,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}, openPremiumStatusInfo: { _, _, _, _ in
}, openRecommendedChannelContextMenu: { _, _, _ in
}, openGroupBoostInfo: { _, _ in
}, openStickerEditor: {
}, requestMessageUpdate: { _, _ in
}, cancelInteractiveKeyboardGestures: {
}, dismissTextInput: {
@ -8795,7 +8796,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}
})
case .chatFolders:
let controller = self.context.sharedContext.makeFilterSettingsController(context: self.context, modal: false, dismissed: nil)
let controller = self.context.sharedContext.makeFilterSettingsController(context: self.context, modal: false, scrollToTags: false, dismissed: nil)
push(controller)
case .notificationsAndSounds:
if let settings = self.data?.globalSettings {

View File

@ -295,7 +295,23 @@ final class PeerInfoStoryGridScreenComponent: Component {
let _ = paneNode.scrollToTop()
}
func openCreateStory() {
guard let component = self.component else {
return
}
if let rootController = component.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface {
let coordinator = rootController.openStoryCamera(customTarget: nil, transitionIn: nil, transitionedIn: {}, transitionOut: { _, _ in return nil })
coordinator?.animateIn()
}
}
private var isUpdating = false
func update(component: PeerInfoStoryGridScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
self.component = component
self.state = state
@ -313,7 +329,7 @@ final class PeerInfoStoryGridScreenComponent: Component {
var bottomInset: CGFloat = environment.safeInsets.bottom
if self.selectedCount != 0 {
if self.selectedCount != 0 || (component.scope == .saved && self.paneNode?.isEmpty == false) {
let selectionPanel: ComponentView<Empty>
var selectionPanelTransition = transition
if let current = self.selectionPanel {
@ -327,7 +343,7 @@ final class PeerInfoStoryGridScreenComponent: Component {
let buttonText: String
switch component.scope {
case .saved:
buttonText = environment.strings.ChatList_Context_Archive
buttonText = self.selectedCount > 0 ? environment.strings.ChatList_Context_Archive : environment.strings.StoryList_SavedAddAction
case .archive:
buttonText = environment.strings.StoryList_SaveToProfile
}
@ -344,7 +360,7 @@ final class PeerInfoStoryGridScreenComponent: Component {
guard let self, let component = self.component, let environment = self.environment else {
return
}
guard let paneNode = self.paneNode, !paneNode.selectedIds.isEmpty else {
guard let paneNode = self.paneNode else {
return
}
@ -361,21 +377,25 @@ final class PeerInfoStoryGridScreenComponent: Component {
switch component.scope {
case .saved:
let selectedCount = paneNode.selectedItems.count
let _ = component.context.engine.messages.updateStoriesArePinned(peerId: component.peerId, ids: paneNode.selectedItems, isPinned: false).start()
paneNode.setIsSelectionModeActive(false)
(self.environment?.controller() as? PeerInfoStoryGridScreen)?.updateTitle()
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme)
let title: String = presentationData.strings.StoryList_TooltipStoriesSavedToProfile(Int32(selectedCount))
environment.controller()?.present(UndoOverlayController(
presentationData: presentationData,
content: .info(title: nil, text: title, timeout: nil, customUndoText: nil),
elevatedLayout: false,
animateInAsReplacement: false,
action: { _ in return false }
), in: .current)
if selectedCount == 0 {
self.openCreateStory()
} else {
let _ = component.context.engine.messages.updateStoriesArePinned(peerId: component.peerId, ids: paneNode.selectedItems, isPinned: false).start()
paneNode.setIsSelectionModeActive(false)
(self.environment?.controller() as? PeerInfoStoryGridScreen)?.updateTitle()
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme)
let title: String = presentationData.strings.StoryList_TooltipStoriesSavedToProfile(Int32(selectedCount))
environment.controller()?.present(UndoOverlayController(
presentationData: presentationData,
content: .info(title: nil, text: title, timeout: nil, customUndoText: nil),
elevatedLayout: false,
animateInAsReplacement: false,
action: { _ in return false }
), in: .current)
}
case .archive:
let _ = component.context.engine.messages.updateStoriesArePinned(peerId: component.peerId, ids: paneNode.selectedItems, isPinned: true).start()
@ -449,10 +469,28 @@ final class PeerInfoStoryGridScreenComponent: Component {
},
listContext: nil
)
paneNode.isEmptyUpdated = { [weak self] _ in
guard let self else {
return
}
if !self.isUpdating {
self.state?.updated(transition: .immediate)
}
}
self.paneNode = paneNode
self.addSubview(paneNode.view)
if let selectionPanelView = self.selectionPanel?.view {
self.insertSubview(paneNode.view, belowSubview: selectionPanelView)
} else {
self.addSubview(paneNode.view)
}
paneNode.emptyAction = { [weak self] in
guard let self else {
return
}
self.openCreateStory()
}
paneNode.additionalEmptyAction = { [weak self] in
guard let self, let component = self.component else {
return
}

View File

@ -948,6 +948,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
}
}
public var isEmptyUpdated: (Bool) -> Void = { _ in }
public private(set) var isSelectionModeActive: Bool
private var currentParams: (size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, navigationHeight: CGFloat, presentationData: PresentationData)?
@ -985,6 +987,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
public var openCurrentDate: (() -> Void)?
public var paneDidScroll: (() -> Void)?
public var emptyAction: (() -> Void)?
public var additionalEmptyAction: (() -> Void)?
public var ensureRectVisible: ((UIView, CGRect) -> Void)?
@ -1729,6 +1732,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
private func updateHistory(items: SparseItemGrid.Items, synchronous: Bool, reloadAtTop: Bool) {
self.items = items
self.isEmptyUpdated(self.isEmpty)
if let (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, navigationHeight, presentationData) = self.currentParams {
var gridSnapshot: UIView?
@ -2027,14 +2031,21 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
context: self.context,
theme: presentationData.theme,
animationName: "StoryListEmpty",
title: self.isArchive ? presentationData.strings.StoryList_ArchivedEmptyState_Title : presentationData.strings.StoryList_SavedEmptyState_Title,
text: self.isArchive ? presentationData.strings.StoryList_ArchivedEmptyState_Text : presentationData.strings.StoryList_SavedEmptyState_Text,
actionTitle: self.isArchive ? nil : presentationData.strings.StoryList_SavedEmptyAction,
title: self.isArchive ? presentationData.strings.StoryList_ArchivedEmptyState_Title : presentationData.strings.StoryList_SavedEmptyPosts_Title,
text: self.isArchive ? presentationData.strings.StoryList_ArchivedEmptyState_Text : presentationData.strings.StoryList_SavedEmptyPosts_Text,
actionTitle: self.isArchive ? nil : presentationData.strings.StoryList_SavedAddAction,
action: { [weak self] in
guard let self else {
return
}
self.emptyAction?()
},
additionalActionTitle: self.isArchive ? nil : presentationData.strings.StoryList_SavedEmptyAction,
additionalAction: { [weak self] in
guard let self else {
return
}
self.additionalEmptyAction?()
}
)),
environment: {},

View File

@ -134,6 +134,9 @@ final class StoryAuthorInfoComponent: Component {
if timeString.count < 6 {
combinedString.append(NSAttributedString(string: "\(timeString)", font: Font.regular(11.0), textColor: subtitleColor))
}
if component.isEdited {
combinedString.append(NSAttributedString(string: "\(component.strings.Story_HeaderEdited)", font: Font.regular(11.0), textColor: subtitleColor))
}
subtitle = combinedString
subtitleTruncationType = .middle
} else {

View File

@ -5420,6 +5420,7 @@ public final class StoryItemSetContainerComponent: Component {
var updateProgressImpl: ((Float) -> Void)?
let controller = MediaEditorScreen(
context: context,
mode: .storyEditor,
subject: subject,
isEditing: !repost,
forwardSource: repost ? (component.slice.peer, item) : nil,

View File

@ -179,6 +179,7 @@ final class StoryItemSetContainerSendMessage {
self.inputPanelExternalState?.deleteBackward()
}
},
openStickerEditor: {},
presentController: { [weak self] c, a in
if let self {
self.view?.component?.controller()?.present(c, in: .window(.root), with: a)

View File

@ -1,7 +1,7 @@
{
"images" : [
{
"filename" : "check.pdf",
"filename" : "arrowhead_30.pdf",
"idiom" : "universal"
}
],

View File

@ -0,0 +1,93 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 10.000000 9.771912 cm
1.000000 1.000000 1.000000 scn
0.742462 6.970551 m
0.332412 7.380601 -0.332412 7.380601 -0.742462 6.970551 c
-1.152513 6.560500 -1.152513 5.895677 -0.742462 5.485626 c
0.742462 6.970551 l
h
4.000000 2.228088 m
3.257538 1.485626 l
3.462021 1.281143 3.741785 1.170047 4.030842 1.178541 c
4.319900 1.187036 4.592658 1.314369 4.784780 1.530506 c
4.000000 2.228088 l
h
12.784780 10.530506 m
13.170044 10.963928 13.131004 11.627604 12.697582 12.012868 c
12.264160 12.398132 11.600484 12.359093 11.215220 11.925671 c
12.784780 10.530506 l
h
-0.742462 5.485626 m
3.257538 1.485626 l
4.742462 2.970551 l
0.742462 6.970551 l
-0.742462 5.485626 l
h
4.784780 1.530506 m
12.784780 10.530506 l
11.215220 11.925671 l
3.215220 2.925671 l
4.784780 1.530506 l
h
f
n
Q
endstream
endobj
3 0 obj
839
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 33.000000 33.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000000929 00000 n
0000000951 00000 n
0000001124 00000 n
0000001198 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1257
%%EOF

View File

@ -1,150 +0,0 @@
%PDF-1.7
1 0 obj
<< /Type /XObject
/Length 2 0 R
/Group << /Type /Group
/S /Transparency
>>
/Subtype /Form
/Resources << >>
/BBox [ 0.000000 0.000000 40.000000 40.000000 ]
>>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 11.669922 11.450928 cm
0.000000 0.000000 0.000000 scn
16.557842 15.662290 m
17.172628 15.260314 17.345146 14.436065 16.943171 13.821279 c
8.443170 0.821279 l
8.223975 0.486040 7.865371 0.267438 7.466962 0.226191 c
7.068553 0.184944 6.672771 0.325444 6.389548 0.608668 c
0.389548 6.608668 l
-0.129849 7.128065 -0.129849 7.970175 0.389548 8.489573 c
0.908945 9.008969 1.751055 9.008969 2.270452 8.489573 c
7.112782 3.647242 l
14.716830 15.276961 l
15.118806 15.891748 15.943055 16.064266 16.557842 15.662290 c
h
f*
n
Q
endstream
endobj
2 0 obj
584
endobj
3 0 obj
<< /Type /XObject
/Length 4 0 R
/Group << /Type /Group
/S /Transparency
>>
/Subtype /Form
/Resources << >>
/BBox [ 0.000000 0.000000 40.000000 40.000000 ]
>>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm
0.000000 0.000000 0.000000 scn
0.000000 20.000000 m
0.000000 31.045694 8.954306 40.000000 20.000000 40.000000 c
20.000000 40.000000 l
31.045694 40.000000 40.000000 31.045694 40.000000 20.000000 c
40.000000 20.000000 l
40.000000 8.954306 31.045694 0.000000 20.000000 0.000000 c
20.000000 0.000000 l
8.954306 0.000000 0.000000 8.954306 0.000000 20.000000 c
0.000000 20.000000 l
h
f
n
Q
endstream
endobj
4 0 obj
472
endobj
5 0 obj
<< /XObject << /X1 1 0 R >>
/ExtGState << /E1 << /SMask << /Type /Mask
/G 3 0 R
/S /Alpha
>>
/Type /ExtGState
>> >>
>>
endobj
6 0 obj
<< /Length 7 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
/E1 gs
/X1 Do
Q
endstream
endobj
7 0 obj
46
endobj
8 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 40.000000 40.000000 ]
/Resources 5 0 R
/Contents 6 0 R
/Parent 9 0 R
>>
endobj
9 0 obj
<< /Kids [ 8 0 R ]
/Count 1
/Type /Pages
>>
endobj
10 0 obj
<< /Pages 9 0 R
/Type /Catalog
>>
endobj
xref
0 11
0000000000 65535 f
0000000010 00000 n
0000000842 00000 n
0000000864 00000 n
0000001584 00000 n
0000001606 00000 n
0000001904 00000 n
0000002006 00000 n
0000002027 00000 n
0000002200 00000 n
0000002274 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 10 0 R
/Size 11
>>
startxref
2334
%%EOF

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "magic_30.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,149 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 2.694702 2.078613 cm
0.000000 0.000000 0.000000 scn
9.817018 17.062620 m
9.747100 17.099379 9.663571 17.099379 9.593654 17.062620 c
8.489970 16.482380 l
8.313900 16.389814 8.108116 16.539326 8.141742 16.735382 c
8.352527 17.964352 l
8.365880 18.042206 8.340069 18.121647 8.283504 18.176785 c
7.390605 19.047144 l
7.248161 19.185993 7.326764 19.427906 7.523617 19.456511 c
8.757572 19.635815 l
8.835742 19.647175 8.903318 19.696270 8.938277 19.767105 c
9.490119 20.885258 l
9.578154 21.063637 9.832517 21.063639 9.920552 20.885260 c
10.472394 19.767105 l
10.507353 19.696270 10.574928 19.647175 10.653099 19.635815 c
11.887054 19.456511 l
12.083907 19.427906 12.162510 19.185993 12.020066 19.047144 c
11.127168 18.176785 l
11.070602 18.121647 11.044791 18.042206 11.058144 17.964352 c
11.268929 16.735382 l
11.302555 16.539326 11.096771 16.389814 10.920701 16.482380 c
9.817018 17.062620 l
h
1.785109 15.879396 m
1.735167 15.905652 1.675504 15.905652 1.625563 15.879396 c
0.837217 15.464938 l
0.711453 15.398820 0.564465 15.505613 0.588483 15.645655 c
0.739044 16.523491 l
0.748582 16.579102 0.730145 16.635843 0.689742 16.675226 c
0.051957 17.296913 l
-0.049789 17.396091 0.006356 17.568886 0.146965 17.589317 c
1.028362 17.717392 l
1.084198 17.725506 1.132466 17.760574 1.157437 17.811171 c
1.551610 18.609852 l
1.614492 18.737267 1.796180 18.737267 1.859062 18.609852 c
2.253235 17.811171 l
2.278205 17.760574 2.326474 17.725506 2.382310 17.717392 c
3.263707 17.589317 l
3.404316 17.568886 3.460460 17.396091 3.358715 17.296913 c
2.720930 16.675226 l
2.680526 16.635843 2.662090 16.579100 2.671628 16.523491 c
2.822188 15.645655 l
2.846207 15.505613 2.699219 15.398820 2.573454 15.464939 c
1.785109 15.879396 l
h
3.225563 7.079397 m
3.275504 7.105653 3.335167 7.105653 3.385108 7.079397 c
4.173454 6.664939 l
4.299219 6.598821 4.446208 6.705614 4.422188 6.845654 c
4.271628 7.723491 l
4.262090 7.779101 4.280527 7.835844 4.320930 7.875227 c
4.958715 8.496914 l
5.060461 8.596091 5.004316 8.768887 4.863707 8.789319 c
3.982310 8.917393 l
3.926474 8.925507 3.878206 8.960575 3.853235 9.011171 c
3.459062 9.809853 l
3.396180 9.937266 3.214492 9.937266 3.151610 9.809853 c
2.757437 9.011171 l
2.732466 8.960575 2.684198 8.925507 2.628362 8.917393 c
1.746965 8.789319 l
1.606356 8.768887 1.550211 8.596092 1.651957 8.496914 c
2.289742 7.875228 l
2.330145 7.835844 2.348582 7.779101 2.339044 7.723491 c
2.188483 6.845655 l
2.164464 6.705614 2.311453 6.598821 2.437217 6.664939 c
3.225563 7.079397 l
h
5.069424 14.825876 m
5.641542 15.397993 6.569129 15.397994 7.141247 14.825876 c
18.809877 3.157246 l
19.381996 2.585127 19.381994 1.657541 18.809877 1.085423 c
18.341248 0.616795 l
17.769131 0.044676 16.841543 0.044676 16.269424 0.616795 c
4.600795 12.285424 l
4.028677 12.857543 4.028677 13.785130 4.600795 14.357246 c
5.069424 14.825876 l
h
8.835109 11.251109 m
8.175561 10.591561 l
17.209877 1.557245 l
17.262598 1.504526 17.348076 1.504526 17.400797 1.557245 c
17.869425 2.025875 l
17.922146 2.078596 17.922146 2.164074 17.869425 2.216793 c
8.835109 11.251109 l
h
f*
n
Q
endstream
endobj
3 0 obj
3139
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000003229 00000 n
0000003252 00000 n
0000003425 00000 n
0000003499 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
3558
%%EOF

View File

@ -4584,6 +4584,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
)
self.push(boostController)
})
}, openStickerEditor: { [weak self] in
guard let self else {
return
}
self.openStickerEditor()
}, requestMessageUpdate: { [weak self] id, scroll in
if let self {
self.chatDisplayNode.historyNode.requestMessageUpdate(id, andScrollToItem: scroll)
@ -16857,6 +16862,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let controller = MediaEditorScreen(
context: context,
mode: .storyEditor,
subject: subject,
transitionIn: nil,
transitionOut: { _, _ in

View File

@ -30,6 +30,7 @@ import PremiumUI
import PremiumGiftAttachmentScreen
import TelegramCallsUI
import AutomaticBusinessMessageSetupScreen
import MediaEditorScreen
extension ChatControllerImpl {
enum AttachMenuSubject {
@ -1698,4 +1699,71 @@ extension ChatControllerImpl {
})
})
}
func openStickerEditor() {
let mainController = AttachmentController(context: self.context, updatedPresentationData: self.updatedPresentationData, chatLocation: nil, buttons: [.standalone], initialButton: .standalone, fromMenu: false, hasTextInput: false, makeEntityInputView: {
return nil
})
// controller.forceSourceRect = true
// controller.getSourceRect = getSourceRect
mainController.requestController = { [weak self, weak mainController] _, present in
guard let self else {
return
}
let mediaPickerController = MediaPickerScreen(context: self.context, updatedPresentationData: self.updatedPresentationData, peer: nil, threadTitle: nil, chatLocation: nil, subject: .assets(nil, .createSticker))
mediaPickerController.customSelection = { [weak self, weak mainController] controller, result in
guard let self else {
return
}
if let result = result as? PHAsset {
controller.updateHiddenMediaId(result.localIdentifier)
if let transitionView = controller.transitionView(for: result.localIdentifier, snapshot: false) {
let editorController = MediaEditorScreen(
context: self.context,
mode: .stickerEditor,
subject: .single(.asset(result)),
transitionIn: .gallery(
MediaEditorScreen.TransitionIn.GalleryTransitionIn(
sourceView: transitionView,
sourceRect: transitionView.bounds,
sourceImage: controller.transitionImage(for: result.localIdentifier)
)
),
transitionOut: { finished, isNew in
if !finished {
return MediaEditorScreen.TransitionOut(
destinationView: transitionView,
destinationRect: transitionView.bounds,
destinationCornerRadius: 0.0
)
}
return nil
}, completion: { [weak self, weak mainController] result, commit in
mainController?.dismiss()
Queue.mainQueue().after(0.1) {
commit({})
if let mediaResult = result.media, case let .image(image, _) = mediaResult {
self?.enqueueStickerImage(image, isMemoji: false)
}
}
} as (MediaEditorScreen.Result, @escaping (@escaping () -> Void) -> Void) -> Void
)
editorController.dismissed = { [weak controller] in
controller?.updateHiddenMediaId(nil)
}
self.push(editorController)
// completion(result, transitionView, transitionView.bounds, controller.transitionImage(for: result.localIdentifier), transitionOut, { [weak controller] in
// controller?.updateHiddenMediaId(nil)
// })
}
}
}
present(mediaPickerController, mediaPickerController.mediaPickerContext)
}
mainController.navigationPresentation = .flatModal
mainController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
self.push(mainController)
}
}

View File

@ -173,6 +173,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
}, openPremiumStatusInfo: { _, _, _, _ in
}, openRecommendedChannelContextMenu: { _, _, _ in
}, openGroupBoostInfo: { _, _ in
}, openStickerEditor: {
}, requestMessageUpdate: { _, _ in
}, cancelInteractiveKeyboardGestures: {
}, dismissTextInput: {

View File

@ -1754,6 +1754,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
}, openPremiumStatusInfo: { _, _, _, _ in
}, openRecommendedChannelContextMenu: { _, _, _ in
}, openGroupBoostInfo: { _, _ in
}, openStickerEditor: {
}, requestMessageUpdate: { _, _ in
}, cancelInteractiveKeyboardGestures: {
}, dismissTextInput: {
@ -1882,8 +1883,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return archiveSettingsController(context: context)
}
public func makeFilterSettingsController(context: AccountContext, modal: Bool, dismissed: (() -> Void)?) -> ViewController {
return chatListFilterPresetListController(context: context, mode: modal ? .modal : .default, dismissed: dismissed)
public func makeFilterSettingsController(context: AccountContext, modal: Bool, scrollToTags: Bool, dismissed: (() -> Void)?) -> ViewController {
return chatListFilterPresetListController(context: context, mode: modal ? .modal : .default, scrollToTags: scrollToTags, dismissed: dismissed)
}
public func makeBusinessSetupScreen(context: AccountContext) -> ViewController {
@ -2048,6 +2049,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
mappedSubject = .lastSeen
case .messagePrivacy:
mappedSubject = .messagePrivacy
default:
mappedSubject = .doubleLimits
}
return PremiumDemoScreen(context: context, subject: mappedSubject, action: action)
}

View File

@ -28,6 +28,8 @@ import ImageCompression
import TextFormat
import MediaEditor
import PeerInfoScreen
import PeerInfoStoryGridScreen
import ShareWithPeersScreen
private class DetailsChatPlaceholderNode: ASDisplayNode, NavigationDetailsPlaceholderNode {
private var presentationData: PresentationData
@ -354,6 +356,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
let controller = MediaEditorScreen(
context: context,
mode: .storyEditor,
subject: subject,
customTarget: customTarget,
transitionIn: transitionIn,
@ -509,6 +512,19 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
viewControllers.removeSubrange(range)
self.setViewControllers(viewControllers, animated: false)
}
} else if self.viewControllers.contains(where: { $0 is PeerInfoStoryGridScreen }) {
var viewControllers: [UIViewController] = []
for i in (0 ..< self.viewControllers.count) {
let controller = self.viewControllers[i]
if i == 0 {
viewControllers.append(controller)
} else if controller is MediaEditorScreen {
viewControllers.append(controller)
} else if controller is ShareWithPeersScreen {
viewControllers.append(controller)
}
}
self.setViewControllers(viewControllers, animated: false)
}
}