diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index b4858876e5..574efdf128 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -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"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 5e07f87427..05d835f15d 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -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 } diff --git a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift index 0dd5519c5f..3f6998ac75 100644 --- a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift +++ b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift @@ -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() { } diff --git a/submodules/DrawingUI/Sources/DrawingLocationEntity.swift b/submodules/DrawingUI/Sources/DrawingLocationEntity.swift new file mode 100644 index 0000000000..c4a556d798 --- /dev/null +++ b/submodules/DrawingUI/Sources/DrawingLocationEntity.swift @@ -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 + } +} diff --git a/submodules/DrawingUI/Sources/DrawingTextEntity.swift b/submodules/DrawingUI/Sources/DrawingTextEntity.swift index c7a9b1cedf..a4be3b3d72 100644 --- a/submodules/DrawingUI/Sources/DrawingTextEntity.swift +++ b/submodules/DrawingUI/Sources/DrawingTextEntity.swift @@ -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() diff --git a/submodules/DrawingUI/Sources/StickerPickerScreen.swift b/submodules/DrawingUI/Sources/StickerPickerScreen.swift index 80d03ef30c..26852d7f4e 100644 --- a/submodules/DrawingUI/Sources/StickerPickerScreen.swift +++ b/submodules/DrawingUI/Sources/StickerPickerScreen.swift @@ -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, 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 + 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() + 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 + } +} diff --git a/submodules/LocationUI/Sources/LocationPickerController.swift b/submodules/LocationUI/Sources/LocationPickerController.swift index 7edbc72961..79283aabda 100644 --- a/submodules/LocationUI/Sources/LocationPickerController.swift +++ b/submodules/LocationUI/Sources/LocationPickerController.swift @@ -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, .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 +} diff --git a/submodules/PremiumUI/BUILD b/submodules/PremiumUI/BUILD index a356dc5792..4db415faef 100644 --- a/submodules/PremiumUI/BUILD +++ b/submodules/PremiumUI/BUILD @@ -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 = [ diff --git a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift index 79c67b5d25..b540e9f0bd 100644 --- a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift @@ -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 ) ) ), diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index b20521e926..cce72ea417 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -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 ) ) ), diff --git a/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift b/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift index ce43abcc7c..f32cf2d63f 100644 --- a/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift @@ -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, diff --git a/submodules/PremiumUI/Sources/StoriesPageComponent.swift b/submodules/PremiumUI/Sources/StoriesPageComponent.swift new file mode 100644 index 0000000000..1b0a0c6f72 --- /dev/null +++ b/submodules/PremiumUI/Sources/StoriesPageComponent.swift @@ -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() + + 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, 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, 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.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] = [] + + 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? + + 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.self) + let topPanel = Child(BlurredBackgroundComponent.self) + let topSeparator = Child(Rectangle.self) + let title = Child(MultilineTextComponent.self) + let secondaryTitle = Child(MultilineTextComponent.self) + + let resetScroll = ActionSlot() + + 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( + 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 + } + } +} diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index f2dba25b43..5e932c4f5b 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -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, diff --git a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift index 0160594a78..099a9f2824 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift @@ -686,7 +686,7 @@ public func privacyAndSecurityController( let privacySettingsPromise = Promise() 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() diff --git a/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift b/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift index 4fc7027914..7875072cbe 100644 --- a/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift +++ b/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift @@ -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) diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index 346d282ab2..819a48243c 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -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) diff --git a/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift b/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift index e40e3c4dda..0391b18969 100644 --- a/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift +++ b/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift @@ -37,7 +37,7 @@ public struct UserLimitsConfiguration: Equatable { maxReactionsPerMessage: 1, maxSharedFolderInviteLinks: 3, maxSharedFolderJoin: 2, - maxStoryCaptionLength: 1024, + maxStoryCaptionLength: 200, maxExpiringStoriesCount: 100 ) } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift index ca3b9669f5..3490ee486d 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift @@ -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 { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift index e93f6cbe13..06c24282d2 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift @@ -866,6 +866,62 @@ public extension TelegramEngine.EngineData.Item { } } + public struct IsBlocked: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { + public typealias Result = EnginePeerCachedInfoItem + + 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 + + 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 diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Privacy/BlockedPeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/BlockedPeers.swift index 565c4f96aa..e6c0e96c8e 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Privacy/BlockedPeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/BlockedPeers.swift @@ -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 { return account.postbox.transaction { transaction -> Signal in if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) { + let flags: Int32 = 0 let signal: Signal 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 { + return account.postbox.transaction { transaction -> Signal in + if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) { + let flags: Int32 = 1 << 0 + let signal: Signal + 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 in + return .single(nil) + } + |> mapToSignal { result -> Signal 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 +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Privacy/BlockedPeersContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/BlockedPeersContext.swift index ef2b18f8f5..2721c459e8 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Privacy/BlockedPeersContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/BlockedPeersContext.swift @@ -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 { + 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] = [] + 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 { 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) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Privacy/TelegramEnginePrivacy.swift b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/TelegramEnginePrivacy.swift index b0a02f8dc8..1b8fc0a08b 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Privacy/TelegramEnginePrivacy.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/TelegramEnginePrivacy.swift @@ -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 { + return _internal_requestUpdatePeerIsBlockedFromStories(account: self.account, peerId: peerId, isBlocked: isBlocked) + } + public func activeSessions() -> ActiveSessionsContext { return ActiveSessionsContext(account: self.account) } diff --git a/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift index 7a9acbb357..3ee179fae5 100644 --- a/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift +++ b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift @@ -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, diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift index ee5d65e026..fee8d9f5e6 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift @@ -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, diff --git a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift index 02dd162f1f..419db7c137 100644 --- a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift +++ b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift @@ -674,6 +674,7 @@ public final class EmojiStatusSelectionController: ViewController { customLayout: nil, externalBackground: nil, externalExpansionView: nil, + customContentView: nil, useOpaqueTheme: true, hideBackground: false, stateContext: nil, diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index 4d01faf340..7b8316becd 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -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(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, curveNearBounds: Bool, displaySearch: Bool, isSearchActivated: Bool, customLayout: CustomLayout?) { + init( + layoutType: ItemLayoutType, + width: CGFloat, + containerInsets: UIEdgeInsets, + itemGroups: [ItemGroupDescription], + expandedGroupIds: Set, + 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 diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchContent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchContent.swift index 20618a1c0d..0c721d3a65 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchContent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchContent.swift @@ -407,6 +407,7 @@ public final class EmojiSearchContent: ASDisplayNode, EntitySearchContainerNode customLayout: nil, externalBackground: nil, externalExpansionView: nil, + customContentView: nil, useOpaqueTheme: true, hideBackground: false, stateContext: nil, diff --git a/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift b/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift index f53a7f3c60..7f584ee820 100644 --- a/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift +++ b/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift @@ -948,6 +948,7 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent { customLayout: nil, externalBackground: nil, externalExpansionView: nil, + customContentView: nil, useOpaqueTheme: true, hideBackground: false, stateContext: nil, diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift index 176514f72a..ac98be140d 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift @@ -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) } } } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingLocationEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingLocationEntity.swift new file mode 100644 index 0000000000..a24acebaa0 --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingLocationEntity.swift @@ -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 + } +} diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift index c394b77b3d..1eb826899c 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift @@ -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 [] diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD index d4f5aae5b4..a6aab0363b 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD +++ b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD @@ -42,6 +42,7 @@ swift_library( "//submodules/TelegramUI/Components/CameraButtonComponent", "//submodules/ChatPresentationInterfaceState", "//submodules/DeviceAccess", + "//submodules/LocationUI", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 8f92f92060..c08168500f 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -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 } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift index a5f27b2cc5..19d80e0e21 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift @@ -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 diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift index f3562d2e2a..ba05c0056f 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift @@ -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() } diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift index a4db8ab186..590245dbf5 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift @@ -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 + 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 = Set() + fileprivate let storiesGrayList: BlockedPeersContext? private var stateDisposable: Disposable? private let stateSubject = Promise() @@ -2015,13 +2097,25 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { public init( context: AccountContext, - subject: Subject = .chats, + subject: Subject = .chats(grayList: false), initialPeerIds: Set = 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] = [] @@ -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.Id: Optional]), NoError> in + |> mapToSignal { chatList, contacts, initialPeers, grayListPeers -> Signal<(EngineChatList, EngineContactList, [EnginePeer.Id: Optional], [EnginePeer.Id: Optional], [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.Id: Optional]) in - return (chatList, contacts, initialPeers, participantCountMap) + |> map { participantCountMap -> (EngineChatList, EngineContactList, [EnginePeer.Id: Optional], [EnginePeer.Id: Optional], [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() + for peer in grayListPeers { + grayListPeersIds.insert(peer.id) + } + var existingIds = Set() 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 diff --git a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/BUILD b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/BUILD index d9bf6ecfdd..90a414e411 100644 --- a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/BUILD +++ b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/BUILD @@ -25,6 +25,7 @@ swift_library( "//submodules/AppBundle", "//submodules/PeerPresenceStatusManager", "//submodules/TelegramUI/Components/EmojiStatusComponent", + "//submodules/ContextUI", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift index daca4991ec..d8ab22121d 100644 --- a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift +++ b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift @@ -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() @@ -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) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift index 07410b03a1..8655290c7a 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift @@ -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, diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 18fc41d6f1..4d7a0abde2 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -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) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift index ea9fed6c84..62706f48c6 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift @@ -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 diff --git a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift index 93810487c9..0adb2fef2f 100644 --- a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift +++ b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift @@ -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 { diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Location.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Location.imageset/Contents.json index 2342459940..6573fb7841 100644 --- a/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Location.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Location.imageset/Contents.json @@ -8,5 +8,8 @@ "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" } }