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.PrivacyTooltipCloseFriends" = "This story is shown to your close friends.";
|
||||
"Story.PrivacyTooltipSelectedContacts" = "This story is shown to selected contacts.";
|
||||
"Story.PrivacyTooltipSelectedContactsCount" = "This story is now shown to %@ contacts.";
|
||||
"Story.PrivacyTooltipSelectedContactsCount" = "This story is now shown to %@.";
|
||||
"Story.PrivacyTooltipNobody" = "This story is shown only to you.";
|
||||
"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.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
|
||||
}
|
||||
|
||||
public enum CaptionEntitiesAvailability {
|
||||
case enabled
|
||||
case premium
|
||||
}
|
||||
|
||||
static var defaultValue: StoriesConfiguration {
|
||||
return StoriesConfiguration(posting: .disabled)
|
||||
return StoriesConfiguration(posting: .disabled, captionEntities: .premium)
|
||||
}
|
||||
|
||||
public let posting: PostingAvailability
|
||||
public let captionEntities: CaptionEntitiesAvailability
|
||||
|
||||
fileprivate init(posting: PostingAvailability) {
|
||||
fileprivate init(posting: PostingAvailability, captionEntities: CaptionEntitiesAvailability) {
|
||||
self.posting = posting
|
||||
self.captionEntities = captionEntities
|
||||
}
|
||||
|
||||
public static func with(appConfiguration: AppConfiguration) -> StoriesConfiguration {
|
||||
if let data = appConfiguration.data, let postingString = data["stories_posting"] as? String {
|
||||
var posting: PostingAvailability
|
||||
switch postingString {
|
||||
case "enabled":
|
||||
posting = .enabled
|
||||
case "premium":
|
||||
posting = .premium
|
||||
default:
|
||||
if let data = appConfiguration.data {
|
||||
let posting: PostingAvailability
|
||||
let captionEntities: CaptionEntitiesAvailability
|
||||
if let postingString = data["stories_posting"] as? String {
|
||||
switch postingString {
|
||||
case "enabled":
|
||||
posting = .enabled
|
||||
case "premium":
|
||||
posting = .premium
|
||||
default:
|
||||
posting = .disabled
|
||||
}
|
||||
} else {
|
||||
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 {
|
||||
return .defaultValue
|
||||
}
|
||||
|
@ -28,6 +28,8 @@ private func makeEntityView(context: AccountContext, entity: DrawingEntity) -> D
|
||||
return DrawingVectorEntityView(context: context, entity: entity)
|
||||
} else if let entity = entity as? DrawingMediaEntity {
|
||||
return DrawingMediaEntityView(context: context, entity: entity)
|
||||
} else if let entity = entity as? DrawingLocationEntity {
|
||||
return DrawingLocationEntityView(context: context, entity: entity)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
@ -47,6 +49,9 @@ private func prepareForRendering(entityView: DrawingEntityView) {
|
||||
if let entityView = entityView as? DrawingVectorEntityView {
|
||||
entityView.entity.renderImage = entityView.getRenderImage()
|
||||
}
|
||||
if let entityView = entityView as? DrawingLocationEntityView {
|
||||
entityView.entity.renderImage = entityView.getRenderImage()
|
||||
}
|
||||
}
|
||||
|
||||
public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
||||
@ -347,6 +352,14 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
||||
text.fontSize = 0.08
|
||||
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
|
||||
if let self, let entityView {
|
||||
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
|
||||
@ -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")
|
||||
}
|
||||
|
||||
func selectedTapAction() -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
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 maxIndex: Int = 0
|
||||
|
||||
@ -1008,7 +1008,7 @@ private class DrawingTextLayoutManager: NSLayoutManager {
|
||||
var strokeOffset: CGPoint = .zero
|
||||
|
||||
var frameColor: UIColor?
|
||||
var frameWidthInset: CGFloat = 0.0
|
||||
var frameInsets = UIEdgeInsets()
|
||||
|
||||
var textAlignment: NSTextAlignment = .natural
|
||||
|
||||
@ -1040,7 +1040,7 @@ private class DrawingTextLayoutManager: NSLayoutManager {
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -1249,7 +1249,7 @@ final class SimpleTextLayer: CATextLayer {
|
||||
final class DrawingTextView: UITextView, NSLayoutManagerDelegate {
|
||||
var characterLayers: [CALayer] = []
|
||||
|
||||
fileprivate var drawingLayoutManager: DrawingTextLayoutManager {
|
||||
var drawingLayoutManager: DrawingTextLayoutManager {
|
||||
return self.layoutManager as! DrawingTextLayoutManager
|
||||
}
|
||||
|
||||
@ -1277,9 +1277,9 @@ final class DrawingTextView: UITextView, NSLayoutManagerDelegate {
|
||||
self.setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
var frameWidthInset: CGFloat = 0.0 {
|
||||
var frameInsets: UIEdgeInsets = .zero {
|
||||
didSet {
|
||||
self.drawingLayoutManager.frameWidthInset = self.frameWidthInset
|
||||
self.drawingLayoutManager.frameInsets = self.frameInsets
|
||||
self.setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
@ -1378,10 +1378,6 @@ final class DrawingTextView: UITextView, NSLayoutManagerDelegate {
|
||||
func layoutManager(_ layoutManager: NSLayoutManager, didCompleteLayoutFor textContainer: NSTextContainer?, atEnd layoutFinishedFlag: Bool) {
|
||||
self.updateCharLayers()
|
||||
if layoutFinishedFlag {
|
||||
// if self.needsLayersUpdate {
|
||||
// self.needsLayersUpdate = false
|
||||
// self.updateCharLayers()
|
||||
// }
|
||||
if let onLayoutUpdate = self.onLayoutUpdate {
|
||||
self.onLayoutUpdate = nil
|
||||
onLayoutUpdate()
|
||||
|
@ -416,6 +416,8 @@ public class StickerPickerScreen: ViewController {
|
||||
}
|
||||
}
|
||||
|
||||
private var storyStickersContentView: StoryStickersContentView?
|
||||
|
||||
init(context: AccountContext, controller: StickerPickerScreen, theme: PresentationTheme) {
|
||||
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
self.controller = controller
|
||||
@ -440,15 +442,19 @@ public class StickerPickerScreen: ViewController {
|
||||
self.wrappingView.addSubview(self.containerView)
|
||||
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(),
|
||||
controller.inputData,
|
||||
self.stickerSearchState.get(),
|
||||
self.emojiSearchState.get()
|
||||
)
|
||||
|
||||
|
||||
self.contentDisposable.set(signal.start(next: { [weak self] inputData, stickerSearchState, emojiSearchState in
|
||||
self.contentDisposable.set(data.start(next: { [weak self] inputData, stickerSearchState, emojiSearchState in
|
||||
if let strongSelf = self {
|
||||
let presentationData = strongSelf.presentationData
|
||||
var inputData = inputData
|
||||
@ -910,6 +916,7 @@ public class StickerPickerScreen: ViewController {
|
||||
customLayout: nil,
|
||||
externalBackground: nil,
|
||||
externalExpansionView: nil,
|
||||
customContentView: nil,
|
||||
useOpaqueTheme: false,
|
||||
hideBackground: true,
|
||||
stateContext: nil,
|
||||
@ -1174,6 +1181,7 @@ public class StickerPickerScreen: ViewController {
|
||||
customLayout: nil,
|
||||
externalBackground: nil,
|
||||
externalExpansionView: nil,
|
||||
customContentView: self.storyStickersContentView,
|
||||
useOpaqueTheme: false,
|
||||
hideBackground: true,
|
||||
stateContext: nil,
|
||||
@ -1373,7 +1381,7 @@ public class StickerPickerScreen: ViewController {
|
||||
deviceMetrics: layout.deviceMetrics,
|
||||
bottomInset: bottomInset,
|
||||
content: content,
|
||||
backgroundColor: self.theme.list.itemBlocksBackgroundColor,
|
||||
backgroundColor: self.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.85),
|
||||
separatorColor: self.theme.list.blocksBackgroundColor,
|
||||
getController: { [weak self] in
|
||||
if let self {
|
||||
@ -1640,6 +1648,7 @@ public class StickerPickerScreen: ViewController {
|
||||
public var completion: (DrawingStickerEntity.Content?) -> Void = { _ in }
|
||||
|
||||
public var presentGallery: () -> Void = { }
|
||||
public var presentLocationPicker: () -> Void = { }
|
||||
|
||||
public init(context: AccountContext, inputData: Signal<StickerPickerInputData, NoError>, defaultToEmoji: Bool = false) {
|
||||
self.context = context
|
||||
@ -1697,3 +1706,92 @@ public class StickerPickerScreen: ViewController {
|
||||
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() {
|
||||
}
|
||||
}
|
||||
|
||||
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/AnimationCache:AnimationCache",
|
||||
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
|
||||
"//submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent",
|
||||
"//submodules/AttachmentUI:AttachmentUI",
|
||||
],
|
||||
visibility = [
|
||||
|
@ -201,6 +201,7 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent {
|
||||
let titleColor = theme.list.itemPrimaryTextColor
|
||||
let subtitleColor = theme.list.itemSecondaryTextColor
|
||||
let arrowColor = theme.list.disclosureArrowColor
|
||||
let accentColor = theme.list.itemAccentColor
|
||||
|
||||
let textFont = Font.regular(15.0)
|
||||
let boldTextFont = Font.semibold(15.0)
|
||||
@ -361,7 +362,8 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent {
|
||||
titleColor: titleColor,
|
||||
subtitle: perk.subtitle(strings: strings),
|
||||
subtitleColor: subtitleColor,
|
||||
arrowColor: arrowColor
|
||||
arrowColor: arrowColor,
|
||||
accentColor: accentColor
|
||||
)
|
||||
)
|
||||
),
|
||||
|
@ -1082,6 +1082,8 @@ final class PerkComponent: CombinedComponent {
|
||||
let subtitle: String
|
||||
let subtitleColor: UIColor
|
||||
let arrowColor: UIColor
|
||||
let accentColor: UIColor
|
||||
let badge: String?
|
||||
|
||||
init(
|
||||
iconName: String,
|
||||
@ -1090,7 +1092,9 @@ final class PerkComponent: CombinedComponent {
|
||||
titleColor: UIColor,
|
||||
subtitle: String,
|
||||
subtitleColor: UIColor,
|
||||
arrowColor: UIColor
|
||||
arrowColor: UIColor,
|
||||
accentColor: UIColor,
|
||||
badge: String? = nil
|
||||
) {
|
||||
self.iconName = iconName
|
||||
self.iconBackgroundColors = iconBackgroundColors
|
||||
@ -1099,6 +1103,8 @@ final class PerkComponent: CombinedComponent {
|
||||
self.subtitle = subtitle
|
||||
self.subtitleColor = subtitleColor
|
||||
self.arrowColor = arrowColor
|
||||
self.accentColor = accentColor
|
||||
self.badge = badge
|
||||
}
|
||||
|
||||
static func ==(lhs: PerkComponent, rhs: PerkComponent) -> Bool {
|
||||
@ -1123,6 +1129,12 @@ final class PerkComponent: CombinedComponent {
|
||||
if lhs.arrowColor != rhs.arrowColor {
|
||||
return false
|
||||
}
|
||||
if lhs.accentColor != rhs.accentColor {
|
||||
return false
|
||||
}
|
||||
if lhs.badge != rhs.badge {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -1132,6 +1144,8 @@ final class PerkComponent: CombinedComponent {
|
||||
let title = Child(MultilineTextComponent.self)
|
||||
let subtitle = Child(MultilineTextComponent.self)
|
||||
let arrow = Child(BundleIconComponent.self)
|
||||
let badgeBackground = Child(RoundedRectangle.self)
|
||||
let badgeText = Child(MultilineTextComponent.self)
|
||||
|
||||
return { context in
|
||||
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))
|
||||
)
|
||||
|
||||
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
|
||||
.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,
|
||||
subtitle: perk.subtitle(strings: strings),
|
||||
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(
|
||||
AnyComponentWithIdentity(
|
||||
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
|
||||
),
|
||||
externalExpansionView: self.view,
|
||||
customContentView: nil,
|
||||
useOpaqueTheme: false,
|
||||
hideBackground: false,
|
||||
stateContext: nil,
|
||||
|
@ -686,7 +686,7 @@ public func privacyAndSecurityController(
|
||||
let privacySettingsPromise = Promise<AccountPrivacySettings?>()
|
||||
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 webSessionsContext = webSessionsContext ?? context.engine.privacy.webSessions()
|
||||
|
||||
|
@ -631,7 +631,7 @@ private func privacySearchableItems(context: AccountContext, privacySettings: Ac
|
||||
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
|
||||
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
|
||||
presentSelectivePrivacySettings(context, .presence, present)
|
||||
|
@ -1326,7 +1326,7 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox:
|
||||
channelsToPoll[peerId] = nil
|
||||
}
|
||||
}
|
||||
case let .updatePeerBlocked(peerId, blocked):
|
||||
case let .updatePeerBlocked(flags, peerId):
|
||||
let userPeerId = peerId.peerId
|
||||
updatedState.updateCachedPeerData(userPeerId, { current in
|
||||
let previous: CachedUserData
|
||||
@ -1335,7 +1335,13 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox:
|
||||
} else {
|
||||
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):
|
||||
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,
|
||||
maxSharedFolderInviteLinks: 3,
|
||||
maxSharedFolderJoin: 2,
|
||||
maxStoryCaptionLength: 1024,
|
||||
maxStoryCaptionLength: 200,
|
||||
maxExpiringStoriesCount: 100
|
||||
)
|
||||
}
|
||||
|
@ -149,6 +149,7 @@ public struct CachedUserFlags: OptionSet {
|
||||
}
|
||||
|
||||
public static let translationHidden = CachedUserFlags(rawValue: 1 << 0)
|
||||
public static let isBlockedFromMyStories = CachedUserFlags(rawValue: 1 << 1)
|
||||
}
|
||||
|
||||
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 typealias Result = Bool
|
||||
|
||||
|
@ -4,46 +4,15 @@ import SwiftSignalKit
|
||||
import TelegramApi
|
||||
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> {
|
||||
return account.postbox.transaction { transaction -> Signal<Void, NoError> in
|
||||
if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) {
|
||||
let flags: Int32 = 0
|
||||
let signal: Signal<Api.Bool, MTRpcError>
|
||||
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 {
|
||||
signal = account.network.request(Api.functions.contacts.unblock(id: inputPeer))
|
||||
signal = account.network.request(Api.functions.contacts.unblock(flags: flags, id: inputPeer))
|
||||
}
|
||||
return signal
|
||||
|> map(Optional.init)
|
||||
@ -70,3 +39,45 @@ func _internal_requestUpdatePeerIsBlocked(account: Account, peerId: PeerId, isBl
|
||||
}
|
||||
} |> 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 enum Subject {
|
||||
case blocked
|
||||
case stories
|
||||
}
|
||||
|
||||
private let account: Account
|
||||
private let subject: Subject
|
||||
private var _state: BlockedPeersContextState {
|
||||
didSet {
|
||||
if self._state != oldValue {
|
||||
@ -35,10 +41,12 @@ public final class BlockedPeersContext {
|
||||
|
||||
private let disposable = MetaDisposable()
|
||||
|
||||
public init(account: Account) {
|
||||
public init(account: Account, subject: Subject) {
|
||||
assert(Queue.mainQueue().isCurrent())
|
||||
|
||||
self.account = account
|
||||
self.subject = subject
|
||||
|
||||
self._state = BlockedPeersContextState(isLoadingMore: false, canLoadMore: true, totalCount: nil, peers: [])
|
||||
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)
|
||||
let postbox = self.account.postbox
|
||||
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
|
||||
|> mapToSignal { result -> Signal<(peers: [RenderedPeer], canLoadMore: Bool, totalCount: Int?), NoError> 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> {
|
||||
assert(Queue.mainQueue().isCurrent())
|
||||
|
||||
let postbox = self.account.postbox
|
||||
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 transaction.getPeer(peerId).flatMap(apiInputPeer)
|
||||
}
|
||||
@ -136,7 +180,7 @@ public final class BlockedPeersContext {
|
||||
guard let inputPeer = inputPeer else {
|
||||
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
|
||||
return .generic
|
||||
}
|
||||
@ -150,7 +194,13 @@ public final class BlockedPeersContext {
|
||||
} else {
|
||||
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())
|
||||
let postbox = self.account.postbox
|
||||
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 transaction.getPeer(peerId).flatMap(apiInputPeer)
|
||||
}
|
||||
@ -196,7 +253,7 @@ public final class BlockedPeersContext {
|
||||
guard let inputPeer = inputPeer else {
|
||||
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
|
||||
return .generic
|
||||
}
|
||||
@ -210,7 +267,13 @@ public final class BlockedPeersContext {
|
||||
} else {
|
||||
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)
|
||||
|
@ -13,6 +13,10 @@ public extension TelegramEngine {
|
||||
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 {
|
||||
return ActiveSessionsContext(account: self.account)
|
||||
}
|
||||
|
@ -686,6 +686,7 @@ final class AvatarEditorScreenComponent: Component {
|
||||
customLayout: nil,
|
||||
externalBackground: nil,
|
||||
externalExpansionView: nil,
|
||||
customContentView: nil,
|
||||
useOpaqueTheme: true,
|
||||
hideBackground: true,
|
||||
stateContext: nil,
|
||||
@ -815,6 +816,7 @@ final class AvatarEditorScreenComponent: Component {
|
||||
customLayout: nil,
|
||||
externalBackground: nil,
|
||||
externalExpansionView: nil,
|
||||
customContentView: nil,
|
||||
useOpaqueTheme: true,
|
||||
hideBackground: true,
|
||||
stateContext: nil,
|
||||
|
@ -1309,6 +1309,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
customLayout: nil,
|
||||
externalBackground: nil,
|
||||
externalExpansionView: nil,
|
||||
customContentView: nil,
|
||||
useOpaqueTheme: false,
|
||||
hideBackground: false,
|
||||
stateContext: self.stateContext?.emojiState,
|
||||
@ -1608,13 +1609,13 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
customLayout: nil,
|
||||
externalBackground: nil,
|
||||
externalExpansionView: nil,
|
||||
customContentView: nil,
|
||||
useOpaqueTheme: false,
|
||||
hideBackground: false,
|
||||
stateContext: nil,
|
||||
addImage: nil
|
||||
)
|
||||
|
||||
|
||||
self.inputDataDisposable = (combineLatest(queue: .mainQueue(),
|
||||
updatedInputData,
|
||||
.single(self.currentInputData.gifs) |> then(self.gifComponent.get() |> map(Optional.init)),
|
||||
@ -2510,6 +2511,7 @@ public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputVi
|
||||
customLayout: nil,
|
||||
externalBackground: nil,
|
||||
externalExpansionView: nil,
|
||||
customContentView: nil,
|
||||
useOpaqueTheme: false,
|
||||
hideBackground: hideBackground,
|
||||
stateContext: nil,
|
||||
|
@ -674,6 +674,7 @@ public final class EmojiStatusSelectionController: ViewController {
|
||||
customLayout: nil,
|
||||
externalBackground: nil,
|
||||
externalExpansionView: nil,
|
||||
customContentView: nil,
|
||||
useOpaqueTheme: true,
|
||||
hideBackground: false,
|
||||
stateContext: nil,
|
||||
|
@ -2210,6 +2210,12 @@ public protocol EmojiContentPeekBehavior: AnyObject {
|
||||
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 static let staticEmojiMapping: [(EmojiPagerContentComponent.StaticEmojiSegment, [String])] = {
|
||||
guard let path = getAppBundle().path(forResource: "emoji1016", ofType: "txt") else {
|
||||
@ -2263,7 +2269,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
self.isDisabled = isDisabled
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public struct CustomLayout: Equatable {
|
||||
public var itemsPerRow: Int
|
||||
public var itemSize: CGFloat
|
||||
@ -2322,6 +2328,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
public let customLayout: CustomLayout?
|
||||
public let externalBackground: ExternalBackground?
|
||||
public weak var externalExpansionView: UIView?
|
||||
public let customContentView: EmojiCustomContentView?
|
||||
public let useOpaqueTheme: Bool
|
||||
public let hideBackground: Bool
|
||||
public let scrollingStickersGridPromise = ValuePromise<Bool>(false)
|
||||
@ -2350,6 +2357,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
customLayout: CustomLayout?,
|
||||
externalBackground: ExternalBackground?,
|
||||
externalExpansionView: UIView?,
|
||||
customContentView: EmojiCustomContentView?,
|
||||
useOpaqueTheme: Bool,
|
||||
hideBackground: Bool,
|
||||
stateContext: StateContext?,
|
||||
@ -2376,6 +2384,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
self.customLayout = customLayout
|
||||
self.externalBackground = externalBackground
|
||||
self.externalExpansionView = externalExpansionView
|
||||
self.customContentView = customContentView
|
||||
self.useOpaqueTheme = useOpaqueTheme
|
||||
self.hideBackground = hideBackground
|
||||
self.stateContext = stateContext
|
||||
@ -2849,6 +2858,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
var verticalGroupDefaultSpacing: CGFloat
|
||||
var verticalGroupFeaturedSpacing: CGFloat
|
||||
var itemsPerRow: Int
|
||||
var customContentHeight: CGFloat
|
||||
var contentSize: CGSize
|
||||
|
||||
var searchInsets: UIEdgeInsets
|
||||
@ -2857,9 +2867,21 @@ public final class EmojiPagerContentComponent: Component {
|
||||
var premiumButtonInset: 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.width = width
|
||||
self.customContentHeight = customContentHeight
|
||||
|
||||
self.premiumButtonInset = 6.0
|
||||
self.premiumButtonHeight = 50.0
|
||||
@ -2926,6 +2948,8 @@ public final class EmojiPagerContentComponent: Component {
|
||||
self.itemInsets.left = floorToScreenPixels((width - actualContentWidth) / 2.0)
|
||||
self.itemInsets.right = self.itemInsets.left
|
||||
|
||||
self.itemInsets.top += self.customContentHeight
|
||||
|
||||
if displaySearch {
|
||||
self.itemInsets.top += self.searchHeight - 4.0
|
||||
}
|
||||
@ -3651,6 +3675,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
private let placeholdersContainerView: UIView
|
||||
private var visibleSearchHeader: EmojiSearchHeaderView?
|
||||
private var visibleEmptySearchResultsView: EmptySearchResultsView?
|
||||
private var visibleCustomContentView: EmojiCustomContentView?
|
||||
private var visibleItemPlaceholderViews: [ItemLayer.Key: ItemPlaceholderView] = [:]
|
||||
private var visibleFillPlaceholdersViews: [Int: ItemPlaceholderView] = [:]
|
||||
private var visibleItemSelectionLayers: [ItemLayer.Key: ItemSelectionLayer] = [:]
|
||||
@ -6357,6 +6382,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
var updatedItemPositions: [VisualItemKey: CGPoint]?
|
||||
|
||||
let contentAnimation = transition.userData(ContentAnimation.self)
|
||||
let useOpaqueTheme = component.inputInteractionHolder.inputInteraction?.useOpaqueTheme ?? false
|
||||
|
||||
var transitionHintInstalledGroupId: AnyHashable?
|
||||
var transitionHintExpandedGroupId: AnyHashable?
|
||||
@ -6483,6 +6509,30 @@ public final class EmojiPagerContentComponent: Component {
|
||||
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] = []
|
||||
for itemGroup in component.contentItemGroups {
|
||||
itemGroups.append(ItemGroupDescription(
|
||||
@ -6508,6 +6558,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
curveNearBounds: component.warpContentsOnEdges,
|
||||
displaySearch: component.displaySearchWithPlaceholder != nil,
|
||||
isSearchActivated: self.isSearchActivated,
|
||||
customContentHeight: customContentHeight,
|
||||
customLayout: component.inputInteractionHolder.inputInteraction?.customLayout
|
||||
)
|
||||
let itemLayout = extractedExpr
|
||||
@ -6704,7 +6755,6 @@ public final class EmojiPagerContentComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
let useOpaqueTheme = component.inputInteractionHolder.inputInteraction?.useOpaqueTheme ?? false
|
||||
|
||||
if let displaySearchWithPlaceholder = component.displaySearchWithPlaceholder {
|
||||
let visibleSearchHeader: EmojiSearchHeaderView
|
||||
@ -6812,8 +6862,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
visibleSearchHeader.tintContainerView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
if let emptySearchResults = component.emptySearchResults {
|
||||
let visibleEmptySearchResultsView: EmptySearchResultsView
|
||||
var emptySearchResultsTransition = transition
|
||||
|
@ -407,6 +407,7 @@ public final class EmojiSearchContent: ASDisplayNode, EntitySearchContainerNode
|
||||
customLayout: nil,
|
||||
externalBackground: nil,
|
||||
externalExpansionView: nil,
|
||||
customContentView: nil,
|
||||
useOpaqueTheme: true,
|
||||
hideBackground: false,
|
||||
stateContext: nil,
|
||||
|
@ -948,6 +948,7 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent {
|
||||
customLayout: nil,
|
||||
externalBackground: nil,
|
||||
externalExpansionView: nil,
|
||||
customContentView: nil,
|
||||
useOpaqueTheme: true,
|
||||
hideBackground: false,
|
||||
stateContext: nil,
|
||||
|
@ -10,6 +10,7 @@ public enum CodableDrawingEntity: Equatable {
|
||||
case simpleShape(DrawingSimpleShapeEntity)
|
||||
case bubble(DrawingBubbleEntity)
|
||||
case vector(DrawingVectorEntity)
|
||||
case location(DrawingLocationEntity)
|
||||
|
||||
public init?(entity: DrawingEntity) {
|
||||
if let entity = entity as? DrawingStickerEntity {
|
||||
@ -22,6 +23,8 @@ public enum CodableDrawingEntity: Equatable {
|
||||
self = .bubble(entity)
|
||||
} else if let entity = entity as? DrawingVectorEntity {
|
||||
self = .vector(entity)
|
||||
} else if let entity = entity as? DrawingLocationEntity {
|
||||
self = .location(entity)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
@ -39,6 +42,8 @@ public enum CodableDrawingEntity: Equatable {
|
||||
return entity
|
||||
case let .vector(entity):
|
||||
return entity
|
||||
case let .location(entity):
|
||||
return entity
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -55,6 +60,7 @@ extension CodableDrawingEntity: Codable {
|
||||
case simpleShape
|
||||
case bubble
|
||||
case vector
|
||||
case location
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
@ -71,6 +77,8 @@ extension CodableDrawingEntity: Codable {
|
||||
self = .bubble(try container.decode(DrawingBubbleEntity.self, forKey: .entity))
|
||||
case .vector:
|
||||
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):
|
||||
try container.encode(EntityType.vector, forKey: .type)
|
||||
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 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 angle = -entity.rotation
|
||||
let scale = entity.scale * 0.5 * textScale
|
||||
let angle: CGFloat
|
||||
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(
|
||||
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)!
|
||||
|
||||
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] {
|
||||
@ -70,13 +84,14 @@ func composerEntitiesForDrawingEntity(account: Account, textScale: CGFloat, enti
|
||||
} else if let entity = entity as? DrawingTextEntity {
|
||||
var entities: [MediaEditorComposerEntity] = []
|
||||
entities.append(prerenderTextTransformations(entity: entity, image: renderImage, textScale: textScale, colorSpace: colorSpace))
|
||||
|
||||
if let renderSubEntities = entity.renderSubEntities {
|
||||
for subEntity in renderSubEntities {
|
||||
entities.append(contentsOf: composerEntitiesForDrawingEntity(account: account, textScale: textScale, entity: subEntity, colorSpace: colorSpace, tintColor: entity.color.toUIColor()))
|
||||
}
|
||||
}
|
||||
return entities
|
||||
} else if let entity = entity as? DrawingLocationEntity {
|
||||
return [prerenderTextTransformations(entity: entity, image: renderImage, textScale: textScale, colorSpace: colorSpace)]
|
||||
}
|
||||
}
|
||||
return []
|
||||
|
@ -42,6 +42,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/CameraButtonComponent",
|
||||
"//submodules/ChatPresentationInterfaceState",
|
||||
"//submodules/DeviceAccess",
|
||||
"//submodules/LocationUI",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -31,6 +31,7 @@ import ChatEntityKeyboardInputNode
|
||||
import ChatPresentationInterfaceState
|
||||
import TextFormat
|
||||
import DeviceAccess
|
||||
import LocationUI
|
||||
|
||||
enum DrawingScreenType {
|
||||
case drawing
|
||||
@ -1148,6 +1149,18 @@ final class MediaEditorScreenComponent: Component {
|
||||
forwardAction: nil,
|
||||
moreAction: 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
|
||||
guard let self, let environment = self.environment, let controller = environment.controller() as? MediaEditorScreen else {
|
||||
return
|
||||
@ -1179,6 +1192,7 @@ final class MediaEditorScreenComponent: Component {
|
||||
timeoutSelected: timeoutSelected,
|
||||
displayGradient: false,
|
||||
bottomInset: 0.0,
|
||||
isFormattingLocked: false,
|
||||
hideKeyboard: self.currentInputMode == .emoji,
|
||||
forceIsEditing: self.currentInputMode == .emoji,
|
||||
disabledPlaceholder: nil
|
||||
@ -2684,6 +2698,25 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
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) {
|
||||
guard let layout = self.validLayout, case .compact = layout.metrics.widthClass else {
|
||||
return
|
||||
@ -2852,6 +2885,12 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
self.presentGallery()
|
||||
}
|
||||
}
|
||||
controller.presentLocationPicker = { [weak self, weak controller] in
|
||||
if let self {
|
||||
controller?.dismiss(animated: true)
|
||||
self.presentLocationPicker()
|
||||
}
|
||||
}
|
||||
self.stickerScreen = controller
|
||||
self.controller?.present(controller, in: .window(.root))
|
||||
return
|
||||
@ -3175,6 +3214,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
public var willDismiss: () -> Void = { }
|
||||
|
||||
private var closeFriends = Promise<[EnginePeer]>()
|
||||
private let storiesGrayList: BlockedPeersContext
|
||||
|
||||
private let hapticFeedback = HapticFeedback()
|
||||
|
||||
@ -3199,6 +3239,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
self.transitionOut = transitionOut
|
||||
self.completion = completion
|
||||
|
||||
self.storiesGrayList = BlockedPeersContext(account: context.account, subject: .stories)
|
||||
|
||||
if let transitionIn, case .camera = transitionIn {
|
||||
self.isSavingAvailable = true
|
||||
}
|
||||
@ -3271,7 +3313,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
context: self.context,
|
||||
subject: .stories(editing: false),
|
||||
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
|
||||
guard let self else {
|
||||
@ -3299,7 +3342,23 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
guard let self else {
|
||||
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 {
|
||||
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
|
||||
if privacy.base == .nobody {
|
||||
subject = .chats
|
||||
if grayList {
|
||||
subject = .chats(grayList: true)
|
||||
} else if privacy.base == .nobody {
|
||||
subject = .chats(grayList: false)
|
||||
} else {
|
||||
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
|
||||
guard let self else {
|
||||
return
|
||||
@ -3341,7 +3407,10 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
guard let self else {
|
||||
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()
|
||||
self.closeFriends.set(.single(peers))
|
||||
completion(EngineStoryPrivacy(base: .closeFriends, additionallyIncludePeers: []))
|
||||
@ -3349,7 +3418,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
completion(result)
|
||||
}
|
||||
},
|
||||
editCategory: { _, _, _ in }
|
||||
editCategory: { _, _, _ in },
|
||||
editGrayList: { _, _, _ in }
|
||||
)
|
||||
controller.dismissed = {
|
||||
self.node.mediaEditor?.play()
|
||||
@ -3435,15 +3505,54 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
self.present(contextController, in: .window(.root))
|
||||
}
|
||||
|
||||
private func presentTimeoutPremiumSuggestion() {
|
||||
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
let text = presentationData.strings.Story_Editor_TooltipPremiumExpiration
|
||||
fileprivate func presentTimeoutPremiumSuggestion() {
|
||||
self.dismissAllTooltips()
|
||||
|
||||
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
|
||||
if case .undo = action, let self {
|
||||
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .settings)
|
||||
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 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)
|
||||
}
|
||||
return false }
|
||||
|
@ -271,6 +271,8 @@ final class StoryPreviewComponent: Component {
|
||||
forwardAction: {},
|
||||
moreAction: { _, _ in },
|
||||
presentVoiceMessagesUnavailableTooltip: nil,
|
||||
presentTextLengthLimitTooltip: nil,
|
||||
presentTextFormattingTooltip: nil,
|
||||
paste: { _ in },
|
||||
audioRecorder: nil,
|
||||
videoRecordingStatus: nil,
|
||||
@ -282,6 +284,7 @@ final class StoryPreviewComponent: Component {
|
||||
timeoutSelected: false,
|
||||
displayGradient: false,
|
||||
bottomInset: 0.0,
|
||||
isFormattingLocked: false,
|
||||
hideKeyboard: false,
|
||||
forceIsEditing: false,
|
||||
disabledPlaceholder: nil
|
||||
|
@ -84,6 +84,8 @@ public final class MessageInputPanelComponent: Component {
|
||||
public let forwardAction: (() -> Void)?
|
||||
public let moreAction: ((UIView, ContextGesture?) -> Void)?
|
||||
public let presentVoiceMessagesUnavailableTooltip: ((UIView) -> Void)?
|
||||
public let presentTextLengthLimitTooltip: (() -> Void)?
|
||||
public let presentTextFormattingTooltip: (() -> Void)?
|
||||
public let paste: (TextFieldComponent.PasteData) -> Void
|
||||
public let audioRecorder: ManagedAudioRecorder?
|
||||
public let videoRecordingStatus: InstantVideoControllerRecordingStatus?
|
||||
@ -95,6 +97,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
public let timeoutSelected: Bool
|
||||
public let displayGradient: Bool
|
||||
public let bottomInset: CGFloat
|
||||
public let isFormattingLocked: Bool
|
||||
public let hideKeyboard: Bool
|
||||
public let forceIsEditing: Bool
|
||||
public let disabledPlaceholder: String?
|
||||
@ -126,6 +129,8 @@ public final class MessageInputPanelComponent: Component {
|
||||
forwardAction: (() -> Void)?,
|
||||
moreAction: ((UIView, ContextGesture?) -> Void)?,
|
||||
presentVoiceMessagesUnavailableTooltip: ((UIView) -> Void)?,
|
||||
presentTextLengthLimitTooltip: (() -> Void)?,
|
||||
presentTextFormattingTooltip: (() -> Void)?,
|
||||
paste: @escaping (TextFieldComponent.PasteData) -> Void,
|
||||
audioRecorder: ManagedAudioRecorder?,
|
||||
videoRecordingStatus: InstantVideoControllerRecordingStatus?,
|
||||
@ -137,6 +142,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
timeoutSelected: Bool,
|
||||
displayGradient: Bool,
|
||||
bottomInset: CGFloat,
|
||||
isFormattingLocked: Bool,
|
||||
hideKeyboard: Bool,
|
||||
forceIsEditing: Bool,
|
||||
disabledPlaceholder: String?
|
||||
@ -167,6 +173,8 @@ public final class MessageInputPanelComponent: Component {
|
||||
self.forwardAction = forwardAction
|
||||
self.moreAction = moreAction
|
||||
self.presentVoiceMessagesUnavailableTooltip = presentVoiceMessagesUnavailableTooltip
|
||||
self.presentTextLengthLimitTooltip = presentTextLengthLimitTooltip
|
||||
self.presentTextFormattingTooltip = presentTextFormattingTooltip
|
||||
self.paste = paste
|
||||
self.audioRecorder = audioRecorder
|
||||
self.videoRecordingStatus = videoRecordingStatus
|
||||
@ -178,6 +186,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
self.timeoutSelected = timeoutSelected
|
||||
self.displayGradient = displayGradient
|
||||
self.bottomInset = bottomInset
|
||||
self.isFormattingLocked = isFormattingLocked
|
||||
self.hideKeyboard = hideKeyboard
|
||||
self.forceIsEditing = forceIsEditing
|
||||
self.disabledPlaceholder = disabledPlaceholder
|
||||
@ -244,6 +253,9 @@ public final class MessageInputPanelComponent: Component {
|
||||
if lhs.bottomInset != rhs.bottomInset {
|
||||
return false
|
||||
}
|
||||
if lhs.isFormattingLocked != rhs.isFormattingLocked {
|
||||
return false
|
||||
}
|
||||
if (lhs.forwardAction == nil) != (rhs.forwardAction == nil) {
|
||||
return false
|
||||
}
|
||||
@ -567,6 +579,10 @@ public final class MessageInputPanelComponent: Component {
|
||||
textColor: UIColor(rgb: 0xffffff),
|
||||
insets: UIEdgeInsets(top: 9.0, left: 8.0, bottom: 10.0, right: 48.0),
|
||||
hideKeyboard: component.hideKeyboard,
|
||||
formatMenuAvailability: component.isFormattingLocked ? .locked : .available,
|
||||
lockedFormatAction: {
|
||||
component.presentTextFormattingTooltip?()
|
||||
},
|
||||
present: { c in
|
||||
component.presentController(c)
|
||||
},
|
||||
@ -907,6 +923,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
} else {
|
||||
if let maxLength = component.maxLength, self.textFieldExternalState.textLength > maxLength {
|
||||
self.animateError()
|
||||
component.presentTextLengthLimitTooltip?()
|
||||
} else {
|
||||
component.sendMessageAction()
|
||||
}
|
||||
@ -916,6 +933,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
if case .up = action {
|
||||
if let maxLength = component.maxLength, self.textFieldExternalState.textLength > maxLength {
|
||||
self.animateError()
|
||||
component.presentTextLengthLimitTooltip?()
|
||||
} else {
|
||||
component.sendMessageAction()
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ import PeerListItemComponent
|
||||
import LottieComponent
|
||||
import TooltipUI
|
||||
import OverlayStatusController
|
||||
import Markdown
|
||||
|
||||
final class ShareWithPeersScreenComponent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
@ -38,6 +39,7 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
let optionItems: [OptionItem]
|
||||
let completion: (EngineStoryPrivacy, Bool, Bool, [EnginePeer]) -> Void
|
||||
let editCategory: (EngineStoryPrivacy, Bool, Bool) -> Void
|
||||
let editGrayList: (EngineStoryPrivacy, Bool, Bool) -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
@ -50,7 +52,8 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
categoryItems: [CategoryItem],
|
||||
optionItems: [OptionItem],
|
||||
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.stateContext = stateContext
|
||||
@ -63,6 +66,7 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
self.optionItems = optionItems
|
||||
self.completion = completion
|
||||
self.editCategory = editCategory
|
||||
self.editGrayList = editGrayList
|
||||
}
|
||||
|
||||
static func ==(lhs: ShareWithPeersScreenComponent, rhs: ShareWithPeersScreenComponent) -> Bool {
|
||||
@ -971,6 +975,76 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
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 {
|
||||
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))
|
||||
@ -1641,7 +1715,7 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
}
|
||||
navigationButtonsWidth += navigationLeftButtonSize.width + navigationSideInset
|
||||
|
||||
var actionButtonTitle = environment.strings.Story_Privacy_SaveSettings
|
||||
var actionButtonTitle = environment.strings.Story_Privacy_SaveList
|
||||
let title: String
|
||||
switch component.stateContext.subject {
|
||||
case let .stories(editing):
|
||||
@ -1651,8 +1725,12 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
title = environment.strings.Story_Privacy_ShareStory
|
||||
actionButtonTitle = environment.strings.Story_Privacy_PostStory
|
||||
}
|
||||
case .chats:
|
||||
title = environment.strings.Story_Privacy_CategorySelectedContacts
|
||||
case let .chats(grayList):
|
||||
if grayList {
|
||||
title = environment.strings.Story_Privacy_HideMyStoriesFrom
|
||||
} else {
|
||||
title = environment.strings.Story_Privacy_CategorySelectedContacts
|
||||
}
|
||||
case let .contacts(category):
|
||||
switch category {
|
||||
case .closeFriends:
|
||||
@ -1976,24 +2054,27 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
||||
let presences: [EnginePeer.Id: EnginePeer.Presence]
|
||||
let participants: [EnginePeer.Id: Int]
|
||||
let closeFriendsPeers: [EnginePeer]
|
||||
let grayListPeers: [EnginePeer]
|
||||
|
||||
fileprivate init(
|
||||
peers: [EnginePeer],
|
||||
presences: [EnginePeer.Id: EnginePeer.Presence],
|
||||
participants: [EnginePeer.Id: Int],
|
||||
closeFriendsPeers: [EnginePeer]
|
||||
closeFriendsPeers: [EnginePeer],
|
||||
grayListPeers: [EnginePeer]
|
||||
) {
|
||||
self.peers = peers
|
||||
self.presences = presences
|
||||
self.participants = participants
|
||||
self.closeFriendsPeers = closeFriendsPeers
|
||||
self.grayListPeers = grayListPeers
|
||||
}
|
||||
}
|
||||
|
||||
public final class StateContext {
|
||||
public enum Subject: Equatable {
|
||||
case stories(editing: Bool)
|
||||
case chats
|
||||
case chats(grayList: Bool)
|
||||
case contacts(EngineStoryPrivacy.Base)
|
||||
case search(query: String, onlyContacts: Bool)
|
||||
}
|
||||
@ -2002,6 +2083,7 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
||||
|
||||
public let subject: Subject
|
||||
public private(set) var initialPeerIds: Set<EnginePeer.Id> = Set()
|
||||
fileprivate let storiesGrayList: BlockedPeersContext?
|
||||
|
||||
private var stateDisposable: Disposable?
|
||||
private let stateSubject = Promise<State>()
|
||||
@ -2015,13 +2097,25 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
subject: Subject = .chats,
|
||||
subject: Subject = .chats(grayList: false),
|
||||
initialPeerIds: Set<EnginePeer.Id> = Set(),
|
||||
closeFriends: Signal<[EnginePeer], NoError> = .single([])
|
||||
closeFriends: Signal<[EnginePeer], NoError> = .single([]),
|
||||
storiesGrayList: BlockedPeersContext? = nil
|
||||
) {
|
||||
self.subject = subject
|
||||
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 {
|
||||
case .stories:
|
||||
var peerSignals: [Signal<EnginePeer?, NoError>] = []
|
||||
@ -2033,8 +2127,8 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
||||
|
||||
let peers = combineLatest(peerSignals)
|
||||
|
||||
self.stateDisposable = combineLatest(queue: Queue.mainQueue(), peers, closeFriends)
|
||||
.start(next: { [weak self] peers, closeFriends in
|
||||
self.stateDisposable = combineLatest(queue: Queue.mainQueue(), peers, closeFriends, grayListPeers)
|
||||
.start(next: { [weak self] peers, closeFriends, grayListPeers in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
@ -2043,28 +2137,30 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
||||
peers: peers.compactMap { $0 },
|
||||
presences: [:],
|
||||
participants: [:],
|
||||
closeFriendsPeers: closeFriends
|
||||
closeFriendsPeers: closeFriends,
|
||||
grayListPeers: grayListPeers
|
||||
)
|
||||
self.stateValue = state
|
||||
self.stateSubject.set(.single(state))
|
||||
|
||||
self.readySubject.set(true)
|
||||
})
|
||||
case .chats:
|
||||
case let .chats(isGrayList):
|
||||
self.stateDisposable = (combineLatest(
|
||||
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(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(
|
||||
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
|
||||
return (chatList, contacts, initialPeers, participantCountMap)
|
||||
|> map { participantCountMap -> (EngineChatList, EngineContactList, [EnginePeer.Id: Optional<EnginePeer>], [EnginePeer.Id: Optional<Int>], [EnginePeer]) in
|
||||
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 {
|
||||
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 selectedPeers: [EnginePeer] = []
|
||||
|
||||
if isGrayList {
|
||||
self.initialPeerIds = Set(grayListPeers.map { $0.id })
|
||||
}
|
||||
|
||||
for item in chatList.items.reversed() {
|
||||
if self.initialPeerIds.contains(item.renderedPeer.peerId), let peer = item.renderedPeer.peer {
|
||||
selectedPeers.append(peer)
|
||||
existingIds.insert(peer.id)
|
||||
if let peer = item.renderedPeer.peer {
|
||||
if self.initialPeerIds.contains(peer.id) || isGrayList && grayListPeersIds.contains(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] = [:]
|
||||
for item in chatList.items {
|
||||
presences[item.renderedPeer.peerId] = item.presence
|
||||
@ -2106,7 +2223,7 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
||||
if peer.id == context.account.peerId {
|
||||
return false
|
||||
}
|
||||
if peer.isService {
|
||||
if peer.isService || peer.isDeleted {
|
||||
return false
|
||||
}
|
||||
if case let .user(user) = peer {
|
||||
@ -2136,7 +2253,8 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
||||
peers: peers,
|
||||
presences: presences,
|
||||
participants: participants,
|
||||
closeFriendsPeers: []
|
||||
closeFriendsPeers: [],
|
||||
grayListPeers: grayListPeers
|
||||
)
|
||||
self.stateValue = state
|
||||
self.stateSubject.set(.single(state))
|
||||
@ -2198,7 +2316,8 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
||||
peers: peers,
|
||||
presences: contactList.presences,
|
||||
participants: [:],
|
||||
closeFriendsPeers: []
|
||||
closeFriendsPeers: [],
|
||||
grayListPeers: []
|
||||
)
|
||||
|
||||
self.stateValue = state
|
||||
@ -2248,6 +2367,10 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
||||
return false
|
||||
} else if user.botInfo != nil {
|
||||
return false
|
||||
} else if peer.isService {
|
||||
return false
|
||||
} else if user.isDeleted {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
@ -2265,7 +2388,8 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
||||
},
|
||||
presences: [:],
|
||||
participants: participants,
|
||||
closeFriendsPeers: []
|
||||
closeFriendsPeers: [],
|
||||
grayListPeers: []
|
||||
)
|
||||
self.stateValue = state
|
||||
self.stateSubject.set(.single(state))
|
||||
@ -2301,7 +2425,8 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
||||
mentions: [String] = [],
|
||||
stateContext: StateContext,
|
||||
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
|
||||
|
||||
@ -2411,7 +2536,8 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
||||
categoryItems: categoryItems,
|
||||
optionItems: optionItems,
|
||||
completion: completion,
|
||||
editCategory: editCategory
|
||||
editCategory: editCategory,
|
||||
editGrayList: editGrayList
|
||||
), navigationBarAppearance: .none, theme: .dark)
|
||||
|
||||
self.statusBar.statusBarStyle = .Ignore
|
||||
|
@ -25,6 +25,7 @@ swift_library(
|
||||
"//submodules/AppBundle",
|
||||
"//submodules/PeerPresenceStatusManager",
|
||||
"//submodules/TelegramUI/Components/EmojiStatusComponent",
|
||||
"//submodules/ContextUI",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -15,6 +15,7 @@ import TelegramStringFormatting
|
||||
import AppBundle
|
||||
import PeerPresenceStatusManager
|
||||
import EmojiStatusComponent
|
||||
import ContextUI
|
||||
|
||||
private let avatarFont = avatarPlaceholderFont(size: 15.0)
|
||||
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 hasNext: Bool
|
||||
let action: (EnginePeer) -> Void
|
||||
let contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)?
|
||||
let openStories: ((EnginePeer, AvatarNode) -> Void)?
|
||||
|
||||
public init(
|
||||
@ -74,6 +76,7 @@ public final class PeerListItemComponent: Component {
|
||||
selectionState: SelectionState,
|
||||
hasNext: Bool,
|
||||
action: @escaping (EnginePeer) -> Void,
|
||||
contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)? = nil,
|
||||
openStories: ((EnginePeer, AvatarNode) -> Void)? = nil
|
||||
) {
|
||||
self.context = context
|
||||
@ -90,6 +93,7 @@ public final class PeerListItemComponent: Component {
|
||||
self.selectionState = selectionState
|
||||
self.hasNext = hasNext
|
||||
self.action = action
|
||||
self.contextAction = contextAction
|
||||
self.openStories = openStories
|
||||
}
|
||||
|
||||
@ -136,7 +140,8 @@ public final class PeerListItemComponent: Component {
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
public final class View: ContextControllerSourceView {
|
||||
private let extractedContainerView: ContextExtractedContentContainingView
|
||||
private let containerButton: HighlightTrackingButton
|
||||
|
||||
private let title = ComponentView<Empty>()
|
||||
@ -173,9 +178,12 @@ public final class PeerListItemComponent: Component {
|
||||
return value
|
||||
}
|
||||
|
||||
private var isExtractedToContextMenu: Bool = false
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.separatorLayer = SimpleLayer()
|
||||
|
||||
self.extractedContainerView = ContextExtractedContentContainingView()
|
||||
self.containerButton = HighlightTrackingButton()
|
||||
self.containerButton.isExclusiveTouch = true
|
||||
|
||||
@ -186,14 +194,49 @@ public final class PeerListItemComponent: Component {
|
||||
|
||||
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.addSubview(self.containerButton)
|
||||
self.containerButton.layer.addSublayer(self.avatarNode.layer)
|
||||
|
||||
self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
|
||||
|
||||
self.addSubview(self.avatarButtonView)
|
||||
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) {
|
||||
@ -219,6 +262,8 @@ public final class PeerListItemComponent: Component {
|
||||
if let hint = transition.userData(TransitionHint.self) {
|
||||
synchronousLoad = hint.synchronousLoad
|
||||
}
|
||||
|
||||
self.isGestureEnabled = component.contextAction != nil
|
||||
|
||||
let themeUpdated = self.component?.theme !== component.theme
|
||||
|
||||
@ -270,7 +315,7 @@ public final class PeerListItemComponent: Component {
|
||||
labelData = ("", false)
|
||||
}
|
||||
|
||||
let contextInset: CGFloat = 0.0
|
||||
let contextInset: CGFloat = self.isExtractedToContextMenu ? 12.0 : 0.0
|
||||
|
||||
let height: CGFloat
|
||||
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)))
|
||||
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))
|
||||
transition.setFrame(view: self.containerButton, frame: containerFrame)
|
||||
|
||||
|
@ -1641,7 +1641,7 @@ private final class StoryContainerScreenComponent: Component {
|
||||
size: availableSize,
|
||||
metrics: environment.metrics,
|
||||
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),
|
||||
additionalInsets: UIEdgeInsets(),
|
||||
statusBarHeight: nil,
|
||||
|
@ -1943,6 +1943,8 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
self.voiceMessagesRestrictedTooltipController = controller
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.2, curve: .easeInOut)))
|
||||
},
|
||||
presentTextLengthLimitTooltip: nil,
|
||||
presentTextFormattingTooltip: nil,
|
||||
paste: { [weak self] data in
|
||||
guard let self else {
|
||||
return
|
||||
@ -1971,6 +1973,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
timeoutSelected: false,
|
||||
displayGradient: false,
|
||||
bottomInset: component.inputHeight != 0.0 || inputNodeVisible ? 0.0 : bottomContentInset,
|
||||
isFormattingLocked: false,
|
||||
hideKeyboard: self.sendMessageContext.currentInputMode == .media,
|
||||
forceIsEditing: self.sendMessageContext.currentInputMode == .media,
|
||||
disabledPlaceholder: disabledPlaceholder
|
||||
@ -2206,6 +2209,106 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
}
|
||||
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
|
||||
guard let self else {
|
||||
return
|
||||
@ -3233,7 +3336,8 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
text = component.strings.Story_PrivacyTooltipCloseFriends
|
||||
} else if privacy.base == .nobody {
|
||||
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 {
|
||||
text = component.strings.Story_PrivacyTooltipNobody
|
||||
}
|
||||
@ -3300,6 +3404,13 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
}
|
||||
self.openItemPrivacySettings(initialPrivacy: privacy)
|
||||
})
|
||||
},
|
||||
editGrayList: { [weak self] privacy, _, _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let _ = self
|
||||
let _ = privacy
|
||||
}
|
||||
)
|
||||
controller.dismissed = { [weak self] in
|
||||
@ -3321,7 +3432,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
}
|
||||
let subject: ShareWithPeersScreen.StateContext.Subject
|
||||
if privacy.base == .nobody {
|
||||
subject = .chats
|
||||
subject = .chats(grayList: false)
|
||||
} else {
|
||||
subject = .contacts(privacy.base)
|
||||
}
|
||||
@ -3345,7 +3456,8 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
completion(result)
|
||||
}
|
||||
},
|
||||
editCategory: { _, _, _ in }
|
||||
editCategory: { _, _, _ in },
|
||||
editGrayList: { _, _, _ in }
|
||||
)
|
||||
controller.dismissed = { [weak self] in
|
||||
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] {
|
||||
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 moreAction: (UIView, ContextGesture?) -> Void
|
||||
let openPeer: (EnginePeer) -> Void
|
||||
let peerContextAction: (EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void
|
||||
let openPeerStories: (EnginePeer, AvatarNode) -> Void
|
||||
let openPremiumIntro: () -> Void
|
||||
|
||||
@ -79,6 +80,7 @@ final class StoryItemSetViewListComponent: Component {
|
||||
deleteAction: @escaping () -> Void,
|
||||
moreAction: @escaping (UIView, ContextGesture?) -> Void,
|
||||
openPeer: @escaping (EnginePeer) -> Void,
|
||||
peerContextAction: @escaping (EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void,
|
||||
openPeerStories: @escaping (EnginePeer, AvatarNode) -> Void,
|
||||
openPremiumIntro: @escaping () -> Void
|
||||
) {
|
||||
@ -98,6 +100,7 @@ final class StoryItemSetViewListComponent: Component {
|
||||
self.deleteAction = deleteAction
|
||||
self.moreAction = moreAction
|
||||
self.openPeer = openPeer
|
||||
self.peerContextAction = peerContextAction
|
||||
self.openPeerStories = openPeerStories
|
||||
self.openPremiumIntro = openPremiumIntro
|
||||
}
|
||||
@ -505,6 +508,9 @@ final class StoryItemSetViewListComponent: Component {
|
||||
}
|
||||
component.openPeer(peer)
|
||||
},
|
||||
contextAction: { peer, view, gesture in
|
||||
component.peerContextAction(peer, view, gesture)
|
||||
},
|
||||
openStories: { [weak self] peer, avatarNode in
|
||||
guard let self, let component = self.component else {
|
||||
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 strings: PresentationStrings
|
||||
public let externalState: ExternalState
|
||||
@ -83,6 +89,8 @@ public final class TextFieldComponent: Component {
|
||||
public let textColor: UIColor
|
||||
public let insets: UIEdgeInsets
|
||||
public let hideKeyboard: Bool
|
||||
public let formatMenuAvailability: FormatMenuAvailability
|
||||
public let lockedFormatAction: () -> Void
|
||||
public let present: (ViewController) -> Void
|
||||
public let paste: (PasteData) -> Void
|
||||
|
||||
@ -94,6 +102,8 @@ public final class TextFieldComponent: Component {
|
||||
textColor: UIColor,
|
||||
insets: UIEdgeInsets,
|
||||
hideKeyboard: Bool,
|
||||
formatMenuAvailability: FormatMenuAvailability,
|
||||
lockedFormatAction: @escaping () -> Void,
|
||||
present: @escaping (ViewController) -> Void,
|
||||
paste: @escaping (PasteData) -> Void
|
||||
) {
|
||||
@ -104,6 +114,8 @@ public final class TextFieldComponent: Component {
|
||||
self.textColor = textColor
|
||||
self.insets = insets
|
||||
self.hideKeyboard = hideKeyboard
|
||||
self.formatMenuAvailability = formatMenuAvailability
|
||||
self.lockedFormatAction = lockedFormatAction
|
||||
self.present = present
|
||||
self.paste = paste
|
||||
}
|
||||
@ -127,6 +139,9 @@ public final class TextFieldComponent: Component {
|
||||
if lhs.hideKeyboard != rhs.hideKeyboard {
|
||||
return false
|
||||
}
|
||||
if lhs.formatMenuAvailability != rhs.formatMenuAvailability {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -387,6 +402,22 @@ public final class TextFieldComponent: Component {
|
||||
return UIMenu(children: suggestedActions)
|
||||
}
|
||||
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] = [
|
||||
UIAction(title: strings.TextFormat_Bold, image: nil) { [weak self] action in
|
||||
if let self {
|
||||
|
@ -8,5 +8,8 @@
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user