mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Story location picking
This commit is contained in:
parent
b3146436dc
commit
b70586eb28
@ -9555,7 +9555,7 @@ Sorry for the inconvenience.";
|
|||||||
"Story.PrivacyTooltipContacts" = "This story is shown to all your contacts.";
|
"Story.PrivacyTooltipContacts" = "This story is shown to all your contacts.";
|
||||||
"Story.PrivacyTooltipCloseFriends" = "This story is shown to your close friends.";
|
"Story.PrivacyTooltipCloseFriends" = "This story is shown to your close friends.";
|
||||||
"Story.PrivacyTooltipSelectedContacts" = "This story is shown to selected contacts.";
|
"Story.PrivacyTooltipSelectedContacts" = "This story is shown to selected contacts.";
|
||||||
"Story.PrivacyTooltipSelectedContactsCount" = "This story is now shown to %@ contacts.";
|
"Story.PrivacyTooltipSelectedContactsCount" = "This story is now shown to %@.";
|
||||||
"Story.PrivacyTooltipNobody" = "This story is shown only to you.";
|
"Story.PrivacyTooltipNobody" = "This story is shown only to you.";
|
||||||
"Story.PrivacyTooltipEveryone" = "This story is shown to everyone.";
|
"Story.PrivacyTooltipEveryone" = "This story is shown to everyone.";
|
||||||
|
|
||||||
@ -9735,3 +9735,15 @@ Sorry for the inconvenience.";
|
|||||||
"Story.TooltipPrivacyCloseFriends2" = "You are seeing this story because **%@** added you to their list of Close Friends.";
|
"Story.TooltipPrivacyCloseFriends2" = "You are seeing this story because **%@** added you to their list of Close Friends.";
|
||||||
|
|
||||||
"Story.Editor.VideoTooShort" = "A video must be at least 1 second long.";
|
"Story.Editor.VideoTooShort" = "A video must be at least 1 second long.";
|
||||||
|
|
||||||
|
"Story.PrivacyTooltipSelectedContacts.Contacts_1" = "1 contact";
|
||||||
|
"Story.PrivacyTooltipSelectedContacts.Contacts_any" = "%@ contacts";
|
||||||
|
|
||||||
|
"Story.Privacy.GrayListSelect" = "[Select people]() who will never see your stories.";
|
||||||
|
"Story.Privacy.GrayListSelected" = "[%@]() will never see your stories.";
|
||||||
|
"Story.Privacy.GrayListPeople_1" = "1 person";
|
||||||
|
"Story.Privacy.GrayListPeople_any" = "%@ people";
|
||||||
|
|
||||||
|
"Story.Privacy.HideMyStoriesFrom" = "Hide My Stories From";
|
||||||
|
|
||||||
|
"Story.Privacy.SaveList" = "Save List";
|
||||||
|
@ -1089,28 +1089,50 @@ public struct StoriesConfiguration {
|
|||||||
case disabled
|
case disabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum CaptionEntitiesAvailability {
|
||||||
|
case enabled
|
||||||
|
case premium
|
||||||
|
}
|
||||||
|
|
||||||
static var defaultValue: StoriesConfiguration {
|
static var defaultValue: StoriesConfiguration {
|
||||||
return StoriesConfiguration(posting: .disabled)
|
return StoriesConfiguration(posting: .disabled, captionEntities: .premium)
|
||||||
}
|
}
|
||||||
|
|
||||||
public let posting: PostingAvailability
|
public let posting: PostingAvailability
|
||||||
|
public let captionEntities: CaptionEntitiesAvailability
|
||||||
|
|
||||||
fileprivate init(posting: PostingAvailability) {
|
fileprivate init(posting: PostingAvailability, captionEntities: CaptionEntitiesAvailability) {
|
||||||
self.posting = posting
|
self.posting = posting
|
||||||
|
self.captionEntities = captionEntities
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func with(appConfiguration: AppConfiguration) -> StoriesConfiguration {
|
public static func with(appConfiguration: AppConfiguration) -> StoriesConfiguration {
|
||||||
if let data = appConfiguration.data, let postingString = data["stories_posting"] as? String {
|
if let data = appConfiguration.data {
|
||||||
var posting: PostingAvailability
|
let posting: PostingAvailability
|
||||||
switch postingString {
|
let captionEntities: CaptionEntitiesAvailability
|
||||||
case "enabled":
|
if let postingString = data["stories_posting"] as? String {
|
||||||
posting = .enabled
|
switch postingString {
|
||||||
case "premium":
|
case "enabled":
|
||||||
posting = .premium
|
posting = .enabled
|
||||||
default:
|
case "premium":
|
||||||
|
posting = .premium
|
||||||
|
default:
|
||||||
|
posting = .disabled
|
||||||
|
}
|
||||||
|
} else {
|
||||||
posting = .disabled
|
posting = .disabled
|
||||||
}
|
}
|
||||||
return StoriesConfiguration(posting: posting)
|
if let entitiesString = data["stories_entities"] as? String {
|
||||||
|
switch entitiesString {
|
||||||
|
case "enabled":
|
||||||
|
captionEntities = .enabled
|
||||||
|
default:
|
||||||
|
captionEntities = .premium
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
captionEntities = .premium
|
||||||
|
}
|
||||||
|
return StoriesConfiguration(posting: posting, captionEntities: captionEntities)
|
||||||
} else {
|
} else {
|
||||||
return .defaultValue
|
return .defaultValue
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,8 @@ private func makeEntityView(context: AccountContext, entity: DrawingEntity) -> D
|
|||||||
return DrawingVectorEntityView(context: context, entity: entity)
|
return DrawingVectorEntityView(context: context, entity: entity)
|
||||||
} else if let entity = entity as? DrawingMediaEntity {
|
} else if let entity = entity as? DrawingMediaEntity {
|
||||||
return DrawingMediaEntityView(context: context, entity: entity)
|
return DrawingMediaEntityView(context: context, entity: entity)
|
||||||
|
} else if let entity = entity as? DrawingLocationEntity {
|
||||||
|
return DrawingLocationEntityView(context: context, entity: entity)
|
||||||
} else {
|
} else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -47,6 +49,9 @@ private func prepareForRendering(entityView: DrawingEntityView) {
|
|||||||
if let entityView = entityView as? DrawingVectorEntityView {
|
if let entityView = entityView as? DrawingVectorEntityView {
|
||||||
entityView.entity.renderImage = entityView.getRenderImage()
|
entityView.entity.renderImage = entityView.getRenderImage()
|
||||||
}
|
}
|
||||||
|
if let entityView = entityView as? DrawingLocationEntityView {
|
||||||
|
entityView.entity.renderImage = entityView.getRenderImage()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
||||||
@ -347,6 +352,14 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
|||||||
text.fontSize = 0.08
|
text.fontSize = 0.08
|
||||||
text.scale = zoomScale
|
text.scale = zoomScale
|
||||||
}
|
}
|
||||||
|
} else if let location = entity as? DrawingLocationEntity {
|
||||||
|
location.position = center
|
||||||
|
if setup {
|
||||||
|
location.rotation = rotation
|
||||||
|
location.referenceDrawingSize = self.size
|
||||||
|
location.width = floor(self.size.width * 0.9)
|
||||||
|
location.scale = zoomScale
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -653,7 +666,9 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
|||||||
selectionView.tapped = { [weak self, weak entityView] in
|
selectionView.tapped = { [weak self, weak entityView] in
|
||||||
if let self, let entityView {
|
if let self, let entityView {
|
||||||
let entityViews = self.subviews.filter { $0 is DrawingEntityView }
|
let entityViews = self.subviews.filter { $0 is DrawingEntityView }
|
||||||
self.requestedMenuForEntityView(entityView, entityViews.last === entityView)
|
if !entityView.selectedTapAction() {
|
||||||
|
self.requestedMenuForEntityView(entityView, entityViews.last === entityView)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
entityView.selectionView = selectionView
|
entityView.selectionView = selectionView
|
||||||
@ -912,6 +927,10 @@ public class DrawingEntityView: UIView {
|
|||||||
self.layer.animateKeyframes(values: values as [NSNumber], keyTimes: keyTimes as [NSNumber], duration: 0.3, keyPath: "transform.scale")
|
self.layer.animateKeyframes(values: values as [NSNumber], keyTimes: keyTimes as [NSNumber], duration: 0.3, keyPath: "transform.scale")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func selectedTapAction() -> Bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
public func play() {
|
public func play() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
499
submodules/DrawingUI/Sources/DrawingLocationEntity.swift
Normal file
499
submodules/DrawingUI/Sources/DrawingLocationEntity.swift
Normal file
@ -0,0 +1,499 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Display
|
||||||
|
import SwiftSignalKit
|
||||||
|
import AccountContext
|
||||||
|
import MediaEditor
|
||||||
|
|
||||||
|
private func generateIcon(style: DrawingLocationEntity.Style) -> UIImage? {
|
||||||
|
guard let image = UIImage(bundleImageName: "Chat/Attach Menu/Location") else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return generateImage(image.size, contextGenerator: { size, context in
|
||||||
|
context.clear(CGRect(origin: .zero, size: size))
|
||||||
|
|
||||||
|
if let cgImage = image.cgImage {
|
||||||
|
context.clip(to: CGRect(origin: .zero, size: size), mask: cgImage)
|
||||||
|
}
|
||||||
|
if [.black, .white].contains(style) {
|
||||||
|
let green: UIColor
|
||||||
|
let blue: UIColor
|
||||||
|
|
||||||
|
if case .black = style {
|
||||||
|
green = UIColor(rgb: 0x39e69a)
|
||||||
|
blue = UIColor(rgb: 0x1c9ae0)
|
||||||
|
} else {
|
||||||
|
green = UIColor(rgb: 0x1eb67a)
|
||||||
|
blue = UIColor(rgb: 0x1d9ae2)
|
||||||
|
}
|
||||||
|
|
||||||
|
var locations: [CGFloat] = [0.0, 1.0]
|
||||||
|
let colorsArray = [green.cgColor, blue.cgColor] as NSArray
|
||||||
|
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||||
|
let gradient = CGGradient(colorsSpace: colorSpace, colors: colorsArray, locations: &locations)!
|
||||||
|
|
||||||
|
context.drawLinearGradient(gradient, start: CGPoint(x: size.width, y: 0.0), end: CGPoint(x: 0.0, y: 0.0), options: CGGradientDrawingOptions())
|
||||||
|
} else {
|
||||||
|
context.setFillColor(UIColor.white.cgColor)
|
||||||
|
context.fill(CGRect(origin: .zero, size: size))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class DrawingLocationEntityView: DrawingEntityView, UITextViewDelegate {
|
||||||
|
private var locationEntity: DrawingLocationEntity {
|
||||||
|
return self.entity as! DrawingLocationEntity
|
||||||
|
}
|
||||||
|
|
||||||
|
let backgroundView: UIView
|
||||||
|
let textView: DrawingTextView
|
||||||
|
let iconView: UIImageView
|
||||||
|
|
||||||
|
init(context: AccountContext, entity: DrawingLocationEntity) {
|
||||||
|
self.backgroundView = UIView()
|
||||||
|
self.backgroundView.clipsToBounds = true
|
||||||
|
|
||||||
|
self.textView = DrawingTextView(frame: .zero)
|
||||||
|
self.textView.clipsToBounds = false
|
||||||
|
|
||||||
|
self.textView.backgroundColor = .clear
|
||||||
|
self.textView.isEditable = false
|
||||||
|
self.textView.isSelectable = false
|
||||||
|
self.textView.contentInset = .zero
|
||||||
|
self.textView.showsHorizontalScrollIndicator = false
|
||||||
|
self.textView.showsVerticalScrollIndicator = false
|
||||||
|
self.textView.scrollsToTop = false
|
||||||
|
self.textView.isScrollEnabled = false
|
||||||
|
self.textView.textContainerInset = .zero
|
||||||
|
self.textView.minimumZoomScale = 1.0
|
||||||
|
self.textView.maximumZoomScale = 1.0
|
||||||
|
self.textView.keyboardAppearance = .dark
|
||||||
|
self.textView.autocorrectionType = .default
|
||||||
|
self.textView.spellCheckingType = .no
|
||||||
|
|
||||||
|
self.iconView = UIImageView()
|
||||||
|
|
||||||
|
super.init(context: context, entity: entity)
|
||||||
|
|
||||||
|
self.textView.delegate = self
|
||||||
|
self.addSubview(self.backgroundView)
|
||||||
|
self.addSubview(self.textView)
|
||||||
|
self.addSubview(self.iconView)
|
||||||
|
|
||||||
|
self.update(animated: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
private var textSize: CGSize = .zero
|
||||||
|
public override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||||
|
self.textView.setNeedsLayersUpdate()
|
||||||
|
var result = self.textView.sizeThatFits(CGSize(width: self.locationEntity.width, height: .greatestFiniteMagnitude))
|
||||||
|
self.textSize = result
|
||||||
|
result.width = floorToScreenPixels(max(224.0, ceil(result.width) + 20.0) + result.height * 0.5)
|
||||||
|
result.height = ceil(result.height * 1.2);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override func sizeToFit() {
|
||||||
|
let center = self.center
|
||||||
|
let transform = self.transform
|
||||||
|
self.transform = .identity
|
||||||
|
super.sizeToFit()
|
||||||
|
self.center = center
|
||||||
|
self.transform = transform
|
||||||
|
}
|
||||||
|
|
||||||
|
public override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
let iconSize = floor(self.bounds.height * 0.6)
|
||||||
|
self.iconView.frame = CGRect(origin: CGPoint(x: floor(iconSize * 0.2), y: floorToScreenPixels((self.bounds.height - iconSize) / 2.0)), size: CGSize(width: iconSize, height: iconSize))
|
||||||
|
self.textView.frame = CGRect(origin: CGPoint(x: self.bounds.width - self.textSize.width, y: floorToScreenPixels((self.bounds.height - self.textSize.height) / 2.0)), size: self.textSize)
|
||||||
|
self.backgroundView.frame = self.bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
override func selectedTapAction() -> Bool {
|
||||||
|
let values = [self.entity.scale, self.entity.scale * 0.93, self.entity.scale]
|
||||||
|
let keyTimes = [0.0, 0.33, 1.0]
|
||||||
|
self.layer.animateKeyframes(values: values as [NSNumber], keyTimes: keyTimes as [NSNumber], duration: 0.3, keyPath: "transform.scale")
|
||||||
|
|
||||||
|
let updatedStyle: DrawingLocationEntity.Style
|
||||||
|
switch self.locationEntity.style {
|
||||||
|
case .white:
|
||||||
|
updatedStyle = .black
|
||||||
|
case .black:
|
||||||
|
updatedStyle = .transparent
|
||||||
|
case .transparent:
|
||||||
|
updatedStyle = .white
|
||||||
|
case .blur:
|
||||||
|
updatedStyle = .white
|
||||||
|
}
|
||||||
|
self.locationEntity.style = updatedStyle
|
||||||
|
|
||||||
|
// if let snapshotView = self.snapshotView(afterScreenUpdates: false) {
|
||||||
|
// self.addSubview(snapshotView)
|
||||||
|
//
|
||||||
|
// snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in
|
||||||
|
// snapshotView?.removeFromSuperview()
|
||||||
|
// })
|
||||||
|
// self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||||
|
// }
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private var displayFontSize: CGFloat {
|
||||||
|
let minFontSize = max(10.0, max(self.locationEntity.referenceDrawingSize.width, self.locationEntity.referenceDrawingSize.height) * 0.025)
|
||||||
|
let maxFontSize = max(10.0, max(self.locationEntity.referenceDrawingSize.width, self.locationEntity.referenceDrawingSize.height) * 0.25)
|
||||||
|
let fontSize = minFontSize + (maxFontSize - minFontSize) * 0.07
|
||||||
|
return fontSize
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateText() {
|
||||||
|
let text = NSMutableAttributedString(string: self.locationEntity.title.uppercased())
|
||||||
|
let range = NSMakeRange(0, text.length)
|
||||||
|
let fontSize = self.displayFontSize
|
||||||
|
|
||||||
|
self.textView.drawingLayoutManager.textContainers.first?.lineFragmentPadding = floor(fontSize * 0.24)
|
||||||
|
|
||||||
|
let font = Font.with(size: fontSize, design: .camera, weight: .semibold)
|
||||||
|
text.addAttribute(.font, value: font, range: range)
|
||||||
|
self.textView.font = font
|
||||||
|
|
||||||
|
let paragraphStyle = NSMutableParagraphStyle()
|
||||||
|
paragraphStyle.alignment = .right
|
||||||
|
text.addAttribute(.paragraphStyle, value: paragraphStyle, range: range)
|
||||||
|
|
||||||
|
let textColor: UIColor
|
||||||
|
switch self.locationEntity.style {
|
||||||
|
case .white:
|
||||||
|
textColor = .black
|
||||||
|
case .black, .transparent, .blur:
|
||||||
|
textColor = .white
|
||||||
|
}
|
||||||
|
|
||||||
|
text.addAttribute(.foregroundColor, value: textColor, range: range)
|
||||||
|
|
||||||
|
self.textView.attributedText = text
|
||||||
|
self.textView.visualText = text
|
||||||
|
}
|
||||||
|
|
||||||
|
private var currentStyle: DrawingLocationEntity.Style?
|
||||||
|
public override func update(animated: Bool = false) {
|
||||||
|
self.center = self.locationEntity.position
|
||||||
|
self.transform = CGAffineTransformScale(CGAffineTransformMakeRotation(self.locationEntity.rotation), self.locationEntity.scale, self.locationEntity.scale)
|
||||||
|
|
||||||
|
self.textView.frameInsets = UIEdgeInsets(top: 0.15, left: 0.0, bottom: 0.15, right: 0.0)
|
||||||
|
switch self.locationEntity.style {
|
||||||
|
case .white:
|
||||||
|
self.textView.textColor = .black
|
||||||
|
self.backgroundView.backgroundColor = .white
|
||||||
|
case .black:
|
||||||
|
self.textView.textColor = .white
|
||||||
|
self.backgroundView.backgroundColor = .black
|
||||||
|
case .transparent:
|
||||||
|
self.textView.textColor = .white
|
||||||
|
self.backgroundView.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.2)
|
||||||
|
case .blur:
|
||||||
|
self.textView.textColor = .white
|
||||||
|
self.backgroundView.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.2)
|
||||||
|
}
|
||||||
|
self.textView.textAlignment = .right
|
||||||
|
|
||||||
|
self.updateText()
|
||||||
|
|
||||||
|
self.sizeToFit()
|
||||||
|
|
||||||
|
if self.currentStyle != self.locationEntity.style {
|
||||||
|
self.currentStyle = self.locationEntity.style
|
||||||
|
self.iconView.image = generateIcon(style: self.locationEntity.style)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.backgroundView.layer.cornerRadius = self.textSize.height * 0.18
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
self.backgroundView.layer.cornerCurve = .continuous
|
||||||
|
}
|
||||||
|
|
||||||
|
super.update(animated: animated)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func updateSelectionView() {
|
||||||
|
guard let selectionView = self.selectionView as? DrawingLocationEntititySelectionView else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.pushIdentityTransformForMeasurement()
|
||||||
|
|
||||||
|
selectionView.transform = .identity
|
||||||
|
let bounds = self.selectionBounds
|
||||||
|
let center = bounds.center
|
||||||
|
|
||||||
|
let scale = self.superview?.superview?.layer.value(forKeyPath: "transform.scale.x") as? CGFloat ?? 1.0
|
||||||
|
selectionView.center = self.convert(center, to: selectionView.superview)
|
||||||
|
|
||||||
|
selectionView.bounds = CGRect(origin: .zero, size: CGSize(width: (bounds.width * self.locationEntity.scale) * scale + selectionView.selectionInset * 2.0, height: (bounds.height * self.locationEntity.scale) * scale + selectionView.selectionInset * 2.0))
|
||||||
|
selectionView.transform = CGAffineTransformMakeRotation(self.locationEntity.rotation)
|
||||||
|
|
||||||
|
self.popIdentityTransformForMeasurement()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func makeSelectionView() -> DrawingEntitySelectionView? {
|
||||||
|
if let selectionView = self.selectionView {
|
||||||
|
return selectionView
|
||||||
|
}
|
||||||
|
let selectionView = DrawingLocationEntititySelectionView()
|
||||||
|
selectionView.entityView = self
|
||||||
|
return selectionView
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRenderImage() -> UIImage? {
|
||||||
|
let rect = self.bounds
|
||||||
|
UIGraphicsBeginImageContextWithOptions(rect.size, false, 2.0)
|
||||||
|
self.drawHierarchy(in: rect, afterScreenUpdates: true)
|
||||||
|
let image = UIGraphicsGetImageFromCurrentImageContext()
|
||||||
|
UIGraphicsEndImageContext()
|
||||||
|
return image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class DrawingLocationEntititySelectionView: DrawingEntitySelectionView {
|
||||||
|
private let border = SimpleShapeLayer()
|
||||||
|
private let leftHandle = SimpleShapeLayer()
|
||||||
|
private let rightHandle = SimpleShapeLayer()
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
let handleBounds = CGRect(origin: .zero, size: entitySelectionViewHandleSize)
|
||||||
|
let handles = [
|
||||||
|
self.leftHandle,
|
||||||
|
self.rightHandle
|
||||||
|
]
|
||||||
|
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.backgroundColor = .clear
|
||||||
|
self.isOpaque = false
|
||||||
|
|
||||||
|
self.border.lineCap = .round
|
||||||
|
self.border.fillColor = UIColor.clear.cgColor
|
||||||
|
self.border.strokeColor = UIColor(rgb: 0xffffff, alpha: 0.5).cgColor
|
||||||
|
self.layer.addSublayer(self.border)
|
||||||
|
|
||||||
|
for handle in handles {
|
||||||
|
handle.bounds = handleBounds
|
||||||
|
handle.fillColor = UIColor(rgb: 0x0a60ff).cgColor
|
||||||
|
handle.strokeColor = UIColor(rgb: 0xffffff).cgColor
|
||||||
|
handle.rasterizationScale = UIScreen.main.scale
|
||||||
|
handle.shouldRasterize = true
|
||||||
|
|
||||||
|
self.layer.addSublayer(handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.snapTool.onSnapUpdated = { [weak self] type, snapped in
|
||||||
|
if let self, let entityView = self.entityView {
|
||||||
|
entityView.onSnapUpdated(type, snapped)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
var scale: CGFloat = 1.0 {
|
||||||
|
didSet {
|
||||||
|
self.setNeedsLayout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override var selectionInset: CGFloat {
|
||||||
|
return 15.0
|
||||||
|
}
|
||||||
|
|
||||||
|
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private let snapTool = DrawingEntitySnapTool()
|
||||||
|
|
||||||
|
private var currentHandle: CALayer?
|
||||||
|
override func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
|
||||||
|
guard let entityView = self.entityView, let entity = entityView.entity as? DrawingLocationEntity else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let location = gestureRecognizer.location(in: self)
|
||||||
|
|
||||||
|
switch gestureRecognizer.state {
|
||||||
|
case .began:
|
||||||
|
self.snapTool.maybeSkipFromStart(entityView: entityView, position: entity.position)
|
||||||
|
|
||||||
|
if let sublayers = self.layer.sublayers {
|
||||||
|
for layer in sublayers {
|
||||||
|
if layer.frame.contains(location) {
|
||||||
|
self.currentHandle = layer
|
||||||
|
self.snapTool.maybeSkipFromStart(entityView: entityView, rotation: entity.rotation)
|
||||||
|
entityView.onInteractionUpdated(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.currentHandle = self.layer
|
||||||
|
entityView.onInteractionUpdated(true)
|
||||||
|
case .changed:
|
||||||
|
if self.currentHandle == nil {
|
||||||
|
self.currentHandle = self.layer
|
||||||
|
}
|
||||||
|
|
||||||
|
let delta = gestureRecognizer.translation(in: entityView.superview)
|
||||||
|
let parentLocation = gestureRecognizer.location(in: self.superview)
|
||||||
|
let velocity = gestureRecognizer.velocity(in: entityView.superview)
|
||||||
|
|
||||||
|
var updatedScale = entity.scale
|
||||||
|
var updatedPosition = entity.position
|
||||||
|
var updatedRotation = entity.rotation
|
||||||
|
|
||||||
|
if self.currentHandle === self.leftHandle || self.currentHandle === self.rightHandle {
|
||||||
|
if gestureRecognizer.numberOfTouches > 1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var deltaX = gestureRecognizer.translation(in: self).x
|
||||||
|
if self.currentHandle === self.leftHandle {
|
||||||
|
deltaX *= -1.0
|
||||||
|
}
|
||||||
|
let scaleDelta = (self.bounds.size.width + deltaX * 2.0) / self.bounds.size.width
|
||||||
|
updatedScale = max(0.01, updatedScale * scaleDelta)
|
||||||
|
|
||||||
|
let newAngle: CGFloat
|
||||||
|
if self.currentHandle === self.leftHandle {
|
||||||
|
newAngle = atan2(self.center.y - parentLocation.y, self.center.x - parentLocation.x)
|
||||||
|
} else {
|
||||||
|
newAngle = atan2(parentLocation.y - self.center.y, parentLocation.x - self.center.x)
|
||||||
|
}
|
||||||
|
var delta = newAngle - updatedRotation
|
||||||
|
if delta < -.pi {
|
||||||
|
delta = 2.0 * .pi + delta
|
||||||
|
}
|
||||||
|
let velocityValue = sqrt(velocity.x * velocity.x + velocity.y * velocity.y) / 1000.0
|
||||||
|
updatedRotation = self.snapTool.update(entityView: entityView, velocity: velocityValue, delta: delta, updatedRotation: newAngle, skipMultiplier: 1.0)
|
||||||
|
} else if self.currentHandle === self.layer {
|
||||||
|
updatedPosition.x += delta.x
|
||||||
|
updatedPosition.y += delta.y
|
||||||
|
|
||||||
|
updatedPosition = self.snapTool.update(entityView: entityView, velocity: velocity, delta: delta, updatedPosition: updatedPosition, size: entityView.frame.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
entity.scale = updatedScale
|
||||||
|
entity.position = updatedPosition
|
||||||
|
entity.rotation = updatedRotation
|
||||||
|
entityView.update()
|
||||||
|
|
||||||
|
gestureRecognizer.setTranslation(.zero, in: entityView)
|
||||||
|
case .ended, .cancelled:
|
||||||
|
self.snapTool.reset()
|
||||||
|
if self.currentHandle != nil {
|
||||||
|
self.snapTool.rotationReset()
|
||||||
|
}
|
||||||
|
entityView.onInteractionUpdated(false)
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
entityView.onPositionUpdated(entity.position)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer) {
|
||||||
|
guard let entityView = self.entityView as? DrawingLocationEntityView, let entity = entityView.entity as? DrawingLocationEntity else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch gestureRecognizer.state {
|
||||||
|
case .began, .changed:
|
||||||
|
if case .began = gestureRecognizer.state {
|
||||||
|
entityView.onInteractionUpdated(true)
|
||||||
|
}
|
||||||
|
let scale = gestureRecognizer.scale
|
||||||
|
entity.scale = max(0.1, entity.scale * scale)
|
||||||
|
entityView.update()
|
||||||
|
|
||||||
|
gestureRecognizer.scale = 1.0
|
||||||
|
case .ended, .cancelled:
|
||||||
|
entityView.onInteractionUpdated(false)
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func handleRotate(_ gestureRecognizer: UIRotationGestureRecognizer) {
|
||||||
|
guard let entityView = self.entityView as? DrawingLocationEntityView, let entity = entityView.entity as? DrawingLocationEntity else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let velocity = gestureRecognizer.velocity
|
||||||
|
var updatedRotation = entity.rotation
|
||||||
|
var rotation: CGFloat = 0.0
|
||||||
|
|
||||||
|
switch gestureRecognizer.state {
|
||||||
|
case .began:
|
||||||
|
self.snapTool.maybeSkipFromStart(entityView: entityView, rotation: entity.rotation)
|
||||||
|
entityView.onInteractionUpdated(true)
|
||||||
|
case .changed:
|
||||||
|
rotation = gestureRecognizer.rotation
|
||||||
|
updatedRotation += rotation
|
||||||
|
|
||||||
|
updatedRotation = self.snapTool.update(entityView: entityView, velocity: velocity, delta: rotation, updatedRotation: updatedRotation)
|
||||||
|
entity.rotation = updatedRotation
|
||||||
|
entityView.update()
|
||||||
|
|
||||||
|
gestureRecognizer.rotation = 0.0
|
||||||
|
case .ended, .cancelled:
|
||||||
|
self.snapTool.rotationReset()
|
||||||
|
entityView.onInteractionUpdated(false)
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
entityView.onPositionUpdated(entity.position)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
||||||
|
return self.bounds.insetBy(dx: -22.0, dy: -22.0).contains(point)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
let inset = self.selectionInset - 10.0
|
||||||
|
|
||||||
|
let bounds = CGRect(origin: .zero, size: CGSize(width: entitySelectionViewHandleSize.width / self.scale, height: entitySelectionViewHandleSize.height / self.scale))
|
||||||
|
let handleSize = CGSize(width: 9.0 / self.scale, height: 9.0 / self.scale)
|
||||||
|
let handlePath = CGPath(ellipseIn: CGRect(origin: CGPoint(x: (bounds.width - handleSize.width) / 2.0, y: (bounds.height - handleSize.height) / 2.0), size: handleSize), transform: nil)
|
||||||
|
let lineWidth = (1.0 + UIScreenPixel) / self.scale
|
||||||
|
|
||||||
|
let handles = [
|
||||||
|
self.leftHandle,
|
||||||
|
self.rightHandle
|
||||||
|
]
|
||||||
|
|
||||||
|
for handle in handles {
|
||||||
|
handle.path = handlePath
|
||||||
|
handle.bounds = bounds
|
||||||
|
handle.lineWidth = lineWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
self.leftHandle.position = CGPoint(x: inset, y: self.bounds.midY)
|
||||||
|
self.rightHandle.position = CGPoint(x: self.bounds.maxX - inset, y: self.bounds.midY)
|
||||||
|
|
||||||
|
let width: CGFloat = self.bounds.width - inset * 2.0
|
||||||
|
let height: CGFloat = self.bounds.height - inset * 2.0
|
||||||
|
let cornerRadius: CGFloat = 12.0 - self.scale
|
||||||
|
|
||||||
|
let perimeter: CGFloat = 2.0 * (width + height - cornerRadius * (4.0 - .pi))
|
||||||
|
let count = 12
|
||||||
|
let relativeDashLength: CGFloat = 0.25
|
||||||
|
let dashLength = perimeter / CGFloat(count)
|
||||||
|
self.border.lineDashPattern = [dashLength * relativeDashLength, dashLength * relativeDashLength] as [NSNumber]
|
||||||
|
|
||||||
|
self.border.lineWidth = 2.0 / self.scale
|
||||||
|
self.border.path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: inset, y: inset), size: CGSize(width: width, height: height)), cornerRadius: cornerRadius).cgPath
|
||||||
|
}
|
||||||
|
}
|
@ -996,7 +996,7 @@ final class DrawingTextEntititySelectionView: DrawingEntitySelectionView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class DrawingTextLayoutManager: NSLayoutManager {
|
final class DrawingTextLayoutManager: NSLayoutManager {
|
||||||
var radius: CGFloat
|
var radius: CGFloat
|
||||||
var maxIndex: Int = 0
|
var maxIndex: Int = 0
|
||||||
|
|
||||||
@ -1008,7 +1008,7 @@ private class DrawingTextLayoutManager: NSLayoutManager {
|
|||||||
var strokeOffset: CGPoint = .zero
|
var strokeOffset: CGPoint = .zero
|
||||||
|
|
||||||
var frameColor: UIColor?
|
var frameColor: UIColor?
|
||||||
var frameWidthInset: CGFloat = 0.0
|
var frameInsets = UIEdgeInsets()
|
||||||
|
|
||||||
var textAlignment: NSTextAlignment = .natural
|
var textAlignment: NSTextAlignment = .natural
|
||||||
|
|
||||||
@ -1040,7 +1040,7 @@ private class DrawingTextLayoutManager: NSLayoutManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !ignoreRange {
|
if !ignoreRange {
|
||||||
let newRect = CGRect(origin: CGPoint(x: usedRect.minX - self.frameWidthInset, y: usedRect.minY), size: CGSize(width: usedRect.width + self.frameWidthInset * 2.0, height: usedRect.height))
|
let newRect = CGRect(origin: CGPoint(x: usedRect.minX - floorToScreenPixels(self.frameInsets.left * usedRect.height), y: usedRect.minY - floorToScreenPixels(self.frameInsets.top * usedRect.height)), size: CGSize(width: usedRect.width + floorToScreenPixels((self.frameInsets.left + self.frameInsets.right) * usedRect.height), height: usedRect.height + floorToScreenPixels((self.frameInsets.top + self.frameInsets.bottom) * usedRect.height)))
|
||||||
self.rectArray.append(newRect)
|
self.rectArray.append(newRect)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1249,7 +1249,7 @@ final class SimpleTextLayer: CATextLayer {
|
|||||||
final class DrawingTextView: UITextView, NSLayoutManagerDelegate {
|
final class DrawingTextView: UITextView, NSLayoutManagerDelegate {
|
||||||
var characterLayers: [CALayer] = []
|
var characterLayers: [CALayer] = []
|
||||||
|
|
||||||
fileprivate var drawingLayoutManager: DrawingTextLayoutManager {
|
var drawingLayoutManager: DrawingTextLayoutManager {
|
||||||
return self.layoutManager as! DrawingTextLayoutManager
|
return self.layoutManager as! DrawingTextLayoutManager
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1277,9 +1277,9 @@ final class DrawingTextView: UITextView, NSLayoutManagerDelegate {
|
|||||||
self.setNeedsDisplay()
|
self.setNeedsDisplay()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var frameWidthInset: CGFloat = 0.0 {
|
var frameInsets: UIEdgeInsets = .zero {
|
||||||
didSet {
|
didSet {
|
||||||
self.drawingLayoutManager.frameWidthInset = self.frameWidthInset
|
self.drawingLayoutManager.frameInsets = self.frameInsets
|
||||||
self.setNeedsDisplay()
|
self.setNeedsDisplay()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1378,10 +1378,6 @@ final class DrawingTextView: UITextView, NSLayoutManagerDelegate {
|
|||||||
func layoutManager(_ layoutManager: NSLayoutManager, didCompleteLayoutFor textContainer: NSTextContainer?, atEnd layoutFinishedFlag: Bool) {
|
func layoutManager(_ layoutManager: NSLayoutManager, didCompleteLayoutFor textContainer: NSTextContainer?, atEnd layoutFinishedFlag: Bool) {
|
||||||
self.updateCharLayers()
|
self.updateCharLayers()
|
||||||
if layoutFinishedFlag {
|
if layoutFinishedFlag {
|
||||||
// if self.needsLayersUpdate {
|
|
||||||
// self.needsLayersUpdate = false
|
|
||||||
// self.updateCharLayers()
|
|
||||||
// }
|
|
||||||
if let onLayoutUpdate = self.onLayoutUpdate {
|
if let onLayoutUpdate = self.onLayoutUpdate {
|
||||||
self.onLayoutUpdate = nil
|
self.onLayoutUpdate = nil
|
||||||
onLayoutUpdate()
|
onLayoutUpdate()
|
||||||
|
@ -416,6 +416,8 @@ public class StickerPickerScreen: ViewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var storyStickersContentView: StoryStickersContentView?
|
||||||
|
|
||||||
init(context: AccountContext, controller: StickerPickerScreen, theme: PresentationTheme) {
|
init(context: AccountContext, controller: StickerPickerScreen, theme: PresentationTheme) {
|
||||||
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
@ -440,15 +442,19 @@ public class StickerPickerScreen: ViewController {
|
|||||||
self.wrappingView.addSubview(self.containerView)
|
self.wrappingView.addSubview(self.containerView)
|
||||||
self.containerView.addSubview(self.hostView)
|
self.containerView.addSubview(self.hostView)
|
||||||
|
|
||||||
let signal = combineLatest(
|
self.storyStickersContentView = StoryStickersContentView(frame: .zero)
|
||||||
|
self.storyStickersContentView?.locationAction = { [weak self] in
|
||||||
|
self?.controller?.presentLocationPicker()
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = combineLatest(
|
||||||
queue: Queue.mainQueue(),
|
queue: Queue.mainQueue(),
|
||||||
controller.inputData,
|
controller.inputData,
|
||||||
self.stickerSearchState.get(),
|
self.stickerSearchState.get(),
|
||||||
self.emojiSearchState.get()
|
self.emojiSearchState.get()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.contentDisposable.set(data.start(next: { [weak self] inputData, stickerSearchState, emojiSearchState in
|
||||||
self.contentDisposable.set(signal.start(next: { [weak self] inputData, stickerSearchState, emojiSearchState in
|
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
let presentationData = strongSelf.presentationData
|
let presentationData = strongSelf.presentationData
|
||||||
var inputData = inputData
|
var inputData = inputData
|
||||||
@ -910,6 +916,7 @@ public class StickerPickerScreen: ViewController {
|
|||||||
customLayout: nil,
|
customLayout: nil,
|
||||||
externalBackground: nil,
|
externalBackground: nil,
|
||||||
externalExpansionView: nil,
|
externalExpansionView: nil,
|
||||||
|
customContentView: nil,
|
||||||
useOpaqueTheme: false,
|
useOpaqueTheme: false,
|
||||||
hideBackground: true,
|
hideBackground: true,
|
||||||
stateContext: nil,
|
stateContext: nil,
|
||||||
@ -1174,6 +1181,7 @@ public class StickerPickerScreen: ViewController {
|
|||||||
customLayout: nil,
|
customLayout: nil,
|
||||||
externalBackground: nil,
|
externalBackground: nil,
|
||||||
externalExpansionView: nil,
|
externalExpansionView: nil,
|
||||||
|
customContentView: self.storyStickersContentView,
|
||||||
useOpaqueTheme: false,
|
useOpaqueTheme: false,
|
||||||
hideBackground: true,
|
hideBackground: true,
|
||||||
stateContext: nil,
|
stateContext: nil,
|
||||||
@ -1373,7 +1381,7 @@ public class StickerPickerScreen: ViewController {
|
|||||||
deviceMetrics: layout.deviceMetrics,
|
deviceMetrics: layout.deviceMetrics,
|
||||||
bottomInset: bottomInset,
|
bottomInset: bottomInset,
|
||||||
content: content,
|
content: content,
|
||||||
backgroundColor: self.theme.list.itemBlocksBackgroundColor,
|
backgroundColor: self.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.85),
|
||||||
separatorColor: self.theme.list.blocksBackgroundColor,
|
separatorColor: self.theme.list.blocksBackgroundColor,
|
||||||
getController: { [weak self] in
|
getController: { [weak self] in
|
||||||
if let self {
|
if let self {
|
||||||
@ -1640,6 +1648,7 @@ public class StickerPickerScreen: ViewController {
|
|||||||
public var completion: (DrawingStickerEntity.Content?) -> Void = { _ in }
|
public var completion: (DrawingStickerEntity.Content?) -> Void = { _ in }
|
||||||
|
|
||||||
public var presentGallery: () -> Void = { }
|
public var presentGallery: () -> Void = { }
|
||||||
|
public var presentLocationPicker: () -> Void = { }
|
||||||
|
|
||||||
public init(context: AccountContext, inputData: Signal<StickerPickerInputData, NoError>, defaultToEmoji: Bool = false) {
|
public init(context: AccountContext, inputData: Signal<StickerPickerInputData, NoError>, defaultToEmoji: Bool = false) {
|
||||||
self.context = context
|
self.context = context
|
||||||
@ -1697,3 +1706,92 @@ public class StickerPickerScreen: ViewController {
|
|||||||
self.node.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(transition))
|
self.node.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(transition))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final class StoryStickersContentView: UIView, EmojiCustomContentView {
|
||||||
|
override public static var layerClass: AnyClass {
|
||||||
|
return PassthroughLayer.self
|
||||||
|
}
|
||||||
|
|
||||||
|
let tintContainerView = UIView()
|
||||||
|
|
||||||
|
private let backgroundLayer = SimpleLayer()
|
||||||
|
private let tintBackgroundLayer = SimpleLayer()
|
||||||
|
|
||||||
|
private let iconView: UIImageView
|
||||||
|
private let title: ComponentView<Empty>
|
||||||
|
private let button: HighlightTrackingButton
|
||||||
|
|
||||||
|
var locationAction: () -> Void = {}
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
self.iconView = UIImageView(image: UIImage(bundleImageName: "Chat/Attach Menu/Location"))
|
||||||
|
self.iconView.tintColor = .white
|
||||||
|
|
||||||
|
self.title = ComponentView<Empty>()
|
||||||
|
self.button = HighlightTrackingButton()
|
||||||
|
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.layer.addSublayer(self.backgroundLayer)
|
||||||
|
self.tintContainerView.layer.addSublayer(self.tintBackgroundLayer)
|
||||||
|
|
||||||
|
self.addSubview(self.iconView)
|
||||||
|
self.addSubview(self.button)
|
||||||
|
|
||||||
|
self.button.addTarget(self, action: #selector(self.locationPressed), for: .touchUpInside)
|
||||||
|
|
||||||
|
(self.layer as? PassthroughLayer)?.mirrorLayer = self.tintContainerView.layer
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func locationPressed() {
|
||||||
|
self.locationAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(theme: PresentationTheme, useOpaqueTheme: Bool, availableSize: CGSize, transition: Transition) -> CGSize {
|
||||||
|
if useOpaqueTheme {
|
||||||
|
self.backgroundLayer.backgroundColor = theme.chat.inputMediaPanel.panelContentControlOpaqueSelectionColor.cgColor
|
||||||
|
self.tintBackgroundLayer.backgroundColor = UIColor.white.cgColor
|
||||||
|
} else {
|
||||||
|
self.backgroundLayer.backgroundColor = theme.chat.inputMediaPanel.panelContentControlVibrantSelectionColor.cgColor
|
||||||
|
self.tintBackgroundLayer.backgroundColor = UIColor(white: 1.0, alpha: 0.2).cgColor
|
||||||
|
}
|
||||||
|
|
||||||
|
self.backgroundLayer.cornerRadius = 6.0
|
||||||
|
self.tintBackgroundLayer.cornerRadius = 6.0
|
||||||
|
|
||||||
|
let size = CGSize(width: availableSize.width, height: 76.0)
|
||||||
|
let titleSize = self.title.update(
|
||||||
|
transition: .immediate,
|
||||||
|
component: AnyComponent(Text(
|
||||||
|
text: "ADD LOCATION",
|
||||||
|
font: Font.with(size: 23.0, design: .camera),
|
||||||
|
color: .white
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: availableSize
|
||||||
|
)
|
||||||
|
let iconSize = CGSize(width: 20.0, height: 20.0)
|
||||||
|
let padding: CGFloat = 6.0
|
||||||
|
let spacing: CGFloat = 3.0
|
||||||
|
let buttonSize = CGSize(width: padding + iconSize.width + spacing + titleSize.width + padding, height: 34.0)
|
||||||
|
let buttonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - buttonSize.width) / 2.0), y: floorToScreenPixels((size.height - buttonSize.height) / 2.0)), size: buttonSize)
|
||||||
|
|
||||||
|
transition.setFrame(layer: self.backgroundLayer, frame: buttonFrame)
|
||||||
|
transition.setFrame(layer: self.tintBackgroundLayer, frame: buttonFrame)
|
||||||
|
transition.setFrame(view: self.button, frame: buttonFrame)
|
||||||
|
|
||||||
|
transition.setFrame(view: self.iconView, frame: CGRect(origin: CGPoint(x: padding, y: floorToScreenPixels((buttonSize.height - iconSize.height) / 2.0)).offsetBy(buttonFrame.origin), size: iconSize))
|
||||||
|
if let titleView = self.title.view {
|
||||||
|
if titleView.superview == nil {
|
||||||
|
self.insertSubview(titleView, aboveSubview: self.iconView)
|
||||||
|
}
|
||||||
|
transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: padding + iconSize.width + spacing, y: floorToScreenPixels((buttonSize.height - titleSize.height) / 2.0)).offsetBy(buttonFrame.origin), size: titleSize))
|
||||||
|
}
|
||||||
|
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -397,3 +397,23 @@ private final class LocationPickerContext: AttachmentMediaPickerContext {
|
|||||||
func mainButtonAction() {
|
func mainButtonAction() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func standaloneLocationPickerController(
|
||||||
|
context: AccountContext,
|
||||||
|
completion: @escaping (TelegramMediaMap) -> Void
|
||||||
|
) -> ViewController {
|
||||||
|
let presentationData = context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkColorPresentationTheme)
|
||||||
|
let updatedPresentationData: (PresentationData, Signal<PresentationData, NoError>) = (presentationData, .single(presentationData))
|
||||||
|
let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, chatLocation: nil, buttons: [.standalone], initialButton: .standalone, fromMenu: false, hasTextInput: false, makeEntityInputView: {
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
controller.requestController = { _, present in
|
||||||
|
let locationPickerController = LocationPickerController(context: context, updatedPresentationData: updatedPresentationData, mode: .share(peer: nil, selfPeer: nil, hasLiveLocation: false), completion: { location, _ in
|
||||||
|
completion(location)
|
||||||
|
})
|
||||||
|
present(locationPickerController, locationPickerController.mediaPickerContext)
|
||||||
|
}
|
||||||
|
controller.navigationPresentation = .flatModal
|
||||||
|
controller.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
|
||||||
|
return controller
|
||||||
|
}
|
||||||
|
@ -98,6 +98,7 @@ swift_library(
|
|||||||
"//submodules/TelegramUI/Components/EmojiStatusComponent",
|
"//submodules/TelegramUI/Components/EmojiStatusComponent",
|
||||||
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
|
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
|
||||||
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
|
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
|
||||||
|
"//submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent",
|
||||||
"//submodules/AttachmentUI:AttachmentUI",
|
"//submodules/AttachmentUI:AttachmentUI",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
|
@ -201,6 +201,7 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent {
|
|||||||
let titleColor = theme.list.itemPrimaryTextColor
|
let titleColor = theme.list.itemPrimaryTextColor
|
||||||
let subtitleColor = theme.list.itemSecondaryTextColor
|
let subtitleColor = theme.list.itemSecondaryTextColor
|
||||||
let arrowColor = theme.list.disclosureArrowColor
|
let arrowColor = theme.list.disclosureArrowColor
|
||||||
|
let accentColor = theme.list.itemAccentColor
|
||||||
|
|
||||||
let textFont = Font.regular(15.0)
|
let textFont = Font.regular(15.0)
|
||||||
let boldTextFont = Font.semibold(15.0)
|
let boldTextFont = Font.semibold(15.0)
|
||||||
@ -361,7 +362,8 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent {
|
|||||||
titleColor: titleColor,
|
titleColor: titleColor,
|
||||||
subtitle: perk.subtitle(strings: strings),
|
subtitle: perk.subtitle(strings: strings),
|
||||||
subtitleColor: subtitleColor,
|
subtitleColor: subtitleColor,
|
||||||
arrowColor: arrowColor
|
arrowColor: arrowColor,
|
||||||
|
accentColor: accentColor
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
@ -1082,6 +1082,8 @@ final class PerkComponent: CombinedComponent {
|
|||||||
let subtitle: String
|
let subtitle: String
|
||||||
let subtitleColor: UIColor
|
let subtitleColor: UIColor
|
||||||
let arrowColor: UIColor
|
let arrowColor: UIColor
|
||||||
|
let accentColor: UIColor
|
||||||
|
let badge: String?
|
||||||
|
|
||||||
init(
|
init(
|
||||||
iconName: String,
|
iconName: String,
|
||||||
@ -1090,7 +1092,9 @@ final class PerkComponent: CombinedComponent {
|
|||||||
titleColor: UIColor,
|
titleColor: UIColor,
|
||||||
subtitle: String,
|
subtitle: String,
|
||||||
subtitleColor: UIColor,
|
subtitleColor: UIColor,
|
||||||
arrowColor: UIColor
|
arrowColor: UIColor,
|
||||||
|
accentColor: UIColor,
|
||||||
|
badge: String? = nil
|
||||||
) {
|
) {
|
||||||
self.iconName = iconName
|
self.iconName = iconName
|
||||||
self.iconBackgroundColors = iconBackgroundColors
|
self.iconBackgroundColors = iconBackgroundColors
|
||||||
@ -1099,6 +1103,8 @@ final class PerkComponent: CombinedComponent {
|
|||||||
self.subtitle = subtitle
|
self.subtitle = subtitle
|
||||||
self.subtitleColor = subtitleColor
|
self.subtitleColor = subtitleColor
|
||||||
self.arrowColor = arrowColor
|
self.arrowColor = arrowColor
|
||||||
|
self.accentColor = accentColor
|
||||||
|
self.badge = badge
|
||||||
}
|
}
|
||||||
|
|
||||||
static func ==(lhs: PerkComponent, rhs: PerkComponent) -> Bool {
|
static func ==(lhs: PerkComponent, rhs: PerkComponent) -> Bool {
|
||||||
@ -1123,6 +1129,12 @@ final class PerkComponent: CombinedComponent {
|
|||||||
if lhs.arrowColor != rhs.arrowColor {
|
if lhs.arrowColor != rhs.arrowColor {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.accentColor != rhs.accentColor {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.badge != rhs.badge {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1132,6 +1144,8 @@ final class PerkComponent: CombinedComponent {
|
|||||||
let title = Child(MultilineTextComponent.self)
|
let title = Child(MultilineTextComponent.self)
|
||||||
let subtitle = Child(MultilineTextComponent.self)
|
let subtitle = Child(MultilineTextComponent.self)
|
||||||
let arrow = Child(BundleIconComponent.self)
|
let arrow = Child(BundleIconComponent.self)
|
||||||
|
let badgeBackground = Child(RoundedRectangle.self)
|
||||||
|
let badgeText = Child(MultilineTextComponent.self)
|
||||||
|
|
||||||
return { context in
|
return { context in
|
||||||
let component = context.component
|
let component = context.component
|
||||||
@ -1215,6 +1229,32 @@ final class PerkComponent: CombinedComponent {
|
|||||||
.position(CGPoint(x: iconBackground.size.width + sideInset + title.size.width / 2.0, y: textTopInset + title.size.height / 2.0))
|
.position(CGPoint(x: iconBackground.size.width + sideInset + title.size.width / 2.0, y: textTopInset + title.size.height / 2.0))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if let badge = component.badge {
|
||||||
|
let badgeText = badgeText.update(
|
||||||
|
component: MultilineTextComponent(text: .plain(NSAttributedString(string: badge, font: Font.semibold(11.0), textColor: .white))),
|
||||||
|
availableSize: context.availableSize,
|
||||||
|
transition: context.transition
|
||||||
|
)
|
||||||
|
|
||||||
|
let badgeWidth = badgeText.size.width + 7.0
|
||||||
|
let badgeBackground = badgeBackground.update(
|
||||||
|
component: RoundedRectangle(
|
||||||
|
colors: [component.accentColor],
|
||||||
|
cornerRadius: 5.0,
|
||||||
|
gradientDirection: .vertical),
|
||||||
|
availableSize: CGSize(width: badgeWidth, height: 16.0),
|
||||||
|
transition: context.transition
|
||||||
|
)
|
||||||
|
|
||||||
|
context.add(badgeBackground
|
||||||
|
.position(CGPoint(x: iconBackground.size.width + sideInset + title.size.width + badgeWidth / 2.0 + 8.0, y: textTopInset + title.size.height / 2.0 - 1.0))
|
||||||
|
)
|
||||||
|
|
||||||
|
context.add(badgeText
|
||||||
|
.position(CGPoint(x: iconBackground.size.width + sideInset + title.size.width + badgeWidth / 2.0 + 8.0, y: textTopInset + title.size.height / 2.0 - 1.0))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
context.add(subtitle
|
context.add(subtitle
|
||||||
.position(CGPoint(x: iconBackground.size.width + sideInset + subtitle.size.width / 2.0, y: textTopInset + title.size.height + spacing + subtitle.size.height / 2.0))
|
.position(CGPoint(x: iconBackground.size.width + sideInset + subtitle.size.width / 2.0, y: textTopInset + title.size.height + spacing + subtitle.size.height / 2.0))
|
||||||
)
|
)
|
||||||
@ -1696,7 +1736,8 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
|||||||
titleColor: titleColor,
|
titleColor: titleColor,
|
||||||
subtitle: perk.subtitle(strings: strings),
|
subtitle: perk.subtitle(strings: strings),
|
||||||
subtitleColor: subtitleColor,
|
subtitleColor: subtitleColor,
|
||||||
arrowColor: arrowColor
|
arrowColor: arrowColor,
|
||||||
|
accentColor: accentColor
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
@ -407,6 +407,32 @@ public class PremiumLimitsListScreen: ViewController {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
availableItems[.stories] = DemoPagerComponent.Item(
|
||||||
|
AnyComponentWithIdentity(
|
||||||
|
id: PremiumDemoScreen.Subject.stories,
|
||||||
|
component: AnyComponent(
|
||||||
|
StoriesPageComponent(
|
||||||
|
context: context,
|
||||||
|
bottomInset: self.footerNode.frame.height,
|
||||||
|
updatedBottomAlpha: { [weak self] alpha in
|
||||||
|
if let strongSelf = self {
|
||||||
|
strongSelf.footerNode.updateCoverAlpha(alpha, transition: .immediate)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updatedDismissOffset: { [weak self] offset in
|
||||||
|
if let strongSelf = self {
|
||||||
|
strongSelf.updateDismissOffset(offset)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updatedIsDisplaying: { [weak self] isDisplaying in
|
||||||
|
if let strongSelf = self, strongSelf.isExpanded && !isDisplaying {
|
||||||
|
strongSelf.update(isExpanded: false, transition: .animated(duration: 0.2, curve: .easeInOut))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
availableItems[.moreUpload] = DemoPagerComponent.Item(
|
availableItems[.moreUpload] = DemoPagerComponent.Item(
|
||||||
AnyComponentWithIdentity(
|
AnyComponentWithIdentity(
|
||||||
id: PremiumDemoScreen.Subject.moreUpload,
|
id: PremiumDemoScreen.Subject.moreUpload,
|
||||||
|
672
submodules/PremiumUI/Sources/StoriesPageComponent.swift
Normal file
672
submodules/PremiumUI/Sources/StoriesPageComponent.swift
Normal file
@ -0,0 +1,672 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Display
|
||||||
|
import ComponentFlow
|
||||||
|
import SwiftSignalKit
|
||||||
|
import TelegramCore
|
||||||
|
import AccountContext
|
||||||
|
import MultilineTextComponent
|
||||||
|
import BlurredBackgroundComponent
|
||||||
|
import Markdown
|
||||||
|
import TelegramPresentationData
|
||||||
|
import BundleIconComponent
|
||||||
|
import AvatarNode
|
||||||
|
import AvatarStoryIndicatorComponent
|
||||||
|
|
||||||
|
private final class AvatarComponent: Component {
|
||||||
|
let context: AccountContext
|
||||||
|
let peer: EnginePeer
|
||||||
|
|
||||||
|
init(context: AccountContext, peer: EnginePeer) {
|
||||||
|
self.context = context
|
||||||
|
self.peer = peer
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: AvatarComponent, rhs: AvatarComponent) -> Bool {
|
||||||
|
if lhs.context !== rhs.context {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.peer != rhs.peer {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
final class View: UIView {
|
||||||
|
private let avatarNode: AvatarNode
|
||||||
|
private let indicator = ComponentView<Empty>()
|
||||||
|
|
||||||
|
private var component: AvatarComponent?
|
||||||
|
private weak var state: EmptyComponentState?
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0))
|
||||||
|
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.addSubnode(self.avatarNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(component: AvatarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||||
|
self.component = component
|
||||||
|
self.state = state
|
||||||
|
|
||||||
|
let size = CGSize(width: 78.0, height: 78.0)
|
||||||
|
|
||||||
|
self.avatarNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - size.width) / 2.0), y: -22.0), size: size)
|
||||||
|
self.avatarNode.setPeer(
|
||||||
|
context: component.context,
|
||||||
|
theme: component.context.sharedContext.currentPresentationData.with({ $0 }).theme,
|
||||||
|
peer: component.peer,
|
||||||
|
synchronousLoad: true
|
||||||
|
)
|
||||||
|
|
||||||
|
let colors = [
|
||||||
|
UIColor(rgb: 0xbb6de8),
|
||||||
|
UIColor(rgb: 0x738cff),
|
||||||
|
UIColor(rgb: 0x8f76ff)
|
||||||
|
]
|
||||||
|
let indicatorSize = self.indicator.update(
|
||||||
|
transition: .immediate,
|
||||||
|
component: AnyComponent(
|
||||||
|
AvatarStoryIndicatorComponent(
|
||||||
|
hasUnseen: true,
|
||||||
|
hasUnseenCloseFriendsItems: false,
|
||||||
|
colors: AvatarStoryIndicatorComponent.Colors(unseenColors: colors, unseenCloseFriendsColors: colors, seenColors: colors),
|
||||||
|
activeLineWidth: 3.0,
|
||||||
|
inactiveLineWidth: 3.0,
|
||||||
|
counters: AvatarStoryIndicatorComponent.Counters(totalCount: 8, unseenCount: 8)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: 78.0, height: 78.0)
|
||||||
|
)
|
||||||
|
if let view = self.indicator.view {
|
||||||
|
if view.superview == nil {
|
||||||
|
self.addSubview(view)
|
||||||
|
}
|
||||||
|
view.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - indicatorSize.width) / 2.0), y: -22.0), size: indicatorSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
return CGSize(width: availableSize.width, height: 122.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeView() -> View {
|
||||||
|
return View(frame: CGRect())
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||||
|
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class ParagraphComponent: CombinedComponent {
|
||||||
|
let title: String
|
||||||
|
let titleColor: UIColor
|
||||||
|
let text: String
|
||||||
|
let textColor: UIColor
|
||||||
|
let iconName: String
|
||||||
|
let iconColor: UIColor
|
||||||
|
|
||||||
|
public init(
|
||||||
|
title: String,
|
||||||
|
titleColor: UIColor,
|
||||||
|
text: String,
|
||||||
|
textColor: UIColor,
|
||||||
|
iconName: String,
|
||||||
|
iconColor: UIColor
|
||||||
|
) {
|
||||||
|
self.title = title
|
||||||
|
self.titleColor = titleColor
|
||||||
|
self.text = text
|
||||||
|
self.textColor = textColor
|
||||||
|
self.iconName = iconName
|
||||||
|
self.iconColor = iconColor
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: ParagraphComponent, rhs: ParagraphComponent) -> Bool {
|
||||||
|
if lhs.title != rhs.title {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.titleColor != rhs.titleColor {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.text != rhs.text {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.textColor != rhs.textColor {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.iconName != rhs.iconName {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.iconColor != rhs.iconColor {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
static var body: Body {
|
||||||
|
let title = Child(MultilineTextComponent.self)
|
||||||
|
let text = Child(MultilineTextComponent.self)
|
||||||
|
let icon = Child(BundleIconComponent.self)
|
||||||
|
|
||||||
|
return { context in
|
||||||
|
let component = context.component
|
||||||
|
|
||||||
|
let leftInset: CGFloat = 64.0
|
||||||
|
let rightInset: CGFloat = 32.0
|
||||||
|
let textSideInset: CGFloat = leftInset + 8.0
|
||||||
|
let spacing: CGFloat = 5.0
|
||||||
|
|
||||||
|
let textTopInset: CGFloat = 9.0
|
||||||
|
|
||||||
|
let title = title.update(
|
||||||
|
component: MultilineTextComponent(
|
||||||
|
text: .plain(NSAttributedString(
|
||||||
|
string: component.title,
|
||||||
|
font: Font.semibold(15.0),
|
||||||
|
textColor: component.titleColor,
|
||||||
|
paragraphAlignment: .natural
|
||||||
|
)),
|
||||||
|
horizontalAlignment: .center,
|
||||||
|
maximumNumberOfLines: 1
|
||||||
|
),
|
||||||
|
availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude),
|
||||||
|
transition: .immediate
|
||||||
|
)
|
||||||
|
|
||||||
|
let textFont = Font.regular(15.0)
|
||||||
|
let boldTextFont = Font.semibold(15.0)
|
||||||
|
let textColor = component.textColor
|
||||||
|
let markdownAttributes = MarkdownAttributes(
|
||||||
|
body: MarkdownAttributeSet(font: textFont, textColor: textColor),
|
||||||
|
bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor),
|
||||||
|
link: MarkdownAttributeSet(font: textFont, textColor: textColor),
|
||||||
|
linkAttribute: { _ in
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
let text = text.update(
|
||||||
|
component: MultilineTextComponent(
|
||||||
|
text: .markdown(text: component.text, attributes: markdownAttributes),
|
||||||
|
horizontalAlignment: .natural,
|
||||||
|
maximumNumberOfLines: 0,
|
||||||
|
lineSpacing: 0.2
|
||||||
|
),
|
||||||
|
availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: context.availableSize.height),
|
||||||
|
transition: .immediate
|
||||||
|
)
|
||||||
|
|
||||||
|
let icon = icon.update(
|
||||||
|
component: BundleIconComponent(
|
||||||
|
name: component.iconName,
|
||||||
|
tintColor: component.iconColor
|
||||||
|
),
|
||||||
|
availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height),
|
||||||
|
transition: .immediate
|
||||||
|
)
|
||||||
|
|
||||||
|
context.add(title
|
||||||
|
.position(CGPoint(x: textSideInset + title.size.width / 2.0, y: textTopInset + title.size.height / 2.0))
|
||||||
|
)
|
||||||
|
|
||||||
|
context.add(text
|
||||||
|
.position(CGPoint(x: textSideInset + text.size.width / 2.0, y: textTopInset + title.size.height + spacing + text.size.height / 2.0))
|
||||||
|
)
|
||||||
|
|
||||||
|
context.add(icon
|
||||||
|
.position(CGPoint(x: 47.0, y: textTopInset + 18.0))
|
||||||
|
)
|
||||||
|
|
||||||
|
return CGSize(width: context.availableSize.width, height: textTopInset + title.size.height + text.size.height + 25.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class StoriesListComponent: CombinedComponent {
|
||||||
|
typealias EnvironmentType = (Empty, ScrollChildEnvironment)
|
||||||
|
|
||||||
|
let context: AccountContext
|
||||||
|
let topInset: CGFloat
|
||||||
|
let bottomInset: CGFloat
|
||||||
|
|
||||||
|
init(context: AccountContext, topInset: CGFloat, bottomInset: CGFloat) {
|
||||||
|
self.context = context
|
||||||
|
self.topInset = topInset
|
||||||
|
self.bottomInset = bottomInset
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: StoriesListComponent, rhs: StoriesListComponent) -> Bool {
|
||||||
|
if lhs.context !== rhs.context {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.topInset != rhs.topInset {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.bottomInset != rhs.bottomInset {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
final class State: ComponentState {
|
||||||
|
private let context: AccountContext
|
||||||
|
|
||||||
|
private var disposable: Disposable?
|
||||||
|
var limits: EngineConfiguration.UserLimits = .defaultValue
|
||||||
|
var premiumLimits: EngineConfiguration.UserLimits = .defaultValue
|
||||||
|
|
||||||
|
var accountPeer: EnginePeer?
|
||||||
|
|
||||||
|
init(context: AccountContext) {
|
||||||
|
self.context = context
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
self.disposable = (context.engine.data.get(
|
||||||
|
TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false),
|
||||||
|
TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true),
|
||||||
|
TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)
|
||||||
|
)
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] limits, premiumLimits, accountPeer in
|
||||||
|
if let strongSelf = self {
|
||||||
|
strongSelf.limits = limits
|
||||||
|
strongSelf.premiumLimits = premiumLimits
|
||||||
|
strongSelf.accountPeer = accountPeer
|
||||||
|
strongSelf.updated(transition: .immediate)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
self.disposable?.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeState() -> State {
|
||||||
|
return State(context: self.context)
|
||||||
|
}
|
||||||
|
|
||||||
|
static var body: Body {
|
||||||
|
let list = Child(List<Empty>.self)
|
||||||
|
|
||||||
|
return { context in
|
||||||
|
let theme = context.component.context.sharedContext.currentPresentationData.with { $0 }.theme
|
||||||
|
// let strings = context.component.context.sharedContext.currentPresentationData.with { $0 }.strings
|
||||||
|
|
||||||
|
let colors = [
|
||||||
|
UIColor(rgb: 0x0275f3),
|
||||||
|
UIColor(rgb: 0x8698ff),
|
||||||
|
UIColor(rgb: 0xc871ff),
|
||||||
|
UIColor(rgb: 0xc356ad),
|
||||||
|
UIColor(rgb: 0xe85c44),
|
||||||
|
UIColor(rgb: 0xff932b),
|
||||||
|
UIColor(rgb: 0xe9af18)
|
||||||
|
]
|
||||||
|
|
||||||
|
let titleColor = theme.list.itemPrimaryTextColor
|
||||||
|
let textColor = theme.list.itemSecondaryTextColor
|
||||||
|
|
||||||
|
var items: [AnyComponentWithIdentity<Empty>] = []
|
||||||
|
|
||||||
|
if let accountPeer = context.state.accountPeer {
|
||||||
|
items.append(
|
||||||
|
AnyComponentWithIdentity(
|
||||||
|
id: "avatar",
|
||||||
|
component: AnyComponent(AvatarComponent(
|
||||||
|
context: context.component.context,
|
||||||
|
peer: accountPeer
|
||||||
|
))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
items.append(
|
||||||
|
AnyComponentWithIdentity(
|
||||||
|
id: "order",
|
||||||
|
component: AnyComponent(ParagraphComponent(
|
||||||
|
title: "Priority Order",
|
||||||
|
titleColor: titleColor,
|
||||||
|
text: "Get more views as your stories are always displayed first.",
|
||||||
|
textColor: textColor,
|
||||||
|
iconName: "Premium/Stories/Order",
|
||||||
|
iconColor: colors[0]
|
||||||
|
))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
items.append(
|
||||||
|
AnyComponentWithIdentity(
|
||||||
|
id: "stealth",
|
||||||
|
component: AnyComponent(ParagraphComponent(
|
||||||
|
title: "Stealth Mode",
|
||||||
|
titleColor: titleColor,
|
||||||
|
text: "Hide the fact that you viewd other people's stories.",
|
||||||
|
textColor: textColor,
|
||||||
|
iconName: "Premium/Stories/Stealth",
|
||||||
|
iconColor: colors[1]
|
||||||
|
))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
items.append(
|
||||||
|
AnyComponentWithIdentity(
|
||||||
|
id: "views",
|
||||||
|
component: AnyComponent(ParagraphComponent(
|
||||||
|
title: "Permanent Views History",
|
||||||
|
titleColor: titleColor,
|
||||||
|
text: "Check who opens your stories - even after they expire.",
|
||||||
|
textColor: textColor,
|
||||||
|
iconName: "Premium/Stories/Views",
|
||||||
|
iconColor: colors[2]
|
||||||
|
))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
items.append(
|
||||||
|
AnyComponentWithIdentity(
|
||||||
|
id: "expiration",
|
||||||
|
component: AnyComponent(ParagraphComponent(
|
||||||
|
title: "Expiration Durations",
|
||||||
|
titleColor: titleColor,
|
||||||
|
text: "Set custom expiration durations like 6 or 48 hours for your stories.",
|
||||||
|
textColor: textColor,
|
||||||
|
iconName: "Premium/Stories/Expire",
|
||||||
|
iconColor: colors[3]
|
||||||
|
))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
items.append(
|
||||||
|
AnyComponentWithIdentity(
|
||||||
|
id: "save",
|
||||||
|
component: AnyComponent(ParagraphComponent(
|
||||||
|
title: "Save Stories to Gallery",
|
||||||
|
titleColor: titleColor,
|
||||||
|
text: "Save other people's unprotected stories to your Gallery.",
|
||||||
|
textColor: textColor,
|
||||||
|
iconName: "Premium/Stories/Save",
|
||||||
|
iconColor: colors[4]
|
||||||
|
))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
items.append(
|
||||||
|
AnyComponentWithIdentity(
|
||||||
|
id: "captions",
|
||||||
|
component: AnyComponent(ParagraphComponent(
|
||||||
|
title: "Longer Captions",
|
||||||
|
titleColor: titleColor,
|
||||||
|
text: "Add ten times longer captions to your stories.",
|
||||||
|
textColor: textColor,
|
||||||
|
iconName: "Premium/Stories/Caption",
|
||||||
|
iconColor: colors[5]
|
||||||
|
))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
items.append(
|
||||||
|
AnyComponentWithIdentity(
|
||||||
|
id: "format",
|
||||||
|
component: AnyComponent(ParagraphComponent(
|
||||||
|
title: "Links and Formatting",
|
||||||
|
titleColor: titleColor,
|
||||||
|
text: "Add links and formatting in captions to your stories.",
|
||||||
|
textColor: textColor,
|
||||||
|
iconName: "Premium/Stories/Format",
|
||||||
|
iconColor: colors[6]
|
||||||
|
))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
let list = list.update(
|
||||||
|
component: List(items),
|
||||||
|
availableSize: CGSize(width: context.availableSize.width, height: 10000.0),
|
||||||
|
transition: context.transition
|
||||||
|
)
|
||||||
|
|
||||||
|
let contentHeight = context.component.topInset + list.size.height + context.component.bottomInset
|
||||||
|
context.add(list
|
||||||
|
.position(CGPoint(x: list.size.width / 2.0, y: context.component.topInset + list.size.height / 2.0))
|
||||||
|
)
|
||||||
|
|
||||||
|
return CGSize(width: context.availableSize.width, height: contentHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
final class StoriesPageComponent: CombinedComponent {
|
||||||
|
typealias EnvironmentType = DemoPageEnvironment
|
||||||
|
|
||||||
|
let context: AccountContext
|
||||||
|
let bottomInset: CGFloat
|
||||||
|
let updatedBottomAlpha: (CGFloat) -> Void
|
||||||
|
let updatedDismissOffset: (CGFloat) -> Void
|
||||||
|
let updatedIsDisplaying: (Bool) -> Void
|
||||||
|
|
||||||
|
init(context: AccountContext, bottomInset: CGFloat, updatedBottomAlpha: @escaping (CGFloat) -> Void, updatedDismissOffset: @escaping (CGFloat) -> Void, updatedIsDisplaying: @escaping (Bool) -> Void) {
|
||||||
|
self.context = context
|
||||||
|
self.bottomInset = bottomInset
|
||||||
|
self.updatedBottomAlpha = updatedBottomAlpha
|
||||||
|
self.updatedDismissOffset = updatedDismissOffset
|
||||||
|
self.updatedIsDisplaying = updatedIsDisplaying
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: StoriesPageComponent, rhs: StoriesPageComponent) -> Bool {
|
||||||
|
if lhs.context !== rhs.context {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.bottomInset != rhs.bottomInset {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
final class State: ComponentState {
|
||||||
|
let updateBottomAlpha: (CGFloat) -> Void
|
||||||
|
let updateDismissOffset: (CGFloat) -> Void
|
||||||
|
let updatedIsDisplaying: (Bool) -> Void
|
||||||
|
|
||||||
|
var resetScroll: ActionSlot<Void>?
|
||||||
|
|
||||||
|
var topContentOffset: CGFloat = 0.0
|
||||||
|
var bottomContentOffset: CGFloat = 100.0 {
|
||||||
|
didSet {
|
||||||
|
self.updateAlpha()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var position: CGFloat? {
|
||||||
|
didSet {
|
||||||
|
self.updateAlpha()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var isDisplaying = false {
|
||||||
|
didSet {
|
||||||
|
if oldValue != self.isDisplaying {
|
||||||
|
self.updatedIsDisplaying(self.isDisplaying)
|
||||||
|
|
||||||
|
if !self.isDisplaying {
|
||||||
|
self.resetScroll?.invoke(Void())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init(updateBottomAlpha: @escaping (CGFloat) -> Void, updateDismissOffset: @escaping (CGFloat) -> Void, updateIsDisplaying: @escaping (Bool) -> Void) {
|
||||||
|
self.updateBottomAlpha = updateBottomAlpha
|
||||||
|
self.updateDismissOffset = updateDismissOffset
|
||||||
|
self.updatedIsDisplaying = updateIsDisplaying
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateAlpha() {
|
||||||
|
let dismissPosition = min(1.0, abs(self.position ?? 0.0) / 1.3333)
|
||||||
|
let position = min(1.0, abs(self.position ?? 0.0))
|
||||||
|
self.updateDismissOffset(dismissPosition)
|
||||||
|
|
||||||
|
let verticalPosition = 1.0 - min(30.0, self.bottomContentOffset) / 30.0
|
||||||
|
|
||||||
|
let backgroundAlpha: CGFloat = max(position, verticalPosition)
|
||||||
|
self.updateBottomAlpha(backgroundAlpha)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeState() -> State {
|
||||||
|
return State(updateBottomAlpha: self.updatedBottomAlpha, updateDismissOffset: self.updatedDismissOffset, updateIsDisplaying: self.updatedIsDisplaying)
|
||||||
|
}
|
||||||
|
|
||||||
|
static var body: Body {
|
||||||
|
let background = Child(Rectangle.self)
|
||||||
|
let scroll = Child(ScrollComponent<Empty>.self)
|
||||||
|
let topPanel = Child(BlurredBackgroundComponent.self)
|
||||||
|
let topSeparator = Child(Rectangle.self)
|
||||||
|
let title = Child(MultilineTextComponent.self)
|
||||||
|
let secondaryTitle = Child(MultilineTextComponent.self)
|
||||||
|
|
||||||
|
let resetScroll = ActionSlot<Void>()
|
||||||
|
|
||||||
|
return { context in
|
||||||
|
let state = context.state
|
||||||
|
|
||||||
|
let environment = context.environment[DemoPageEnvironment.self].value
|
||||||
|
state.resetScroll = resetScroll
|
||||||
|
state.position = environment.position
|
||||||
|
state.isDisplaying = environment.isDisplaying
|
||||||
|
|
||||||
|
let theme = context.component.context.sharedContext.currentPresentationData.with { $0 }.theme
|
||||||
|
// let strings = context.component.context.sharedContext.currentPresentationData.with { $0 }.strings
|
||||||
|
|
||||||
|
let topInset: CGFloat = 56.0
|
||||||
|
|
||||||
|
let scroll = scroll.update(
|
||||||
|
component: ScrollComponent<Empty>(
|
||||||
|
content: AnyComponent(
|
||||||
|
StoriesListComponent(
|
||||||
|
context: context.component.context,
|
||||||
|
topInset: topInset,
|
||||||
|
bottomInset: context.component.bottomInset + 110.0
|
||||||
|
)
|
||||||
|
),
|
||||||
|
contentInsets: UIEdgeInsets(top: topInset, left: 0.0, bottom: 0.0, right: 0.0),
|
||||||
|
contentOffsetUpdated: { [weak state] topContentOffset, bottomContentOffset in
|
||||||
|
state?.topContentOffset = topContentOffset
|
||||||
|
state?.bottomContentOffset = bottomContentOffset
|
||||||
|
Queue.mainQueue().justDispatch {
|
||||||
|
state?.updated(transition: .immediate)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
contentOffsetWillCommit: { _ in },
|
||||||
|
resetScroll: resetScroll
|
||||||
|
),
|
||||||
|
availableSize: context.availableSize,
|
||||||
|
transition: context.transition
|
||||||
|
)
|
||||||
|
|
||||||
|
let background = background.update(
|
||||||
|
component: Rectangle(color: theme.list.plainBackgroundColor),
|
||||||
|
availableSize: scroll.size,
|
||||||
|
transition: context.transition
|
||||||
|
)
|
||||||
|
context.add(background
|
||||||
|
.position(CGPoint(x: context.availableSize.width / 2.0, y: background.size.height / 2.0))
|
||||||
|
)
|
||||||
|
|
||||||
|
context.add(scroll
|
||||||
|
.position(CGPoint(x: context.availableSize.width / 2.0, y: scroll.size.height / 2.0))
|
||||||
|
)
|
||||||
|
|
||||||
|
let topPanel = topPanel.update(
|
||||||
|
component: BlurredBackgroundComponent(
|
||||||
|
color: theme.rootController.navigationBar.blurredBackgroundColor
|
||||||
|
),
|
||||||
|
availableSize: CGSize(width: context.availableSize.width, height: topInset),
|
||||||
|
transition: context.transition
|
||||||
|
)
|
||||||
|
|
||||||
|
let topSeparator = topSeparator.update(
|
||||||
|
component: Rectangle(
|
||||||
|
color: theme.rootController.navigationBar.separatorColor
|
||||||
|
),
|
||||||
|
availableSize: CGSize(width: context.availableSize.width, height: UIScreenPixel),
|
||||||
|
transition: context.transition
|
||||||
|
)
|
||||||
|
|
||||||
|
let title = title.update(
|
||||||
|
component: MultilineTextComponent(
|
||||||
|
text: .plain(NSAttributedString(string: "Upgraded Stories", font: Font.semibold(20.0), textColor: theme.rootController.navigationBar.primaryTextColor)),
|
||||||
|
horizontalAlignment: .center,
|
||||||
|
truncationType: .end,
|
||||||
|
maximumNumberOfLines: 1
|
||||||
|
),
|
||||||
|
availableSize: context.availableSize,
|
||||||
|
transition: context.transition
|
||||||
|
)
|
||||||
|
|
||||||
|
let secondaryTitle = secondaryTitle.update(
|
||||||
|
component: MultilineTextComponent(
|
||||||
|
text: .plain(NSAttributedString(string: "Exclusive Features in Stories", font: Font.semibold(17.0), textColor: theme.rootController.navigationBar.primaryTextColor)),
|
||||||
|
horizontalAlignment: .center,
|
||||||
|
truncationType: .end,
|
||||||
|
maximumNumberOfLines: 1
|
||||||
|
),
|
||||||
|
availableSize: context.availableSize,
|
||||||
|
transition: context.transition
|
||||||
|
)
|
||||||
|
|
||||||
|
let topPanelAlpha: CGFloat
|
||||||
|
if state.topContentOffset > 78.0 {
|
||||||
|
topPanelAlpha = min(30.0, state.topContentOffset - 78.0) / 30.0
|
||||||
|
} else {
|
||||||
|
topPanelAlpha = 0.0
|
||||||
|
}
|
||||||
|
context.add(topPanel
|
||||||
|
.position(CGPoint(x: context.availableSize.width / 2.0, y: topPanel.size.height / 2.0))
|
||||||
|
.opacity(topPanelAlpha)
|
||||||
|
)
|
||||||
|
context.add(topSeparator
|
||||||
|
.position(CGPoint(x: context.availableSize.width / 2.0, y: topPanel.size.height))
|
||||||
|
.opacity(topPanelAlpha)
|
||||||
|
)
|
||||||
|
|
||||||
|
let titleTopOriginY = topPanel.size.height / 2.0
|
||||||
|
let titleBottomOriginY: CGFloat = 144.0
|
||||||
|
let titleOriginDelta = titleTopOriginY - titleBottomOriginY
|
||||||
|
|
||||||
|
let fraction = max(0.0, min(1.0, abs(state.topContentOffset / titleOriginDelta)))
|
||||||
|
let titleOriginY: CGFloat = titleBottomOriginY + fraction * titleOriginDelta
|
||||||
|
let titleScale = 1.0 - fraction * 0.2
|
||||||
|
|
||||||
|
let titleAlpha: CGFloat
|
||||||
|
if fraction > 0.78 {
|
||||||
|
titleAlpha = max(0.0, 1.0 - (fraction - 0.78) / 0.16)
|
||||||
|
} else {
|
||||||
|
titleAlpha = 1.0
|
||||||
|
}
|
||||||
|
let secondaryTitleAlpha: CGFloat = 1.0 - titleAlpha
|
||||||
|
|
||||||
|
context.add(title
|
||||||
|
.position(CGPoint(x: context.availableSize.width / 2.0, y: titleOriginY))
|
||||||
|
.scale(titleScale)
|
||||||
|
.opacity(titleAlpha)
|
||||||
|
)
|
||||||
|
|
||||||
|
context.add(secondaryTitle
|
||||||
|
.position(CGPoint(x: context.availableSize.width / 2.0, y: titleOriginY))
|
||||||
|
.opacity(secondaryTitleAlpha)
|
||||||
|
)
|
||||||
|
|
||||||
|
return scroll.size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1593,6 +1593,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
effectContainerView: self.backgroundNode.vibrancyEffectView?.contentView
|
effectContainerView: self.backgroundNode.vibrancyEffectView?.contentView
|
||||||
),
|
),
|
||||||
externalExpansionView: self.view,
|
externalExpansionView: self.view,
|
||||||
|
customContentView: nil,
|
||||||
useOpaqueTheme: false,
|
useOpaqueTheme: false,
|
||||||
hideBackground: false,
|
hideBackground: false,
|
||||||
stateContext: nil,
|
stateContext: nil,
|
||||||
|
@ -686,7 +686,7 @@ public func privacyAndSecurityController(
|
|||||||
let privacySettingsPromise = Promise<AccountPrivacySettings?>()
|
let privacySettingsPromise = Promise<AccountPrivacySettings?>()
|
||||||
privacySettingsPromise.set(.single(initialSettings) |> then(context.engine.privacy.requestAccountPrivacySettings() |> map(Optional.init)))
|
privacySettingsPromise.set(.single(initialSettings) |> then(context.engine.privacy.requestAccountPrivacySettings() |> map(Optional.init)))
|
||||||
|
|
||||||
let blockedPeersContext = blockedPeersContext ?? BlockedPeersContext(account: context.account)
|
let blockedPeersContext = blockedPeersContext ?? BlockedPeersContext(account: context.account, subject: .blocked)
|
||||||
let activeSessionsContext = activeSessionsContext ?? context.engine.privacy.activeSessions()
|
let activeSessionsContext = activeSessionsContext ?? context.engine.privacy.activeSessions()
|
||||||
let webSessionsContext = webSessionsContext ?? context.engine.privacy.webSessions()
|
let webSessionsContext = webSessionsContext ?? context.engine.privacy.webSessions()
|
||||||
|
|
||||||
|
@ -631,7 +631,7 @@ private func privacySearchableItems(context: AccountContext, privacySettings: Ac
|
|||||||
presentPrivacySettings(context, present, nil)
|
presentPrivacySettings(context, present, nil)
|
||||||
}),
|
}),
|
||||||
SettingsSearchableItem(id: .privacy(1), title: strings.Settings_BlockedUsers, alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_BlockedUsers), icon: icon, breadcrumbs: [strings.Settings_PrivacySettings], present: { context, _, present in
|
SettingsSearchableItem(id: .privacy(1), title: strings.Settings_BlockedUsers, alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_BlockedUsers), icon: icon, breadcrumbs: [strings.Settings_PrivacySettings], present: { context, _, present in
|
||||||
present(.push, blockedPeersController(context: context, blockedPeersContext: BlockedPeersContext(account: context.account)))
|
present(.push, blockedPeersController(context: context, blockedPeersContext: BlockedPeersContext(account: context.account, subject: .blocked)))
|
||||||
}),
|
}),
|
||||||
SettingsSearchableItem(id: .privacy(2), title: strings.PrivacySettings_LastSeen, alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_LastSeen), icon: icon, breadcrumbs: [strings.Settings_PrivacySettings], present: { context, _, present in
|
SettingsSearchableItem(id: .privacy(2), title: strings.PrivacySettings_LastSeen, alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_LastSeen), icon: icon, breadcrumbs: [strings.Settings_PrivacySettings], present: { context, _, present in
|
||||||
presentSelectivePrivacySettings(context, .presence, present)
|
presentSelectivePrivacySettings(context, .presence, present)
|
||||||
|
@ -1326,7 +1326,7 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox:
|
|||||||
channelsToPoll[peerId] = nil
|
channelsToPoll[peerId] = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case let .updatePeerBlocked(peerId, blocked):
|
case let .updatePeerBlocked(flags, peerId):
|
||||||
let userPeerId = peerId.peerId
|
let userPeerId = peerId.peerId
|
||||||
updatedState.updateCachedPeerData(userPeerId, { current in
|
updatedState.updateCachedPeerData(userPeerId, { current in
|
||||||
let previous: CachedUserData
|
let previous: CachedUserData
|
||||||
@ -1335,7 +1335,13 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox:
|
|||||||
} else {
|
} else {
|
||||||
previous = CachedUserData()
|
previous = CachedUserData()
|
||||||
}
|
}
|
||||||
return previous.withUpdatedIsBlocked(blocked == .boolTrue)
|
var userFlags = previous.flags
|
||||||
|
if (flags & (1 << 1)) != 0 {
|
||||||
|
userFlags.insert(.isBlockedFromMyStories)
|
||||||
|
} else {
|
||||||
|
userFlags.remove(.isBlockedFromMyStories)
|
||||||
|
}
|
||||||
|
return previous.withUpdatedIsBlocked((flags & (1 << 0)) != 0).withUpdatedFlags(userFlags)
|
||||||
})
|
})
|
||||||
case let .updateUserStatus(userId, status):
|
case let .updateUserStatus(userId, status):
|
||||||
updatedState.mergePeerPresences([PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)): status], explicit: true)
|
updatedState.mergePeerPresences([PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)): status], explicit: true)
|
||||||
|
@ -37,7 +37,7 @@ public struct UserLimitsConfiguration: Equatable {
|
|||||||
maxReactionsPerMessage: 1,
|
maxReactionsPerMessage: 1,
|
||||||
maxSharedFolderInviteLinks: 3,
|
maxSharedFolderInviteLinks: 3,
|
||||||
maxSharedFolderJoin: 2,
|
maxSharedFolderJoin: 2,
|
||||||
maxStoryCaptionLength: 1024,
|
maxStoryCaptionLength: 200,
|
||||||
maxExpiringStoriesCount: 100
|
maxExpiringStoriesCount: 100
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -149,6 +149,7 @@ public struct CachedUserFlags: OptionSet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static let translationHidden = CachedUserFlags(rawValue: 1 << 0)
|
public static let translationHidden = CachedUserFlags(rawValue: 1 << 0)
|
||||||
|
public static let isBlockedFromMyStories = CachedUserFlags(rawValue: 1 << 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class EditableBotInfo: PostboxCoding, Equatable {
|
public final class EditableBotInfo: PostboxCoding, Equatable {
|
||||||
|
@ -866,6 +866,62 @@ public extension TelegramEngine.EngineData.Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct IsBlocked: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem {
|
||||||
|
public typealias Result = EnginePeerCachedInfoItem<Bool>
|
||||||
|
|
||||||
|
fileprivate var id: EnginePeer.Id
|
||||||
|
public var mapKey: EnginePeer.Id {
|
||||||
|
return self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(id: EnginePeer.Id) {
|
||||||
|
self.id = id
|
||||||
|
}
|
||||||
|
|
||||||
|
var key: PostboxViewKey {
|
||||||
|
return .cachedPeerData(peerId: self.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func extract(view: PostboxView) -> Result {
|
||||||
|
guard let view = view as? CachedPeerDataView else {
|
||||||
|
preconditionFailure()
|
||||||
|
}
|
||||||
|
if let cachedData = view.cachedPeerData as? CachedUserData {
|
||||||
|
return .known(cachedData.isBlocked)
|
||||||
|
} else {
|
||||||
|
return .unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct IsBlockedFromStories: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem {
|
||||||
|
public typealias Result = EnginePeerCachedInfoItem<Bool>
|
||||||
|
|
||||||
|
fileprivate var id: EnginePeer.Id
|
||||||
|
public var mapKey: EnginePeer.Id {
|
||||||
|
return self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(id: EnginePeer.Id) {
|
||||||
|
self.id = id
|
||||||
|
}
|
||||||
|
|
||||||
|
var key: PostboxViewKey {
|
||||||
|
return .cachedPeerData(peerId: self.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func extract(view: PostboxView) -> Result {
|
||||||
|
guard let view = view as? CachedPeerDataView else {
|
||||||
|
preconditionFailure()
|
||||||
|
}
|
||||||
|
if let cachedData = view.cachedPeerData as? CachedUserData {
|
||||||
|
return .known(cachedData.flags.contains(.isBlockedFromMyStories))
|
||||||
|
} else {
|
||||||
|
return .unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public struct TranslationHidden: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem {
|
public struct TranslationHidden: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem {
|
||||||
public typealias Result = Bool
|
public typealias Result = Bool
|
||||||
|
|
||||||
|
@ -4,46 +4,15 @@ import SwiftSignalKit
|
|||||||
import TelegramApi
|
import TelegramApi
|
||||||
import MtProtoKit
|
import MtProtoKit
|
||||||
|
|
||||||
|
|
||||||
public func requestBlockedPeers(account: Account) -> Signal<[Peer], NoError> {
|
|
||||||
let accountPeerId = account.peerId
|
|
||||||
return account.network.request(Api.functions.contacts.getBlocked(offset: 0, limit: 100))
|
|
||||||
|> retryRequest
|
|
||||||
|> mapToSignal { result -> Signal<[Peer], NoError> in
|
|
||||||
return account.postbox.transaction { transaction -> [Peer] in
|
|
||||||
let apiUsers: [Api.User]
|
|
||||||
let apiChats: [Api.Chat]
|
|
||||||
switch result {
|
|
||||||
case let .blocked(_, chats, users):
|
|
||||||
apiUsers = users
|
|
||||||
apiChats = chats
|
|
||||||
case let .blockedSlice(_, _, chats, users):
|
|
||||||
apiUsers = users
|
|
||||||
apiChats = chats
|
|
||||||
}
|
|
||||||
|
|
||||||
let parsedPeers = AccumulatedPeers(transaction: transaction, chats: apiChats, users: apiUsers)
|
|
||||||
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers)
|
|
||||||
|
|
||||||
var peers: [Peer] = []
|
|
||||||
for id in parsedPeers.allIds {
|
|
||||||
if let peer = transaction.getPeer(id) {
|
|
||||||
peers.append(peer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return peers
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func _internal_requestUpdatePeerIsBlocked(account: Account, peerId: PeerId, isBlocked: Bool) -> Signal<Void, NoError> {
|
func _internal_requestUpdatePeerIsBlocked(account: Account, peerId: PeerId, isBlocked: Bool) -> Signal<Void, NoError> {
|
||||||
return account.postbox.transaction { transaction -> Signal<Void, NoError> in
|
return account.postbox.transaction { transaction -> Signal<Void, NoError> in
|
||||||
if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) {
|
if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) {
|
||||||
|
let flags: Int32 = 0
|
||||||
let signal: Signal<Api.Bool, MTRpcError>
|
let signal: Signal<Api.Bool, MTRpcError>
|
||||||
if isBlocked {
|
if isBlocked {
|
||||||
signal = account.network.request(Api.functions.contacts.block(id: inputPeer))
|
signal = account.network.request(Api.functions.contacts.block(flags: flags, id: inputPeer))
|
||||||
} else {
|
} else {
|
||||||
signal = account.network.request(Api.functions.contacts.unblock(id: inputPeer))
|
signal = account.network.request(Api.functions.contacts.unblock(flags: flags, id: inputPeer))
|
||||||
}
|
}
|
||||||
return signal
|
return signal
|
||||||
|> map(Optional.init)
|
|> map(Optional.init)
|
||||||
@ -70,3 +39,45 @@ func _internal_requestUpdatePeerIsBlocked(account: Account, peerId: PeerId, isBl
|
|||||||
}
|
}
|
||||||
} |> switchToLatest
|
} |> switchToLatest
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func _internal_requestUpdatePeerIsBlockedFromStories(account: Account, peerId: PeerId, isBlocked: Bool) -> Signal<Void, NoError> {
|
||||||
|
return account.postbox.transaction { transaction -> Signal<Void, NoError> in
|
||||||
|
if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) {
|
||||||
|
let flags: Int32 = 1 << 0
|
||||||
|
let signal: Signal<Api.Bool, MTRpcError>
|
||||||
|
if isBlocked {
|
||||||
|
signal = account.network.request(Api.functions.contacts.block(flags: flags, id: inputPeer))
|
||||||
|
} else {
|
||||||
|
signal = account.network.request(Api.functions.contacts.unblock(flags: flags, id: inputPeer))
|
||||||
|
}
|
||||||
|
return signal
|
||||||
|
|> map(Optional.init)
|
||||||
|
|> `catch` { _ -> Signal<Api.Bool?, NoError> in
|
||||||
|
return .single(nil)
|
||||||
|
}
|
||||||
|
|> mapToSignal { result -> Signal<Void, NoError> in
|
||||||
|
return account.postbox.transaction { transaction -> Void in
|
||||||
|
if result != nil {
|
||||||
|
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in
|
||||||
|
let previous: CachedUserData
|
||||||
|
if let current = current as? CachedUserData {
|
||||||
|
previous = current
|
||||||
|
} else {
|
||||||
|
previous = CachedUserData()
|
||||||
|
}
|
||||||
|
var userFlags = previous.flags
|
||||||
|
if isBlocked {
|
||||||
|
userFlags.insert(.isBlockedFromMyStories)
|
||||||
|
} else {
|
||||||
|
userFlags.remove(.isBlockedFromMyStories)
|
||||||
|
}
|
||||||
|
return previous.withUpdatedFlags(userFlags)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return .complete()
|
||||||
|
}
|
||||||
|
} |> switchToLatest
|
||||||
|
}
|
||||||
|
@ -20,7 +20,13 @@ public enum BlockedPeersContextRemoveError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public final class BlockedPeersContext {
|
public final class BlockedPeersContext {
|
||||||
|
public enum Subject {
|
||||||
|
case blocked
|
||||||
|
case stories
|
||||||
|
}
|
||||||
|
|
||||||
private let account: Account
|
private let account: Account
|
||||||
|
private let subject: Subject
|
||||||
private var _state: BlockedPeersContextState {
|
private var _state: BlockedPeersContextState {
|
||||||
didSet {
|
didSet {
|
||||||
if self._state != oldValue {
|
if self._state != oldValue {
|
||||||
@ -35,10 +41,12 @@ public final class BlockedPeersContext {
|
|||||||
|
|
||||||
private let disposable = MetaDisposable()
|
private let disposable = MetaDisposable()
|
||||||
|
|
||||||
public init(account: Account) {
|
public init(account: Account, subject: Subject) {
|
||||||
assert(Queue.mainQueue().isCurrent())
|
assert(Queue.mainQueue().isCurrent())
|
||||||
|
|
||||||
self.account = account
|
self.account = account
|
||||||
|
self.subject = subject
|
||||||
|
|
||||||
self._state = BlockedPeersContextState(isLoadingMore: false, canLoadMore: true, totalCount: nil, peers: [])
|
self._state = BlockedPeersContextState(isLoadingMore: false, canLoadMore: true, totalCount: nil, peers: [])
|
||||||
self._statePromise.set(.single(self._state))
|
self._statePromise.set(.single(self._state))
|
||||||
|
|
||||||
@ -58,7 +66,13 @@ public final class BlockedPeersContext {
|
|||||||
self._state = BlockedPeersContextState(isLoadingMore: true, canLoadMore: self._state.canLoadMore, totalCount: self._state.totalCount, peers: self._state.peers)
|
self._state = BlockedPeersContextState(isLoadingMore: true, canLoadMore: self._state.canLoadMore, totalCount: self._state.totalCount, peers: self._state.peers)
|
||||||
let postbox = self.account.postbox
|
let postbox = self.account.postbox
|
||||||
let accountPeerId = self.account.peerId
|
let accountPeerId = self.account.peerId
|
||||||
self.disposable.set((self.account.network.request(Api.functions.contacts.getBlocked(offset: Int32(self._state.peers.count), limit: 64))
|
|
||||||
|
var flags: Int32 = 0
|
||||||
|
if case .stories = self.subject {
|
||||||
|
flags |= 1 << 0
|
||||||
|
}
|
||||||
|
|
||||||
|
self.disposable.set((self.account.network.request(Api.functions.contacts.getBlocked(flags: flags, offset: Int32(self._state.peers.count), limit: 64))
|
||||||
|> retryRequest
|
|> retryRequest
|
||||||
|> mapToSignal { result -> Signal<(peers: [RenderedPeer], canLoadMore: Bool, totalCount: Int?), NoError> in
|
|> mapToSignal { result -> Signal<(peers: [RenderedPeer], canLoadMore: Bool, totalCount: Int?), NoError> in
|
||||||
return postbox.transaction { transaction -> (peers: [RenderedPeer], canLoadMore: Bool, totalCount: Int?) in
|
return postbox.transaction { transaction -> (peers: [RenderedPeer], canLoadMore: Bool, totalCount: Int?) in
|
||||||
@ -123,11 +137,41 @@ public final class BlockedPeersContext {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func updatePeerIds(_ peerIds: [EnginePeer.Id]) -> Signal<Never, BlockedPeersContextAddError> {
|
||||||
|
assert(Queue.mainQueue().isCurrent())
|
||||||
|
|
||||||
|
let validIds = Set(peerIds)
|
||||||
|
var peersToRemove: [EnginePeer.Id] = []
|
||||||
|
for peer in self._state.peers {
|
||||||
|
if !validIds.contains(peer.peerId) {
|
||||||
|
peersToRemove.append(peer.peerId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var updateSignals: [Signal<Never, BlockedPeersContextAddError>] = []
|
||||||
|
for peerId in peersToRemove {
|
||||||
|
updateSignals.append(self.remove(peerId: peerId) |> mapError { _ in .generic })
|
||||||
|
}
|
||||||
|
for peerId in peerIds {
|
||||||
|
updateSignals.append(self.add(peerId: peerId))
|
||||||
|
}
|
||||||
|
return combineLatest(updateSignals)
|
||||||
|
|> mapToSignal { _ in
|
||||||
|
return .never()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public func add(peerId: PeerId) -> Signal<Never, BlockedPeersContextAddError> {
|
public func add(peerId: PeerId) -> Signal<Never, BlockedPeersContextAddError> {
|
||||||
assert(Queue.mainQueue().isCurrent())
|
assert(Queue.mainQueue().isCurrent())
|
||||||
|
|
||||||
let postbox = self.account.postbox
|
let postbox = self.account.postbox
|
||||||
let network = self.account.network
|
let network = self.account.network
|
||||||
|
let subject = self.subject
|
||||||
|
|
||||||
|
var flags: Int32 = 0
|
||||||
|
if case .stories = self.subject {
|
||||||
|
flags |= 1 << 0
|
||||||
|
}
|
||||||
|
|
||||||
return self.account.postbox.transaction { transaction -> Api.InputPeer? in
|
return self.account.postbox.transaction { transaction -> Api.InputPeer? in
|
||||||
return transaction.getPeer(peerId).flatMap(apiInputPeer)
|
return transaction.getPeer(peerId).flatMap(apiInputPeer)
|
||||||
}
|
}
|
||||||
@ -136,7 +180,7 @@ public final class BlockedPeersContext {
|
|||||||
guard let inputPeer = inputPeer else {
|
guard let inputPeer = inputPeer else {
|
||||||
return .fail(.generic)
|
return .fail(.generic)
|
||||||
}
|
}
|
||||||
return network.request(Api.functions.contacts.block(id: inputPeer))
|
return network.request(Api.functions.contacts.block(flags: flags, id: inputPeer))
|
||||||
|> mapError { _ -> BlockedPeersContextAddError in
|
|> mapError { _ -> BlockedPeersContextAddError in
|
||||||
return .generic
|
return .generic
|
||||||
}
|
}
|
||||||
@ -150,7 +194,13 @@ public final class BlockedPeersContext {
|
|||||||
} else {
|
} else {
|
||||||
previous = CachedUserData()
|
previous = CachedUserData()
|
||||||
}
|
}
|
||||||
return previous.withUpdatedIsBlocked(true)
|
if case .stories = subject {
|
||||||
|
var userFlags = previous.flags
|
||||||
|
userFlags.insert(.isBlockedFromMyStories)
|
||||||
|
return previous.withUpdatedFlags(userFlags)
|
||||||
|
} else {
|
||||||
|
return previous.withUpdatedIsBlocked(true)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -188,6 +238,13 @@ public final class BlockedPeersContext {
|
|||||||
assert(Queue.mainQueue().isCurrent())
|
assert(Queue.mainQueue().isCurrent())
|
||||||
let postbox = self.account.postbox
|
let postbox = self.account.postbox
|
||||||
let network = self.account.network
|
let network = self.account.network
|
||||||
|
let subject = self.subject
|
||||||
|
|
||||||
|
var flags: Int32 = 0
|
||||||
|
if case .stories = self.subject {
|
||||||
|
flags |= 1 << 0
|
||||||
|
}
|
||||||
|
|
||||||
return self.account.postbox.transaction { transaction -> Api.InputPeer? in
|
return self.account.postbox.transaction { transaction -> Api.InputPeer? in
|
||||||
return transaction.getPeer(peerId).flatMap(apiInputPeer)
|
return transaction.getPeer(peerId).flatMap(apiInputPeer)
|
||||||
}
|
}
|
||||||
@ -196,7 +253,7 @@ public final class BlockedPeersContext {
|
|||||||
guard let inputPeer = inputPeer else {
|
guard let inputPeer = inputPeer else {
|
||||||
return .fail(.generic)
|
return .fail(.generic)
|
||||||
}
|
}
|
||||||
return network.request(Api.functions.contacts.unblock(id: inputPeer))
|
return network.request(Api.functions.contacts.unblock(flags: flags, id: inputPeer))
|
||||||
|> mapError { _ -> BlockedPeersContextRemoveError in
|
|> mapError { _ -> BlockedPeersContextRemoveError in
|
||||||
return .generic
|
return .generic
|
||||||
}
|
}
|
||||||
@ -210,7 +267,13 @@ public final class BlockedPeersContext {
|
|||||||
} else {
|
} else {
|
||||||
previous = CachedUserData()
|
previous = CachedUserData()
|
||||||
}
|
}
|
||||||
return previous.withUpdatedIsBlocked(false)
|
if case .stories = subject {
|
||||||
|
var userFlags = previous.flags
|
||||||
|
userFlags.remove(.isBlockedFromMyStories)
|
||||||
|
return previous.withUpdatedFlags(userFlags)
|
||||||
|
} else {
|
||||||
|
return previous.withUpdatedIsBlocked(false)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return transaction.getPeer(peerId)
|
return transaction.getPeer(peerId)
|
||||||
|
@ -13,6 +13,10 @@ public extension TelegramEngine {
|
|||||||
return _internal_requestUpdatePeerIsBlocked(account: self.account, peerId: peerId, isBlocked: isBlocked)
|
return _internal_requestUpdatePeerIsBlocked(account: self.account, peerId: peerId, isBlocked: isBlocked)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func requestUpdatePeerIsBlockedFromStories(peerId: PeerId, isBlocked: Bool) -> Signal<Void, NoError> {
|
||||||
|
return _internal_requestUpdatePeerIsBlockedFromStories(account: self.account, peerId: peerId, isBlocked: isBlocked)
|
||||||
|
}
|
||||||
|
|
||||||
public func activeSessions() -> ActiveSessionsContext {
|
public func activeSessions() -> ActiveSessionsContext {
|
||||||
return ActiveSessionsContext(account: self.account)
|
return ActiveSessionsContext(account: self.account)
|
||||||
}
|
}
|
||||||
|
@ -686,6 +686,7 @@ final class AvatarEditorScreenComponent: Component {
|
|||||||
customLayout: nil,
|
customLayout: nil,
|
||||||
externalBackground: nil,
|
externalBackground: nil,
|
||||||
externalExpansionView: nil,
|
externalExpansionView: nil,
|
||||||
|
customContentView: nil,
|
||||||
useOpaqueTheme: true,
|
useOpaqueTheme: true,
|
||||||
hideBackground: true,
|
hideBackground: true,
|
||||||
stateContext: nil,
|
stateContext: nil,
|
||||||
@ -815,6 +816,7 @@ final class AvatarEditorScreenComponent: Component {
|
|||||||
customLayout: nil,
|
customLayout: nil,
|
||||||
externalBackground: nil,
|
externalBackground: nil,
|
||||||
externalExpansionView: nil,
|
externalExpansionView: nil,
|
||||||
|
customContentView: nil,
|
||||||
useOpaqueTheme: true,
|
useOpaqueTheme: true,
|
||||||
hideBackground: true,
|
hideBackground: true,
|
||||||
stateContext: nil,
|
stateContext: nil,
|
||||||
|
@ -1309,6 +1309,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
|||||||
customLayout: nil,
|
customLayout: nil,
|
||||||
externalBackground: nil,
|
externalBackground: nil,
|
||||||
externalExpansionView: nil,
|
externalExpansionView: nil,
|
||||||
|
customContentView: nil,
|
||||||
useOpaqueTheme: false,
|
useOpaqueTheme: false,
|
||||||
hideBackground: false,
|
hideBackground: false,
|
||||||
stateContext: self.stateContext?.emojiState,
|
stateContext: self.stateContext?.emojiState,
|
||||||
@ -1608,13 +1609,13 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
|||||||
customLayout: nil,
|
customLayout: nil,
|
||||||
externalBackground: nil,
|
externalBackground: nil,
|
||||||
externalExpansionView: nil,
|
externalExpansionView: nil,
|
||||||
|
customContentView: nil,
|
||||||
useOpaqueTheme: false,
|
useOpaqueTheme: false,
|
||||||
hideBackground: false,
|
hideBackground: false,
|
||||||
stateContext: nil,
|
stateContext: nil,
|
||||||
addImage: nil
|
addImage: nil
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
self.inputDataDisposable = (combineLatest(queue: .mainQueue(),
|
self.inputDataDisposable = (combineLatest(queue: .mainQueue(),
|
||||||
updatedInputData,
|
updatedInputData,
|
||||||
.single(self.currentInputData.gifs) |> then(self.gifComponent.get() |> map(Optional.init)),
|
.single(self.currentInputData.gifs) |> then(self.gifComponent.get() |> map(Optional.init)),
|
||||||
@ -2510,6 +2511,7 @@ public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputVi
|
|||||||
customLayout: nil,
|
customLayout: nil,
|
||||||
externalBackground: nil,
|
externalBackground: nil,
|
||||||
externalExpansionView: nil,
|
externalExpansionView: nil,
|
||||||
|
customContentView: nil,
|
||||||
useOpaqueTheme: false,
|
useOpaqueTheme: false,
|
||||||
hideBackground: hideBackground,
|
hideBackground: hideBackground,
|
||||||
stateContext: nil,
|
stateContext: nil,
|
||||||
|
@ -674,6 +674,7 @@ public final class EmojiStatusSelectionController: ViewController {
|
|||||||
customLayout: nil,
|
customLayout: nil,
|
||||||
externalBackground: nil,
|
externalBackground: nil,
|
||||||
externalExpansionView: nil,
|
externalExpansionView: nil,
|
||||||
|
customContentView: nil,
|
||||||
useOpaqueTheme: true,
|
useOpaqueTheme: true,
|
||||||
hideBackground: false,
|
hideBackground: false,
|
||||||
stateContext: nil,
|
stateContext: nil,
|
||||||
|
@ -2210,6 +2210,12 @@ public protocol EmojiContentPeekBehavior: AnyObject {
|
|||||||
func setGestureRecognizerEnabled(view: UIView, isEnabled: Bool, itemAtPoint: @escaping (CGPoint) -> (AnyHashable, CALayer, TelegramMediaFile)?)
|
func setGestureRecognizerEnabled(view: UIView, isEnabled: Bool, itemAtPoint: @escaping (CGPoint) -> (AnyHashable, CALayer, TelegramMediaFile)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public protocol EmojiCustomContentView: UIView {
|
||||||
|
var tintContainerView: UIView { get }
|
||||||
|
|
||||||
|
func update(theme: PresentationTheme, useOpaqueTheme: Bool, availableSize: CGSize, transition: Transition) -> CGSize
|
||||||
|
}
|
||||||
|
|
||||||
public final class EmojiPagerContentComponent: Component {
|
public final class EmojiPagerContentComponent: Component {
|
||||||
public static let staticEmojiMapping: [(EmojiPagerContentComponent.StaticEmojiSegment, [String])] = {
|
public static let staticEmojiMapping: [(EmojiPagerContentComponent.StaticEmojiSegment, [String])] = {
|
||||||
guard let path = getAppBundle().path(forResource: "emoji1016", ofType: "txt") else {
|
guard let path = getAppBundle().path(forResource: "emoji1016", ofType: "txt") else {
|
||||||
@ -2263,7 +2269,7 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
self.isDisabled = isDisabled
|
self.isDisabled = isDisabled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct CustomLayout: Equatable {
|
public struct CustomLayout: Equatable {
|
||||||
public var itemsPerRow: Int
|
public var itemsPerRow: Int
|
||||||
public var itemSize: CGFloat
|
public var itemSize: CGFloat
|
||||||
@ -2322,6 +2328,7 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
public let customLayout: CustomLayout?
|
public let customLayout: CustomLayout?
|
||||||
public let externalBackground: ExternalBackground?
|
public let externalBackground: ExternalBackground?
|
||||||
public weak var externalExpansionView: UIView?
|
public weak var externalExpansionView: UIView?
|
||||||
|
public let customContentView: EmojiCustomContentView?
|
||||||
public let useOpaqueTheme: Bool
|
public let useOpaqueTheme: Bool
|
||||||
public let hideBackground: Bool
|
public let hideBackground: Bool
|
||||||
public let scrollingStickersGridPromise = ValuePromise<Bool>(false)
|
public let scrollingStickersGridPromise = ValuePromise<Bool>(false)
|
||||||
@ -2350,6 +2357,7 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
customLayout: CustomLayout?,
|
customLayout: CustomLayout?,
|
||||||
externalBackground: ExternalBackground?,
|
externalBackground: ExternalBackground?,
|
||||||
externalExpansionView: UIView?,
|
externalExpansionView: UIView?,
|
||||||
|
customContentView: EmojiCustomContentView?,
|
||||||
useOpaqueTheme: Bool,
|
useOpaqueTheme: Bool,
|
||||||
hideBackground: Bool,
|
hideBackground: Bool,
|
||||||
stateContext: StateContext?,
|
stateContext: StateContext?,
|
||||||
@ -2376,6 +2384,7 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
self.customLayout = customLayout
|
self.customLayout = customLayout
|
||||||
self.externalBackground = externalBackground
|
self.externalBackground = externalBackground
|
||||||
self.externalExpansionView = externalExpansionView
|
self.externalExpansionView = externalExpansionView
|
||||||
|
self.customContentView = customContentView
|
||||||
self.useOpaqueTheme = useOpaqueTheme
|
self.useOpaqueTheme = useOpaqueTheme
|
||||||
self.hideBackground = hideBackground
|
self.hideBackground = hideBackground
|
||||||
self.stateContext = stateContext
|
self.stateContext = stateContext
|
||||||
@ -2849,6 +2858,7 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
var verticalGroupDefaultSpacing: CGFloat
|
var verticalGroupDefaultSpacing: CGFloat
|
||||||
var verticalGroupFeaturedSpacing: CGFloat
|
var verticalGroupFeaturedSpacing: CGFloat
|
||||||
var itemsPerRow: Int
|
var itemsPerRow: Int
|
||||||
|
var customContentHeight: CGFloat
|
||||||
var contentSize: CGSize
|
var contentSize: CGSize
|
||||||
|
|
||||||
var searchInsets: UIEdgeInsets
|
var searchInsets: UIEdgeInsets
|
||||||
@ -2857,9 +2867,21 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
var premiumButtonInset: CGFloat
|
var premiumButtonInset: CGFloat
|
||||||
var premiumButtonHeight: CGFloat
|
var premiumButtonHeight: CGFloat
|
||||||
|
|
||||||
init(layoutType: ItemLayoutType, width: CGFloat, containerInsets: UIEdgeInsets, itemGroups: [ItemGroupDescription], expandedGroupIds: Set<AnyHashable>, curveNearBounds: Bool, displaySearch: Bool, isSearchActivated: Bool, customLayout: CustomLayout?) {
|
init(
|
||||||
|
layoutType: ItemLayoutType,
|
||||||
|
width: CGFloat,
|
||||||
|
containerInsets: UIEdgeInsets,
|
||||||
|
itemGroups: [ItemGroupDescription],
|
||||||
|
expandedGroupIds: Set<AnyHashable>,
|
||||||
|
curveNearBounds: Bool,
|
||||||
|
displaySearch: Bool,
|
||||||
|
isSearchActivated: Bool,
|
||||||
|
customContentHeight: CGFloat,
|
||||||
|
customLayout: CustomLayout?
|
||||||
|
) {
|
||||||
self.layoutType = layoutType
|
self.layoutType = layoutType
|
||||||
self.width = width
|
self.width = width
|
||||||
|
self.customContentHeight = customContentHeight
|
||||||
|
|
||||||
self.premiumButtonInset = 6.0
|
self.premiumButtonInset = 6.0
|
||||||
self.premiumButtonHeight = 50.0
|
self.premiumButtonHeight = 50.0
|
||||||
@ -2926,6 +2948,8 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
self.itemInsets.left = floorToScreenPixels((width - actualContentWidth) / 2.0)
|
self.itemInsets.left = floorToScreenPixels((width - actualContentWidth) / 2.0)
|
||||||
self.itemInsets.right = self.itemInsets.left
|
self.itemInsets.right = self.itemInsets.left
|
||||||
|
|
||||||
|
self.itemInsets.top += self.customContentHeight
|
||||||
|
|
||||||
if displaySearch {
|
if displaySearch {
|
||||||
self.itemInsets.top += self.searchHeight - 4.0
|
self.itemInsets.top += self.searchHeight - 4.0
|
||||||
}
|
}
|
||||||
@ -3651,6 +3675,7 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
private let placeholdersContainerView: UIView
|
private let placeholdersContainerView: UIView
|
||||||
private var visibleSearchHeader: EmojiSearchHeaderView?
|
private var visibleSearchHeader: EmojiSearchHeaderView?
|
||||||
private var visibleEmptySearchResultsView: EmptySearchResultsView?
|
private var visibleEmptySearchResultsView: EmptySearchResultsView?
|
||||||
|
private var visibleCustomContentView: EmojiCustomContentView?
|
||||||
private var visibleItemPlaceholderViews: [ItemLayer.Key: ItemPlaceholderView] = [:]
|
private var visibleItemPlaceholderViews: [ItemLayer.Key: ItemPlaceholderView] = [:]
|
||||||
private var visibleFillPlaceholdersViews: [Int: ItemPlaceholderView] = [:]
|
private var visibleFillPlaceholdersViews: [Int: ItemPlaceholderView] = [:]
|
||||||
private var visibleItemSelectionLayers: [ItemLayer.Key: ItemSelectionLayer] = [:]
|
private var visibleItemSelectionLayers: [ItemLayer.Key: ItemSelectionLayer] = [:]
|
||||||
@ -6357,6 +6382,7 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
var updatedItemPositions: [VisualItemKey: CGPoint]?
|
var updatedItemPositions: [VisualItemKey: CGPoint]?
|
||||||
|
|
||||||
let contentAnimation = transition.userData(ContentAnimation.self)
|
let contentAnimation = transition.userData(ContentAnimation.self)
|
||||||
|
let useOpaqueTheme = component.inputInteractionHolder.inputInteraction?.useOpaqueTheme ?? false
|
||||||
|
|
||||||
var transitionHintInstalledGroupId: AnyHashable?
|
var transitionHintInstalledGroupId: AnyHashable?
|
||||||
var transitionHintExpandedGroupId: AnyHashable?
|
var transitionHintExpandedGroupId: AnyHashable?
|
||||||
@ -6483,6 +6509,30 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
calculateUpdatedItemPositions = true
|
calculateUpdatedItemPositions = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var customContentHeight: CGFloat = 0.0
|
||||||
|
if let customContentView = component.inputInteractionHolder.inputInteraction?.customContentView {
|
||||||
|
var customContentViewTransition = transition
|
||||||
|
if let _ = self.visibleCustomContentView {
|
||||||
|
|
||||||
|
} else {
|
||||||
|
customContentViewTransition = .immediate
|
||||||
|
self.visibleCustomContentView = customContentView
|
||||||
|
self.scrollView.addSubview(customContentView)
|
||||||
|
self.mirrorContentScrollView.addSubview(customContentView.tintContainerView)
|
||||||
|
}
|
||||||
|
let availableCustomContentSize = availableSize
|
||||||
|
let customContentViewSize = customContentView.update(theme: keyboardChildEnvironment.theme, useOpaqueTheme: useOpaqueTheme, availableSize: availableCustomContentSize, transition: customContentViewTransition)
|
||||||
|
customContentViewTransition.setFrame(view: customContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: pagerEnvironment.containerInsets.top + (component.displaySearchWithPlaceholder != nil ? 54.0 : 0.0)), size: customContentViewSize))
|
||||||
|
|
||||||
|
customContentHeight = customContentViewSize.height
|
||||||
|
} else {
|
||||||
|
if let visibleCustomContentView = self.visibleCustomContentView {
|
||||||
|
self.visibleCustomContentView = nil
|
||||||
|
visibleCustomContentView.removeFromSuperview()
|
||||||
|
visibleCustomContentView.tintContainerView.removeFromSuperview()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var itemGroups: [ItemGroupDescription] = []
|
var itemGroups: [ItemGroupDescription] = []
|
||||||
for itemGroup in component.contentItemGroups {
|
for itemGroup in component.contentItemGroups {
|
||||||
itemGroups.append(ItemGroupDescription(
|
itemGroups.append(ItemGroupDescription(
|
||||||
@ -6508,6 +6558,7 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
curveNearBounds: component.warpContentsOnEdges,
|
curveNearBounds: component.warpContentsOnEdges,
|
||||||
displaySearch: component.displaySearchWithPlaceholder != nil,
|
displaySearch: component.displaySearchWithPlaceholder != nil,
|
||||||
isSearchActivated: self.isSearchActivated,
|
isSearchActivated: self.isSearchActivated,
|
||||||
|
customContentHeight: customContentHeight,
|
||||||
customLayout: component.inputInteractionHolder.inputInteraction?.customLayout
|
customLayout: component.inputInteractionHolder.inputInteraction?.customLayout
|
||||||
)
|
)
|
||||||
let itemLayout = extractedExpr
|
let itemLayout = extractedExpr
|
||||||
@ -6704,7 +6755,6 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let useOpaqueTheme = component.inputInteractionHolder.inputInteraction?.useOpaqueTheme ?? false
|
|
||||||
|
|
||||||
if let displaySearchWithPlaceholder = component.displaySearchWithPlaceholder {
|
if let displaySearchWithPlaceholder = component.displaySearchWithPlaceholder {
|
||||||
let visibleSearchHeader: EmojiSearchHeaderView
|
let visibleSearchHeader: EmojiSearchHeaderView
|
||||||
@ -6812,8 +6862,7 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
visibleSearchHeader.tintContainerView.removeFromSuperview()
|
visibleSearchHeader.tintContainerView.removeFromSuperview()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if let emptySearchResults = component.emptySearchResults {
|
if let emptySearchResults = component.emptySearchResults {
|
||||||
let visibleEmptySearchResultsView: EmptySearchResultsView
|
let visibleEmptySearchResultsView: EmptySearchResultsView
|
||||||
var emptySearchResultsTransition = transition
|
var emptySearchResultsTransition = transition
|
||||||
|
@ -407,6 +407,7 @@ public final class EmojiSearchContent: ASDisplayNode, EntitySearchContainerNode
|
|||||||
customLayout: nil,
|
customLayout: nil,
|
||||||
externalBackground: nil,
|
externalBackground: nil,
|
||||||
externalExpansionView: nil,
|
externalExpansionView: nil,
|
||||||
|
customContentView: nil,
|
||||||
useOpaqueTheme: true,
|
useOpaqueTheme: true,
|
||||||
hideBackground: false,
|
hideBackground: false,
|
||||||
stateContext: nil,
|
stateContext: nil,
|
||||||
|
@ -948,6 +948,7 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent {
|
|||||||
customLayout: nil,
|
customLayout: nil,
|
||||||
externalBackground: nil,
|
externalBackground: nil,
|
||||||
externalExpansionView: nil,
|
externalExpansionView: nil,
|
||||||
|
customContentView: nil,
|
||||||
useOpaqueTheme: true,
|
useOpaqueTheme: true,
|
||||||
hideBackground: false,
|
hideBackground: false,
|
||||||
stateContext: nil,
|
stateContext: nil,
|
||||||
|
@ -10,6 +10,7 @@ public enum CodableDrawingEntity: Equatable {
|
|||||||
case simpleShape(DrawingSimpleShapeEntity)
|
case simpleShape(DrawingSimpleShapeEntity)
|
||||||
case bubble(DrawingBubbleEntity)
|
case bubble(DrawingBubbleEntity)
|
||||||
case vector(DrawingVectorEntity)
|
case vector(DrawingVectorEntity)
|
||||||
|
case location(DrawingLocationEntity)
|
||||||
|
|
||||||
public init?(entity: DrawingEntity) {
|
public init?(entity: DrawingEntity) {
|
||||||
if let entity = entity as? DrawingStickerEntity {
|
if let entity = entity as? DrawingStickerEntity {
|
||||||
@ -22,6 +23,8 @@ public enum CodableDrawingEntity: Equatable {
|
|||||||
self = .bubble(entity)
|
self = .bubble(entity)
|
||||||
} else if let entity = entity as? DrawingVectorEntity {
|
} else if let entity = entity as? DrawingVectorEntity {
|
||||||
self = .vector(entity)
|
self = .vector(entity)
|
||||||
|
} else if let entity = entity as? DrawingLocationEntity {
|
||||||
|
self = .location(entity)
|
||||||
} else {
|
} else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -39,6 +42,8 @@ public enum CodableDrawingEntity: Equatable {
|
|||||||
return entity
|
return entity
|
||||||
case let .vector(entity):
|
case let .vector(entity):
|
||||||
return entity
|
return entity
|
||||||
|
case let .location(entity):
|
||||||
|
return entity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -55,6 +60,7 @@ extension CodableDrawingEntity: Codable {
|
|||||||
case simpleShape
|
case simpleShape
|
||||||
case bubble
|
case bubble
|
||||||
case vector
|
case vector
|
||||||
|
case location
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(from decoder: Decoder) throws {
|
public init(from decoder: Decoder) throws {
|
||||||
@ -71,6 +77,8 @@ extension CodableDrawingEntity: Codable {
|
|||||||
self = .bubble(try container.decode(DrawingBubbleEntity.self, forKey: .entity))
|
self = .bubble(try container.decode(DrawingBubbleEntity.self, forKey: .entity))
|
||||||
case .vector:
|
case .vector:
|
||||||
self = .vector(try container.decode(DrawingVectorEntity.self, forKey: .entity))
|
self = .vector(try container.decode(DrawingVectorEntity.self, forKey: .entity))
|
||||||
|
case .location:
|
||||||
|
self = .location(try container.decode(DrawingLocationEntity.self, forKey: .entity))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,6 +100,9 @@ extension CodableDrawingEntity: Codable {
|
|||||||
case let .vector(payload):
|
case let .vector(payload):
|
||||||
try container.encode(EntityType.vector, forKey: .type)
|
try container.encode(EntityType.vector, forKey: .type)
|
||||||
try container.encode(payload, forKey: .entity)
|
try container.encode(payload, forKey: .entity)
|
||||||
|
case let .location(payload):
|
||||||
|
try container.encode(EntityType.location, forKey: .type)
|
||||||
|
try container.encode(payload, forKey: .entity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,151 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Display
|
||||||
|
import AccountContext
|
||||||
|
import TextFormat
|
||||||
|
import Postbox
|
||||||
|
import TelegramCore
|
||||||
|
|
||||||
|
public final class DrawingLocationEntity: DrawingEntity, Codable {
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case uuid
|
||||||
|
case title
|
||||||
|
case style
|
||||||
|
case location
|
||||||
|
case referenceDrawingSize
|
||||||
|
case position
|
||||||
|
case width
|
||||||
|
case scale
|
||||||
|
case rotation
|
||||||
|
case renderImage
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Style: Codable, Equatable {
|
||||||
|
case white
|
||||||
|
case black
|
||||||
|
case transparent
|
||||||
|
case blur
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public var uuid: UUID
|
||||||
|
public var isAnimated: Bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
public var title: String
|
||||||
|
public var style: Style
|
||||||
|
public var location: TelegramMediaMap
|
||||||
|
public var color: DrawingColor = .clear
|
||||||
|
public var lineWidth: CGFloat = 0.0
|
||||||
|
|
||||||
|
public var referenceDrawingSize: CGSize
|
||||||
|
public var position: CGPoint
|
||||||
|
public var width: CGFloat
|
||||||
|
public var scale: CGFloat
|
||||||
|
public var rotation: CGFloat
|
||||||
|
|
||||||
|
public var center: CGPoint {
|
||||||
|
return self.position
|
||||||
|
}
|
||||||
|
|
||||||
|
public var renderImage: UIImage?
|
||||||
|
public var renderSubEntities: [DrawingEntity]?
|
||||||
|
|
||||||
|
public var isMedia: Bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(title: String, style: Style, location: TelegramMediaMap) {
|
||||||
|
self.uuid = UUID()
|
||||||
|
|
||||||
|
self.title = title
|
||||||
|
self.style = style
|
||||||
|
self.location = location
|
||||||
|
|
||||||
|
self.referenceDrawingSize = .zero
|
||||||
|
self.position = .zero
|
||||||
|
self.width = 100.0
|
||||||
|
self.scale = 1.0
|
||||||
|
self.rotation = 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
self.uuid = try container.decode(UUID.self, forKey: .uuid)
|
||||||
|
self.title = try container.decode(String.self, forKey: .title)
|
||||||
|
self.style = try container.decode(Style.self, forKey: .style)
|
||||||
|
|
||||||
|
let locationData = try container.decode(AdaptedPostboxDecoder.RawObjectData.self, forKey: .location)
|
||||||
|
self.location = TelegramMediaMap(decoder: PostboxDecoder(buffer: MemoryBuffer(data: locationData.data)))
|
||||||
|
|
||||||
|
self.referenceDrawingSize = try container.decode(CGSize.self, forKey: .referenceDrawingSize)
|
||||||
|
self.position = try container.decode(CGPoint.self, forKey: .position)
|
||||||
|
self.width = try container.decode(CGFloat.self, forKey: .width)
|
||||||
|
self.scale = try container.decode(CGFloat.self, forKey: .scale)
|
||||||
|
self.rotation = try container.decode(CGFloat.self, forKey: .rotation)
|
||||||
|
if let renderImageData = try? container.decodeIfPresent(Data.self, forKey: .renderImage) {
|
||||||
|
self.renderImage = UIImage(data: renderImageData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
try container.encode(self.uuid, forKey: .uuid)
|
||||||
|
try container.encode(self.title, forKey: .title)
|
||||||
|
try container.encode(self.style, forKey: .style)
|
||||||
|
// try container.encode(PostboxEncoder().encodeObjectToRawData(self.location), forKey: .location)
|
||||||
|
try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize)
|
||||||
|
try container.encode(self.position, forKey: .position)
|
||||||
|
try container.encode(self.width, forKey: .width)
|
||||||
|
try container.encode(self.scale, forKey: .scale)
|
||||||
|
try container.encode(self.rotation, forKey: .rotation)
|
||||||
|
if let renderImage, let data = renderImage.pngData() {
|
||||||
|
try container.encode(data, forKey: .renderImage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func duplicate() -> DrawingEntity {
|
||||||
|
let newEntity = DrawingLocationEntity(title: self.title, style: self.style, location: self.location)
|
||||||
|
newEntity.referenceDrawingSize = self.referenceDrawingSize
|
||||||
|
newEntity.position = self.position
|
||||||
|
newEntity.width = self.width
|
||||||
|
newEntity.scale = self.scale
|
||||||
|
newEntity.rotation = self.rotation
|
||||||
|
return newEntity
|
||||||
|
}
|
||||||
|
|
||||||
|
public func isEqual(to other: DrawingEntity) -> Bool {
|
||||||
|
guard let other = other as? DrawingLocationEntity else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if self.uuid != other.uuid {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if self.title != other.title {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if self.style != other.style {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if self.location != other.location {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if self.referenceDrawingSize != other.referenceDrawingSize {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if self.position != other.position {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if self.width != other.width {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if self.scale != other.scale {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if self.rotation != other.rotation {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
@ -12,11 +12,25 @@ import TelegramAnimatedStickerNode
|
|||||||
import YuvConversion
|
import YuvConversion
|
||||||
import StickerResources
|
import StickerResources
|
||||||
|
|
||||||
private func prerenderTextTransformations(entity: DrawingTextEntity, image: UIImage, textScale: CGFloat, colorSpace: CGColorSpace) -> MediaEditorComposerStaticEntity {
|
private func prerenderTextTransformations(entity: DrawingEntity, image: UIImage, textScale: CGFloat, colorSpace: CGColorSpace) -> MediaEditorComposerStaticEntity {
|
||||||
let imageSize = image.size
|
let imageSize = image.size
|
||||||
|
|
||||||
let angle = -entity.rotation
|
let angle: CGFloat
|
||||||
let scale = entity.scale * 0.5 * textScale
|
var scale: CGFloat
|
||||||
|
let position: CGPoint
|
||||||
|
|
||||||
|
if let entity = entity as? DrawingTextEntity {
|
||||||
|
angle = -entity.rotation
|
||||||
|
scale = entity.scale
|
||||||
|
position = entity.position
|
||||||
|
} else if let entity = entity as? DrawingLocationEntity {
|
||||||
|
angle = -entity.rotation
|
||||||
|
scale = entity.scale
|
||||||
|
position = entity.position
|
||||||
|
} else {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
scale *= 0.5 * textScale
|
||||||
|
|
||||||
let rotatedSize = CGSize(
|
let rotatedSize = CGSize(
|
||||||
width: abs(imageSize.width * cos(angle)) + abs(imageSize.height * sin(angle)),
|
width: abs(imageSize.width * cos(angle)) + abs(imageSize.height * sin(angle)),
|
||||||
@ -43,7 +57,7 @@ private func prerenderTextTransformations(entity: DrawingTextEntity, image: UIIm
|
|||||||
}
|
}
|
||||||
}, scale: 1.0)!
|
}, scale: 1.0)!
|
||||||
|
|
||||||
return MediaEditorComposerStaticEntity(image: CIImage(image: newImage, options: [.colorSpace: colorSpace])!, position: entity.position, scale: 1.0, rotation: 0.0, baseSize: nil, baseDrawingSize: CGSize(width: 1080, height: 1920), mirrored: false)
|
return MediaEditorComposerStaticEntity(image: CIImage(image: newImage, options: [.colorSpace: colorSpace])!, position: position, scale: 1.0, rotation: 0.0, baseSize: nil, baseDrawingSize: CGSize(width: 1080, height: 1920), mirrored: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func composerEntitiesForDrawingEntity(account: Account, textScale: CGFloat, entity: DrawingEntity, colorSpace: CGColorSpace, tintColor: UIColor? = nil) -> [MediaEditorComposerEntity] {
|
func composerEntitiesForDrawingEntity(account: Account, textScale: CGFloat, entity: DrawingEntity, colorSpace: CGColorSpace, tintColor: UIColor? = nil) -> [MediaEditorComposerEntity] {
|
||||||
@ -70,13 +84,14 @@ func composerEntitiesForDrawingEntity(account: Account, textScale: CGFloat, enti
|
|||||||
} else if let entity = entity as? DrawingTextEntity {
|
} else if let entity = entity as? DrawingTextEntity {
|
||||||
var entities: [MediaEditorComposerEntity] = []
|
var entities: [MediaEditorComposerEntity] = []
|
||||||
entities.append(prerenderTextTransformations(entity: entity, image: renderImage, textScale: textScale, colorSpace: colorSpace))
|
entities.append(prerenderTextTransformations(entity: entity, image: renderImage, textScale: textScale, colorSpace: colorSpace))
|
||||||
|
|
||||||
if let renderSubEntities = entity.renderSubEntities {
|
if let renderSubEntities = entity.renderSubEntities {
|
||||||
for subEntity in renderSubEntities {
|
for subEntity in renderSubEntities {
|
||||||
entities.append(contentsOf: composerEntitiesForDrawingEntity(account: account, textScale: textScale, entity: subEntity, colorSpace: colorSpace, tintColor: entity.color.toUIColor()))
|
entities.append(contentsOf: composerEntitiesForDrawingEntity(account: account, textScale: textScale, entity: subEntity, colorSpace: colorSpace, tintColor: entity.color.toUIColor()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return entities
|
return entities
|
||||||
|
} else if let entity = entity as? DrawingLocationEntity {
|
||||||
|
return [prerenderTextTransformations(entity: entity, image: renderImage, textScale: textScale, colorSpace: colorSpace)]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return []
|
return []
|
||||||
|
@ -42,6 +42,7 @@ swift_library(
|
|||||||
"//submodules/TelegramUI/Components/CameraButtonComponent",
|
"//submodules/TelegramUI/Components/CameraButtonComponent",
|
||||||
"//submodules/ChatPresentationInterfaceState",
|
"//submodules/ChatPresentationInterfaceState",
|
||||||
"//submodules/DeviceAccess",
|
"//submodules/DeviceAccess",
|
||||||
|
"//submodules/LocationUI",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
@ -31,6 +31,7 @@ import ChatEntityKeyboardInputNode
|
|||||||
import ChatPresentationInterfaceState
|
import ChatPresentationInterfaceState
|
||||||
import TextFormat
|
import TextFormat
|
||||||
import DeviceAccess
|
import DeviceAccess
|
||||||
|
import LocationUI
|
||||||
|
|
||||||
enum DrawingScreenType {
|
enum DrawingScreenType {
|
||||||
case drawing
|
case drawing
|
||||||
@ -1148,6 +1149,18 @@ final class MediaEditorScreenComponent: Component {
|
|||||||
forwardAction: nil,
|
forwardAction: nil,
|
||||||
moreAction: nil,
|
moreAction: nil,
|
||||||
presentVoiceMessagesUnavailableTooltip: nil,
|
presentVoiceMessagesUnavailableTooltip: nil,
|
||||||
|
presentTextLengthLimitTooltip: { [weak self] in
|
||||||
|
guard let self, let controller = self.environment?.controller() as? MediaEditorScreen else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
controller.presentCaptionLimitPremiumSuggestion()
|
||||||
|
},
|
||||||
|
presentTextFormattingTooltip: { [weak self] in
|
||||||
|
guard let self, let controller = self.environment?.controller() as? MediaEditorScreen else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
controller.presentCaptionEntitiesPremiumSuggestion()
|
||||||
|
},
|
||||||
paste: { [weak self] data in
|
paste: { [weak self] data in
|
||||||
guard let self, let environment = self.environment, let controller = environment.controller() as? MediaEditorScreen else {
|
guard let self, let environment = self.environment, let controller = environment.controller() as? MediaEditorScreen else {
|
||||||
return
|
return
|
||||||
@ -1179,6 +1192,7 @@ final class MediaEditorScreenComponent: Component {
|
|||||||
timeoutSelected: timeoutSelected,
|
timeoutSelected: timeoutSelected,
|
||||||
displayGradient: false,
|
displayGradient: false,
|
||||||
bottomInset: 0.0,
|
bottomInset: 0.0,
|
||||||
|
isFormattingLocked: false,
|
||||||
hideKeyboard: self.currentInputMode == .emoji,
|
hideKeyboard: self.currentInputMode == .emoji,
|
||||||
forceIsEditing: self.currentInputMode == .emoji,
|
forceIsEditing: self.currentInputMode == .emoji,
|
||||||
disabledPlaceholder: nil
|
disabledPlaceholder: nil
|
||||||
@ -2684,6 +2698,25 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||||||
controller.push(galleryController)
|
controller.push(galleryController)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func presentLocationPicker() {
|
||||||
|
guard let controller = self.controller else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let locationController = standaloneLocationPickerController(context: self.context, completion: { [weak self] location in
|
||||||
|
if let self {
|
||||||
|
let title = location.venue?.title ?? "LOCATION"
|
||||||
|
self.interaction?.insertEntity(DrawingLocationEntity(title: title, style: .white, location: location), scale: 1.0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
locationController.customModalStyleOverlayTransitionFactorUpdated = { [weak self, weak locationController] transition in
|
||||||
|
if let self, let locationController {
|
||||||
|
let transitionFactor = locationController.modalStyleOverlayTransitionFactor
|
||||||
|
self.updateModalTransitionFactor(transitionFactor, transition: transition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
controller.push(locationController)
|
||||||
|
}
|
||||||
|
|
||||||
func updateModalTransitionFactor(_ value: CGFloat, transition: ContainedViewLayoutTransition) {
|
func updateModalTransitionFactor(_ value: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||||
guard let layout = self.validLayout, case .compact = layout.metrics.widthClass else {
|
guard let layout = self.validLayout, case .compact = layout.metrics.widthClass else {
|
||||||
return
|
return
|
||||||
@ -2852,6 +2885,12 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||||||
self.presentGallery()
|
self.presentGallery()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
controller.presentLocationPicker = { [weak self, weak controller] in
|
||||||
|
if let self {
|
||||||
|
controller?.dismiss(animated: true)
|
||||||
|
self.presentLocationPicker()
|
||||||
|
}
|
||||||
|
}
|
||||||
self.stickerScreen = controller
|
self.stickerScreen = controller
|
||||||
self.controller?.present(controller, in: .window(.root))
|
self.controller?.present(controller, in: .window(.root))
|
||||||
return
|
return
|
||||||
@ -3175,6 +3214,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||||||
public var willDismiss: () -> Void = { }
|
public var willDismiss: () -> Void = { }
|
||||||
|
|
||||||
private var closeFriends = Promise<[EnginePeer]>()
|
private var closeFriends = Promise<[EnginePeer]>()
|
||||||
|
private let storiesGrayList: BlockedPeersContext
|
||||||
|
|
||||||
private let hapticFeedback = HapticFeedback()
|
private let hapticFeedback = HapticFeedback()
|
||||||
|
|
||||||
@ -3199,6 +3239,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||||||
self.transitionOut = transitionOut
|
self.transitionOut = transitionOut
|
||||||
self.completion = completion
|
self.completion = completion
|
||||||
|
|
||||||
|
self.storiesGrayList = BlockedPeersContext(account: context.account, subject: .stories)
|
||||||
|
|
||||||
if let transitionIn, case .camera = transitionIn {
|
if let transitionIn, case .camera = transitionIn {
|
||||||
self.isSavingAvailable = true
|
self.isSavingAvailable = true
|
||||||
}
|
}
|
||||||
@ -3271,7 +3313,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||||||
context: self.context,
|
context: self.context,
|
||||||
subject: .stories(editing: false),
|
subject: .stories(editing: false),
|
||||||
initialPeerIds: Set(privacy.privacy.additionallyIncludePeers),
|
initialPeerIds: Set(privacy.privacy.additionallyIncludePeers),
|
||||||
closeFriends: self.closeFriends.get()
|
closeFriends: self.closeFriends.get(),
|
||||||
|
storiesGrayList: self.storiesGrayList
|
||||||
)
|
)
|
||||||
let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in
|
let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
@ -3299,7 +3342,23 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.openEditCategory(privacy: privacy, isForwardingDisabled: !allowScreenshots, pin: pin, completion: { [weak self] privacy in
|
self.openEditCategory(privacy: privacy, isForwardingDisabled: !allowScreenshots, pin: pin, grayList: false, completion: { [weak self] privacy in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.openPrivacySettings(MediaEditorResultPrivacy(
|
||||||
|
privacy: privacy,
|
||||||
|
timeout: timeout,
|
||||||
|
isForwardingDisabled: !allowScreenshots,
|
||||||
|
pin: pin
|
||||||
|
), completion: completion)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
editGrayList: { [weak self] privacy, allowScreenshots, pin in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.openEditCategory(privacy: privacy, isForwardingDisabled: !allowScreenshots, pin: pin, grayList: true, completion: { [weak self] privacy in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -3319,14 +3378,21 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private func openEditCategory(privacy: EngineStoryPrivacy, isForwardingDisabled: Bool, pin: Bool, completion: @escaping (EngineStoryPrivacy) -> Void) {
|
private func openEditCategory(privacy: EngineStoryPrivacy, isForwardingDisabled: Bool, pin: Bool, grayList: Bool, completion: @escaping (EngineStoryPrivacy) -> Void) {
|
||||||
let subject: ShareWithPeersScreen.StateContext.Subject
|
let subject: ShareWithPeersScreen.StateContext.Subject
|
||||||
if privacy.base == .nobody {
|
if grayList {
|
||||||
subject = .chats
|
subject = .chats(grayList: true)
|
||||||
|
} else if privacy.base == .nobody {
|
||||||
|
subject = .chats(grayList: false)
|
||||||
} else {
|
} else {
|
||||||
subject = .contacts(privacy.base)
|
subject = .contacts(privacy.base)
|
||||||
}
|
}
|
||||||
let stateContext = ShareWithPeersScreen.StateContext(context: self.context, subject: subject, initialPeerIds: Set(privacy.additionallyIncludePeers))
|
let stateContext = ShareWithPeersScreen.StateContext(
|
||||||
|
context: self.context,
|
||||||
|
subject: subject,
|
||||||
|
initialPeerIds: Set(privacy.additionallyIncludePeers),
|
||||||
|
storiesGrayList: self.storiesGrayList
|
||||||
|
)
|
||||||
let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in
|
let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
@ -3341,7 +3407,10 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if case .closeFriends = privacy.base {
|
if grayList {
|
||||||
|
let _ = self.storiesGrayList.updatePeerIds(result.additionallyIncludePeers).start()
|
||||||
|
completion(privacy)
|
||||||
|
} else if case .closeFriends = privacy.base {
|
||||||
let _ = self.context.engine.privacy.updateCloseFriends(peerIds: result.additionallyIncludePeers).start()
|
let _ = self.context.engine.privacy.updateCloseFriends(peerIds: result.additionallyIncludePeers).start()
|
||||||
self.closeFriends.set(.single(peers))
|
self.closeFriends.set(.single(peers))
|
||||||
completion(EngineStoryPrivacy(base: .closeFriends, additionallyIncludePeers: []))
|
completion(EngineStoryPrivacy(base: .closeFriends, additionallyIncludePeers: []))
|
||||||
@ -3349,7 +3418,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||||||
completion(result)
|
completion(result)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
editCategory: { _, _, _ in }
|
editCategory: { _, _, _ in },
|
||||||
|
editGrayList: { _, _, _ in }
|
||||||
)
|
)
|
||||||
controller.dismissed = {
|
controller.dismissed = {
|
||||||
self.node.mediaEditor?.play()
|
self.node.mediaEditor?.play()
|
||||||
@ -3435,15 +3505,54 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||||||
self.present(contextController, in: .window(.root))
|
self.present(contextController, in: .window(.root))
|
||||||
}
|
}
|
||||||
|
|
||||||
private func presentTimeoutPremiumSuggestion() {
|
fileprivate func presentTimeoutPremiumSuggestion() {
|
||||||
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
self.dismissAllTooltips()
|
||||||
|
|
||||||
let text = presentationData.strings.Story_Editor_TooltipPremiumExpiration
|
|
||||||
|
|
||||||
let context = self.context
|
let context = self.context
|
||||||
|
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||||
|
|
||||||
|
let text = presentationData.strings.Story_Editor_TooltipPremiumExpiration
|
||||||
|
|
||||||
let controller = UndoOverlayController(presentationData: presentationData, content: .autoDelete(isOn: true, title: nil, text: text, customUndoText: nil), elevatedLayout: false, position: .top, animateInAsReplacement: false, action: { [weak self] action in
|
let controller = UndoOverlayController(presentationData: presentationData, content: .autoDelete(isOn: true, title: nil, text: text, customUndoText: nil), elevatedLayout: false, position: .top, animateInAsReplacement: false, action: { [weak self] action in
|
||||||
if case .undo = action, let self {
|
if case .info = action, let self {
|
||||||
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .settings)
|
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .stories)
|
||||||
|
self.push(controller)
|
||||||
|
}
|
||||||
|
return false }
|
||||||
|
)
|
||||||
|
self.present(controller, in: .current)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate func presentCaptionLimitPremiumSuggestion() {
|
||||||
|
self.dismissAllTooltips()
|
||||||
|
|
||||||
|
let context = self.context
|
||||||
|
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||||
|
|
||||||
|
let title = presentationData.strings.Story_Editor_TooltipPremiumCaptionLimitTitle
|
||||||
|
let text = presentationData.strings.Story_Editor_TooltipPremiumCaptionLimitText
|
||||||
|
|
||||||
|
let controller = UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_read", scale: 0.25, colors: [:], title: title, text: text, customUndoText: nil, timeout: nil), elevatedLayout: false, position: .top, animateInAsReplacement: false, action: { [weak self] action in
|
||||||
|
if case .info = action, let self {
|
||||||
|
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .stories)
|
||||||
|
self.push(controller)
|
||||||
|
}
|
||||||
|
return false }
|
||||||
|
)
|
||||||
|
self.present(controller, in: .current)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate func presentCaptionEntitiesPremiumSuggestion() {
|
||||||
|
self.dismissAllTooltips()
|
||||||
|
|
||||||
|
let context = self.context
|
||||||
|
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||||
|
|
||||||
|
let text = presentationData.strings.Story_Editor_TooltipPremiumCaptionEntities
|
||||||
|
|
||||||
|
let controller = UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: text), elevatedLayout: false, position: .top, animateInAsReplacement: false, action: { [weak self] action in
|
||||||
|
if case .info = action, let self {
|
||||||
|
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .stories)
|
||||||
self.push(controller)
|
self.push(controller)
|
||||||
}
|
}
|
||||||
return false }
|
return false }
|
||||||
|
@ -271,6 +271,8 @@ final class StoryPreviewComponent: Component {
|
|||||||
forwardAction: {},
|
forwardAction: {},
|
||||||
moreAction: { _, _ in },
|
moreAction: { _, _ in },
|
||||||
presentVoiceMessagesUnavailableTooltip: nil,
|
presentVoiceMessagesUnavailableTooltip: nil,
|
||||||
|
presentTextLengthLimitTooltip: nil,
|
||||||
|
presentTextFormattingTooltip: nil,
|
||||||
paste: { _ in },
|
paste: { _ in },
|
||||||
audioRecorder: nil,
|
audioRecorder: nil,
|
||||||
videoRecordingStatus: nil,
|
videoRecordingStatus: nil,
|
||||||
@ -282,6 +284,7 @@ final class StoryPreviewComponent: Component {
|
|||||||
timeoutSelected: false,
|
timeoutSelected: false,
|
||||||
displayGradient: false,
|
displayGradient: false,
|
||||||
bottomInset: 0.0,
|
bottomInset: 0.0,
|
||||||
|
isFormattingLocked: false,
|
||||||
hideKeyboard: false,
|
hideKeyboard: false,
|
||||||
forceIsEditing: false,
|
forceIsEditing: false,
|
||||||
disabledPlaceholder: nil
|
disabledPlaceholder: nil
|
||||||
|
@ -84,6 +84,8 @@ public final class MessageInputPanelComponent: Component {
|
|||||||
public let forwardAction: (() -> Void)?
|
public let forwardAction: (() -> Void)?
|
||||||
public let moreAction: ((UIView, ContextGesture?) -> Void)?
|
public let moreAction: ((UIView, ContextGesture?) -> Void)?
|
||||||
public let presentVoiceMessagesUnavailableTooltip: ((UIView) -> Void)?
|
public let presentVoiceMessagesUnavailableTooltip: ((UIView) -> Void)?
|
||||||
|
public let presentTextLengthLimitTooltip: (() -> Void)?
|
||||||
|
public let presentTextFormattingTooltip: (() -> Void)?
|
||||||
public let paste: (TextFieldComponent.PasteData) -> Void
|
public let paste: (TextFieldComponent.PasteData) -> Void
|
||||||
public let audioRecorder: ManagedAudioRecorder?
|
public let audioRecorder: ManagedAudioRecorder?
|
||||||
public let videoRecordingStatus: InstantVideoControllerRecordingStatus?
|
public let videoRecordingStatus: InstantVideoControllerRecordingStatus?
|
||||||
@ -95,6 +97,7 @@ public final class MessageInputPanelComponent: Component {
|
|||||||
public let timeoutSelected: Bool
|
public let timeoutSelected: Bool
|
||||||
public let displayGradient: Bool
|
public let displayGradient: Bool
|
||||||
public let bottomInset: CGFloat
|
public let bottomInset: CGFloat
|
||||||
|
public let isFormattingLocked: Bool
|
||||||
public let hideKeyboard: Bool
|
public let hideKeyboard: Bool
|
||||||
public let forceIsEditing: Bool
|
public let forceIsEditing: Bool
|
||||||
public let disabledPlaceholder: String?
|
public let disabledPlaceholder: String?
|
||||||
@ -126,6 +129,8 @@ public final class MessageInputPanelComponent: Component {
|
|||||||
forwardAction: (() -> Void)?,
|
forwardAction: (() -> Void)?,
|
||||||
moreAction: ((UIView, ContextGesture?) -> Void)?,
|
moreAction: ((UIView, ContextGesture?) -> Void)?,
|
||||||
presentVoiceMessagesUnavailableTooltip: ((UIView) -> Void)?,
|
presentVoiceMessagesUnavailableTooltip: ((UIView) -> Void)?,
|
||||||
|
presentTextLengthLimitTooltip: (() -> Void)?,
|
||||||
|
presentTextFormattingTooltip: (() -> Void)?,
|
||||||
paste: @escaping (TextFieldComponent.PasteData) -> Void,
|
paste: @escaping (TextFieldComponent.PasteData) -> Void,
|
||||||
audioRecorder: ManagedAudioRecorder?,
|
audioRecorder: ManagedAudioRecorder?,
|
||||||
videoRecordingStatus: InstantVideoControllerRecordingStatus?,
|
videoRecordingStatus: InstantVideoControllerRecordingStatus?,
|
||||||
@ -137,6 +142,7 @@ public final class MessageInputPanelComponent: Component {
|
|||||||
timeoutSelected: Bool,
|
timeoutSelected: Bool,
|
||||||
displayGradient: Bool,
|
displayGradient: Bool,
|
||||||
bottomInset: CGFloat,
|
bottomInset: CGFloat,
|
||||||
|
isFormattingLocked: Bool,
|
||||||
hideKeyboard: Bool,
|
hideKeyboard: Bool,
|
||||||
forceIsEditing: Bool,
|
forceIsEditing: Bool,
|
||||||
disabledPlaceholder: String?
|
disabledPlaceholder: String?
|
||||||
@ -167,6 +173,8 @@ public final class MessageInputPanelComponent: Component {
|
|||||||
self.forwardAction = forwardAction
|
self.forwardAction = forwardAction
|
||||||
self.moreAction = moreAction
|
self.moreAction = moreAction
|
||||||
self.presentVoiceMessagesUnavailableTooltip = presentVoiceMessagesUnavailableTooltip
|
self.presentVoiceMessagesUnavailableTooltip = presentVoiceMessagesUnavailableTooltip
|
||||||
|
self.presentTextLengthLimitTooltip = presentTextLengthLimitTooltip
|
||||||
|
self.presentTextFormattingTooltip = presentTextFormattingTooltip
|
||||||
self.paste = paste
|
self.paste = paste
|
||||||
self.audioRecorder = audioRecorder
|
self.audioRecorder = audioRecorder
|
||||||
self.videoRecordingStatus = videoRecordingStatus
|
self.videoRecordingStatus = videoRecordingStatus
|
||||||
@ -178,6 +186,7 @@ public final class MessageInputPanelComponent: Component {
|
|||||||
self.timeoutSelected = timeoutSelected
|
self.timeoutSelected = timeoutSelected
|
||||||
self.displayGradient = displayGradient
|
self.displayGradient = displayGradient
|
||||||
self.bottomInset = bottomInset
|
self.bottomInset = bottomInset
|
||||||
|
self.isFormattingLocked = isFormattingLocked
|
||||||
self.hideKeyboard = hideKeyboard
|
self.hideKeyboard = hideKeyboard
|
||||||
self.forceIsEditing = forceIsEditing
|
self.forceIsEditing = forceIsEditing
|
||||||
self.disabledPlaceholder = disabledPlaceholder
|
self.disabledPlaceholder = disabledPlaceholder
|
||||||
@ -244,6 +253,9 @@ public final class MessageInputPanelComponent: Component {
|
|||||||
if lhs.bottomInset != rhs.bottomInset {
|
if lhs.bottomInset != rhs.bottomInset {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.isFormattingLocked != rhs.isFormattingLocked {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if (lhs.forwardAction == nil) != (rhs.forwardAction == nil) {
|
if (lhs.forwardAction == nil) != (rhs.forwardAction == nil) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -567,6 +579,10 @@ public final class MessageInputPanelComponent: Component {
|
|||||||
textColor: UIColor(rgb: 0xffffff),
|
textColor: UIColor(rgb: 0xffffff),
|
||||||
insets: UIEdgeInsets(top: 9.0, left: 8.0, bottom: 10.0, right: 48.0),
|
insets: UIEdgeInsets(top: 9.0, left: 8.0, bottom: 10.0, right: 48.0),
|
||||||
hideKeyboard: component.hideKeyboard,
|
hideKeyboard: component.hideKeyboard,
|
||||||
|
formatMenuAvailability: component.isFormattingLocked ? .locked : .available,
|
||||||
|
lockedFormatAction: {
|
||||||
|
component.presentTextFormattingTooltip?()
|
||||||
|
},
|
||||||
present: { c in
|
present: { c in
|
||||||
component.presentController(c)
|
component.presentController(c)
|
||||||
},
|
},
|
||||||
@ -907,6 +923,7 @@ public final class MessageInputPanelComponent: Component {
|
|||||||
} else {
|
} else {
|
||||||
if let maxLength = component.maxLength, self.textFieldExternalState.textLength > maxLength {
|
if let maxLength = component.maxLength, self.textFieldExternalState.textLength > maxLength {
|
||||||
self.animateError()
|
self.animateError()
|
||||||
|
component.presentTextLengthLimitTooltip?()
|
||||||
} else {
|
} else {
|
||||||
component.sendMessageAction()
|
component.sendMessageAction()
|
||||||
}
|
}
|
||||||
@ -916,6 +933,7 @@ public final class MessageInputPanelComponent: Component {
|
|||||||
if case .up = action {
|
if case .up = action {
|
||||||
if let maxLength = component.maxLength, self.textFieldExternalState.textLength > maxLength {
|
if let maxLength = component.maxLength, self.textFieldExternalState.textLength > maxLength {
|
||||||
self.animateError()
|
self.animateError()
|
||||||
|
component.presentTextLengthLimitTooltip?()
|
||||||
} else {
|
} else {
|
||||||
component.sendMessageAction()
|
component.sendMessageAction()
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,7 @@ import PeerListItemComponent
|
|||||||
import LottieComponent
|
import LottieComponent
|
||||||
import TooltipUI
|
import TooltipUI
|
||||||
import OverlayStatusController
|
import OverlayStatusController
|
||||||
|
import Markdown
|
||||||
|
|
||||||
final class ShareWithPeersScreenComponent: Component {
|
final class ShareWithPeersScreenComponent: Component {
|
||||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||||
@ -38,6 +39,7 @@ final class ShareWithPeersScreenComponent: Component {
|
|||||||
let optionItems: [OptionItem]
|
let optionItems: [OptionItem]
|
||||||
let completion: (EngineStoryPrivacy, Bool, Bool, [EnginePeer]) -> Void
|
let completion: (EngineStoryPrivacy, Bool, Bool, [EnginePeer]) -> Void
|
||||||
let editCategory: (EngineStoryPrivacy, Bool, Bool) -> Void
|
let editCategory: (EngineStoryPrivacy, Bool, Bool) -> Void
|
||||||
|
let editGrayList: (EngineStoryPrivacy, Bool, Bool) -> Void
|
||||||
|
|
||||||
init(
|
init(
|
||||||
context: AccountContext,
|
context: AccountContext,
|
||||||
@ -50,7 +52,8 @@ final class ShareWithPeersScreenComponent: Component {
|
|||||||
categoryItems: [CategoryItem],
|
categoryItems: [CategoryItem],
|
||||||
optionItems: [OptionItem],
|
optionItems: [OptionItem],
|
||||||
completion: @escaping (EngineStoryPrivacy, Bool, Bool, [EnginePeer]) -> Void,
|
completion: @escaping (EngineStoryPrivacy, Bool, Bool, [EnginePeer]) -> Void,
|
||||||
editCategory: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void
|
editCategory: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void,
|
||||||
|
editGrayList: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void
|
||||||
) {
|
) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.stateContext = stateContext
|
self.stateContext = stateContext
|
||||||
@ -63,6 +66,7 @@ final class ShareWithPeersScreenComponent: Component {
|
|||||||
self.optionItems = optionItems
|
self.optionItems = optionItems
|
||||||
self.completion = completion
|
self.completion = completion
|
||||||
self.editCategory = editCategory
|
self.editCategory = editCategory
|
||||||
|
self.editGrayList = editGrayList
|
||||||
}
|
}
|
||||||
|
|
||||||
static func ==(lhs: ShareWithPeersScreenComponent, rhs: ShareWithPeersScreenComponent) -> Bool {
|
static func ==(lhs: ShareWithPeersScreenComponent, rhs: ShareWithPeersScreenComponent) -> Bool {
|
||||||
@ -971,6 +975,76 @@ final class ShareWithPeersScreenComponent: Component {
|
|||||||
itemTransition.setFrame(view: itemView, frame: itemFrame)
|
itemTransition.setFrame(view: itemView, frame: itemFrame)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let sectionFooter: ComponentView<Empty>
|
||||||
|
var sectionFooterTransition = transition
|
||||||
|
if let current = self.visibleSectionFooters[section.id] {
|
||||||
|
sectionFooter = current
|
||||||
|
} else {
|
||||||
|
if !transition.animation.isImmediate {
|
||||||
|
sectionFooterTransition = .immediate
|
||||||
|
}
|
||||||
|
sectionFooter = ComponentView()
|
||||||
|
self.visibleSectionFooters[section.id] = sectionFooter
|
||||||
|
}
|
||||||
|
|
||||||
|
let body = MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.freeTextColor)
|
||||||
|
let bold = MarkdownAttributeSet(font: Font.semibold(13.0), textColor: environment.theme.list.freeTextColor)
|
||||||
|
let link = MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemAccentColor)
|
||||||
|
|
||||||
|
let footerText: String
|
||||||
|
if let grayListPeers = component.stateContext.stateValue?.grayListPeers, !grayListPeers.isEmpty {
|
||||||
|
let footerValue = environment.strings.Story_Privacy_GrayListPeople(Int32(grayListPeers.count))
|
||||||
|
footerText = environment.strings.Story_Privacy_GrayListSelected(footerValue).string
|
||||||
|
} else {
|
||||||
|
footerText = environment.strings.Story_Privacy_GrayListSelect
|
||||||
|
}
|
||||||
|
let footerSize = sectionFooter.update(
|
||||||
|
transition: sectionFooterTransition,
|
||||||
|
component: AnyComponent(MultilineTextComponent(
|
||||||
|
text: .markdown(text: footerText, attributes: MarkdownAttributes(
|
||||||
|
body: body,
|
||||||
|
bold: bold,
|
||||||
|
link: link,
|
||||||
|
linkAttribute: { url in
|
||||||
|
return ("URL", url)
|
||||||
|
}
|
||||||
|
)),
|
||||||
|
maximumNumberOfLines: 0,
|
||||||
|
lineSpacing: 0.2,
|
||||||
|
highlightColor: UIColor(rgb: 0x007aff, alpha: 0.2),
|
||||||
|
highlightAction: { attributes in
|
||||||
|
if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] {
|
||||||
|
return NSAttributedString.Key(rawValue: "URL")
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tapAction: { [weak self] _, _ in
|
||||||
|
guard let self, let environment = self.environment, let controller = environment.controller() as? ShareWithPeersScreen else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
component.editGrayList(
|
||||||
|
EngineStoryPrivacy(base: .nobody, additionallyIncludePeers: []),
|
||||||
|
self.selectedOptions.contains(.screenshot),
|
||||||
|
self.selectedOptions.contains(.pin)
|
||||||
|
)
|
||||||
|
controller.dismissAllTooltips()
|
||||||
|
controller.dismiss()
|
||||||
|
}
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: itemLayout.containerSize.width - 16.0 * 2.0, height: itemLayout.contentHeight)
|
||||||
|
)
|
||||||
|
let footerFrame = CGRect(origin: CGPoint(x: itemLayout.sideInset + 16.0, y: sectionOffset + section.totalHeight + 7.0), size: footerSize)
|
||||||
|
if let footerView = sectionFooter.view {
|
||||||
|
if footerView.superview == nil {
|
||||||
|
self.itemContainerView.addSubview(footerView)
|
||||||
|
}
|
||||||
|
sectionFooterTransition.setFrame(view: footerView, frame: footerFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
sectionOffset += footerSize.height
|
||||||
} else if section.id == 1 {
|
} else if section.id == 1 {
|
||||||
for i in 0 ..< stateValue.peers.count {
|
for i in 0 ..< stateValue.peers.count {
|
||||||
let itemFrame = CGRect(origin: CGPoint(x: itemLayout.sideInset, y: sectionOffset + section.insets.top + CGFloat(i) * section.itemHeight), size: CGSize(width: itemLayout.containerSize.width, height: section.itemHeight))
|
let itemFrame = CGRect(origin: CGPoint(x: itemLayout.sideInset, y: sectionOffset + section.insets.top + CGFloat(i) * section.itemHeight), size: CGSize(width: itemLayout.containerSize.width, height: section.itemHeight))
|
||||||
@ -1641,7 +1715,7 @@ final class ShareWithPeersScreenComponent: Component {
|
|||||||
}
|
}
|
||||||
navigationButtonsWidth += navigationLeftButtonSize.width + navigationSideInset
|
navigationButtonsWidth += navigationLeftButtonSize.width + navigationSideInset
|
||||||
|
|
||||||
var actionButtonTitle = environment.strings.Story_Privacy_SaveSettings
|
var actionButtonTitle = environment.strings.Story_Privacy_SaveList
|
||||||
let title: String
|
let title: String
|
||||||
switch component.stateContext.subject {
|
switch component.stateContext.subject {
|
||||||
case let .stories(editing):
|
case let .stories(editing):
|
||||||
@ -1651,8 +1725,12 @@ final class ShareWithPeersScreenComponent: Component {
|
|||||||
title = environment.strings.Story_Privacy_ShareStory
|
title = environment.strings.Story_Privacy_ShareStory
|
||||||
actionButtonTitle = environment.strings.Story_Privacy_PostStory
|
actionButtonTitle = environment.strings.Story_Privacy_PostStory
|
||||||
}
|
}
|
||||||
case .chats:
|
case let .chats(grayList):
|
||||||
title = environment.strings.Story_Privacy_CategorySelectedContacts
|
if grayList {
|
||||||
|
title = environment.strings.Story_Privacy_HideMyStoriesFrom
|
||||||
|
} else {
|
||||||
|
title = environment.strings.Story_Privacy_CategorySelectedContacts
|
||||||
|
}
|
||||||
case let .contacts(category):
|
case let .contacts(category):
|
||||||
switch category {
|
switch category {
|
||||||
case .closeFriends:
|
case .closeFriends:
|
||||||
@ -1976,24 +2054,27 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
|||||||
let presences: [EnginePeer.Id: EnginePeer.Presence]
|
let presences: [EnginePeer.Id: EnginePeer.Presence]
|
||||||
let participants: [EnginePeer.Id: Int]
|
let participants: [EnginePeer.Id: Int]
|
||||||
let closeFriendsPeers: [EnginePeer]
|
let closeFriendsPeers: [EnginePeer]
|
||||||
|
let grayListPeers: [EnginePeer]
|
||||||
|
|
||||||
fileprivate init(
|
fileprivate init(
|
||||||
peers: [EnginePeer],
|
peers: [EnginePeer],
|
||||||
presences: [EnginePeer.Id: EnginePeer.Presence],
|
presences: [EnginePeer.Id: EnginePeer.Presence],
|
||||||
participants: [EnginePeer.Id: Int],
|
participants: [EnginePeer.Id: Int],
|
||||||
closeFriendsPeers: [EnginePeer]
|
closeFriendsPeers: [EnginePeer],
|
||||||
|
grayListPeers: [EnginePeer]
|
||||||
) {
|
) {
|
||||||
self.peers = peers
|
self.peers = peers
|
||||||
self.presences = presences
|
self.presences = presences
|
||||||
self.participants = participants
|
self.participants = participants
|
||||||
self.closeFriendsPeers = closeFriendsPeers
|
self.closeFriendsPeers = closeFriendsPeers
|
||||||
|
self.grayListPeers = grayListPeers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class StateContext {
|
public final class StateContext {
|
||||||
public enum Subject: Equatable {
|
public enum Subject: Equatable {
|
||||||
case stories(editing: Bool)
|
case stories(editing: Bool)
|
||||||
case chats
|
case chats(grayList: Bool)
|
||||||
case contacts(EngineStoryPrivacy.Base)
|
case contacts(EngineStoryPrivacy.Base)
|
||||||
case search(query: String, onlyContacts: Bool)
|
case search(query: String, onlyContacts: Bool)
|
||||||
}
|
}
|
||||||
@ -2002,6 +2083,7 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
|||||||
|
|
||||||
public let subject: Subject
|
public let subject: Subject
|
||||||
public private(set) var initialPeerIds: Set<EnginePeer.Id> = Set()
|
public private(set) var initialPeerIds: Set<EnginePeer.Id> = Set()
|
||||||
|
fileprivate let storiesGrayList: BlockedPeersContext?
|
||||||
|
|
||||||
private var stateDisposable: Disposable?
|
private var stateDisposable: Disposable?
|
||||||
private let stateSubject = Promise<State>()
|
private let stateSubject = Promise<State>()
|
||||||
@ -2015,13 +2097,25 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
|||||||
|
|
||||||
public init(
|
public init(
|
||||||
context: AccountContext,
|
context: AccountContext,
|
||||||
subject: Subject = .chats,
|
subject: Subject = .chats(grayList: false),
|
||||||
initialPeerIds: Set<EnginePeer.Id> = Set(),
|
initialPeerIds: Set<EnginePeer.Id> = Set(),
|
||||||
closeFriends: Signal<[EnginePeer], NoError> = .single([])
|
closeFriends: Signal<[EnginePeer], NoError> = .single([]),
|
||||||
|
storiesGrayList: BlockedPeersContext? = nil
|
||||||
) {
|
) {
|
||||||
self.subject = subject
|
self.subject = subject
|
||||||
self.initialPeerIds = initialPeerIds
|
self.initialPeerIds = initialPeerIds
|
||||||
|
self.storiesGrayList = storiesGrayList
|
||||||
|
|
||||||
|
let grayListPeers: Signal<[EnginePeer], NoError>
|
||||||
|
if let storiesGrayList {
|
||||||
|
grayListPeers = storiesGrayList.state
|
||||||
|
|> map { state -> [EnginePeer] in
|
||||||
|
return state.peers.compactMap { $0.peer.flatMap(EnginePeer.init) }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
grayListPeers = .single([])
|
||||||
|
}
|
||||||
|
|
||||||
switch subject {
|
switch subject {
|
||||||
case .stories:
|
case .stories:
|
||||||
var peerSignals: [Signal<EnginePeer?, NoError>] = []
|
var peerSignals: [Signal<EnginePeer?, NoError>] = []
|
||||||
@ -2033,8 +2127,8 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
|||||||
|
|
||||||
let peers = combineLatest(peerSignals)
|
let peers = combineLatest(peerSignals)
|
||||||
|
|
||||||
self.stateDisposable = combineLatest(queue: Queue.mainQueue(), peers, closeFriends)
|
self.stateDisposable = combineLatest(queue: Queue.mainQueue(), peers, closeFriends, grayListPeers)
|
||||||
.start(next: { [weak self] peers, closeFriends in
|
.start(next: { [weak self] peers, closeFriends, grayListPeers in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -2043,28 +2137,30 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
|||||||
peers: peers.compactMap { $0 },
|
peers: peers.compactMap { $0 },
|
||||||
presences: [:],
|
presences: [:],
|
||||||
participants: [:],
|
participants: [:],
|
||||||
closeFriendsPeers: closeFriends
|
closeFriendsPeers: closeFriends,
|
||||||
|
grayListPeers: grayListPeers
|
||||||
)
|
)
|
||||||
self.stateValue = state
|
self.stateValue = state
|
||||||
self.stateSubject.set(.single(state))
|
self.stateSubject.set(.single(state))
|
||||||
|
|
||||||
self.readySubject.set(true)
|
self.readySubject.set(true)
|
||||||
})
|
})
|
||||||
case .chats:
|
case let .chats(isGrayList):
|
||||||
self.stateDisposable = (combineLatest(
|
self.stateDisposable = (combineLatest(
|
||||||
context.engine.messages.chatList(group: .root, count: 200) |> take(1),
|
context.engine.messages.chatList(group: .root, count: 200) |> take(1),
|
||||||
context.engine.data.get(TelegramEngine.EngineData.Item.Contacts.List(includePresences: true)),
|
context.engine.data.get(TelegramEngine.EngineData.Item.Contacts.List(includePresences: true)),
|
||||||
context.engine.data.get(EngineDataMap(Array(self.initialPeerIds).map(TelegramEngine.EngineData.Item.Peer.Peer.init)))
|
context.engine.data.get(EngineDataMap(Array(self.initialPeerIds).map(TelegramEngine.EngineData.Item.Peer.Peer.init))),
|
||||||
|
grayListPeers
|
||||||
)
|
)
|
||||||
|> mapToSignal { chatList, contacts, initialPeers -> Signal<(EngineChatList, EngineContactList, [EnginePeer.Id: Optional<EnginePeer>], [EnginePeer.Id: Optional<Int>]), NoError> in
|
|> mapToSignal { chatList, contacts, initialPeers, grayListPeers -> Signal<(EngineChatList, EngineContactList, [EnginePeer.Id: Optional<EnginePeer>], [EnginePeer.Id: Optional<Int>], [EnginePeer]), NoError> in
|
||||||
return context.engine.data.subscribe(
|
return context.engine.data.subscribe(
|
||||||
EngineDataMap(chatList.items.map(\.renderedPeer.peerId).map(TelegramEngine.EngineData.Item.Peer.ParticipantCount.init))
|
EngineDataMap(chatList.items.map(\.renderedPeer.peerId).map(TelegramEngine.EngineData.Item.Peer.ParticipantCount.init))
|
||||||
)
|
)
|
||||||
|> map { participantCountMap -> (EngineChatList, EngineContactList, [EnginePeer.Id: Optional<EnginePeer>], [EnginePeer.Id: Optional<Int>]) in
|
|> map { participantCountMap -> (EngineChatList, EngineContactList, [EnginePeer.Id: Optional<EnginePeer>], [EnginePeer.Id: Optional<Int>], [EnginePeer]) in
|
||||||
return (chatList, contacts, initialPeers, participantCountMap)
|
return (chatList, contacts, initialPeers, participantCountMap, grayListPeers)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] chatList, contacts, initialPeers, participantCounts in
|
|> deliverOnMainQueue).start(next: { [weak self] chatList, contacts, initialPeers, participantCounts, grayListPeers in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -2076,12 +2172,24 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var grayListPeersIds = Set<EnginePeer.Id>()
|
||||||
|
for peer in grayListPeers {
|
||||||
|
grayListPeersIds.insert(peer.id)
|
||||||
|
}
|
||||||
|
|
||||||
var existingIds = Set<EnginePeer.Id>()
|
var existingIds = Set<EnginePeer.Id>()
|
||||||
var selectedPeers: [EnginePeer] = []
|
var selectedPeers: [EnginePeer] = []
|
||||||
|
|
||||||
|
if isGrayList {
|
||||||
|
self.initialPeerIds = Set(grayListPeers.map { $0.id })
|
||||||
|
}
|
||||||
|
|
||||||
for item in chatList.items.reversed() {
|
for item in chatList.items.reversed() {
|
||||||
if self.initialPeerIds.contains(item.renderedPeer.peerId), let peer = item.renderedPeer.peer {
|
if let peer = item.renderedPeer.peer {
|
||||||
selectedPeers.append(peer)
|
if self.initialPeerIds.contains(peer.id) || isGrayList && grayListPeersIds.contains(peer.id) {
|
||||||
existingIds.insert(peer.id)
|
selectedPeers.append(peer)
|
||||||
|
existingIds.insert(peer.id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2092,6 +2200,15 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isGrayList {
|
||||||
|
for peer in grayListPeers {
|
||||||
|
if !existingIds.contains(peer.id) {
|
||||||
|
selectedPeers.append(peer)
|
||||||
|
existingIds.insert(peer.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var presences: [EnginePeer.Id: EnginePeer.Presence] = [:]
|
var presences: [EnginePeer.Id: EnginePeer.Presence] = [:]
|
||||||
for item in chatList.items {
|
for item in chatList.items {
|
||||||
presences[item.renderedPeer.peerId] = item.presence
|
presences[item.renderedPeer.peerId] = item.presence
|
||||||
@ -2106,7 +2223,7 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
|||||||
if peer.id == context.account.peerId {
|
if peer.id == context.account.peerId {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if peer.isService {
|
if peer.isService || peer.isDeleted {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if case let .user(user) = peer {
|
if case let .user(user) = peer {
|
||||||
@ -2136,7 +2253,8 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
|||||||
peers: peers,
|
peers: peers,
|
||||||
presences: presences,
|
presences: presences,
|
||||||
participants: participants,
|
participants: participants,
|
||||||
closeFriendsPeers: []
|
closeFriendsPeers: [],
|
||||||
|
grayListPeers: grayListPeers
|
||||||
)
|
)
|
||||||
self.stateValue = state
|
self.stateValue = state
|
||||||
self.stateSubject.set(.single(state))
|
self.stateSubject.set(.single(state))
|
||||||
@ -2198,7 +2316,8 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
|||||||
peers: peers,
|
peers: peers,
|
||||||
presences: contactList.presences,
|
presences: contactList.presences,
|
||||||
participants: [:],
|
participants: [:],
|
||||||
closeFriendsPeers: []
|
closeFriendsPeers: [],
|
||||||
|
grayListPeers: []
|
||||||
)
|
)
|
||||||
|
|
||||||
self.stateValue = state
|
self.stateValue = state
|
||||||
@ -2248,6 +2367,10 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
|||||||
return false
|
return false
|
||||||
} else if user.botInfo != nil {
|
} else if user.botInfo != nil {
|
||||||
return false
|
return false
|
||||||
|
} else if peer.isService {
|
||||||
|
return false
|
||||||
|
} else if user.isDeleted {
|
||||||
|
return false
|
||||||
} else {
|
} else {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -2265,7 +2388,8 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
|||||||
},
|
},
|
||||||
presences: [:],
|
presences: [:],
|
||||||
participants: participants,
|
participants: participants,
|
||||||
closeFriendsPeers: []
|
closeFriendsPeers: [],
|
||||||
|
grayListPeers: []
|
||||||
)
|
)
|
||||||
self.stateValue = state
|
self.stateValue = state
|
||||||
self.stateSubject.set(.single(state))
|
self.stateSubject.set(.single(state))
|
||||||
@ -2301,7 +2425,8 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
|||||||
mentions: [String] = [],
|
mentions: [String] = [],
|
||||||
stateContext: StateContext,
|
stateContext: StateContext,
|
||||||
completion: @escaping (EngineStoryPrivacy, Bool, Bool, [EnginePeer]) -> Void,
|
completion: @escaping (EngineStoryPrivacy, Bool, Bool, [EnginePeer]) -> Void,
|
||||||
editCategory: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void
|
editCategory: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void,
|
||||||
|
editGrayList: @escaping (EngineStoryPrivacy, Bool, Bool) -> Void
|
||||||
) {
|
) {
|
||||||
self.context = context
|
self.context = context
|
||||||
|
|
||||||
@ -2411,7 +2536,8 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
|||||||
categoryItems: categoryItems,
|
categoryItems: categoryItems,
|
||||||
optionItems: optionItems,
|
optionItems: optionItems,
|
||||||
completion: completion,
|
completion: completion,
|
||||||
editCategory: editCategory
|
editCategory: editCategory,
|
||||||
|
editGrayList: editGrayList
|
||||||
), navigationBarAppearance: .none, theme: .dark)
|
), navigationBarAppearance: .none, theme: .dark)
|
||||||
|
|
||||||
self.statusBar.statusBarStyle = .Ignore
|
self.statusBar.statusBarStyle = .Ignore
|
||||||
|
@ -25,6 +25,7 @@ swift_library(
|
|||||||
"//submodules/AppBundle",
|
"//submodules/AppBundle",
|
||||||
"//submodules/PeerPresenceStatusManager",
|
"//submodules/PeerPresenceStatusManager",
|
||||||
"//submodules/TelegramUI/Components/EmojiStatusComponent",
|
"//submodules/TelegramUI/Components/EmojiStatusComponent",
|
||||||
|
"//submodules/ContextUI",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
@ -15,6 +15,7 @@ import TelegramStringFormatting
|
|||||||
import AppBundle
|
import AppBundle
|
||||||
import PeerPresenceStatusManager
|
import PeerPresenceStatusManager
|
||||||
import EmojiStatusComponent
|
import EmojiStatusComponent
|
||||||
|
import ContextUI
|
||||||
|
|
||||||
private let avatarFont = avatarPlaceholderFont(size: 15.0)
|
private let avatarFont = avatarPlaceholderFont(size: 15.0)
|
||||||
private let readIconImage: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/MenuReadIcon"), color: .white)?.withRenderingMode(.alwaysTemplate)
|
private let readIconImage: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/MenuReadIcon"), color: .white)?.withRenderingMode(.alwaysTemplate)
|
||||||
@ -57,6 +58,7 @@ public final class PeerListItemComponent: Component {
|
|||||||
let selectionState: SelectionState
|
let selectionState: SelectionState
|
||||||
let hasNext: Bool
|
let hasNext: Bool
|
||||||
let action: (EnginePeer) -> Void
|
let action: (EnginePeer) -> Void
|
||||||
|
let contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)?
|
||||||
let openStories: ((EnginePeer, AvatarNode) -> Void)?
|
let openStories: ((EnginePeer, AvatarNode) -> Void)?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
@ -74,6 +76,7 @@ public final class PeerListItemComponent: Component {
|
|||||||
selectionState: SelectionState,
|
selectionState: SelectionState,
|
||||||
hasNext: Bool,
|
hasNext: Bool,
|
||||||
action: @escaping (EnginePeer) -> Void,
|
action: @escaping (EnginePeer) -> Void,
|
||||||
|
contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)? = nil,
|
||||||
openStories: ((EnginePeer, AvatarNode) -> Void)? = nil
|
openStories: ((EnginePeer, AvatarNode) -> Void)? = nil
|
||||||
) {
|
) {
|
||||||
self.context = context
|
self.context = context
|
||||||
@ -90,6 +93,7 @@ public final class PeerListItemComponent: Component {
|
|||||||
self.selectionState = selectionState
|
self.selectionState = selectionState
|
||||||
self.hasNext = hasNext
|
self.hasNext = hasNext
|
||||||
self.action = action
|
self.action = action
|
||||||
|
self.contextAction = contextAction
|
||||||
self.openStories = openStories
|
self.openStories = openStories
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,7 +140,8 @@ public final class PeerListItemComponent: Component {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class View: UIView {
|
public final class View: ContextControllerSourceView {
|
||||||
|
private let extractedContainerView: ContextExtractedContentContainingView
|
||||||
private let containerButton: HighlightTrackingButton
|
private let containerButton: HighlightTrackingButton
|
||||||
|
|
||||||
private let title = ComponentView<Empty>()
|
private let title = ComponentView<Empty>()
|
||||||
@ -173,9 +178,12 @@ public final class PeerListItemComponent: Component {
|
|||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var isExtractedToContextMenu: Bool = false
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
self.separatorLayer = SimpleLayer()
|
self.separatorLayer = SimpleLayer()
|
||||||
|
|
||||||
|
self.extractedContainerView = ContextExtractedContentContainingView()
|
||||||
self.containerButton = HighlightTrackingButton()
|
self.containerButton = HighlightTrackingButton()
|
||||||
self.containerButton.isExclusiveTouch = true
|
self.containerButton.isExclusiveTouch = true
|
||||||
|
|
||||||
@ -186,14 +194,49 @@ public final class PeerListItemComponent: Component {
|
|||||||
|
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.addSubview(self.extractedContainerView)
|
||||||
|
self.targetViewForActivationProgress = self.extractedContainerView.contentView
|
||||||
|
|
||||||
|
self.extractedContainerView.contentView.addSubview(self.containerButton)
|
||||||
|
|
||||||
self.layer.addSublayer(self.separatorLayer)
|
self.layer.addSublayer(self.separatorLayer)
|
||||||
self.addSubview(self.containerButton)
|
|
||||||
self.containerButton.layer.addSublayer(self.avatarNode.layer)
|
self.containerButton.layer.addSublayer(self.avatarNode.layer)
|
||||||
|
|
||||||
self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
|
self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
|
||||||
|
|
||||||
self.addSubview(self.avatarButtonView)
|
self.addSubview(self.avatarButtonView)
|
||||||
self.avatarButtonView.addTarget(self, action: #selector(self.avatarButtonPressed), for: .touchUpInside)
|
self.avatarButtonView.addTarget(self, action: #selector(self.avatarButtonPressed), for: .touchUpInside)
|
||||||
|
|
||||||
|
self.extractedContainerView.isExtractedToContextPreviewUpdated = { [weak self] value in
|
||||||
|
guard let self, let component = self.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.containerButton.clipsToBounds = value
|
||||||
|
self.containerButton.backgroundColor = value ? component.theme.rootController.navigationBar.blurredBackgroundColor : nil
|
||||||
|
self.containerButton.layer.cornerRadius = value ? 10.0 : 0.0
|
||||||
|
}
|
||||||
|
self.extractedContainerView.willUpdateIsExtractedToContextPreview = { [weak self] value, transition in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.isExtractedToContextMenu = value
|
||||||
|
|
||||||
|
let mappedTransition: Transition
|
||||||
|
if value {
|
||||||
|
mappedTransition = Transition(transition)
|
||||||
|
} else {
|
||||||
|
mappedTransition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut))
|
||||||
|
}
|
||||||
|
self.state?.updated(transition: mappedTransition)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.activated = { [weak self] gesture, _ in
|
||||||
|
guard let self, let component = self.component, let peer = component.peer else {
|
||||||
|
gesture.cancel()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
component.contextAction?(peer, self.extractedContainerView, gesture)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
@ -219,6 +262,8 @@ public final class PeerListItemComponent: Component {
|
|||||||
if let hint = transition.userData(TransitionHint.self) {
|
if let hint = transition.userData(TransitionHint.self) {
|
||||||
synchronousLoad = hint.synchronousLoad
|
synchronousLoad = hint.synchronousLoad
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.isGestureEnabled = component.contextAction != nil
|
||||||
|
|
||||||
let themeUpdated = self.component?.theme !== component.theme
|
let themeUpdated = self.component?.theme !== component.theme
|
||||||
|
|
||||||
@ -270,7 +315,7 @@ public final class PeerListItemComponent: Component {
|
|||||||
labelData = ("", false)
|
labelData = ("", false)
|
||||||
}
|
}
|
||||||
|
|
||||||
let contextInset: CGFloat = 0.0
|
let contextInset: CGFloat = self.isExtractedToContextMenu ? 12.0 : 0.0
|
||||||
|
|
||||||
let height: CGFloat
|
let height: CGFloat
|
||||||
let titleFont: UIFont
|
let titleFont: UIFont
|
||||||
@ -528,6 +573,11 @@ public final class PeerListItemComponent: Component {
|
|||||||
transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: leftInset, y: height), size: CGSize(width: availableSize.width - leftInset, height: UIScreenPixel)))
|
transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: leftInset, y: height), size: CGSize(width: availableSize.width - leftInset, height: UIScreenPixel)))
|
||||||
self.separatorLayer.isHidden = !component.hasNext
|
self.separatorLayer.isHidden = !component.hasNext
|
||||||
|
|
||||||
|
let resultBounds = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height))
|
||||||
|
transition.setFrame(view: self.extractedContainerView, frame: resultBounds)
|
||||||
|
transition.setFrame(view: self.extractedContainerView.contentView, frame: resultBounds)
|
||||||
|
self.extractedContainerView.contentRect = resultBounds
|
||||||
|
|
||||||
let containerFrame = CGRect(origin: CGPoint(x: contextInset, y: verticalInset), size: CGSize(width: availableSize.width - contextInset * 2.0, height: height - verticalInset * 2.0))
|
let containerFrame = CGRect(origin: CGPoint(x: contextInset, y: verticalInset), size: CGSize(width: availableSize.width - contextInset * 2.0, height: height - verticalInset * 2.0))
|
||||||
transition.setFrame(view: self.containerButton, frame: containerFrame)
|
transition.setFrame(view: self.containerButton, frame: containerFrame)
|
||||||
|
|
||||||
|
@ -1641,7 +1641,7 @@ private final class StoryContainerScreenComponent: Component {
|
|||||||
size: availableSize,
|
size: availableSize,
|
||||||
metrics: environment.metrics,
|
metrics: environment.metrics,
|
||||||
deviceMetrics: environment.deviceMetrics,
|
deviceMetrics: environment.deviceMetrics,
|
||||||
intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: contentDerivedBottomInset + presentationContextInsets.bottom, right: 0.0),
|
intrinsicInsets: UIEdgeInsets(top: environment.statusBarHeight, left: 0.0, bottom: contentDerivedBottomInset + presentationContextInsets.bottom, right: 0.0),
|
||||||
safeInsets: UIEdgeInsets(top: 0.0, left: presentationContextInsets.left, bottom: 0.0, right: presentationContextInsets.right),
|
safeInsets: UIEdgeInsets(top: 0.0, left: presentationContextInsets.left, bottom: 0.0, right: presentationContextInsets.right),
|
||||||
additionalInsets: UIEdgeInsets(),
|
additionalInsets: UIEdgeInsets(),
|
||||||
statusBarHeight: nil,
|
statusBarHeight: nil,
|
||||||
|
@ -1943,6 +1943,8 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
self.voiceMessagesRestrictedTooltipController = controller
|
self.voiceMessagesRestrictedTooltipController = controller
|
||||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.2, curve: .easeInOut)))
|
self.state?.updated(transition: Transition(animation: .curve(duration: 0.2, curve: .easeInOut)))
|
||||||
},
|
},
|
||||||
|
presentTextLengthLimitTooltip: nil,
|
||||||
|
presentTextFormattingTooltip: nil,
|
||||||
paste: { [weak self] data in
|
paste: { [weak self] data in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
@ -1971,6 +1973,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
timeoutSelected: false,
|
timeoutSelected: false,
|
||||||
displayGradient: false,
|
displayGradient: false,
|
||||||
bottomInset: component.inputHeight != 0.0 || inputNodeVisible ? 0.0 : bottomContentInset,
|
bottomInset: component.inputHeight != 0.0 || inputNodeVisible ? 0.0 : bottomContentInset,
|
||||||
|
isFormattingLocked: false,
|
||||||
hideKeyboard: self.sendMessageContext.currentInputMode == .media,
|
hideKeyboard: self.sendMessageContext.currentInputMode == .media,
|
||||||
forceIsEditing: self.sendMessageContext.currentInputMode == .media,
|
forceIsEditing: self.sendMessageContext.currentInputMode == .media,
|
||||||
disabledPlaceholder: disabledPlaceholder
|
disabledPlaceholder: disabledPlaceholder
|
||||||
@ -2206,6 +2209,106 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
}
|
}
|
||||||
self.navigateToPeer(peer: peer, chat: false)
|
self.navigateToPeer(peer: peer, chat: false)
|
||||||
},
|
},
|
||||||
|
peerContextAction: { [weak self] peer, sourceView, gesture in
|
||||||
|
guard let self, let component = self.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = (component.context.engine.data.get(
|
||||||
|
TelegramEngine.EngineData.Item.Peer.IsBlocked(id: peer.id),
|
||||||
|
TelegramEngine.EngineData.Item.Peer.IsBlockedFromStories(id: peer.id),
|
||||||
|
TelegramEngine.EngineData.Item.Peer.IsContact(id: peer.id)
|
||||||
|
) |> deliverOnMainQueue).start(next: { [weak self] isBlocked, isBlockedFromStories, isContact in
|
||||||
|
var isBlockedValue = false
|
||||||
|
var isBlockedFromStoriesValue = false
|
||||||
|
|
||||||
|
if case let .known(value) = isBlocked {
|
||||||
|
isBlockedValue = value
|
||||||
|
}
|
||||||
|
if case let .known(value) = isBlockedFromStories {
|
||||||
|
isBlockedFromStoriesValue = value
|
||||||
|
}
|
||||||
|
|
||||||
|
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
|
||||||
|
var itemList: [ContextMenuItem] = []
|
||||||
|
|
||||||
|
if isBlockedFromStoriesValue {
|
||||||
|
itemList.append(.action(ContextMenuActionItem(text: "Show My Stories To \(peer.compactDisplayTitle)", icon: { theme in
|
||||||
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Stories"), color: theme.contextMenu.primaryColor)
|
||||||
|
}, action: { [weak self] _, f in
|
||||||
|
f(.default)
|
||||||
|
let _ = component.context.engine.privacy.requestUpdatePeerIsBlockedFromStories(peerId: peer.id, isBlocked: false).start()
|
||||||
|
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
|
||||||
|
self.component?.presentController(UndoOverlayController(
|
||||||
|
presentationData: presentationData,
|
||||||
|
content: .info(title: nil, text: "**\(peer.compactDisplayTitle)** will now see your stories.", timeout: nil),
|
||||||
|
elevatedLayout: false,
|
||||||
|
position: .top,
|
||||||
|
animateInAsReplacement: false,
|
||||||
|
action: { _ in return false }
|
||||||
|
), nil)
|
||||||
|
})))
|
||||||
|
} else {
|
||||||
|
itemList.append(.action(ContextMenuActionItem(text: "Hide My Stories From \(peer.compactDisplayTitle)", icon: { theme in
|
||||||
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Stories"), color: theme.contextMenu.primaryColor)
|
||||||
|
}, action: { [weak self] _, f in
|
||||||
|
f(.default)
|
||||||
|
let _ = component.context.engine.privacy.requestUpdatePeerIsBlockedFromStories(peerId: peer.id, isBlocked: true).start()
|
||||||
|
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
|
||||||
|
self.component?.presentController(UndoOverlayController(
|
||||||
|
presentationData: presentationData,
|
||||||
|
content: .info(title: nil, text: "**\(peer.compactDisplayTitle)** will not see your stories anymore.", timeout: nil),
|
||||||
|
elevatedLayout: false,
|
||||||
|
position: .top,
|
||||||
|
animateInAsReplacement: false,
|
||||||
|
action: { _ in return false }
|
||||||
|
), nil)
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
if isContact {
|
||||||
|
itemList.append(.action(ContextMenuActionItem(text: "Delete Contact", textColor: .destructive, icon: { theme in
|
||||||
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor)
|
||||||
|
}, action: { _, f in
|
||||||
|
f(.default)
|
||||||
|
|
||||||
|
})))
|
||||||
|
} else {
|
||||||
|
if isBlockedValue {
|
||||||
|
|
||||||
|
} else {
|
||||||
|
itemList.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_ContextMenuBlock, textColor: .destructive, icon: { theme in
|
||||||
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Restrict"), color: theme.contextMenu.destructiveColor)
|
||||||
|
}, action: { _, f in
|
||||||
|
f(.default)
|
||||||
|
|
||||||
|
let _ = component.context.engine.privacy.requestUpdatePeerIsBlocked(peerId: peer.id, isBlocked: true).start()
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let items = ContextController.Items(content: .list(itemList))
|
||||||
|
|
||||||
|
let controller = ContextController(
|
||||||
|
account: component.context.account,
|
||||||
|
presentationData: presentationData,
|
||||||
|
source: .extracted(ListContextExtractedContentSource(contentView: sourceView)),
|
||||||
|
items: .single(items),
|
||||||
|
recognizer: nil,
|
||||||
|
gesture: gesture
|
||||||
|
)
|
||||||
|
component.presentInGlobalOverlay(controller, nil)
|
||||||
|
})
|
||||||
|
},
|
||||||
openPeerStories: { [weak self] peer, avatarNode in
|
openPeerStories: { [weak self] peer, avatarNode in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
@ -3233,7 +3336,8 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
text = component.strings.Story_PrivacyTooltipCloseFriends
|
text = component.strings.Story_PrivacyTooltipCloseFriends
|
||||||
} else if privacy.base == .nobody {
|
} else if privacy.base == .nobody {
|
||||||
if !privacy.additionallyIncludePeers.isEmpty {
|
if !privacy.additionallyIncludePeers.isEmpty {
|
||||||
text = component.strings.Story_PrivacyTooltipSelectedContactsCount("\(privacy.additionallyIncludePeers.count)").string
|
let value = component.strings.Story_PrivacyTooltipSelectedContacts_Contacts(Int32(privacy.additionallyIncludePeers.count))
|
||||||
|
text = component.strings.Story_PrivacyTooltipSelectedContactsCount(value).string
|
||||||
} else {
|
} else {
|
||||||
text = component.strings.Story_PrivacyTooltipNobody
|
text = component.strings.Story_PrivacyTooltipNobody
|
||||||
}
|
}
|
||||||
@ -3300,6 +3404,13 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
}
|
}
|
||||||
self.openItemPrivacySettings(initialPrivacy: privacy)
|
self.openItemPrivacySettings(initialPrivacy: privacy)
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
editGrayList: { [weak self] privacy, _, _ in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let _ = self
|
||||||
|
let _ = privacy
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
controller.dismissed = { [weak self] in
|
controller.dismissed = { [weak self] in
|
||||||
@ -3321,7 +3432,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
}
|
}
|
||||||
let subject: ShareWithPeersScreen.StateContext.Subject
|
let subject: ShareWithPeersScreen.StateContext.Subject
|
||||||
if privacy.base == .nobody {
|
if privacy.base == .nobody {
|
||||||
subject = .chats
|
subject = .chats(grayList: false)
|
||||||
} else {
|
} else {
|
||||||
subject = .contacts(privacy.base)
|
subject = .contacts(privacy.base)
|
||||||
}
|
}
|
||||||
@ -3345,7 +3456,8 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
completion(result)
|
completion(result)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
editCategory: { _, _, _ in }
|
editCategory: { _, _, _ in },
|
||||||
|
editGrayList: { _, _, _ in }
|
||||||
)
|
)
|
||||||
controller.dismissed = { [weak self] in
|
controller.dismissed = { [weak self] in
|
||||||
if let self {
|
if let self {
|
||||||
@ -4304,6 +4416,27 @@ final class HeaderContextReferenceContentSource: ContextReferenceContentSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final class ListContextExtractedContentSource: ContextExtractedContentSource {
|
||||||
|
let keepInPlace: Bool = false
|
||||||
|
let ignoreContentTouches: Bool = false
|
||||||
|
let blurBackground: Bool = true
|
||||||
|
|
||||||
|
private let contentView: ContextExtractedContentContainingView
|
||||||
|
|
||||||
|
init(contentView: ContextExtractedContentContainingView) {
|
||||||
|
self.contentView = contentView
|
||||||
|
}
|
||||||
|
|
||||||
|
func takeView() -> ContextControllerTakeViewInfo? {
|
||||||
|
return ContextControllerTakeViewInfo(containingItem: .view(self.contentView), contentAreaInScreenSpace: UIScreen.main.bounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
func putBack() -> ContextControllerPutBackViewInfo? {
|
||||||
|
return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private func generateParabollicMotionKeyframes(from sourcePoint: CGPoint, to targetPosition: CGPoint, elevation: CGFloat, duration: Double, curve: Transition.Animation.Curve, reverse: Bool) -> [CGPoint] {
|
private func generateParabollicMotionKeyframes(from sourcePoint: CGPoint, to targetPosition: CGPoint, elevation: CGFloat, duration: Double, curve: Transition.Animation.Curve, reverse: Bool) -> [CGPoint] {
|
||||||
let midPoint = CGPoint(x: (sourcePoint.x + targetPosition.x) / 2.0, y: sourcePoint.y - elevation)
|
let midPoint = CGPoint(x: (sourcePoint.x + targetPosition.x) / 2.0, y: sourcePoint.y - elevation)
|
||||||
|
|
||||||
|
@ -59,6 +59,7 @@ final class StoryItemSetViewListComponent: Component {
|
|||||||
let deleteAction: () -> Void
|
let deleteAction: () -> Void
|
||||||
let moreAction: (UIView, ContextGesture?) -> Void
|
let moreAction: (UIView, ContextGesture?) -> Void
|
||||||
let openPeer: (EnginePeer) -> Void
|
let openPeer: (EnginePeer) -> Void
|
||||||
|
let peerContextAction: (EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void
|
||||||
let openPeerStories: (EnginePeer, AvatarNode) -> Void
|
let openPeerStories: (EnginePeer, AvatarNode) -> Void
|
||||||
let openPremiumIntro: () -> Void
|
let openPremiumIntro: () -> Void
|
||||||
|
|
||||||
@ -79,6 +80,7 @@ final class StoryItemSetViewListComponent: Component {
|
|||||||
deleteAction: @escaping () -> Void,
|
deleteAction: @escaping () -> Void,
|
||||||
moreAction: @escaping (UIView, ContextGesture?) -> Void,
|
moreAction: @escaping (UIView, ContextGesture?) -> Void,
|
||||||
openPeer: @escaping (EnginePeer) -> Void,
|
openPeer: @escaping (EnginePeer) -> Void,
|
||||||
|
peerContextAction: @escaping (EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void,
|
||||||
openPeerStories: @escaping (EnginePeer, AvatarNode) -> Void,
|
openPeerStories: @escaping (EnginePeer, AvatarNode) -> Void,
|
||||||
openPremiumIntro: @escaping () -> Void
|
openPremiumIntro: @escaping () -> Void
|
||||||
) {
|
) {
|
||||||
@ -98,6 +100,7 @@ final class StoryItemSetViewListComponent: Component {
|
|||||||
self.deleteAction = deleteAction
|
self.deleteAction = deleteAction
|
||||||
self.moreAction = moreAction
|
self.moreAction = moreAction
|
||||||
self.openPeer = openPeer
|
self.openPeer = openPeer
|
||||||
|
self.peerContextAction = peerContextAction
|
||||||
self.openPeerStories = openPeerStories
|
self.openPeerStories = openPeerStories
|
||||||
self.openPremiumIntro = openPremiumIntro
|
self.openPremiumIntro = openPremiumIntro
|
||||||
}
|
}
|
||||||
@ -505,6 +508,9 @@ final class StoryItemSetViewListComponent: Component {
|
|||||||
}
|
}
|
||||||
component.openPeer(peer)
|
component.openPeer(peer)
|
||||||
},
|
},
|
||||||
|
contextAction: { peer, view, gesture in
|
||||||
|
component.peerContextAction(peer, view, gesture)
|
||||||
|
},
|
||||||
openStories: { [weak self] peer, avatarNode in
|
openStories: { [weak self] peer, avatarNode in
|
||||||
guard let self, let component = self.component else {
|
guard let self, let component = self.component else {
|
||||||
return
|
return
|
||||||
|
@ -76,6 +76,12 @@ public final class TextFieldComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum FormatMenuAvailability: Equatable {
|
||||||
|
case available
|
||||||
|
case locked
|
||||||
|
case none
|
||||||
|
}
|
||||||
|
|
||||||
public let context: AccountContext
|
public let context: AccountContext
|
||||||
public let strings: PresentationStrings
|
public let strings: PresentationStrings
|
||||||
public let externalState: ExternalState
|
public let externalState: ExternalState
|
||||||
@ -83,6 +89,8 @@ public final class TextFieldComponent: Component {
|
|||||||
public let textColor: UIColor
|
public let textColor: UIColor
|
||||||
public let insets: UIEdgeInsets
|
public let insets: UIEdgeInsets
|
||||||
public let hideKeyboard: Bool
|
public let hideKeyboard: Bool
|
||||||
|
public let formatMenuAvailability: FormatMenuAvailability
|
||||||
|
public let lockedFormatAction: () -> Void
|
||||||
public let present: (ViewController) -> Void
|
public let present: (ViewController) -> Void
|
||||||
public let paste: (PasteData) -> Void
|
public let paste: (PasteData) -> Void
|
||||||
|
|
||||||
@ -94,6 +102,8 @@ public final class TextFieldComponent: Component {
|
|||||||
textColor: UIColor,
|
textColor: UIColor,
|
||||||
insets: UIEdgeInsets,
|
insets: UIEdgeInsets,
|
||||||
hideKeyboard: Bool,
|
hideKeyboard: Bool,
|
||||||
|
formatMenuAvailability: FormatMenuAvailability,
|
||||||
|
lockedFormatAction: @escaping () -> Void,
|
||||||
present: @escaping (ViewController) -> Void,
|
present: @escaping (ViewController) -> Void,
|
||||||
paste: @escaping (PasteData) -> Void
|
paste: @escaping (PasteData) -> Void
|
||||||
) {
|
) {
|
||||||
@ -104,6 +114,8 @@ public final class TextFieldComponent: Component {
|
|||||||
self.textColor = textColor
|
self.textColor = textColor
|
||||||
self.insets = insets
|
self.insets = insets
|
||||||
self.hideKeyboard = hideKeyboard
|
self.hideKeyboard = hideKeyboard
|
||||||
|
self.formatMenuAvailability = formatMenuAvailability
|
||||||
|
self.lockedFormatAction = lockedFormatAction
|
||||||
self.present = present
|
self.present = present
|
||||||
self.paste = paste
|
self.paste = paste
|
||||||
}
|
}
|
||||||
@ -127,6 +139,9 @@ public final class TextFieldComponent: Component {
|
|||||||
if lhs.hideKeyboard != rhs.hideKeyboard {
|
if lhs.hideKeyboard != rhs.hideKeyboard {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.formatMenuAvailability != rhs.formatMenuAvailability {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -387,6 +402,22 @@ public final class TextFieldComponent: Component {
|
|||||||
return UIMenu(children: suggestedActions)
|
return UIMenu(children: suggestedActions)
|
||||||
}
|
}
|
||||||
let strings = component.strings
|
let strings = component.strings
|
||||||
|
|
||||||
|
if case .none = component.formatMenuAvailability {
|
||||||
|
return UIMenu(children: suggestedActions)
|
||||||
|
}
|
||||||
|
|
||||||
|
if case .locked = component.formatMenuAvailability {
|
||||||
|
var updatedActions = suggestedActions
|
||||||
|
let formatAction = UIAction(title: strings.TextFormat_Format, image: nil) { [weak self] action in
|
||||||
|
if let self {
|
||||||
|
self.component?.lockedFormatAction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updatedActions.insert(formatAction, at: 1)
|
||||||
|
return UIMenu(children: updatedActions)
|
||||||
|
}
|
||||||
|
|
||||||
var actions: [UIAction] = [
|
var actions: [UIAction] = [
|
||||||
UIAction(title: strings.TextFormat_Bold, image: nil) { [weak self] action in
|
UIAction(title: strings.TextFormat_Bold, image: nil) { [weak self] action in
|
||||||
if let self {
|
if let self {
|
||||||
|
@ -8,5 +8,8 @@
|
|||||||
"info" : {
|
"info" : {
|
||||||
"author" : "xcode",
|
"author" : "xcode",
|
||||||
"version" : 1
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"template-rendering-intent" : "template"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user