Story location picking

This commit is contained in:
Ilya Laktyushin 2023-07-30 15:01:25 +02:00
parent b3146436dc
commit b70586eb28
43 changed files with 2397 additions and 133 deletions

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -37,7 +37,7 @@ public struct UserLimitsConfiguration: Equatable {
maxReactionsPerMessage: 1,
maxSharedFolderInviteLinks: 3,
maxSharedFolderJoin: 2,
maxStoryCaptionLength: 1024,
maxStoryCaptionLength: 200,
maxExpiringStoriesCount: 100
)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -674,6 +674,7 @@ public final class EmojiStatusSelectionController: ViewController {
customLayout: nil,
externalBackground: nil,
externalExpansionView: nil,
customContentView: nil,
useOpaqueTheme: true,
hideBackground: false,
stateContext: nil,

View File

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

View File

@ -407,6 +407,7 @@ public final class EmojiSearchContent: ASDisplayNode, EntitySearchContainerNode
customLayout: nil,
externalBackground: nil,
externalExpansionView: nil,
customContentView: nil,
useOpaqueTheme: true,
hideBackground: false,
stateContext: nil,

View File

@ -948,6 +948,7 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent {
customLayout: nil,
externalBackground: nil,
externalExpansionView: nil,
customContentView: nil,
useOpaqueTheme: true,
hideBackground: false,
stateContext: nil,

View File

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

View File

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

View File

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

View File

@ -42,6 +42,7 @@ swift_library(
"//submodules/TelegramUI/Components/CameraButtonComponent",
"//submodules/ChatPresentationInterfaceState",
"//submodules/DeviceAccess",
"//submodules/LocationUI",
],
visibility = [
"//visibility:public",

View File

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

View File

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

View File

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

View File

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

View File

@ -25,6 +25,7 @@ swift_library(
"//submodules/AppBundle",
"//submodules/PeerPresenceStatusManager",
"//submodules/TelegramUI/Components/EmojiStatusComponent",
"//submodules/ContextUI",
],
visibility = [
"//visibility:public",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,5 +8,8 @@
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}