mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-08-08 08:31:13 +00:00
Merge commit 'b7c18980f64fadd1b1292d62f9b1ded9bb2ce486'
This commit is contained in:
commit
6b227b8aa4
@ -11351,6 +11351,9 @@ Sorry for the inconvenience.";
|
||||
"Premium.Business.Away.Title" = "Away Messages";
|
||||
"Premium.Business.Away.Text" = "Define messages that are automatically sent when you are off.";
|
||||
|
||||
"Premium.Business.Links.Title" = "Links";
|
||||
"Premium.Business.Links.Text" = "Create links that start a chat with you, suggesting the first message.";
|
||||
|
||||
"Premium.Business.Intro.Title" = "Intro";
|
||||
"Premium.Business.Intro.Text" = "Customize the message people see before they start a chat with you.";
|
||||
|
||||
@ -11666,3 +11669,5 @@ Sorry for the inconvenience.";
|
||||
|
||||
"Business.Links" = "Links to Chat";
|
||||
"Business.LinksInfo" = "Create links that start a chat with you, suggesting the first message.";
|
||||
|
||||
"Settings.About.PrivacyHelp" = "You can add a few lines about yourself. Choose who can see your bio in [Settings]().";
|
||||
|
@ -952,6 +952,7 @@ public protocol SharedAccountContext: AnyObject {
|
||||
func makeCreateGroupController(context: AccountContext, peerIds: [PeerId], initialTitle: String?, mode: CreateGroupMode, completion: ((PeerId, @escaping () -> Void) -> Void)?) -> ViewController
|
||||
func makeChatRecentActionsController(context: AccountContext, peer: Peer, adminPeerId: PeerId?) -> ViewController
|
||||
func makePrivacyAndSecurityController(context: AccountContext) -> ViewController
|
||||
func makeBioPrivacyController(context: AccountContext, settings: Promise<AccountPrivacySettings?>, present: @escaping (ViewController) -> Void)
|
||||
func makeBirthdayPrivacyController(context: AccountContext, settings: Promise<AccountPrivacySettings?>, openedFromBirthdayScreen: Bool, present: @escaping (ViewController) -> Void)
|
||||
func makeSetupTwoFactorAuthController(context: AccountContext) -> ViewController
|
||||
func makeStorageManagementController(context: AccountContext) -> ViewController
|
||||
|
@ -724,7 +724,7 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
|
||||
hideChatListContacts(context: context)
|
||||
} : nil), directionHint: entry.directionHint)
|
||||
case let .Notice(presentationData, notice):
|
||||
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListStorageInfoItem(context: context, theme: presentationData.theme, strings: presentationData.strings, notice: notice, action: { [weak nodeInteraction] action in
|
||||
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListNoticeItem(context: context, theme: presentationData.theme, strings: presentationData.strings, notice: notice, action: { [weak nodeInteraction] action in
|
||||
switch action {
|
||||
case .activate:
|
||||
switch notice {
|
||||
@ -1060,7 +1060,7 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
|
||||
hideChatListContacts(context: context)
|
||||
} : nil), directionHint: entry.directionHint)
|
||||
case let .Notice(presentationData, notice):
|
||||
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListStorageInfoItem(context: context, theme: presentationData.theme, strings: presentationData.strings, notice: notice, action: { [weak nodeInteraction] action in
|
||||
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListNoticeItem(context: context, theme: presentationData.theme, strings: presentationData.strings, notice: notice, action: { [weak nodeInteraction] action in
|
||||
switch action {
|
||||
case .activate:
|
||||
switch notice {
|
||||
|
@ -12,7 +12,7 @@ import Markdown
|
||||
import AccountContext
|
||||
import MergedAvatarsNode
|
||||
|
||||
class ChatListStorageInfoItem: ListViewItem {
|
||||
class ChatListNoticeItem: ListViewItem {
|
||||
enum Action {
|
||||
case activate
|
||||
case hide
|
||||
@ -43,7 +43,7 @@ class ChatListStorageInfoItem: ListViewItem {
|
||||
|
||||
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||
async {
|
||||
let node = ChatListStorageInfoItemNode()
|
||||
let node = ChatListNoticeItemNode()
|
||||
|
||||
let (nodeLayout, apply) = node.asyncLayout()(self, params, false)
|
||||
|
||||
@ -62,8 +62,8 @@ class ChatListStorageInfoItem: ListViewItem {
|
||||
|
||||
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
|
||||
Queue.mainQueue().async {
|
||||
assert(node() is ChatListStorageInfoItemNode)
|
||||
if let nodeValue = node() as? ChatListStorageInfoItemNode {
|
||||
assert(node() is ChatListNoticeItemNode)
|
||||
if let nodeValue = node() as? ChatListNoticeItemNode {
|
||||
|
||||
let layout = nodeValue.asyncLayout()
|
||||
async {
|
||||
@ -84,7 +84,7 @@ private let separatorHeight = 1.0 / UIScreen.main.scale
|
||||
private let titleFont = Font.semibold(15.0)
|
||||
private let textFont = Font.regular(15.0)
|
||||
|
||||
class ChatListStorageInfoItemNode: ItemListRevealOptionsItemNode {
|
||||
final class ChatListNoticeItemNode: ItemListRevealOptionsItemNode {
|
||||
private let contentContainer: ASDisplayNode
|
||||
private let titleNode: TextNode
|
||||
private let textNode: TextNode
|
||||
@ -100,7 +100,7 @@ class ChatListStorageInfoItemNode: ItemListRevealOptionsItemNode {
|
||||
private var okButton: HighlightableButtonNode?
|
||||
private var cancelButton: HighlightableButtonNode?
|
||||
|
||||
private var item: ChatListStorageInfoItem?
|
||||
private var item: ChatListNoticeItem?
|
||||
|
||||
override var apparentHeight: CGFloat {
|
||||
didSet {
|
||||
@ -145,11 +145,11 @@ class ChatListStorageInfoItemNode: ItemListRevealOptionsItemNode {
|
||||
|
||||
override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
|
||||
let layout = self.asyncLayout()
|
||||
let (_, apply) = layout(item as! ChatListStorageInfoItem, params, nextItem == nil)
|
||||
let (_, apply) = layout(item as! ChatListNoticeItem, params, nextItem == nil)
|
||||
apply()
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: ChatListStorageInfoItem, _ params: ListViewItemLayoutParams, _ isLast: Bool) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
func asyncLayout() -> (_ item: ChatListNoticeItem, _ params: ListViewItemLayoutParams, _ isLast: Bool) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
let previousItem = self.item
|
||||
|
||||
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
||||
@ -321,9 +321,19 @@ class ChatListStorageInfoItemNode: ItemListRevealOptionsItemNode {
|
||||
if let image = strongSelf.arrowNode.image {
|
||||
strongSelf.arrowNode.frame = CGRect(origin: CGPoint(x: layout.size.width - sideInset - image.size.width + 8.0, y: floor((layout.size.height - image.size.height) / 2.0)), size: image.size)
|
||||
}
|
||||
|
||||
var hasCloseButton = false
|
||||
if case .xmasPremiumGift = item.notice {
|
||||
hasCloseButton = true
|
||||
} else if case .setupBirthday = item.notice {
|
||||
hasCloseButton = true
|
||||
} else if case .birthdayPremiumGift = item.notice {
|
||||
hasCloseButton = true
|
||||
}
|
||||
|
||||
if let okButtonLayout, let cancelButtonLayout {
|
||||
strongSelf.arrowNode.isHidden = true
|
||||
strongSelf.closeButton?.isHidden = true
|
||||
|
||||
let okButton: HighlightableButtonNode
|
||||
if let current = strongSelf.okButton {
|
||||
@ -372,7 +382,7 @@ class ChatListStorageInfoItemNode: ItemListRevealOptionsItemNode {
|
||||
okButtonText.frame = CGRect(origin: CGPoint(x: floor((okButtonFrame.width - okButtonLayout.0.size.width) * 0.5), y: floor((okButtonFrame.height - okButtonLayout.0.size.height) * 0.5)), size: okButtonLayout.0.size)
|
||||
cancelButtonText.frame = CGRect(origin: CGPoint(x: floor((cancelButtonFrame.width - cancelButtonLayout.0.size.width) * 0.5), y: floor((cancelButtonFrame.height - cancelButtonLayout.0.size.height) * 0.5)), size: cancelButtonLayout.0.size)
|
||||
} else {
|
||||
strongSelf.arrowNode.isHidden = false
|
||||
strongSelf.arrowNode.isHidden = hasCloseButton
|
||||
|
||||
if let okButton = strongSelf.okButton {
|
||||
strongSelf.okButton = nil
|
||||
@ -390,40 +400,29 @@ class ChatListStorageInfoItemNode: ItemListRevealOptionsItemNode {
|
||||
strongSelf.cancelButtonText = nil
|
||||
cancelButtonText.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
|
||||
let arrowIsHidden = strongSelf.arrowNode.isHidden
|
||||
var hasCloseButton = false
|
||||
if case .xmasPremiumGift = item.notice {
|
||||
hasCloseButton = true
|
||||
} else if case .setupBirthday = item.notice {
|
||||
hasCloseButton = true
|
||||
} else if case .birthdayPremiumGift = item.notice {
|
||||
hasCloseButton = true
|
||||
}
|
||||
|
||||
if hasCloseButton {
|
||||
strongSelf.arrowNode.isHidden = true
|
||||
|
||||
let closeButton: HighlightableButtonNode
|
||||
if let current = strongSelf.closeButton {
|
||||
closeButton = current
|
||||
if hasCloseButton {
|
||||
let closeButton: HighlightableButtonNode
|
||||
if let current = strongSelf.closeButton {
|
||||
closeButton = current
|
||||
} else {
|
||||
closeButton = HighlightableButtonNode()
|
||||
closeButton.hitTestSlop = UIEdgeInsets(top: -8.0, left: -8.0, bottom: -8.0, right: -8.0)
|
||||
closeButton.addTarget(self, action: #selector(strongSelf.closePressed), forControlEvents: [.touchUpInside])
|
||||
strongSelf.contentContainer.addSubnode(closeButton)
|
||||
strongSelf.closeButton = closeButton
|
||||
}
|
||||
|
||||
if themeUpdated {
|
||||
closeButton.setImage(PresentationResourcesItemList.itemListCloseIconImage(item.theme), for: .normal)
|
||||
}
|
||||
|
||||
let closeButtonSize = closeButton.measure(CGSize(width: 100.0, height: 100.0))
|
||||
closeButton.frame = CGRect(origin: CGPoint(x: layout.size.width - sideInset - closeButtonSize.width, y: floor((layout.size.height - closeButtonSize.height) / 2.0)), size: closeButtonSize)
|
||||
} else {
|
||||
closeButton = HighlightableButtonNode()
|
||||
closeButton.hitTestSlop = UIEdgeInsets(top: -8.0, left: -8.0, bottom: -8.0, right: -8.0)
|
||||
closeButton.addTarget(self, action: #selector(strongSelf.closePressed), forControlEvents: [.touchUpInside])
|
||||
strongSelf.contentContainer.addSubnode(closeButton)
|
||||
strongSelf.closeButton = closeButton
|
||||
strongSelf.closeButton?.removeFromSupernode()
|
||||
strongSelf.closeButton = nil
|
||||
}
|
||||
|
||||
if themeUpdated {
|
||||
closeButton.setImage(PresentationResourcesItemList.itemListCloseIconImage(item.theme), for: .normal)
|
||||
}
|
||||
|
||||
let closeButtonSize = closeButton.measure(CGSize(width: 100.0, height: 100.0))
|
||||
closeButton.frame = CGRect(origin: CGPoint(x: layout.size.width - sideInset - closeButtonSize.width, y: floor((layout.size.height - closeButtonSize.height) / 2.0)), size: closeButtonSize)
|
||||
} else {
|
||||
strongSelf.arrowNode.isHidden = arrowIsHidden
|
||||
}
|
||||
|
||||
strongSelf.contentSize = layout.contentSize
|
@ -1518,7 +1518,7 @@ final class ContextControllerActionsStackNode: ASDisplayNode {
|
||||
|
||||
let transition: ContainedViewLayoutTransition
|
||||
if animated {
|
||||
transition = .animated(duration: self.itemContainers.count == 1 ? 0.3 : 0.45, curve: .spring)
|
||||
transition = .animated(duration: self.itemContainers.count == 1 ? 0.3 : 0.45, curve: .spring)
|
||||
} else {
|
||||
transition = .immediate
|
||||
}
|
||||
|
@ -69,6 +69,9 @@ public final class PeekController: ViewController, ContextControllerProtocol {
|
||||
|
||||
public var getOverlayViews: (() -> [UIView])?
|
||||
|
||||
public var appeared: (() -> Void)?
|
||||
public var disappeared: (() -> Void)?
|
||||
|
||||
private var animatedIn = false
|
||||
|
||||
private let _ready = Promise<Bool>()
|
||||
|
@ -245,7 +245,11 @@ final class PeekControllerNode: ViewControllerTracingNode {
|
||||
case .contained:
|
||||
containerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - contentSize.width) / 2.0), y: floor((layout.size.height - contentSize.height) / 2.0)), size: contentSize)
|
||||
case .freeform:
|
||||
containerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - contentSize.width) / 2.0), y: floor((layout.size.height - contentSize.height) / 3.0)), size: contentSize)
|
||||
var fraction: CGFloat = 1.0 / 3.0
|
||||
if let _ = self.controller?.appeared {
|
||||
fraction *= 1.33
|
||||
}
|
||||
containerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - contentSize.width) / 2.0), y: floor((layout.size.height - contentSize.height) * fraction)), size: contentSize)
|
||||
}
|
||||
actionsFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - actionsSize.width) / 2.0), y: containerFrame.maxY + 64.0), size: actionsSize)
|
||||
}
|
||||
@ -267,8 +271,15 @@ final class PeekControllerNode: ViewControllerTracingNode {
|
||||
|
||||
let offset = CGPoint(x: rect.midX - self.containerNode.position.x, y: rect.midY - self.containerNode.position.y)
|
||||
self.containerNode.layer.animateSpring(from: NSValue(cgPoint: offset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.4, initialVelocity: 0.0, damping: 110.0, additive: true)
|
||||
self.containerNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4, initialVelocity: 0.0, damping: 110.0)
|
||||
self.containerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
||||
|
||||
if let appeared = self.controller?.appeared {
|
||||
appeared()
|
||||
let scale = rect.width / self.contentNode.frame.width
|
||||
self.containerNode.layer.animateSpring(from: scale as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4, initialVelocity: 0.0, damping: 110.0)
|
||||
} else {
|
||||
self.containerNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4, initialVelocity: 0.0, damping: 110.0)
|
||||
self.containerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
||||
}
|
||||
|
||||
if let topAccessoryNode = self.topAccessoryNode {
|
||||
topAccessoryNode.layer.animateSpring(from: NSValue(cgPoint: offset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.4, initialVelocity: 0.0, damping: 110.0, additive: true)
|
||||
@ -293,13 +304,35 @@ final class PeekControllerNode: ViewControllerTracingNode {
|
||||
let springDuration: Double = 0.42 * animationDurationFactor
|
||||
let springDamping: CGFloat = 104.0
|
||||
|
||||
var scaleCompleted = false
|
||||
var positionCompleted = false
|
||||
let outCompletion = { [weak self] in
|
||||
if scaleCompleted && positionCompleted {
|
||||
self?.controller?.disappeared?()
|
||||
}
|
||||
}
|
||||
|
||||
let offset = CGPoint(x: rect.midX - self.containerNode.position.x, y: rect.midY - self.containerNode.position.y)
|
||||
self.containerNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint()), to: NSValue(cgPoint: offset), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, additive: true, completion: { _ in
|
||||
self.containerNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint()), to: NSValue(cgPoint: offset), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, removeOnCompletion: false, additive: true, completion: { _ in
|
||||
positionCompleted = true
|
||||
outCompletion()
|
||||
completion()
|
||||
})
|
||||
self.containerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
||||
self.containerNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.25, removeOnCompletion: false)
|
||||
|
||||
|
||||
if let _ = self.controller?.disappeared {
|
||||
let scale = rect.width / self.contentNode.frame.width
|
||||
self.containerNode.layer.animateScale(from: 1.0, to: scale, duration: 0.25, removeOnCompletion: false, completion: { _ in
|
||||
scaleCompleted = true
|
||||
outCompletion()
|
||||
})
|
||||
} else {
|
||||
self.containerNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.25, removeOnCompletion: false, completion: { _ in
|
||||
scaleCompleted = true
|
||||
outCompletion()
|
||||
})
|
||||
self.containerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
if !self.actionsStackNode.alpha.isZero {
|
||||
let actionsOffset = CGPoint(x: rect.midX - self.actionsStackNode.position.x, y: rect.midY - self.actionsStackNode.position.y)
|
||||
self.actionsStackNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2 * animationDurationFactor, removeOnCompletion: false)
|
||||
|
@ -97,9 +97,6 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
||||
private let angleLayer = SimpleShapeLayer()
|
||||
private let bin = ComponentView<Empty>()
|
||||
|
||||
private let stickerOverlayLayer = SimpleShapeLayer()
|
||||
private let stickerFrameLayer = SimpleShapeLayer()
|
||||
|
||||
public var onInteractionUpdated: (Bool) -> Void = { _ in }
|
||||
public var edgePreviewUpdated: (Bool) -> Void = { _ in }
|
||||
|
||||
@ -145,13 +142,6 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
||||
self.angleLayer.opacity = 0.0
|
||||
self.angleLayer.lineDashPattern = [12, 12] as [NSNumber]
|
||||
|
||||
self.stickerOverlayLayer.fillColor = UIColor(rgb: 0x000000, alpha: 0.7).cgColor
|
||||
|
||||
self.stickerFrameLayer.fillColor = UIColor.clear.cgColor
|
||||
self.stickerFrameLayer.strokeColor = UIColor(rgb: 0xffffff, alpha: 0.55).cgColor
|
||||
self.stickerFrameLayer.lineDashPattern = [24, 24] as [NSNumber]
|
||||
self.stickerFrameLayer.lineCap = .round
|
||||
|
||||
self.addSubview(self.topEdgeView)
|
||||
self.addSubview(self.leftEdgeView)
|
||||
self.addSubview(self.rightEdgeView)
|
||||
@ -160,25 +150,12 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
||||
self.addSubview(self.xAxisView)
|
||||
self.addSubview(self.yAxisView)
|
||||
self.layer.addSublayer(self.angleLayer)
|
||||
|
||||
if isStickerEditor {
|
||||
self.layer.addSublayer(self.stickerOverlayLayer)
|
||||
self.layer.addSublayer(self.stickerFrameLayer)
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public override func addSubview(_ view: UIView) {
|
||||
super.addSubview(view)
|
||||
if self.stickerOverlayLayer.superlayer != nil, view is DrawingEntityView {
|
||||
self.layer.addSublayer(self.stickerOverlayLayer)
|
||||
self.layer.addSublayer(self.stickerFrameLayer)
|
||||
}
|
||||
}
|
||||
|
||||
public override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
@ -214,25 +191,6 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
||||
self.angleLayer.path = anglePath
|
||||
self.angleLayer.lineWidth = width
|
||||
self.angleLayer.bounds = CGRect(origin: .zero, size: CGSize(width: 3000.0, height: width))
|
||||
|
||||
let frameWidth = floor(self.bounds.width * 0.97)
|
||||
let frameRect = CGRect(origin: CGPoint(x: floor((self.bounds.width - frameWidth) / 2.0), y: floor((self.bounds.height - frameWidth) / 2.0)), size: CGSize(width: frameWidth, height: frameWidth))
|
||||
|
||||
self.stickerOverlayLayer.frame = self.bounds
|
||||
|
||||
let overlayOuterRect = UIBezierPath(rect: self.bounds)
|
||||
let overlayInnerRect = UIBezierPath(cgPath: CGPath(roundedRect: frameRect, cornerWidth: frameWidth / 8.0, cornerHeight: frameWidth / 8.0, transform: nil))
|
||||
let overlayLineWidth: CGFloat = 2.0 * 2.2
|
||||
|
||||
overlayOuterRect.append(overlayInnerRect)
|
||||
overlayOuterRect.usesEvenOddFillRule = true
|
||||
|
||||
self.stickerOverlayLayer.path = overlayOuterRect.cgPath
|
||||
self.stickerOverlayLayer.fillRule = .evenOdd
|
||||
|
||||
self.stickerFrameLayer.frame = self.bounds
|
||||
self.stickerFrameLayer.lineWidth = overlayLineWidth
|
||||
self.stickerFrameLayer.path = CGPath(roundedRect: frameRect.insetBy(dx: -overlayLineWidth / 2.0, dy: -overlayLineWidth / 2.0), cornerWidth: frameWidth / 8.0 * 1.02, cornerHeight: frameWidth / 8.0 * 1.02, transform: nil)
|
||||
}
|
||||
|
||||
public var entities: [DrawingEntity] {
|
||||
|
@ -24,6 +24,7 @@ typedef NS_ENUM(NSUInteger, FFMpegAVFramePixelFormat) {
|
||||
@property (nonatomic, readonly) FFMpegAVFramePixelFormat pixelFormat;
|
||||
|
||||
- (instancetype)init;
|
||||
- (instancetype)initWithPixelFormat:(FFMpegAVFramePixelFormat)pixelFormat width:(int32_t)width height:(int32_t)height;
|
||||
|
||||
- (void *)impl;
|
||||
|
||||
|
@ -3,10 +3,12 @@
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class FFMpegAVFrame;
|
||||
|
||||
@interface FFMpegVideoWriter : NSObject
|
||||
|
||||
- (bool)setupWithOutputPath:(NSString *)outputPath width:(int)width height:(int)height;
|
||||
- (bool)encodeFrame:(CVPixelBufferRef)pixelBuffer;
|
||||
- (bool)setupWithOutputPath:(NSString *)outputPath width:(int)width height:(int)height bitrate:(int64_t)bitrate framerate:(int32_t)framerate;
|
||||
- (bool)encodeFrame:(FFMpegAVFrame *)frame;
|
||||
- (bool)finalizeVideo;
|
||||
|
||||
@end
|
||||
|
@ -18,6 +18,26 @@
|
||||
return self;
|
||||
}
|
||||
|
||||
-(instancetype)initWithPixelFormat:(FFMpegAVFramePixelFormat)pixelFormat width:(int32_t)width height:(int32_t)height {
|
||||
self = [super init];
|
||||
if (self != nil) {
|
||||
_impl = av_frame_alloc();
|
||||
switch (pixelFormat) {
|
||||
case FFMpegAVFramePixelFormatYUV:
|
||||
_impl->format = AV_PIX_FMT_YUV420P;
|
||||
break;
|
||||
case FFMpegAVFramePixelFormatYUVA:
|
||||
_impl->format = AV_PIX_FMT_YUVA420P;
|
||||
break;
|
||||
}
|
||||
_impl->width = width;
|
||||
_impl->height = height;
|
||||
|
||||
av_frame_get_buffer(_impl, 0);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
if (_impl) {
|
||||
av_frame_free(&_impl);
|
||||
|
@ -1,5 +1,5 @@
|
||||
#import <FFMpegBinding/FFMpegVideoWriter.h>
|
||||
#import <FFMpegBinding/FrameConverter.h>
|
||||
#import <FFMpegBinding/FFMpegAVFrame.h>
|
||||
|
||||
#include "libavformat/avformat.h"
|
||||
#include "libavcodec/avcodec.h"
|
||||
@ -24,7 +24,7 @@
|
||||
return self;
|
||||
}
|
||||
|
||||
- (bool)setupWithOutputPath:(NSString *)outputPath width:(int)width height:(int)height {
|
||||
- (bool)setupWithOutputPath:(NSString *)outputPath width:(int)width height:(int)height bitrate:(int64_t)bitrate framerate:(int32_t)framerate {
|
||||
avformat_alloc_output_context2(&_formatContext, NULL, "matroska", [outputPath UTF8String]);
|
||||
if (!_formatContext) {
|
||||
return false;
|
||||
@ -47,13 +47,14 @@
|
||||
_codecContext->codec_id = AV_CODEC_ID_VP9;
|
||||
_codecContext->codec_type = AVMEDIA_TYPE_VIDEO;
|
||||
_codecContext->pix_fmt = AV_PIX_FMT_YUVA420P;
|
||||
_codecContext->color_range = AVCOL_RANGE_MPEG;
|
||||
_codecContext->color_primaries = AVCOL_PRI_BT709;
|
||||
_codecContext->colorspace = AVCOL_SPC_BT709;
|
||||
_codecContext->width = width;
|
||||
_codecContext->height = height;
|
||||
_codecContext->time_base = (AVRational){1, 30};
|
||||
_codecContext->framerate = (AVRational){30, 1};
|
||||
_codecContext->bit_rate = 200000;
|
||||
// _codecContext->gop_size = 10;
|
||||
// _codecContext->max_b_frames = 1;
|
||||
_codecContext->time_base = (AVRational){1, framerate};
|
||||
_codecContext->framerate = (AVRational){framerate, 1};
|
||||
_codecContext->bit_rate = bitrate;
|
||||
|
||||
if (_formatContext->oformat->flags & AVFMT_GLOBALHEADER) {
|
||||
_codecContext->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
|
||||
@ -88,62 +89,22 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
- (bool)encodeFrame:(CVPixelBufferRef)pixelBuffer {
|
||||
- (bool)encodeFrame:(FFMpegAVFrame *)frame {
|
||||
if (!_codecContext || !_stream) {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.framePts++;
|
||||
|
||||
AVFrame *frame = av_frame_alloc();
|
||||
if (!frame) {
|
||||
return false;
|
||||
}
|
||||
AVFrame *frameImpl = (AVFrame *)[frame impl];
|
||||
|
||||
int width = (int)CVPixelBufferGetWidth(pixelBuffer);
|
||||
int height = (int)CVPixelBufferGetHeight(pixelBuffer);
|
||||
|
||||
frame->format = _codecContext->pix_fmt;
|
||||
frame->width = width;
|
||||
frame->height = height;
|
||||
frameImpl->pts = self.framePts;
|
||||
frameImpl->color_range = AVCOL_RANGE_MPEG;
|
||||
frameImpl->color_primaries = AVCOL_PRI_BT709;
|
||||
frameImpl->colorspace = AVCOL_SPC_BT709;
|
||||
|
||||
if (av_frame_get_buffer(frame, 0) < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
CVPixelBufferLockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
|
||||
|
||||
uint8_t *yBaseAddress = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0);
|
||||
size_t yStride = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0);
|
||||
|
||||
uint8_t *uvBaseAddress = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1);
|
||||
size_t uvStride = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1);
|
||||
|
||||
uint8_t *aBaseAddress = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 2);
|
||||
size_t aStride = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 2);
|
||||
|
||||
for (int i = 0; i < height; i++) {
|
||||
memcpy(frame->data[0] + i * frame->linesize[0], yBaseAddress + i * yStride, width);
|
||||
}
|
||||
|
||||
for (int i = 0; i < height / 2; i++) {
|
||||
for (int j = 0; j < width / 2; j++) {
|
||||
frame->data[1][i * frame->linesize[1] + j] = uvBaseAddress[i * uvStride + 2 * j];
|
||||
frame->data[2][i * frame->linesize[2] + j] = uvBaseAddress[i * uvStride + 2 * j + 1];
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < height; i++) {
|
||||
memcpy(frame->data[3] + i * frame->linesize[3], aBaseAddress + i * aStride, width);
|
||||
}
|
||||
|
||||
CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
|
||||
|
||||
frame->pts = self.framePts;
|
||||
|
||||
int sendRet = avcodec_send_frame(_codecContext, frame);
|
||||
int sendRet = avcodec_send_frame(_codecContext, frameImpl);
|
||||
if (sendRet < 0) {
|
||||
av_frame_free(&frame);
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -170,11 +131,26 @@
|
||||
}
|
||||
}
|
||||
|
||||
av_frame_free(&frame);
|
||||
return true;
|
||||
}
|
||||
|
||||
- (bool)finalizeVideo {
|
||||
int sendRet = avcodec_send_frame(_codecContext, NULL);
|
||||
if (sendRet >= 0) {
|
||||
AVPacket pkt;
|
||||
av_init_packet(&pkt);
|
||||
pkt.data = NULL;
|
||||
pkt.size = 0;
|
||||
|
||||
while (avcodec_receive_packet(_codecContext, &pkt) == 0) {
|
||||
av_packet_rescale_ts(&pkt, _codecContext->time_base, _stream->time_base);
|
||||
pkt.stream_index = _stream->index;
|
||||
|
||||
av_interleaved_write_frame(_formatContext, &pkt);
|
||||
av_packet_unref(&pkt);
|
||||
}
|
||||
}
|
||||
|
||||
av_write_trailer(_formatContext);
|
||||
|
||||
avio_closep(&_formatContext->pb);
|
||||
|
@ -875,8 +875,8 @@ func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFlo
|
||||
for line in textItem.lines {
|
||||
let lineFrame = frameForLine(line, boundingWidth: boundingWidth, alignment: alignment)
|
||||
for imageItem in line.imageItems {
|
||||
if case let .image(image) = media[imageItem.id] {
|
||||
let item = InstantPageImageItem(frame: imageItem.frame.offsetBy(dx: lineFrame.minX + offset.x, dy: offset.y), webPage: webpage, media: InstantPageMedia(index: -1, media: .image(image), url: nil, caption: nil, credit: nil), interactive: false, roundCorners: false, fit: false)
|
||||
if let media = media[imageItem.id] {
|
||||
let item = InstantPageImageItem(frame: imageItem.frame.offsetBy(dx: lineFrame.minX + offset.x, dy: offset.y), webPage: webpage, media: InstantPageMedia(index: -1, media: media, url: nil, caption: nil, credit: nil), interactive: false, roundCorners: false, fit: false)
|
||||
additionalItems.append(item)
|
||||
|
||||
if item.frame.minY < topInset {
|
||||
|
@ -81,6 +81,8 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode {
|
||||
|
||||
private var item: ItemListTextItem?
|
||||
|
||||
private var chevronImage: UIImage?
|
||||
|
||||
public var tag: ItemListItemTag? {
|
||||
return self.item?.tag
|
||||
}
|
||||
@ -117,6 +119,8 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode {
|
||||
|
||||
public func asyncLayout() -> (_ item: ItemListTextItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
let makeTitleLayout = TextNode.asyncLayout(self.textNode)
|
||||
let currentChevronImage = self.chevronImage
|
||||
let currentItem = self.item
|
||||
|
||||
return { item, params, neighbors in
|
||||
let leftInset: CGFloat = 15.0
|
||||
@ -127,6 +131,12 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode {
|
||||
let largeTitleFont = Font.semibold(floor(item.presentationData.fontSize.itemListBaseFontSize))
|
||||
let titleBoldFont = Font.semibold(item.presentationData.fontSize.itemListBaseHeaderFontSize)
|
||||
|
||||
var themeUpdated = false
|
||||
var chevronImage = currentChevronImage
|
||||
if currentItem?.presentationData.theme !== item.presentationData.theme {
|
||||
themeUpdated = true
|
||||
}
|
||||
|
||||
let attributedText: NSAttributedString
|
||||
switch item.text {
|
||||
case let .plain(text):
|
||||
@ -134,9 +144,18 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode {
|
||||
case let .large(text):
|
||||
attributedText = NSAttributedString(string: text, font: largeTitleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor)
|
||||
case let .markdown(text):
|
||||
attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: item.presentationData.theme.list.freeTextColor), bold: MarkdownAttributeSet(font: titleBoldFont, textColor: item.presentationData.theme.list.freeTextColor), link: MarkdownAttributeSet(font: titleFont, textColor: item.presentationData.theme.list.itemAccentColor), linkAttribute: { contents in
|
||||
let mutableAttributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: item.presentationData.theme.list.freeTextColor), bold: MarkdownAttributeSet(font: titleBoldFont, textColor: item.presentationData.theme.list.freeTextColor), link: MarkdownAttributeSet(font: titleFont, textColor: item.presentationData.theme.list.itemAccentColor), linkAttribute: { contents in
|
||||
return (TelegramTextAttributes.URL, contents)
|
||||
}))
|
||||
})).mutableCopy() as! NSMutableAttributedString
|
||||
if let _ = text.range(of: ">]"), let range = mutableAttributedText.string.range(of: ">") {
|
||||
if themeUpdated || currentChevronImage == nil {
|
||||
chevronImage = generateTintedImage(image: UIImage(bundleImageName: "Contact List/SubtitleArrow"), color: item.presentationData.theme.list.itemAccentColor)
|
||||
}
|
||||
if let chevronImage {
|
||||
mutableAttributedText.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: mutableAttributedText.string))
|
||||
}
|
||||
}
|
||||
attributedText = mutableAttributedText
|
||||
}
|
||||
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset * 2.0 - params.leftInset - params.rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
@ -158,6 +177,7 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode {
|
||||
return (layout, { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.item = item
|
||||
strongSelf.chevronImage = chevronImage
|
||||
|
||||
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height))
|
||||
strongSelf.activateArea.accessibilityLabel = attributedText.string
|
||||
|
@ -118,6 +118,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/EntityKeyboard",
|
||||
"//submodules/TelegramUI/Components/PremiumPeerShortcutComponent",
|
||||
"//submodules/TelegramUI/Components/EmojiActionIconComponent",
|
||||
"//submodules/TelegramUI/Components/ScrollComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 9.3 KiB |
Binary file not shown.
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 9.3 KiB |
@ -10,6 +10,7 @@ import BlurredBackgroundComponent
|
||||
import Markdown
|
||||
import TelegramPresentationData
|
||||
import BundleIconComponent
|
||||
import ScrollComponent
|
||||
|
||||
private final class HeaderComponent: Component {
|
||||
let context: AccountContext
|
||||
@ -307,7 +308,8 @@ private final class BusinessListComponent: CombinedComponent {
|
||||
UIColor(rgb: 0xbc4395),
|
||||
UIColor(rgb: 0x9b4fed),
|
||||
UIColor(rgb: 0x8958ff),
|
||||
UIColor(rgb: 0x676bff)
|
||||
UIColor(rgb: 0x676bff),
|
||||
UIColor(rgb: 0x007aff)
|
||||
]
|
||||
|
||||
let titleColor = theme.list.itemPrimaryTextColor
|
||||
@ -396,6 +398,20 @@ private final class BusinessListComponent: CombinedComponent {
|
||||
)
|
||||
)
|
||||
|
||||
items.append(
|
||||
AnyComponentWithIdentity(
|
||||
id: "links",
|
||||
component: AnyComponent(ParagraphComponent(
|
||||
title: strings.Premium_Business_Links_Title,
|
||||
titleColor: titleColor,
|
||||
text: strings.Premium_Business_Links_Text,
|
||||
textColor: textColor,
|
||||
iconName: "Premium/Business/Links",
|
||||
iconColor: colors[5]
|
||||
))
|
||||
)
|
||||
)
|
||||
|
||||
items.append(
|
||||
AnyComponentWithIdentity(
|
||||
id: "intro",
|
||||
@ -405,7 +421,7 @@ private final class BusinessListComponent: CombinedComponent {
|
||||
text: strings.Premium_Business_Intro_Text,
|
||||
textColor: textColor,
|
||||
iconName: "Premium/Business/Intro",
|
||||
iconColor: colors[5]
|
||||
iconColor: colors[6]
|
||||
))
|
||||
)
|
||||
)
|
||||
@ -419,7 +435,7 @@ private final class BusinessListComponent: CombinedComponent {
|
||||
text: strings.Premium_Business_Chatbots_Text,
|
||||
textColor: textColor,
|
||||
iconName: "Premium/Business/Chatbots",
|
||||
iconColor: colors[6]
|
||||
iconColor: colors[7]
|
||||
))
|
||||
)
|
||||
)
|
||||
|
@ -9,6 +9,7 @@ import MultilineTextComponent
|
||||
import BlurredBackgroundComponent
|
||||
import Markdown
|
||||
import TelegramPresentationData
|
||||
import ScrollComponent
|
||||
|
||||
private final class LimitComponent: CombinedComponent {
|
||||
let title: String
|
||||
|
@ -21,6 +21,7 @@ import BlurredBackgroundComponent
|
||||
import UndoUI
|
||||
import ConfettiEffect
|
||||
import PremiumPeerShortcutComponent
|
||||
import ScrollComponent
|
||||
|
||||
func requiredBoostSubjectLevel(subject: BoostSubject, group: Bool, context: AccountContext, configuration: PremiumConfiguration) -> Int32 {
|
||||
switch subject {
|
||||
|
@ -345,7 +345,8 @@ class PremiumCoinComponent: Component {
|
||||
return
|
||||
}
|
||||
|
||||
if #available(iOS 17.0, *), let material = node.geometry?.materials.first {
|
||||
// if #available(iOS 17.0, *), let material = node.geometry?.materials.first {
|
||||
if let material = node.geometry?.materials.first {
|
||||
material.metalness.intensity = 0.3
|
||||
}
|
||||
|
||||
|
@ -20,6 +20,7 @@ import ConfettiEffect
|
||||
import TextFormat
|
||||
import UniversalMediaPlayer
|
||||
import InstantPageCache
|
||||
import ScrollComponent
|
||||
|
||||
extension PremiumGiftSource {
|
||||
var identifier: String? {
|
||||
|
@ -33,6 +33,7 @@ import EmojiStatusSelectionComponent
|
||||
import EmojiStatusComponent
|
||||
import EntityKeyboard
|
||||
import EmojiActionIconComponent
|
||||
import ScrollComponent
|
||||
|
||||
public enum PremiumSource: Equatable {
|
||||
public static func == (lhs: PremiumSource, rhs: PremiumSource) -> Bool {
|
||||
|
@ -1,146 +0,0 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import ComponentFlow
|
||||
import Display
|
||||
|
||||
final class ScrollChildEnvironment: Equatable {
|
||||
public let insets: UIEdgeInsets
|
||||
|
||||
public init(insets: UIEdgeInsets) {
|
||||
self.insets = insets
|
||||
}
|
||||
|
||||
public static func ==(lhs: ScrollChildEnvironment, rhs: ScrollChildEnvironment) -> Bool {
|
||||
if lhs.insets != rhs.insets {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
final class ScrollComponent<ChildEnvironment: Equatable>: Component {
|
||||
typealias EnvironmentType = ChildEnvironment
|
||||
|
||||
class ExternalState {
|
||||
var contentHeight: CGFloat = 0.0
|
||||
}
|
||||
|
||||
let content: AnyComponent<(ChildEnvironment, ScrollChildEnvironment)>
|
||||
let externalState: ExternalState?
|
||||
let contentInsets: UIEdgeInsets
|
||||
let contentOffsetUpdated: (_ top: CGFloat, _ bottom: CGFloat) -> Void
|
||||
let contentOffsetWillCommit: (UnsafeMutablePointer<CGPoint>) -> Void
|
||||
let resetScroll: ActionSlot<Void>
|
||||
|
||||
public init(
|
||||
content: AnyComponent<(ChildEnvironment, ScrollChildEnvironment)>,
|
||||
externalState: ExternalState? = nil,
|
||||
contentInsets: UIEdgeInsets,
|
||||
contentOffsetUpdated: @escaping (_ top: CGFloat, _ bottom: CGFloat) -> Void,
|
||||
contentOffsetWillCommit: @escaping (UnsafeMutablePointer<CGPoint>) -> Void,
|
||||
resetScroll: ActionSlot<Void> = ActionSlot()
|
||||
) {
|
||||
self.content = content
|
||||
self.externalState = externalState
|
||||
self.contentInsets = contentInsets
|
||||
self.contentOffsetUpdated = contentOffsetUpdated
|
||||
self.contentOffsetWillCommit = contentOffsetWillCommit
|
||||
self.resetScroll = resetScroll
|
||||
}
|
||||
|
||||
public static func ==(lhs: ScrollComponent, rhs: ScrollComponent) -> Bool {
|
||||
if lhs.content != rhs.content {
|
||||
return false
|
||||
}
|
||||
if lhs.contentInsets != rhs.contentInsets {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIScrollView, UIScrollViewDelegate {
|
||||
private var component: ScrollComponent<ChildEnvironment>?
|
||||
private let contentView: ComponentHostView<(ChildEnvironment, ScrollChildEnvironment)>
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.contentView = ComponentHostView()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
|
||||
self.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
self.delegate = self
|
||||
self.showsVerticalScrollIndicator = false
|
||||
self.showsHorizontalScrollIndicator = false
|
||||
self.canCancelContentTouches = true
|
||||
|
||||
self.addSubview(self.contentView)
|
||||
}
|
||||
|
||||
public override func touchesShouldCancel(in view: UIView) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
private var ignoreDidScroll = false
|
||||
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
guard let component = self.component, !self.ignoreDidScroll else {
|
||||
return
|
||||
}
|
||||
let topOffset = scrollView.contentOffset.y
|
||||
let bottomOffset = max(0.0, scrollView.contentSize.height - scrollView.contentOffset.y - scrollView.frame.height)
|
||||
component.contentOffsetUpdated(topOffset, bottomOffset)
|
||||
}
|
||||
|
||||
public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
|
||||
guard let component = self.component, !self.ignoreDidScroll else {
|
||||
return
|
||||
}
|
||||
component.contentOffsetWillCommit(targetContentOffset)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(component: ScrollComponent<ChildEnvironment>, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ChildEnvironment>, transition: Transition) -> CGSize {
|
||||
let contentSize = self.contentView.update(
|
||||
transition: transition,
|
||||
component: component.content,
|
||||
environment: {
|
||||
environment[ChildEnvironment.self]
|
||||
ScrollChildEnvironment(insets: component.contentInsets)
|
||||
},
|
||||
containerSize: CGSize(width: availableSize.width, height: .greatestFiniteMagnitude)
|
||||
)
|
||||
transition.setFrame(view: self.contentView, frame: CGRect(origin: .zero, size: contentSize), completion: nil)
|
||||
|
||||
component.resetScroll.connect { [weak self] _ in
|
||||
self?.setContentOffset(.zero, animated: false)
|
||||
}
|
||||
|
||||
if self.contentSize != contentSize {
|
||||
self.ignoreDidScroll = true
|
||||
self.contentSize = contentSize
|
||||
self.ignoreDidScroll = false
|
||||
}
|
||||
if self.scrollIndicatorInsets != component.contentInsets {
|
||||
self.scrollIndicatorInsets = component.contentInsets
|
||||
}
|
||||
component.externalState?.contentHeight = contentSize.height
|
||||
|
||||
self.component = component
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ChildEnvironment>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
@ -12,6 +12,7 @@ import TelegramPresentationData
|
||||
import BundleIconComponent
|
||||
import AvatarNode
|
||||
import AvatarStoryIndicatorComponent
|
||||
import ScrollComponent
|
||||
|
||||
private final class AvatarComponent: Component {
|
||||
let context: AccountContext
|
||||
|
@ -16,6 +16,30 @@ public func makePrivacyAndSecurityController(context: AccountContext) -> ViewCon
|
||||
return privacyAndSecurityController(context: context, focusOnItemTag: PrivacyAndSecurityEntryTag.autoArchive)
|
||||
}
|
||||
|
||||
public func makeBioPrivacyController(context: AccountContext, settings: Promise<AccountPrivacySettings?>, present: @escaping (ViewController) -> Void) {
|
||||
let signal = settings.get()
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue
|
||||
|
||||
let _ = signal.startStandalone(next: { info in
|
||||
if let info = info {
|
||||
present(selectivePrivacySettingsController(context: context, kind: .bio, current: info.bio, updated: { updated, _, _, _ in
|
||||
let applySetting: Signal<Void, NoError> = settings.get()
|
||||
|> filter { $0 != nil }
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue
|
||||
|> mapToSignal { value -> Signal<Void, NoError> in
|
||||
if let value = value {
|
||||
settings.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: value.groupInvitations, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: value.profilePhoto, forwards: value.forwards, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, voiceMessages: value.voiceMessages, bio: updated, birthday: value.birthday, globalSettings: value.globalSettings, accountRemovalTimeout: value.accountRemovalTimeout, messageAutoremoveTimeout: value.messageAutoremoveTimeout)))
|
||||
}
|
||||
return .complete()
|
||||
}
|
||||
let _ = applySetting.startStandalone()
|
||||
}))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public func makeBirthdayPrivacyController(context: AccountContext, settings: Promise<AccountPrivacySettings?>, openedFromBirthdayScreen: Bool, present: @escaping (ViewController) -> Void) {
|
||||
let signal = settings.get()
|
||||
|> take(1)
|
||||
|
@ -1344,7 +1344,7 @@ private func monetizationEntries(
|
||||
|
||||
var entries: [StatsEntry] = []
|
||||
//TODO:localize
|
||||
entries.append(.adsHeader(presentationData.theme, "Telegram shares 50% of the revenue from ads displayed in your channel. [Learn More]()"))
|
||||
entries.append(.adsHeader(presentationData.theme, "Telegram shares 50% of the revenue from ads displayed in your channel. [Learn More >]()"))
|
||||
|
||||
entries.append(.adsImpressionsTitle(presentationData.theme, "AD IMPRESSIONS (BY HOURS)"))
|
||||
if !stats.topHoursGraph.isEmpty {
|
||||
@ -1361,7 +1361,7 @@ private func monetizationEntries(
|
||||
|
||||
entries.append(.adsBalanceTitle(presentationData.theme, "AVAILABLE BALANCE"))
|
||||
entries.append(.adsBalance(presentationData.theme, data, false, diamond, state.monetizationAddress))
|
||||
entries.append(.adsBalanceInfo(presentationData.theme, "We will transfer your balance to the TON wallet address you specify. [Learn More]()"))
|
||||
entries.append(.adsBalanceInfo(presentationData.theme, "We will transfer your balance to the TON wallet address you specify. [Learn More >]()"))
|
||||
|
||||
entries.append(.adsTransactionsTitle(presentationData.theme, "TRANSACTION HISTORY"))
|
||||
|
||||
|
@ -252,6 +252,10 @@ public final class StickerPreviewPeekContentNode: ASDisplayNode, PeekControllerC
|
||||
}
|
||||
|
||||
let textSize = self.textNode.measure(CGSize(width: 100.0, height: 100.0))
|
||||
if textSize.height.isZero {
|
||||
topOffset = 0.0
|
||||
textSpacing = 0.0
|
||||
}
|
||||
|
||||
let imageSize = dimensions.cgSize.aspectFitted(boundingSize)
|
||||
self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))()
|
||||
@ -275,7 +279,7 @@ public final class StickerPreviewPeekContentNode: ASDisplayNode, PeekControllerC
|
||||
|
||||
self.textNode.frame = CGRect(origin: CGPoint(x: floor((imageFrame.size.width - textSize.width) / 2.0) - centerOffset, y: -textSize.height - textSpacing), size: textSize)
|
||||
|
||||
if self.item.file?.isCustomEmoji == true {
|
||||
if self.item.file?.isCustomEmoji == true || textSize.height.isZero {
|
||||
return CGSize(width: boundingSize.width, height: imageFrame.height)
|
||||
} else {
|
||||
return CGSize(width: boundingSize.width, height: imageFrame.height + textSize.height + textSpacing)
|
||||
|
@ -21,8 +21,9 @@ public final class AdMessageAttribute: MessageAttribute {
|
||||
public let buttonText: String?
|
||||
public let sponsorInfo: String?
|
||||
public let additionalInfo: String?
|
||||
public let canReport: Bool
|
||||
|
||||
public init(opaqueId: Data, messageType: MessageType, displayAvatar: Bool, target: MessageTarget, buttonText: String?, sponsorInfo: String?, additionalInfo: String?) {
|
||||
public init(opaqueId: Data, messageType: MessageType, displayAvatar: Bool, target: MessageTarget, buttonText: String?, sponsorInfo: String?, additionalInfo: String?, canReport: Bool) {
|
||||
self.opaqueId = opaqueId
|
||||
self.messageType = messageType
|
||||
self.displayAvatar = displayAvatar
|
||||
@ -30,6 +31,7 @@ public final class AdMessageAttribute: MessageAttribute {
|
||||
self.buttonText = buttonText
|
||||
self.sponsorInfo = sponsorInfo
|
||||
self.additionalInfo = additionalInfo
|
||||
self.canReport = canReport
|
||||
}
|
||||
|
||||
public init(decoder: PostboxDecoder) {
|
||||
|
@ -18,6 +18,7 @@ private class AdMessagesHistoryContextImpl {
|
||||
case buttonText
|
||||
case sponsorInfo
|
||||
case additionalInfo
|
||||
case canReport
|
||||
}
|
||||
|
||||
enum MessageType: Int32, Codable {
|
||||
@ -168,6 +169,7 @@ private class AdMessagesHistoryContextImpl {
|
||||
public let buttonText: String?
|
||||
public let sponsorInfo: String?
|
||||
public let additionalInfo: String?
|
||||
public let canReport: Bool
|
||||
|
||||
public init(
|
||||
opaqueId: Data,
|
||||
@ -181,7 +183,8 @@ private class AdMessagesHistoryContextImpl {
|
||||
startParam: String?,
|
||||
buttonText: String?,
|
||||
sponsorInfo: String?,
|
||||
additionalInfo: String?
|
||||
additionalInfo: String?,
|
||||
canReport: Bool
|
||||
) {
|
||||
self.opaqueId = opaqueId
|
||||
self.messageType = messageType
|
||||
@ -195,6 +198,7 @@ private class AdMessagesHistoryContextImpl {
|
||||
self.buttonText = buttonText
|
||||
self.sponsorInfo = sponsorInfo
|
||||
self.additionalInfo = additionalInfo
|
||||
self.canReport = canReport
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
@ -225,6 +229,8 @@ private class AdMessagesHistoryContextImpl {
|
||||
|
||||
self.sponsorInfo = try container.decodeIfPresent(String.self, forKey: .sponsorInfo)
|
||||
self.additionalInfo = try container.decodeIfPresent(String.self, forKey: .additionalInfo)
|
||||
|
||||
self.canReport = try container.decodeIfPresent(Bool.self, forKey: .displayAvatar) ?? false
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
@ -250,6 +256,8 @@ private class AdMessagesHistoryContextImpl {
|
||||
|
||||
try container.encodeIfPresent(self.sponsorInfo, forKey: .sponsorInfo)
|
||||
try container.encodeIfPresent(self.additionalInfo, forKey: .additionalInfo)
|
||||
|
||||
try container.encode(self.canReport, forKey: .canReport)
|
||||
}
|
||||
|
||||
public static func ==(lhs: CachedMessage, rhs: CachedMessage) -> Bool {
|
||||
@ -291,6 +299,9 @@ private class AdMessagesHistoryContextImpl {
|
||||
if lhs.additionalInfo != rhs.additionalInfo {
|
||||
return false
|
||||
}
|
||||
if lhs.canReport != rhs.canReport {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -315,7 +326,7 @@ private class AdMessagesHistoryContextImpl {
|
||||
case .recommended:
|
||||
mappedMessageType = .recommended
|
||||
}
|
||||
attributes.append(AdMessageAttribute(opaqueId: self.opaqueId, messageType: mappedMessageType, displayAvatar: self.displayAvatar, target: target, buttonText: self.buttonText, sponsorInfo: self.sponsorInfo, additionalInfo: self.additionalInfo))
|
||||
attributes.append(AdMessageAttribute(opaqueId: self.opaqueId, messageType: mappedMessageType, displayAvatar: self.displayAvatar, target: target, buttonText: self.buttonText, sponsorInfo: self.sponsorInfo, additionalInfo: self.additionalInfo, canReport: self.canReport))
|
||||
if !self.textEntities.isEmpty {
|
||||
let attribute = TextEntitiesMessageAttribute(entities: self.textEntities)
|
||||
attributes.append(attribute)
|
||||
@ -604,6 +615,7 @@ private class AdMessagesHistoryContextImpl {
|
||||
|
||||
let isRecommended = (flags & (1 << 5)) != 0
|
||||
var displayAvatar = (flags & (1 << 6)) != 0
|
||||
let canReport = (flags & (1 << 12)) != 0
|
||||
|
||||
var target: CachedMessage.Target?
|
||||
if let fromId = fromId {
|
||||
@ -685,7 +697,8 @@ private class AdMessagesHistoryContextImpl {
|
||||
startParam: startParam,
|
||||
buttonText: buttonText,
|
||||
sponsorInfo: sponsorInfo,
|
||||
additionalInfo: additionalInfo
|
||||
additionalInfo: additionalInfo,
|
||||
canReport: canReport
|
||||
))
|
||||
}
|
||||
}
|
||||
|
@ -443,6 +443,8 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/Chat/ChatEmptyNode",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatMediaInputStickerGridItem",
|
||||
"//submodules/TelegramUI/Components/Settings/BusinessLinkNameAlertController",
|
||||
"//submodules/TelegramUI/Components/Ads/AdsInfoScreen",
|
||||
"//submodules/TelegramUI/Components/Ads/AdsReportScreen",
|
||||
] + select({
|
||||
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
|
||||
"//build-system:ios_sim_arm64": [],
|
||||
|
@ -28,7 +28,6 @@ swift_library(
|
||||
"//submodules/AppBundle",
|
||||
"//submodules/TelegramStringFormatting",
|
||||
"//submodules/PresentationDataUtils",
|
||||
"//submodules/Components/SheetComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -65,6 +65,7 @@ private final class ScrollContent: CombinedComponent {
|
||||
let icon = Child(BundleIconComponent.self)
|
||||
|
||||
let title = Child(BalancedTextComponent.self)
|
||||
let text = Child(BalancedTextComponent.self)
|
||||
let list = Child(List<Empty>.self)
|
||||
let actionButton = Child(SolidRoundedButtonComponent.self)
|
||||
|
||||
@ -98,7 +99,7 @@ private final class ScrollContent: CombinedComponent {
|
||||
//TODO:localize
|
||||
|
||||
let spacing: CGFloat = 16.0
|
||||
var contentSize = CGSize(width: context.availableSize.width, height: 32.0)
|
||||
var contentSize = CGSize(width: context.availableSize.width, height: 30.0)
|
||||
|
||||
let iconSize = CGSize(width: 90.0, height: 90.0)
|
||||
let gradientImage: UIImage
|
||||
@ -128,12 +129,11 @@ private final class ScrollContent: CombinedComponent {
|
||||
availableSize: CGSize(width: 90, height: 90),
|
||||
transition: .immediate
|
||||
)
|
||||
|
||||
context.add(icon
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + iconBackground.size.height / 2.0))
|
||||
)
|
||||
contentSize.height += iconSize.height
|
||||
contentSize.height += spacing + 5.0
|
||||
contentSize.height += spacing + 1.0
|
||||
|
||||
let title = title.update(
|
||||
component: BalancedTextComponent(
|
||||
@ -149,6 +149,22 @@ private final class ScrollContent: CombinedComponent {
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + title.size.height / 2.0))
|
||||
)
|
||||
contentSize.height += title.size.height
|
||||
contentSize.height += spacing - 8.0
|
||||
|
||||
let text = text.update(
|
||||
component: BalancedTextComponent(
|
||||
text: .plain(NSAttributedString(string: "Telegram Ads are very different from ads on other platforms. Ads such as this one:", font: textFont, textColor: secondaryTextColor)),
|
||||
horizontalAlignment: .center,
|
||||
maximumNumberOfLines: 0,
|
||||
lineSpacing: 0.2
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height),
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(text
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + text.size.height / 2.0))
|
||||
)
|
||||
contentSize.height += text.size.height
|
||||
contentSize.height += spacing
|
||||
|
||||
|
||||
@ -215,7 +231,7 @@ private final class ScrollContent: CombinedComponent {
|
||||
maximumNumberOfLines: 0,
|
||||
lineSpacing: 0.2
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.4, height: context.availableSize.height),
|
||||
availableSize: CGSize(width: context.availableSize.width - sideInset * 3.5, height: context.availableSize.height),
|
||||
transition: .immediate
|
||||
)
|
||||
|
||||
@ -223,7 +239,7 @@ private final class ScrollContent: CombinedComponent {
|
||||
state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: linkColor)!, theme)
|
||||
}
|
||||
|
||||
let infoString = "Anyone can create an ad to display in this channel – with minimal budgets. Check out the Telegram Ad Platform for details. [Learn More >]()"
|
||||
let infoString = "Anyone can create ads to display in this channel – with minimal budgets. Check out the Telegram Ad Platform for details. [Learn More >]()"
|
||||
let infoAttributedString = parseMarkdownIntoAttributedString(infoString, attributes: markdownAttributes).mutableCopy() as! NSMutableAttributedString
|
||||
if let range = infoAttributedString.string.range(of: ">"), let chevronImage = state.cachedChevronImage?.0 {
|
||||
infoAttributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: infoAttributedString.string))
|
||||
@ -235,12 +251,12 @@ private final class ScrollContent: CombinedComponent {
|
||||
maximumNumberOfLines: 0,
|
||||
lineSpacing: 0.2
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width - (textSideInset + sideInset - 2.0) * 2.0, height: context.availableSize.height),
|
||||
availableSize: CGSize(width: context.availableSize.width - sideInset * 3.5, height: context.availableSize.height),
|
||||
transition: .immediate
|
||||
)
|
||||
|
||||
let infoPadding: CGFloat = 17.0
|
||||
let infoSpacing: CGFloat = 12.0
|
||||
let infoPadding: CGFloat = 13.0
|
||||
let infoSpacing: CGFloat = 6.0
|
||||
let totalInfoHeight = infoPadding + infoTitle.size.height + infoSpacing + infoText.size.height + infoPadding
|
||||
|
||||
let infoBackground = infoBackground.update(
|
||||
|
@ -20,14 +20,16 @@ swift_library(
|
||||
"//submodules/Components/ComponentDisplayAdapters",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/Components/BalancedTextComponent",
|
||||
"//submodules/Components/SolidRoundedButtonComponent",
|
||||
"//submodules/Components/BundleIconComponent",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/AppBundle",
|
||||
"//submodules/ItemListUI",
|
||||
"//submodules/TelegramStringFormatting",
|
||||
"//submodules/PresentationDataUtils",
|
||||
"//submodules/Components/SheetComponent",
|
||||
"//submodules/UndoUI",
|
||||
"//submodules/TelegramUI/Components/ListSectionComponent",
|
||||
"//submodules/TelegramUI/Components/ListActionItemComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -7,7 +7,7 @@
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
void splitRGBAIntoYUVAPlanes(uint8_t const *argb, uint8_t *outY, uint8_t *outU, uint8_t *outV, uint8_t *outA, int width, int height, int bytesPerRow);
|
||||
void splitRGBAIntoYUVAPlanes(uint8_t const *argb, uint8_t *outY, uint8_t *outU, uint8_t *outV, uint8_t *outA, int width, int height, int bytesPerRow, bool restrictedRange, bool keepColorsOrder);
|
||||
void combineYUVAPlanesIntoARGB(uint8_t *argb, uint8_t const *inY, uint8_t const *inU, uint8_t const *inV, uint8_t const *inA, int width, int height, int bytesPerRow);
|
||||
void scaleImagePlane(uint8_t *outPlane, int outWidth, int outHeight, int outBytesPerRow, uint8_t const *inPlane, int inWidth, int inHeight, int inBytesPerRow);
|
||||
|
||||
|
@ -3,14 +3,18 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <Accelerate/Accelerate.h>
|
||||
|
||||
static uint8_t permuteMap[4] = { 3, 2, 1, 0};
|
||||
static uint8_t permuteMap[4] = { 3, 2, 1, 0 };
|
||||
static uint8_t invertedPermuteMap[4] = { 3, 0, 1, 2 };
|
||||
|
||||
void splitRGBAIntoYUVAPlanes(uint8_t const *argb, uint8_t *outY, uint8_t *outU, uint8_t *outV, uint8_t *outA, int width, int height, int bytesPerRow) {
|
||||
void splitRGBAIntoYUVAPlanes(uint8_t const *argb, uint8_t *outY, uint8_t *outU, uint8_t *outV, uint8_t *outA, int width, int height, int bytesPerRow, bool restrictedRange, bool keepColorsOrder) {
|
||||
static vImage_ARGBToYpCbCr info;
|
||||
static vImage_ARGBToYpCbCr restrictedInfo;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
vImage_YpCbCrPixelRange pixelRange = (vImage_YpCbCrPixelRange){ 0, 128, 255, 255, 255, 1, 255, 0 };
|
||||
vImage_YpCbCrPixelRange restrictedPixelRange = (vImage_YpCbCrPixelRange){ 16, 128, 235, 240, 255, 0, 255, 0 };
|
||||
vImageConvert_ARGBToYpCbCr_GenerateConversion(kvImage_ARGBToYpCbCrMatrix_ITU_R_709_2, &pixelRange, &info, kvImageARGB8888, kvImage420Yp8_Cb8_Cr8, 0);
|
||||
vImageConvert_ARGBToYpCbCr_GenerateConversion(kvImage_ARGBToYpCbCrMatrix_ITU_R_709_2, &restrictedPixelRange, &restrictedInfo, kvImageARGB8888, kvImage420Yp8_Cb8_Cr8, 0);
|
||||
});
|
||||
|
||||
vImage_Error error = kvImageNoError;
|
||||
@ -45,7 +49,7 @@ void splitRGBAIntoYUVAPlanes(uint8_t const *argb, uint8_t *outY, uint8_t *outU,
|
||||
destA.height = height;
|
||||
destA.rowBytes = width;
|
||||
|
||||
error = vImageConvert_ARGB8888To420Yp8_Cb8_Cr8(&src, &destYp, &destCb, &destCr, &info, permuteMap, kvImageDoNotTile);
|
||||
error = vImageConvert_ARGB8888To420Yp8_Cb8_Cr8(&src, &destYp, &destCb, &destCr, restrictedRange ? &restrictedInfo : &info, keepColorsOrder ? invertedPermuteMap : permuteMap, kvImageDoNotTile);
|
||||
if (error != kvImageNoError) {
|
||||
return;
|
||||
}
|
||||
|
@ -365,7 +365,9 @@ extension ImageARGB {
|
||||
aBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self),
|
||||
Int32(self.argbPlane.width),
|
||||
Int32(self.argbPlane.height),
|
||||
Int32(self.argbPlane.bytesPerRow)
|
||||
Int32(self.argbPlane.bytesPerRow),
|
||||
false,
|
||||
false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -153,7 +153,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
|
||||
self.activateAction?()
|
||||
}
|
||||
|
||||
public typealias AsyncLayout = (_ presentationData: ChatPresentationData, _ automaticDownloadSettings: MediaAutoDownloadSettings, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ context: AccountContext, _ controllerInteraction: ChatControllerInteraction, _ message: Message, _ messageRead: Bool, _ chatLocation: ChatLocation, _ title: String?, _ subtitle: NSAttributedString?, _ text: String?, _ entities: [MessageTextEntity]?, _ media: (Media, ChatMessageAttachedContentNodeMediaFlags)?, _ mediaBadge: String?, _ actionIcon: ChatMessageAttachedContentActionIcon?, _ actionTitle: String?, _ displayLine: Bool, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ constrainedSize: CGSize, _ animationCache: AnimationCache, _ animationRenderer: MultiAnimationRenderer) -> (CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void)))
|
||||
public typealias AsyncLayout = (_ presentationData: ChatPresentationData, _ automaticDownloadSettings: MediaAutoDownloadSettings, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ context: AccountContext, _ controllerInteraction: ChatControllerInteraction, _ message: Message, _ messageRead: Bool, _ chatLocation: ChatLocation, _ title: String?, _ titleBadge: String?, _ subtitle: NSAttributedString?, _ text: String?, _ entities: [MessageTextEntity]?, _ media: (Media, ChatMessageAttachedContentNodeMediaFlags)?, _ mediaBadge: String?, _ actionIcon: ChatMessageAttachedContentActionIcon?, _ actionTitle: String?, _ displayLine: Bool, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ constrainedSize: CGSize, _ animationCache: AnimationCache, _ animationRenderer: MultiAnimationRenderer) -> (CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void)))
|
||||
|
||||
public func makeProgress() -> Promise<Bool> {
|
||||
let progress = Promise<Bool>()
|
||||
@ -178,7 +178,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
|
||||
let makeActionButtonLayout = ChatMessageAttachedContentButtonNode.asyncLayout(self.actionButton)
|
||||
let makeStatusLayout = ChatMessageDateAndStatusNode.asyncLayout(self.statusNode)
|
||||
|
||||
return { [weak self] presentationData, automaticDownloadSettings, associatedData, attributes, context, controllerInteraction, message, messageRead, chatLocation, title, subtitle, text, entities, mediaAndFlags, mediaBadge, actionIcon, actionTitle, displayLine, layoutConstants, preparePosition, constrainedSize, animationCache, animationRenderer in
|
||||
return { [weak self] presentationData, automaticDownloadSettings, associatedData, attributes, context, controllerInteraction, message, messageRead, chatLocation, title, titleBadge, subtitle, text, entities, mediaAndFlags, mediaBadge, actionIcon, actionTitle, displayLine, layoutConstants, preparePosition, constrainedSize, animationCache, animationRenderer in
|
||||
let isPreview = presentationData.isPreview
|
||||
let fontSize: CGFloat
|
||||
if message.adAttribute != nil {
|
||||
|
@ -52,7 +52,7 @@ public final class ChatMessageEventLogPreviousDescriptionContentNode: ChatMessag
|
||||
}
|
||||
let mediaAndFlags: (Media, ChatMessageAttachedContentNodeMediaFlags)? = nil
|
||||
|
||||
let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, true, .peer(id: item.message.id.peerId), title, nil, text, messageEntities, mediaAndFlags, nil, nil, nil, true, layoutConstants, preparePosition, constrainedSize, item.controllerInteraction.presentationContext.animationCache, item.controllerInteraction.presentationContext.animationRenderer)
|
||||
let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, true, .peer(id: item.message.id.peerId), title, nil, nil, text, messageEntities, mediaAndFlags, nil, nil, nil, true, layoutConstants, preparePosition, constrainedSize, item.controllerInteraction.presentationContext.animationCache, item.controllerInteraction.presentationContext.animationRenderer)
|
||||
|
||||
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)
|
||||
|
||||
|
@ -47,7 +47,7 @@ public final class ChatMessageEventLogPreviousLinkContentNode: ChatMessageBubble
|
||||
let text: String = item.message.text
|
||||
let mediaAndFlags: (Media, ChatMessageAttachedContentNodeMediaFlags)? = nil
|
||||
|
||||
let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, true, .peer(id: item.message.id.peerId), title, nil, text, messageEntities, mediaAndFlags, nil, nil, nil, true, layoutConstants, preparePosition, constrainedSize, item.controllerInteraction.presentationContext.animationCache, item.controllerInteraction.presentationContext.animationRenderer)
|
||||
let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, true, .peer(id: item.message.id.peerId), title, nil, nil, text, messageEntities, mediaAndFlags, nil, nil, nil, true, layoutConstants, preparePosition, constrainedSize, item.controllerInteraction.presentationContext.animationCache, item.controllerInteraction.presentationContext.animationRenderer)
|
||||
|
||||
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)
|
||||
|
||||
|
@ -52,7 +52,7 @@ public final class ChatMessageEventLogPreviousMessageContentNode: ChatMessageBub
|
||||
}
|
||||
let mediaAndFlags: (Media, ChatMessageAttachedContentNodeMediaFlags)? = nil
|
||||
|
||||
let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, true, .peer(id: item.message.id.peerId), title, nil, text, messageEntities, mediaAndFlags, nil, nil, nil, true, layoutConstants, preparePosition, constrainedSize, item.controllerInteraction.presentationContext.animationCache, item.controllerInteraction.presentationContext.animationRenderer)
|
||||
let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, true, .peer(id: item.message.id.peerId), title, nil, nil, text, messageEntities, mediaAndFlags, nil, nil, nil, true, layoutConstants, preparePosition, constrainedSize, item.controllerInteraction.presentationContext.animationCache, item.controllerInteraction.presentationContext.animationRenderer)
|
||||
|
||||
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)
|
||||
|
||||
|
@ -80,7 +80,7 @@ public final class ChatMessageGameBubbleContentNode: ChatMessageBubbleContentNod
|
||||
}
|
||||
}
|
||||
|
||||
let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, item.read, .peer(id: item.message.id.peerId), title, nil, item.message.text.isEmpty ? text : item.message.text, item.message.text.isEmpty ? nil : messageEntities, mediaAndFlags, nil, nil, nil, true, layoutConstants, preparePosition, constrainedSize, item.controllerInteraction.presentationContext.animationCache, item.controllerInteraction.presentationContext.animationRenderer)
|
||||
let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, item.read, .peer(id: item.message.id.peerId), title, nil, nil, item.message.text.isEmpty ? text : item.message.text, item.message.text.isEmpty ? nil : messageEntities, mediaAndFlags, nil, nil, nil, true, layoutConstants, preparePosition, constrainedSize, item.controllerInteraction.presentationContext.animationCache, item.controllerInteraction.presentationContext.animationRenderer)
|
||||
|
||||
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)
|
||||
|
||||
|
@ -76,7 +76,7 @@ public final class ChatMessageInvoiceBubbleContentNode: ChatMessageBubbleContent
|
||||
}
|
||||
}
|
||||
|
||||
let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, automaticDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, item.read, item.chatLocation, title, subtitle, text, nil, mediaAndFlags, nil, nil, nil, false, layoutConstants, preparePosition, constrainedSize, item.controllerInteraction.presentationContext.animationCache, item.controllerInteraction.presentationContext.animationRenderer)
|
||||
let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, automaticDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, item.read, item.chatLocation, title, nil, subtitle, text, nil, mediaAndFlags, nil, nil, nil, false, layoutConstants, preparePosition, constrainedSize, item.controllerInteraction.presentationContext.animationCache, item.controllerInteraction.presentationContext.animationRenderer)
|
||||
|
||||
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)
|
||||
|
||||
|
@ -236,6 +236,7 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent
|
||||
var subtitle: NSAttributedString?
|
||||
var text: String?
|
||||
var entities: [MessageTextEntity]?
|
||||
var titleBadge: String?
|
||||
var mediaAndFlags: (Media, ChatMessageAttachedContentNodeMediaFlags)?
|
||||
var badge: String?
|
||||
|
||||
@ -505,6 +506,8 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent
|
||||
}
|
||||
}
|
||||
|
||||
titleBadge = "what's this?"
|
||||
|
||||
if let buttonText = adAttribute.buttonText {
|
||||
actionTitle = buttonText.uppercased()
|
||||
} else if let author = item.message.author as? TelegramUser, author.botInfo != nil {
|
||||
@ -532,7 +535,7 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent
|
||||
displayLine = true
|
||||
}
|
||||
|
||||
let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, item.read, item.chatLocation, title, subtitle, text, entities, mediaAndFlags, badge, actionIcon, actionTitle, displayLine, layoutConstants, preparePosition, constrainedSize, item.controllerInteraction.presentationContext.animationCache, item.controllerInteraction.presentationContext.animationRenderer)
|
||||
let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.attributes, item.context, item.controllerInteraction, item.message, item.read, item.chatLocation, title, titleBadge, subtitle, text, entities, mediaAndFlags, badge, actionIcon, actionTitle, displayLine, layoutConstants, preparePosition, constrainedSize, item.controllerInteraction.presentationContext.animationCache, item.controllerInteraction.presentationContext.animationRenderer)
|
||||
|
||||
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)
|
||||
|
||||
|
@ -1517,6 +1517,8 @@ public extension EmojiPagerContentComponent {
|
||||
displaySearchWithPlaceholder = strings.EmojiSearch_SearchEmojiPlaceholder
|
||||
} else if [.profilePhoto, .groupPhoto].contains(subject) {
|
||||
displaySearchWithPlaceholder = strings.Common_Search
|
||||
} else if case .stickerAlt = subject {
|
||||
displaySearchWithPlaceholder = strings.Common_Search
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -71,6 +71,7 @@ swift_library(
|
||||
"//submodules/WallpaperBackgroundNode",
|
||||
"//submodules/ImageTransparency",
|
||||
"//submodules/FFMpegBinding",
|
||||
"//submodules/TelegramUI/Components/AnimationCache/ImageDCT",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -152,16 +152,6 @@ final class MediaEditorComposer {
|
||||
if var compositedImage {
|
||||
let scale = self.outputDimensions.width / compositedImage.extent.width
|
||||
compositedImage = compositedImage.samplingLinear().transformed(by: CGAffineTransform(scaleX: scale, y: scale))
|
||||
|
||||
if self.isFirst {
|
||||
let path = NSTemporaryDirectory() + "test22.png"
|
||||
if let cgImage = self.ciContext?.createCGImage(compositedImage, from: CGRect(origin: .zero, size: compositedImage.extent.size)) {
|
||||
let image = UIImage(cgImage: cgImage)
|
||||
let data = image.pngData()
|
||||
try? data?.write(to: URL(fileURLWithPath: path))
|
||||
self.isFirst = false
|
||||
}
|
||||
}
|
||||
|
||||
self.ciContext?.render(compositedImage, to: pixelBuffer)
|
||||
completion(pixelBuffer)
|
||||
@ -174,7 +164,6 @@ final class MediaEditorComposer {
|
||||
}
|
||||
completion(nil)
|
||||
}
|
||||
private var isFirst = true
|
||||
|
||||
private var cachedTexture: MTLTexture?
|
||||
func textureForImage(_ image: UIImage) -> MTLTexture? {
|
||||
|
@ -2,7 +2,7 @@ import Foundation
|
||||
import UIKit
|
||||
import CoreMedia
|
||||
import FFMpegBinding
|
||||
import YuvConversion
|
||||
import ImageDCT
|
||||
|
||||
final class MediaEditorVideoFFMpegWriter: MediaEditorVideoExportWriter {
|
||||
public static let registerFFMpegGlobals: Void = {
|
||||
@ -10,9 +10,8 @@ final class MediaEditorVideoFFMpegWriter: MediaEditorVideoExportWriter {
|
||||
return
|
||||
}()
|
||||
|
||||
var ffmpegWriter: FFMpegVideoWriter?
|
||||
let ffmpegWriter = FFMpegVideoWriter()
|
||||
var pool: CVPixelBufferPool?
|
||||
var secondPool: CVPixelBufferPool?
|
||||
|
||||
func setup(configuration: MediaEditorVideoExport.Configuration, outputPath: String) {
|
||||
let _ = MediaEditorVideoFFMpegWriter.registerFFMpegGlobals
|
||||
@ -26,8 +25,7 @@ final class MediaEditorVideoFFMpegWriter: MediaEditorVideoExportWriter {
|
||||
let pixelBufferOptions: [String: Any] = [
|
||||
kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA as NSNumber,
|
||||
kCVPixelBufferWidthKey as String: UInt32(width),
|
||||
kCVPixelBufferHeightKey as String: UInt32(height)//,
|
||||
// kCVPixelBufferIOSurfacePropertiesKey as String: [:] as NSDictionary
|
||||
kCVPixelBufferHeightKey as String: UInt32(height)
|
||||
]
|
||||
|
||||
var pool: CVPixelBufferPool?
|
||||
@ -38,25 +36,7 @@ final class MediaEditorVideoFFMpegWriter: MediaEditorVideoExportWriter {
|
||||
}
|
||||
self.pool = pool
|
||||
|
||||
let secondPixelBufferOptions: [String: Any] = [
|
||||
kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_420YpCbCr8VideoRange_8A_TriPlanar as NSNumber,
|
||||
kCVPixelBufferWidthKey as String: UInt32(width),
|
||||
kCVPixelBufferHeightKey as String: UInt32(height)//,
|
||||
// kCVPixelBufferIOSurfacePropertiesKey as String: [:] as NSDictionary
|
||||
]
|
||||
|
||||
var secondPool: CVPixelBufferPool?
|
||||
CVPixelBufferPoolCreate(nil, bufferOptions as CFDictionary, secondPixelBufferOptions as CFDictionary, &secondPool)
|
||||
guard let secondPool else {
|
||||
self.status = .failed
|
||||
return
|
||||
}
|
||||
self.secondPool = secondPool
|
||||
|
||||
let ffmpegWriter = FFMpegVideoWriter()
|
||||
self.ffmpegWriter = ffmpegWriter
|
||||
|
||||
if !ffmpegWriter.setup(withOutputPath: outputPath, width: width, height: height) {
|
||||
if !self.ffmpegWriter.setup(withOutputPath: outputPath, width: width, height: height, bitrate: 200 * 1000, framerate: 30) {
|
||||
self.status = .failed
|
||||
}
|
||||
}
|
||||
@ -83,10 +63,7 @@ final class MediaEditorVideoFFMpegWriter: MediaEditorVideoExportWriter {
|
||||
}
|
||||
|
||||
func finishWriting(completion: @escaping () -> Void) {
|
||||
guard let ffmpegWriter = self.ffmpegWriter else {
|
||||
return
|
||||
}
|
||||
ffmpegWriter.finalizeVideo()
|
||||
self.ffmpegWriter.finalizeVideo()
|
||||
self.status = .completed
|
||||
completion()
|
||||
}
|
||||
@ -113,75 +90,32 @@ final class MediaEditorVideoFFMpegWriter: MediaEditorVideoExportWriter {
|
||||
return false
|
||||
}
|
||||
|
||||
var isFirst = true
|
||||
func appendPixelBuffer(_ buffer: CVPixelBuffer, at time: CMTime) -> Bool {
|
||||
guard let ffmpegWriter = self.ffmpegWriter, let secondPool = self.secondPool else {
|
||||
return false
|
||||
}
|
||||
|
||||
let width = Int32(CVPixelBufferGetWidth(buffer))
|
||||
let height = Int32(CVPixelBufferGetHeight(buffer))
|
||||
let bytesPerRow = Int32(CVPixelBufferGetBytesPerRow(buffer))
|
||||
|
||||
var convertedBuffer: CVPixelBuffer?
|
||||
CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, secondPool, &convertedBuffer)
|
||||
guard let convertedBuffer else {
|
||||
return false
|
||||
}
|
||||
let frame = FFMpegAVFrame(pixelFormat: .YUVA, width: width, height: height)
|
||||
|
||||
CVPixelBufferLockBaseAddress(buffer, CVPixelBufferLockFlags(rawValue: 0))
|
||||
CVPixelBufferLockBaseAddress(buffer, CVPixelBufferLockFlags.readOnly)
|
||||
let src = CVPixelBufferGetBaseAddress(buffer)
|
||||
|
||||
CVPixelBufferLockBaseAddress(convertedBuffer, CVPixelBufferLockFlags(rawValue: 0))
|
||||
let dst = CVPixelBufferGetBaseAddress(convertedBuffer)
|
||||
|
||||
encodeRGBAToYUVA(dst, src, width, height, bytesPerRow, false, false)
|
||||
|
||||
CVPixelBufferUnlockBaseAddress(convertedBuffer, CVPixelBufferLockFlags(rawValue: 0))
|
||||
CVPixelBufferUnlockBaseAddress(buffer, CVPixelBufferLockFlags(rawValue: 0))
|
||||
splitRGBAIntoYUVAPlanes(
|
||||
src,
|
||||
frame.data[0],
|
||||
frame.data[1],
|
||||
frame.data[2],
|
||||
frame.data[3],
|
||||
width,
|
||||
height,
|
||||
bytesPerRow,
|
||||
true,
|
||||
true
|
||||
)
|
||||
|
||||
CVPixelBufferUnlockBaseAddress(buffer, CVPixelBufferLockFlags.readOnly)
|
||||
|
||||
if self.isFirst {
|
||||
let path = NSTemporaryDirectory() + "test.png"
|
||||
let image = self.imageFromCVPixelBuffer(convertedBuffer, orientation: .up)
|
||||
let data = image?.pngData()
|
||||
try? data?.write(to: URL(fileURLWithPath: path))
|
||||
self.isFirst = false
|
||||
}
|
||||
|
||||
return ffmpegWriter.encodeFrame(convertedBuffer)
|
||||
}
|
||||
|
||||
func imageFromCVPixelBuffer(_ pixelBuffer: CVPixelBuffer, orientation: UIImage.Orientation) -> UIImage? {
|
||||
CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly)
|
||||
|
||||
let width = CVPixelBufferGetWidth(pixelBuffer)
|
||||
let height = CVPixelBufferGetHeight(pixelBuffer)
|
||||
let bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer)
|
||||
let baseAddress = CVPixelBufferGetBaseAddress(pixelBuffer)
|
||||
|
||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
|
||||
guard let context = CGContext(
|
||||
data: baseAddress,
|
||||
width: width,
|
||||
height: height,
|
||||
bitsPerComponent: 8,
|
||||
bytesPerRow: bytesPerRow,
|
||||
space: colorSpace,
|
||||
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
|
||||
) else {
|
||||
CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly)
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let cgImage = context.makeImage() else {
|
||||
CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly)
|
||||
return nil
|
||||
}
|
||||
|
||||
CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly)
|
||||
|
||||
return UIImage(cgImage: cgImage, scale: 1.0, orientation: orientation)
|
||||
return self.ffmpegWriter.encode(frame)
|
||||
}
|
||||
|
||||
func markVideoAsFinished() {
|
||||
|
@ -2146,7 +2146,9 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
private let gradientView: UIImageView
|
||||
private var gradientColorsDisposable: Disposable?
|
||||
|
||||
private let stickerTransparentView: UIImageView
|
||||
private var stickerBackgroundView: UIImageView?
|
||||
private var stickerOverlayLayer: SimpleShapeLayer?
|
||||
private var stickerFrameLayer: SimpleShapeLayer?
|
||||
|
||||
fileprivate let entitiesContainerView: UIView
|
||||
let entitiesView: DrawingEntitiesView
|
||||
@ -2216,9 +2218,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
}
|
||||
|
||||
self.gradientView = UIImageView()
|
||||
self.stickerTransparentView = UIImageView()
|
||||
self.stickerTransparentView.clipsToBounds = true
|
||||
|
||||
|
||||
var isStickerEditor = false
|
||||
if case .stickerEditor = controller.mode {
|
||||
isStickerEditor = true
|
||||
@ -2255,7 +2255,9 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
|
||||
if case .stickerEditor = controller.mode {
|
||||
let rowsCount = 40
|
||||
self.stickerTransparentView.image = generateImage(CGSize(width: rowsCount, height: rowsCount), opaque: true, scale: 1.0, rotatedContext: { size, context in
|
||||
let stickerBackgroundView = UIImageView()
|
||||
stickerBackgroundView.clipsToBounds = true
|
||||
stickerBackgroundView.image = generateImage(CGSize(width: rowsCount, height: rowsCount), opaque: true, scale: 1.0, rotatedContext: { size, context in
|
||||
context.setFillColor(UIColor.black.cgColor)
|
||||
context.fill(CGRect(origin: .zero, size: size))
|
||||
context.setFillColor(UIColor(rgb: 0x2b2b2d).cgColor)
|
||||
@ -2269,10 +2271,11 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
}
|
||||
context.fillPath()
|
||||
})
|
||||
self.stickerTransparentView.layer.magnificationFilter = .nearest
|
||||
self.stickerTransparentView.layer.shouldRasterize = true
|
||||
self.stickerTransparentView.layer.rasterizationScale = UIScreenScale
|
||||
self.previewContainerView.addSubview(self.stickerTransparentView)
|
||||
stickerBackgroundView.layer.magnificationFilter = .nearest
|
||||
stickerBackgroundView.layer.shouldRasterize = true
|
||||
stickerBackgroundView.layer.rasterizationScale = UIScreenScale
|
||||
self.stickerBackgroundView = stickerBackgroundView
|
||||
self.previewContainerView.addSubview(stickerBackgroundView)
|
||||
} else {
|
||||
self.previewContainerView.addSubview(self.gradientView)
|
||||
}
|
||||
@ -2281,6 +2284,24 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
self.previewContainerView.addSubview(self.entitiesContainerView)
|
||||
self.entitiesContainerView.addSubview(self.entitiesView)
|
||||
self.entitiesView.addSubview(self.drawingView)
|
||||
|
||||
if case .stickerEditor = controller.mode {
|
||||
let stickerOverlayLayer = SimpleShapeLayer()
|
||||
stickerOverlayLayer.fillColor = UIColor(rgb: 0x000000, alpha: 0.7).cgColor
|
||||
stickerOverlayLayer.fillRule = .evenOdd
|
||||
self.stickerOverlayLayer = stickerOverlayLayer
|
||||
self.previewContainerView.layer.addSublayer(stickerOverlayLayer)
|
||||
|
||||
let stickerFrameLayer = SimpleShapeLayer()
|
||||
stickerFrameLayer.fillColor = UIColor.clear.cgColor
|
||||
stickerFrameLayer.strokeColor = UIColor(rgb: 0xffffff, alpha: 0.55).cgColor
|
||||
stickerFrameLayer.lineDashPattern = [12, 12] as [NSNumber]
|
||||
stickerFrameLayer.lineCap = .round
|
||||
|
||||
self.stickerFrameLayer = stickerFrameLayer
|
||||
self.previewContainerView.layer.addSublayer(stickerFrameLayer)
|
||||
}
|
||||
|
||||
self.previewContainerView.addSubview(self.selectionContainerView)
|
||||
|
||||
self.subjectDisposable = (
|
||||
@ -4408,8 +4429,25 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
transition.setFrame(view: self.selectionContainerView, frame: CGRect(origin: .zero, size: previewFrame.size))
|
||||
|
||||
let stickerFrameWidth = floor(previewSize.width * 0.97)
|
||||
transition.setFrame(view: self.stickerTransparentView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((previewSize.width - stickerFrameWidth) / 2.0), y: floorToScreenPixels((previewSize.height - stickerFrameWidth) / 2.0)), size: CGSize(width: stickerFrameWidth, height: stickerFrameWidth)))
|
||||
self.stickerTransparentView.layer.cornerRadius = stickerFrameWidth / 8.0
|
||||
if let stickerBackgroundView = self.stickerBackgroundView, let stickerOverlayLayer = self.stickerOverlayLayer, let stickerFrameLayer = self.stickerFrameLayer {
|
||||
stickerOverlayLayer.frame = CGRect(origin: .zero, size: previewSize)
|
||||
|
||||
let stickerFrameRect = CGRect(origin: CGPoint(x: floor((previewSize.width - stickerFrameWidth) / 2.0), y: floor((previewSize.height - stickerFrameWidth) / 2.0)), size: CGSize(width: stickerFrameWidth, height: stickerFrameWidth))
|
||||
|
||||
let overlayOuterRect = UIBezierPath(rect: CGRect(origin: .zero, size: previewSize))
|
||||
let overlayInnerRect = UIBezierPath(cgPath: CGPath(roundedRect: stickerFrameRect, cornerWidth: stickerFrameWidth / 8.0, cornerHeight: stickerFrameWidth / 8.0, transform: nil))
|
||||
let overlayLineWidth: CGFloat = 2.0 - UIScreenPixel
|
||||
overlayOuterRect.append(overlayInnerRect)
|
||||
overlayOuterRect.usesEvenOddFillRule = true
|
||||
stickerOverlayLayer.path = overlayOuterRect.cgPath
|
||||
|
||||
stickerFrameLayer.frame = stickerOverlayLayer.frame
|
||||
stickerFrameLayer.lineWidth = overlayLineWidth
|
||||
stickerFrameLayer.path = CGPath(roundedRect: stickerFrameRect.insetBy(dx: -overlayLineWidth / 2.0, dy: -overlayLineWidth / 2.0), cornerWidth: stickerFrameWidth / 8.0 * 1.02, cornerHeight: stickerFrameWidth / 8.0 * 1.02, transform: nil)
|
||||
|
||||
transition.setFrame(view: stickerBackgroundView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((previewSize.width - stickerFrameWidth) / 2.0), y: floorToScreenPixels((previewSize.height - stickerFrameWidth) / 2.0)), size: CGSize(width: stickerFrameWidth, height: stickerFrameWidth)))
|
||||
stickerBackgroundView.layer.cornerRadius = stickerFrameWidth / 8.0
|
||||
}
|
||||
|
||||
self.interaction?.containerLayoutUpdated(layout: layout, transition: transition)
|
||||
|
||||
@ -4558,11 +4596,11 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
|
||||
init(
|
||||
media: MediaResult?,
|
||||
mediaAreas: [MediaArea],
|
||||
caption: NSAttributedString,
|
||||
options: MediaEditorResultPrivacy,
|
||||
stickers: [TelegramMediaFile],
|
||||
randomId: Int64
|
||||
mediaAreas: [MediaArea] = [],
|
||||
caption: NSAttributedString = NSAttributedString(),
|
||||
options: MediaEditorResultPrivacy = MediaEditorResultPrivacy(sendAsPeerId: nil, privacy: EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []), timeout: 0, isForwardingDisabled: false, pin: false),
|
||||
stickers: [TelegramMediaFile] = [],
|
||||
randomId: Int64 = 0
|
||||
) {
|
||||
self.media = media
|
||||
self.mediaAreas = mediaAreas
|
||||
@ -5717,24 +5755,13 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
let values = mediaEditor.values.withUpdatedQualityPreset(.sticker)
|
||||
makeEditorImageComposition(context: self.node.ciContext, postbox: self.context.account.postbox, inputImage: image, dimensions: storyDimensions, outputDimensions: CGSize(width: 512, height: 512), values: values, time: .zero, textScale: 2.0, completion: { [weak self] resultImage in
|
||||
if let self, let resultImage {
|
||||
// let dimensions = CGSize(width: 512, height: 512)
|
||||
// let scaledImage = generateImage(dimensions, contextGenerator: { size, context in
|
||||
// context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
//
|
||||
// context.addPath(CGPath(roundedRect: CGRect(origin: .zero, size: size), cornerWidth: size.width / 8.0, cornerHeight: size.width / 8.0, transform: nil))
|
||||
// context.clip()
|
||||
//
|
||||
// let scaledSize = resultImage.size.aspectFilled(size)
|
||||
// context.draw(resultImage.cgImage!, in: CGRect(origin: CGPoint(x: floor((size.width - scaledSize.width) / 2.0), y: floor((size.height - scaledSize.height) / 2.0)), size: scaledSize))
|
||||
// }, opaque: false, scale: 1.0)!
|
||||
|
||||
self.presentStickerPreview(image: resultImage)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private weak var resultController: PeekController?
|
||||
func presentStickerPreview(image: UIImage) {
|
||||
guard let mediaEditor = self.node.mediaEditor else {
|
||||
return
|
||||
@ -5745,7 +5772,6 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
var isVideo = false
|
||||
if mediaEditor.resultIsVideo {
|
||||
isVideo = true
|
||||
self.performSave(toStickerResource: resource)
|
||||
} else {
|
||||
Queue.concurrentDefaultQueue().async {
|
||||
if let data = try? WebP.convert(toWebP: image, quality: 97.0) {
|
||||
@ -5766,26 +5792,29 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if self.videoExport != nil {
|
||||
return
|
||||
}
|
||||
f(.default)
|
||||
|
||||
self.completion(MediaEditorScreen.Result(
|
||||
media: .sticker(file: file),
|
||||
mediaAreas: [],
|
||||
caption: NSAttributedString(),
|
||||
options: MediaEditorResultPrivacy(sendAsPeerId: nil, privacy: EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []), timeout: 0, isForwardingDisabled: false, pin: false),
|
||||
stickers: [],
|
||||
randomId: 0
|
||||
), { [weak self] finished in
|
||||
self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in
|
||||
self?.dismiss()
|
||||
Queue.mainQueue().justDispatch {
|
||||
finished()
|
||||
}
|
||||
if isVideo {
|
||||
self.uploadSticker(file, action: .send)
|
||||
} else {
|
||||
self.resultController?.disappeared = nil
|
||||
self.completion(MediaEditorScreen.Result(
|
||||
media: .sticker(file: file),
|
||||
mediaAreas: [],
|
||||
caption: NSAttributedString(),
|
||||
options: MediaEditorResultPrivacy(sendAsPeerId: nil, privacy: EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []), timeout: 0, isForwardingDisabled: false, pin: false),
|
||||
stickers: [],
|
||||
randomId: 0
|
||||
), { [weak self] finished in
|
||||
self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in
|
||||
self?.dismiss()
|
||||
Queue.mainQueue().justDispatch {
|
||||
finished()
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
f(.default)
|
||||
})))
|
||||
menuItems.append(.action(ContextMenuActionItem(text: presentationData.strings.Stickers_AddToFavorites, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Fave"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
|
||||
f(.default)
|
||||
@ -5861,6 +5890,10 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
}
|
||||
}
|
||||
|
||||
Queue.mainQueue().justDispatch {
|
||||
self.node.entitiesView.selectEntity(nil)
|
||||
}
|
||||
|
||||
let peekController = PeekController(
|
||||
presentationData: presentationData,
|
||||
content: StickerPreviewPeekContent(
|
||||
@ -5875,14 +5908,28 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
),
|
||||
sourceView: { [weak self] in
|
||||
if let self {
|
||||
let size = CGSize(width: self.view.frame.width, height: self.view.frame.width)
|
||||
return (self.view, CGRect(origin: CGPoint(x: (self.view.frame.width - size.width) / 2.0, y: (self.view.frame.height - size.height) / 2.0), size: size))
|
||||
let previewContainerFrame = self.node.previewContainerView.frame
|
||||
let size = CGSize(width: previewContainerFrame.width, height: previewContainerFrame.width)
|
||||
return (self.view, CGRect(origin: CGPoint(x: previewContainerFrame.midX - size.width / 2.0, y: previewContainerFrame.midY - size.height / 2.0), size: size))
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
},
|
||||
activateImmediately: true
|
||||
)
|
||||
peekController.appeared = { [weak self] in
|
||||
if let self {
|
||||
self.node.entitiesView.alpha = 0.0
|
||||
self.node.previewView.alpha = 0.0
|
||||
}
|
||||
}
|
||||
peekController.disappeared = { [weak self] in
|
||||
if let self {
|
||||
self.node.entitiesView.alpha = 1.0
|
||||
self.node.previewView.alpha = 1.0
|
||||
}
|
||||
}
|
||||
self.resultController = peekController
|
||||
self.present(peekController, in: .window(.root))
|
||||
}
|
||||
|
||||
@ -5891,9 +5938,11 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
case createStickerPack(title: String)
|
||||
case addToStickerPack(pack: StickerPackReference, title: String)
|
||||
case upload
|
||||
case send
|
||||
}
|
||||
|
||||
private func presentCreateStickerPack(file: TelegramMediaFile, completion: @escaping () -> Void) {
|
||||
//TODO:localize
|
||||
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkColorPresentationTheme)
|
||||
|
||||
var dismissImpl: (() -> Void)?
|
||||
@ -5962,69 +6011,118 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
let context = self.context
|
||||
let dimensions = PixelDimensions(width: 512, height: 512)
|
||||
let mimeType = file.mimeType
|
||||
let isVideo = file.mimeType == "video/webm"
|
||||
|
||||
self.updateEditProgress(0.0, cancel: { [weak self] in
|
||||
self?.stickerUploadDisposable.set(nil)
|
||||
})
|
||||
|
||||
enum PrepareStickerStatus {
|
||||
case progress(Float)
|
||||
case complete(TelegramMediaResource)
|
||||
case failed
|
||||
}
|
||||
let resourceSignal: Signal<PrepareStickerStatus, UploadStickerError>
|
||||
if isVideo {
|
||||
self.performSave(toStickerResource: file.resource)
|
||||
resourceSignal = self.videoExportPromise.get()
|
||||
|> castError(UploadStickerError.self)
|
||||
|> filter { $0 != nil }
|
||||
|> take(1)
|
||||
|> mapToSignal { videoExport -> Signal<PrepareStickerStatus, UploadStickerError> in
|
||||
guard let videoExport else {
|
||||
return .complete()
|
||||
}
|
||||
return videoExport.status
|
||||
|> castError(UploadStickerError.self)
|
||||
|> mapToSignal { status -> Signal<PrepareStickerStatus, UploadStickerError> in
|
||||
switch status {
|
||||
case .unknown:
|
||||
return .single(.progress(0.0))
|
||||
case let .progress(progress):
|
||||
return .single(.progress(progress))
|
||||
case .completed:
|
||||
return .single(.complete(file.resource))
|
||||
|> delay(0.05, queue: Queue.mainQueue())
|
||||
case .failed:
|
||||
return .single(.failed)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
resourceSignal = .single(.complete(file.resource))
|
||||
}
|
||||
|
||||
let signal = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|
||||
|> castError(UploadStickerError.self)
|
||||
|> mapToSignal { peer -> Signal<UploadStickerStatus, UploadStickerError> in
|
||||
guard let peer else {
|
||||
return .complete()
|
||||
}
|
||||
return context.engine.stickers.uploadSticker(peer: peer._asPeer(), resource: file.resource, alt: "", dimensions: dimensions, mimeType: mimeType)
|
||||
|> mapToSignal { status -> Signal<UploadStickerStatus, UploadStickerError> in
|
||||
switch status {
|
||||
case .progress:
|
||||
return .single(status)
|
||||
case let .complete(resource, _):
|
||||
let file = stickerFile(resource: resource, size: file.size ?? 0, dimensions: dimensions, isVideo: file.mimeType == "video/webm")
|
||||
switch action {
|
||||
case .addToFavorites:
|
||||
return context.engine.stickers.toggleStickerSaved(file: file, saved: true)
|
||||
|> `catch` { _ -> Signal<SavedStickerResult, UploadStickerError> in
|
||||
return .fail(.generic)
|
||||
}
|
||||
|> map { _ in
|
||||
return status
|
||||
}
|
||||
case let .createStickerPack(title):
|
||||
let sticker = ImportSticker(
|
||||
resource: resource,
|
||||
emojis: ["😀😂"],
|
||||
dimensions: dimensions,
|
||||
mimeType: mimeType,
|
||||
keywords: ""
|
||||
)
|
||||
return context.engine.stickers.createStickerSet(title: title, shortName: "", stickers: [sticker], thumbnail: nil, type: .stickers(content: .image), software: nil)
|
||||
|> `catch` { _ -> Signal<CreateStickerSetStatus, UploadStickerError> in
|
||||
return .fail(.generic)
|
||||
}
|
||||
|> mapToSignal { innerStatus in
|
||||
if case .complete = innerStatus {
|
||||
return resourceSignal
|
||||
|> mapToSignal { result -> Signal<UploadStickerStatus, UploadStickerError> in
|
||||
switch result {
|
||||
case .failed:
|
||||
return .fail(.generic)
|
||||
case let .progress(progress):
|
||||
return .single(.progress(progress * 0.5))
|
||||
case let .complete(resource):
|
||||
return context.engine.stickers.uploadSticker(peer: peer._asPeer(), resource: resource, alt: "", dimensions: dimensions, mimeType: mimeType)
|
||||
|> mapToSignal { status -> Signal<UploadStickerStatus, UploadStickerError> in
|
||||
switch status {
|
||||
case let .progress(progress):
|
||||
return .single(.progress(isVideo ? 0.5 + progress * 0.5 : progress))
|
||||
case let .complete(resource, _):
|
||||
let file = stickerFile(resource: resource, size: file.size ?? 0, dimensions: dimensions, isVideo: isVideo)
|
||||
switch action {
|
||||
case .send:
|
||||
return .single(status)
|
||||
case .addToFavorites:
|
||||
return context.engine.stickers.toggleStickerSaved(file: file, saved: true)
|
||||
|> `catch` { _ -> Signal<SavedStickerResult, UploadStickerError> in
|
||||
return .fail(.generic)
|
||||
}
|
||||
|> map { _ in
|
||||
return status
|
||||
}
|
||||
case let .createStickerPack(title):
|
||||
let sticker = ImportSticker(
|
||||
resource: resource,
|
||||
emojis: ["😀😂"],
|
||||
dimensions: dimensions,
|
||||
mimeType: mimeType,
|
||||
keywords: ""
|
||||
)
|
||||
return context.engine.stickers.createStickerSet(title: title, shortName: "", stickers: [sticker], thumbnail: nil, type: .stickers(content: .image), software: nil)
|
||||
|> `catch` { _ -> Signal<CreateStickerSetStatus, UploadStickerError> in
|
||||
return .fail(.generic)
|
||||
}
|
||||
|> mapToSignal { innerStatus in
|
||||
if case .complete = innerStatus {
|
||||
return .single(status)
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
case let .addToStickerPack(pack, _):
|
||||
let sticker = ImportSticker(
|
||||
resource: resource,
|
||||
emojis: ["😀😂"],
|
||||
dimensions: dimensions,
|
||||
mimeType: mimeType,
|
||||
keywords: ""
|
||||
)
|
||||
return context.engine.stickers.addStickerToStickerSet(packReference: pack, sticker: sticker)
|
||||
|> `catch` { _ -> Signal<Bool, UploadStickerError> in
|
||||
return .fail(.generic)
|
||||
}
|
||||
|> map { _ in
|
||||
return status
|
||||
}
|
||||
case .upload:
|
||||
return .single(status)
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
case let .addToStickerPack(pack, _):
|
||||
let sticker = ImportSticker(
|
||||
resource: resource,
|
||||
emojis: ["😀😂"],
|
||||
dimensions: dimensions,
|
||||
mimeType: mimeType,
|
||||
keywords: ""
|
||||
)
|
||||
return context.engine.stickers.addStickerToStickerSet(packReference: pack, sticker: sticker)
|
||||
|> `catch` { _ -> Signal<Bool, UploadStickerError> in
|
||||
return .fail(.generic)
|
||||
}
|
||||
|> map { _ in
|
||||
return status
|
||||
}
|
||||
case .upload:
|
||||
return .single(status)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -6038,26 +6136,23 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
switch status {
|
||||
case let .progress(progress):
|
||||
self.updateEditProgress(progress, cancel: { [weak self] in
|
||||
self?.videoExport?.cancel()
|
||||
self?.videoExport = nil
|
||||
self?.exportDisposable.set(nil)
|
||||
self?.stickerUploadDisposable.set(nil)
|
||||
})
|
||||
case let .complete(resource, _):
|
||||
let navigationController = self.navigationController as? NavigationController
|
||||
|
||||
let result: MediaEditorScreen.Result
|
||||
if case .upload = action {
|
||||
let file = stickerFile(resource: resource, size: resource.size ?? 0, dimensions: dimensions, isVideo: file.mimeType == "video/webm")
|
||||
result = MediaEditorScreen.Result(
|
||||
media: .sticker(file: file),
|
||||
mediaAreas: [],
|
||||
caption: NSAttributedString(),
|
||||
options: MediaEditorResultPrivacy(sendAsPeerId: nil, privacy: EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []), timeout: 0, isForwardingDisabled: false, pin: false),
|
||||
stickers: [],
|
||||
randomId: 0
|
||||
)
|
||||
} else {
|
||||
switch action {
|
||||
case .upload, .send:
|
||||
let file = stickerFile(resource: resource, size: resource.size ?? 0, dimensions: dimensions, isVideo: isVideo)
|
||||
result = MediaEditorScreen.Result(media: .sticker(file: file))
|
||||
default:
|
||||
result = MediaEditorScreen.Result()
|
||||
}
|
||||
|
||||
|
||||
self.completion(result, { [weak self] finished in
|
||||
self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in
|
||||
guard let self else {
|
||||
@ -6095,7 +6190,12 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
}))
|
||||
}
|
||||
|
||||
private var videoExport: MediaEditorVideoExport?
|
||||
private var videoExport: MediaEditorVideoExport? {
|
||||
didSet {
|
||||
self.videoExportPromise.set(.single(self.videoExport))
|
||||
}
|
||||
}
|
||||
private var videoExportPromise = Promise<MediaEditorVideoExport?>(nil)
|
||||
private var exportDisposable = MetaDisposable()
|
||||
|
||||
fileprivate var isSavingAvailable = false
|
||||
@ -6126,7 +6226,6 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities)
|
||||
|
||||
let isSticker = toStickerResource != nil
|
||||
|
||||
if !isSticker {
|
||||
self.previousSavedValues = mediaEditor.values
|
||||
self.isSavingAvailable = false
|
||||
@ -6155,8 +6254,10 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
}
|
||||
|
||||
if mediaEditor.resultIsVideo {
|
||||
mediaEditor.maybePauseVideo()
|
||||
self.node.entitiesView.pause()
|
||||
if !isSticker {
|
||||
mediaEditor.maybePauseVideo()
|
||||
self.node.entitiesView.pause()
|
||||
}
|
||||
|
||||
let exportSubject: Signal<MediaEditorVideoExport.Subject, NoError>
|
||||
switch subject {
|
||||
@ -6241,8 +6342,10 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
switch status {
|
||||
case .completed:
|
||||
self.videoExport = nil
|
||||
if let toStickerResource, let data = try? Data(contentsOf: URL(fileURLWithPath: outputPath)) {
|
||||
self.context.account.postbox.mediaBox.storeResourceData(toStickerResource.id, data: data)
|
||||
if let toStickerResource {
|
||||
if let data = try? Data(contentsOf: URL(fileURLWithPath: outputPath)) {
|
||||
self.context.account.postbox.mediaBox.storeResourceData(toStickerResource.id, data: data, synchronous: true)
|
||||
}
|
||||
} else {
|
||||
saveToPhotos(outputPath, true)
|
||||
self.node.presentSaveTooltip()
|
||||
@ -6285,16 +6388,18 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
}
|
||||
|
||||
fileprivate func cancelVideoExport() {
|
||||
if let videoExport = self.videoExport {
|
||||
self.previousSavedValues = nil
|
||||
|
||||
videoExport.cancel()
|
||||
self.videoExport = nil
|
||||
self.exportDisposable.set(nil)
|
||||
|
||||
self.node.mediaEditor?.play()
|
||||
self.node.entitiesView.play()
|
||||
guard let videoExport = self.videoExport else {
|
||||
return
|
||||
}
|
||||
videoExport.cancel()
|
||||
|
||||
self.videoExport = nil
|
||||
self.exportDisposable.set(nil)
|
||||
|
||||
self.previousSavedValues = nil
|
||||
|
||||
self.node.mediaEditor?.play()
|
||||
self.node.entitiesView.play()
|
||||
}
|
||||
|
||||
public func updateEditProgress(_ progress: Float, cancel: @escaping () -> Void) {
|
||||
|
@ -49,6 +49,9 @@ private final class StickerPackListContextItemNode: ASDisplayNode, ContextMenuCu
|
||||
|
||||
var i = 0
|
||||
for (pack, topItem) in item.packs {
|
||||
if pack.flags.contains(.isEmoji) {
|
||||
continue
|
||||
}
|
||||
let thumbSize = CGSize(width: 24.0, height: 24.0)
|
||||
let thumbnailResource = pack.thumbnail?.resource ?? topItem?.file.resource
|
||||
let thumbnailIconSource: ContextMenuActionItemIconSource?
|
||||
|
@ -32,6 +32,8 @@ private final class PeerInfoScreenCommentItemNode: PeerInfoScreenItemNode {
|
||||
private var item: PeerInfoScreenCommentItem?
|
||||
private var presentationData: PresentationData?
|
||||
|
||||
private var chevronImage: UIImage?
|
||||
|
||||
override init() {
|
||||
self.textNode = ImmediateTextNode()
|
||||
self.textNode.displaysAsynchronously = false
|
||||
@ -66,6 +68,7 @@ private final class PeerInfoScreenCommentItemNode: PeerInfoScreenItemNode {
|
||||
return 10.0
|
||||
}
|
||||
|
||||
let themeUpdated = self.presentationData?.theme !== presentationData.theme
|
||||
self.item = item
|
||||
self.presentationData = presentationData
|
||||
|
||||
@ -77,10 +80,20 @@ private final class PeerInfoScreenCommentItemNode: PeerInfoScreenItemNode {
|
||||
let textFont = Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize)
|
||||
let textColor = presentationData.theme.list.freeTextColor
|
||||
|
||||
let attributedText = parseMarkdownIntoAttributedString(item.text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: textFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: presentationData.theme.list.itemAccentColor), linkAttribute: { contents in
|
||||
var text = item.text
|
||||
text = text.replacingOccurrences(of: " >]", with: "\u{00A0}>]")
|
||||
let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: textFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: presentationData.theme.list.itemAccentColor), linkAttribute: { contents in
|
||||
return (TelegramTextAttributes.URL, contents)
|
||||
}))
|
||||
|
||||
})).mutableCopy() as! NSMutableAttributedString
|
||||
if let _ = item.text.range(of: ">]"), let range = attributedText.string.range(of: ">") {
|
||||
if themeUpdated || self.chevronImage == nil {
|
||||
self.chevronImage = generateTintedImage(image: UIImage(bundleImageName: "Contact List/SubtitleArrow"), color: presentationData.theme.list.itemAccentColor)
|
||||
}
|
||||
if let chevronImage = self.chevronImage {
|
||||
attributedText.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: attributedText.string))
|
||||
}
|
||||
}
|
||||
|
||||
self.textNode.attributedText = attributedText
|
||||
self.activateArea.accessibilityLabel = attributedText.string
|
||||
|
||||
|
@ -484,6 +484,7 @@ private enum PeerInfoContextSubject {
|
||||
case link(customLink: String?)
|
||||
case businessHours(String)
|
||||
case businessLocation(String)
|
||||
case birthday
|
||||
}
|
||||
|
||||
private enum PeerInfoSettingsSection {
|
||||
@ -586,6 +587,7 @@ private final class PeerInfoInteraction {
|
||||
let openEditing: () -> Void
|
||||
let updateBirthdate: (TelegramBirthday??) -> Void
|
||||
let updateIsEditingBirthdate: (Bool) -> Void
|
||||
let openBioPrivacy: () -> Void
|
||||
let openBirthdatePrivacy: () -> Void
|
||||
let openPremiumGift: () -> Void
|
||||
let editingOpenPersonalChannel: () -> Void
|
||||
@ -646,6 +648,7 @@ private final class PeerInfoInteraction {
|
||||
openEditing: @escaping () -> Void,
|
||||
updateBirthdate: @escaping (TelegramBirthday??) -> Void,
|
||||
updateIsEditingBirthdate: @escaping (Bool) -> Void,
|
||||
openBioPrivacy: @escaping () -> Void,
|
||||
openBirthdatePrivacy: @escaping () -> Void,
|
||||
openPremiumGift: @escaping () -> Void,
|
||||
editingOpenPersonalChannel: @escaping () -> Void
|
||||
@ -705,6 +708,7 @@ private final class PeerInfoInteraction {
|
||||
self.openEditing = openEditing
|
||||
self.updateBirthdate = updateBirthdate
|
||||
self.updateIsEditingBirthdate = updateIsEditingBirthdate
|
||||
self.openBioPrivacy = openBioPrivacy
|
||||
self.openBirthdatePrivacy = openBirthdatePrivacy
|
||||
self.openPremiumGift = openPremiumGift
|
||||
self.editingOpenPersonalChannel = editingOpenPersonalChannel
|
||||
@ -1029,7 +1033,9 @@ private func settingsEditingItems(data: PeerInfoScreenData?, state: PeerInfoStat
|
||||
}, action: {
|
||||
interaction.dismissInput()
|
||||
}, maxLength: Int(data.globalSettings?.userLimits.maxAboutLength ?? 70)))
|
||||
items[.bio]!.append(PeerInfoScreenCommentItem(id: ItemBioHelp, text: presentationData.strings.Settings_About_Help))
|
||||
items[.bio]!.append(PeerInfoScreenCommentItem(id: ItemBioHelp, text: presentationData.strings.Settings_About_PrivacyHelp, linkAction: { _ in
|
||||
interaction.openBioPrivacy()
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@ -1244,7 +1250,11 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
|
||||
if today.day == Int(birthday.day) && today.month == Int(birthday.month) {
|
||||
hasBirthdayToday = true
|
||||
}
|
||||
items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 400, context: context, label: hasBirthdayToday ? presentationData.strings.UserInfo_BirthdayToday : presentationData.strings.UserInfo_Birthday, text: stringForCompactBirthday(birthday, strings: presentationData.strings, showAge: true), textColor: .primary, leftIcon: hasBirthdayToday ? .birthday : nil, icon: hasBirthdayToday ? .premiumGift : nil, action: nil, longTapAction: nil, iconAction: {
|
||||
items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 400, context: context, label: hasBirthdayToday ? presentationData.strings.UserInfo_BirthdayToday : presentationData.strings.UserInfo_Birthday, text: stringForCompactBirthday(birthday, strings: presentationData.strings, showAge: true), textColor: .primary, leftIcon: hasBirthdayToday ? .birthday : nil, icon: hasBirthdayToday ? .premiumGift : nil, action: hasBirthdayToday ? { _, _ in
|
||||
interaction.openPremiumGift()
|
||||
} : nil, longTapAction: { sourceNode in
|
||||
interaction.openPeerInfoContextMenu(.birthday, sourceNode, nil)
|
||||
}, iconAction: {
|
||||
interaction.openPremiumGift()
|
||||
}, contextAction: nil, requestLayout: {
|
||||
}))
|
||||
@ -2677,8 +2687,18 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
},
|
||||
updateIsEditingBirthdate: { [weak self] value in
|
||||
if let self {
|
||||
if value, let data = self.data?.cachedData as? CachedUserData, data.birthday == nil {
|
||||
self.state = self.state.withUpdatingBirthDate(TelegramBirthday(day: 1, month: 1, year: nil))
|
||||
if value {
|
||||
if let data = self.data?.cachedData as? CachedUserData {
|
||||
if data.birthday == nil {
|
||||
self.state = self.state.withUpdatingBirthDate(TelegramBirthday(day: 1, month: 1, year: nil))
|
||||
} else {
|
||||
self.state = self.state.withUpdatingBirthDate(nil)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if self.state.updatingBirthDate != .some(nil) {
|
||||
self.state = self.state.withUpdatingBirthDate(nil)
|
||||
}
|
||||
}
|
||||
self.state = self.state.withIsEditingBirthDate(value)
|
||||
|
||||
@ -2687,6 +2707,11 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
}
|
||||
}
|
||||
},
|
||||
openBioPrivacy: { [weak self] in
|
||||
if let self {
|
||||
self.openBioPrivacy()
|
||||
}
|
||||
},
|
||||
openBirthdatePrivacy: { [weak self] in
|
||||
if let self {
|
||||
self.openBirthdatePrivacy()
|
||||
@ -7581,6 +7606,15 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
})
|
||||
}
|
||||
|
||||
private func openBioPrivacy() {
|
||||
guard let _ = self.data?.globalSettings?.privacySettings else {
|
||||
return
|
||||
}
|
||||
self.context.sharedContext.makeBioPrivacyController(context: self.context, settings: self.privacySettings, present: { [weak self] c in
|
||||
self?.controller?.push(c)
|
||||
})
|
||||
}
|
||||
|
||||
private func openBirthdatePrivacy() {
|
||||
guard let _ = self.data?.globalSettings?.privacySettings else {
|
||||
return
|
||||
@ -7996,6 +8030,29 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
}
|
||||
let context = self.context
|
||||
switch subject {
|
||||
case .birthday:
|
||||
if let cachedData = data.cachedData as? CachedUserData, let birthday = cachedData.birthday {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let text = stringForCompactBirthday(birthday, strings: presentationData.strings)
|
||||
|
||||
let actions: [ContextMenuAction] = [ContextMenuAction(content: .text(title: presentationData.strings.Conversation_ContextMenuCopy, accessibilityLabel: presentationData.strings.Conversation_ContextMenuCopy), action: { [weak self] in
|
||||
UIPasteboard.general.string = text
|
||||
|
||||
self?.controller?.present(UndoOverlayController(presentationData: presentationData, content: .copy(text: presentationData.strings.Conversation_TextCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current)
|
||||
})]
|
||||
let contextMenuController = makeContextMenuController(actions: actions)
|
||||
controller.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self, weak sourceNode] in
|
||||
if let controller = self?.controller, let sourceNode = sourceNode {
|
||||
var rect = sourceNode.bounds.insetBy(dx: 0.0, dy: 2.0)
|
||||
if let sourceRect = sourceRect {
|
||||
rect = sourceRect.insetBy(dx: 0.0, dy: 2.0)
|
||||
}
|
||||
return (sourceNode, rect, controller.displayNode, controller.view.bounds)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}))
|
||||
}
|
||||
case .bio:
|
||||
var text: String?
|
||||
if let cachedData = data.cachedData as? CachedUserData {
|
||||
|
12
submodules/TelegramUI/Images.xcassets/Premium/Business/Links.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Premium/Business/Links.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "linktochat_30.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
158
submodules/TelegramUI/Images.xcassets/Premium/Business/Links.imageset/linktochat_30.pdf
vendored
Normal file
158
submodules/TelegramUI/Images.xcassets/Premium/Business/Links.imageset/linktochat_30.pdf
vendored
Normal file
@ -0,0 +1,158 @@
|
||||
%PDF-1.7
|
||||
|
||||
1 0 obj
|
||||
<< >>
|
||||
endobj
|
||||
|
||||
2 0 obj
|
||||
<< /Length 3 0 R >>
|
||||
stream
|
||||
/DeviceRGB CS
|
||||
/DeviceRGB cs
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 4.334961 4.285645 cm
|
||||
0.000000 0.000000 0.000000 scn
|
||||
10.665000 20.049395 m
|
||||
5.448359 20.049395 1.330000 16.218761 1.330000 11.623485 c
|
||||
1.330000 8.994537 2.547439 6.838058 4.671453 5.283018 c
|
||||
4.906846 5.110682 5.034761 4.842655 5.104862 4.646481 c
|
||||
5.185174 4.421730 5.236406 4.157593 5.250420 3.876801 c
|
||||
5.278440 3.315346 5.159962 2.626087 4.745953 1.991434 c
|
||||
4.593297 1.757420 4.430534 1.547665 4.277983 1.366190 c
|
||||
4.730005 1.417858 5.215943 1.538151 5.606034 1.756918 c
|
||||
6.305136 2.148983 6.750002 2.537632 7.070339 2.831562 c
|
||||
7.096000 2.855108 7.122358 2.879496 7.149155 2.904289 c
|
||||
7.275768 3.021437 7.412188 3.147655 7.531287 3.237131 c
|
||||
7.653394 3.328869 7.961880 3.547728 8.359556 3.456514 c
|
||||
9.096143 3.287567 9.868434 3.197573 10.665000 3.197573 c
|
||||
15.881641 3.197573 20.000000 7.028207 20.000000 11.623485 c
|
||||
20.000000 11.990754 20.297731 12.288485 20.665001 12.288485 c
|
||||
21.032270 12.288485 21.330002 11.990754 21.330002 11.623485 c
|
||||
21.330002 6.177219 16.494055 1.867573 10.665000 1.867573 c
|
||||
9.838774 1.867573 9.033659 1.953581 8.260066 2.116642 c
|
||||
8.213211 2.076147 8.155871 2.023260 8.077589 1.951059 c
|
||||
8.045362 1.921337 8.009587 1.888342 7.969534 1.851589 c
|
||||
7.613387 1.524799 7.082547 1.060091 6.256593 0.596888 c
|
||||
5.612106 0.235451 4.874246 0.083754 4.288342 0.030340 c
|
||||
3.990697 0.003206 3.717034 0.000048 3.493284 0.012119 c
|
||||
3.381668 0.018141 3.276548 0.028267 3.183294 0.042713 c
|
||||
3.102665 0.055204 2.987490 0.077129 2.879746 0.122322 c
|
||||
2.757599 0.173557 2.561313 0.283442 2.445159 0.514097 c
|
||||
2.320866 0.760912 2.361047 0.997538 2.411787 1.141516 c
|
||||
2.459289 1.276306 2.533860 1.385971 2.586927 1.457314 c
|
||||
2.644485 1.534695 2.710716 1.611164 2.772628 1.680155 c
|
||||
2.823253 1.736568 2.875850 1.793650 2.929860 1.852264 c
|
||||
3.152941 2.094366 3.400128 2.362629 3.632015 2.718100 c
|
||||
3.866793 3.078001 3.938666 3.478035 3.922073 3.810507 c
|
||||
3.913780 3.976679 3.883965 4.110666 3.852423 4.198935 c
|
||||
3.844490 4.221134 3.837532 4.237669 3.832081 4.249437 c
|
||||
1.436229 6.025094 0.000000 8.548334 0.000000 11.623485 c
|
||||
0.000000 17.069750 4.835946 21.379395 10.665000 21.379395 c
|
||||
11.032269 21.379395 11.330000 21.081663 11.330000 20.714394 c
|
||||
11.330000 20.347126 11.032269 20.049395 10.665000 20.049395 c
|
||||
h
|
||||
3.819499 4.273321 m
|
||||
3.819511 4.273344 3.819916 4.272768 3.820682 4.271513 c
|
||||
3.819868 4.272675 3.819487 4.273298 3.819499 4.273321 c
|
||||
h
|
||||
8.362708 2.195467 m
|
||||
8.362536 2.194937 8.357435 2.191450 8.347900 2.186579 c
|
||||
8.358110 2.193563 8.362881 2.195997 8.362708 2.195467 c
|
||||
h
|
||||
3.641995 0.646202 m
|
||||
3.641986 0.646227 3.642429 0.646975 3.643422 0.648491 c
|
||||
3.642514 0.646952 3.642005 0.646177 3.641995 0.646202 c
|
||||
h
|
||||
f*
|
||||
n
|
||||
Q
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 12.278564 11.809570 cm
|
||||
0.000000 0.000000 0.000000 scn
|
||||
8.503049 13.383927 m
|
||||
9.624062 14.504941 11.441583 14.504941 12.562596 13.383927 c
|
||||
13.683610 12.262915 13.683610 10.445393 12.562596 9.324379 c
|
||||
10.895930 7.657713 l
|
||||
9.774917 6.536699 7.957396 6.536699 6.836382 7.657713 c
|
||||
6.687177 7.806918 6.558270 7.967865 6.449299 8.137315 c
|
||||
6.250646 8.446222 5.839186 8.535600 5.530279 8.336946 c
|
||||
5.221373 8.138292 5.131994 7.726833 5.330647 7.417926 c
|
||||
5.490635 7.169146 5.679183 6.934008 5.895930 6.717260 c
|
||||
7.536341 5.076850 10.195971 5.076850 11.836382 6.717260 c
|
||||
13.503049 8.383927 l
|
||||
15.143458 10.024338 15.143458 12.683969 13.503049 14.324379 c
|
||||
11.862638 15.964790 9.203007 15.964790 7.562596 14.324379 c
|
||||
5.895930 12.657713 l
|
||||
5.636231 12.398014 5.636231 11.976959 5.895930 11.717260 c
|
||||
6.155629 11.457562 6.576684 11.457562 6.836382 11.717260 c
|
||||
8.503049 13.383927 l
|
||||
h
|
||||
6.230308 2.991029 m
|
||||
5.109295 1.870015 3.291773 1.870015 2.170760 2.991029 c
|
||||
1.049747 4.112042 1.049747 5.929564 2.170760 7.050577 c
|
||||
3.837426 8.717245 l
|
||||
4.958440 9.838259 6.775961 9.838259 7.896974 8.717245 c
|
||||
8.046180 8.568039 8.175087 8.407093 8.284058 8.237642 c
|
||||
8.482711 7.928736 8.894170 7.839358 9.203077 8.038012 c
|
||||
9.511984 8.236666 9.601362 8.648125 9.402708 8.957031 c
|
||||
9.242722 9.205812 9.054173 9.440950 8.837426 9.657698 c
|
||||
7.197016 11.298107 4.537385 11.298107 2.896974 9.657698 c
|
||||
1.230308 7.991030 l
|
||||
-0.410103 6.350618 -0.410103 3.690988 1.230308 2.050577 c
|
||||
2.870718 0.410166 5.530349 0.410166 7.170760 2.050577 c
|
||||
8.837426 3.717243 l
|
||||
9.097125 3.976942 9.097125 4.397997 8.837426 4.657696 c
|
||||
8.577727 4.917395 8.156672 4.917395 7.896974 4.657696 c
|
||||
6.230308 2.991029 l
|
||||
h
|
||||
f*
|
||||
n
|
||||
Q
|
||||
|
||||
endstream
|
||||
endobj
|
||||
|
||||
3 0 obj
|
||||
4349
|
||||
endobj
|
||||
|
||||
4 0 obj
|
||||
<< /Annots []
|
||||
/Type /Page
|
||||
/MediaBox [ 0.000000 0.000000 30.000000 30.000000 ]
|
||||
/Resources 1 0 R
|
||||
/Contents 2 0 R
|
||||
/Parent 5 0 R
|
||||
>>
|
||||
endobj
|
||||
|
||||
5 0 obj
|
||||
<< /Kids [ 4 0 R ]
|
||||
/Count 1
|
||||
/Type /Pages
|
||||
>>
|
||||
endobj
|
||||
|
||||
6 0 obj
|
||||
<< /Pages 5 0 R
|
||||
/Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
|
||||
xref
|
||||
0 7
|
||||
0000000000 65535 f
|
||||
0000000010 00000 n
|
||||
0000000034 00000 n
|
||||
0000004439 00000 n
|
||||
0000004462 00000 n
|
||||
0000004635 00000 n
|
||||
0000004709 00000 n
|
||||
trailer
|
||||
<< /ID [ (some) (id) ]
|
||||
/Root 6 0 R
|
||||
/Size 7
|
||||
>>
|
||||
startxref
|
||||
4768
|
||||
%%EOF
|
@ -36,7 +36,9 @@ import ChatControllerInteraction
|
||||
import ChatMessageItemCommon
|
||||
import ChatMessageItemView
|
||||
import ChatMessageBubbleItemNode
|
||||
|
||||
import AdsInfoScreen
|
||||
import AdsReportScreen
|
||||
|
||||
private struct MessageContextMenuData {
|
||||
let starStatus: Bool?
|
||||
let canReply: Bool
|
||||
@ -475,61 +477,27 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
||||
|
||||
var actions: [ContextMenuItem] = []
|
||||
|
||||
if adAttribute.sponsorInfo != nil || adAttribute.additionalInfo != nil {
|
||||
actions.append(.action(ContextMenuActionItem(text: presentationData.strings.Chat_ContextMenu_AdSponsorInfo, textColor: .primary, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Channels"), color: theme.actionSheet.primaryTextColor)
|
||||
}, iconSource: nil, action: { c, _ in
|
||||
var subItems: [ContextMenuItem] = []
|
||||
|
||||
subItems.append(.action(ContextMenuActionItem(text: presentationData.strings.Common_Back, textColor: .primary, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor)
|
||||
}, iconSource: nil, iconPosition: .left, action: { c, _ in
|
||||
c.popItems()
|
||||
})))
|
||||
|
||||
subItems.append(.separator)
|
||||
|
||||
if let sponsorInfo = adAttribute.sponsorInfo {
|
||||
subItems.append(.action(ContextMenuActionItem(text: sponsorInfo, textColor: .primary, textLayout: .multiline, textFont: .custom(font: Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 0.8)), height: nil, verticalOffset: nil), badge: nil, icon: { theme in
|
||||
return nil
|
||||
}, iconSource: nil, action: { [weak controllerInteraction] c, _ in
|
||||
c.dismiss(completion: {
|
||||
UIPasteboard.general.string = sponsorInfo
|
||||
|
||||
let content: UndoOverlayContent = .copy(text: presentationData.strings.Chat_ContextMenu_AdSponsorInfoCopied)
|
||||
controllerInteraction?.displayUndo(content)
|
||||
})
|
||||
})))
|
||||
}
|
||||
if let additionalInfo = adAttribute.additionalInfo {
|
||||
subItems.append(.action(ContextMenuActionItem(text: additionalInfo, textColor: .primary, textLayout: .multiline, textFont: .custom(font: Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 0.8)), height: nil, verticalOffset: nil), badge: nil, icon: { theme in
|
||||
return nil
|
||||
}, iconSource: nil, action: { [weak controllerInteraction] c, _ in
|
||||
c.dismiss(completion: {
|
||||
UIPasteboard.general.string = additionalInfo
|
||||
|
||||
let content: UndoOverlayContent = .copy(text: presentationData.strings.Chat_ContextMenu_AdSponsorInfoCopied)
|
||||
controllerInteraction?.displayUndo(content)
|
||||
})
|
||||
})))
|
||||
}
|
||||
|
||||
c.pushItems(items: .single(ContextController.Items(content: .list(subItems))))
|
||||
if "".isEmpty {
|
||||
//TODO:localize
|
||||
|
||||
actions.append(.action(ContextMenuActionItem(text: "About This Ad", textColor: .primary, textLayout: .twoLinesMax, textFont: .custom(font: Font.regular(presentationData.listsFontSize.baseDisplaySize - 1.0), height: nil, verticalOffset: nil), badge: nil, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Info"), color: theme.actionSheet.primaryTextColor)
|
||||
}, iconSource: nil, action: { _, f in
|
||||
f(.dismissWithoutContent)
|
||||
controllerInteraction.navigationController()?.pushViewController(AdsInfoScreen(context: context))
|
||||
})))
|
||||
actions.append(.separator)
|
||||
}
|
||||
|
||||
actions.append(.action(ContextMenuActionItem(text: presentationData.strings.SponsoredMessageMenu_Info, textColor: .primary, textLayout: .twoLinesMax, textFont: .custom(font: Font.regular(presentationData.listsFontSize.baseDisplaySize - 1.0), height: nil, verticalOffset: nil), badge: nil, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Info"), color: theme.actionSheet.primaryTextColor)
|
||||
}, iconSource: nil, action: { _, f in
|
||||
f(.dismissWithoutContent)
|
||||
controllerInteraction.navigationController()?.pushViewController(AdInfoScreen(context: context))
|
||||
})))
|
||||
|
||||
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
|
||||
if !chatPresentationInterfaceState.isPremium && !premiumConfiguration.isPremiumDisabled {
|
||||
actions.append(.action(ContextMenuActionItem(text: presentationData.strings.SponsoredMessageMenu_Hide, textColor: .primary, textLayout: .twoLinesMax, textFont: .custom(font: Font.regular(presentationData.listsFontSize.baseDisplaySize - 1.0), height: nil, verticalOffset: nil), badge: nil, icon: { theme in
|
||||
|
||||
actions.append(.action(ContextMenuActionItem(text: "Report Ad", textColor: .primary, textLayout: .twoLinesMax, textFont: .custom(font: Font.regular(presentationData.listsFontSize.baseDisplaySize - 1.0), height: nil, verticalOffset: nil), badge: nil, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Restrict"), color: theme.actionSheet.primaryTextColor)
|
||||
}, iconSource: nil, action: { _, f in
|
||||
f(.dismissWithoutContent)
|
||||
controllerInteraction.navigationController()?.pushViewController(AdsReportScreen(context: context))
|
||||
})))
|
||||
|
||||
actions.append(.separator)
|
||||
|
||||
actions.append(.action(ContextMenuActionItem(text: "Remove Ad", textColor: .primary, textLayout: .twoLinesMax, textFont: .custom(font: Font.regular(presentationData.listsFontSize.baseDisplaySize - 1.0), height: nil, verticalOffset: nil), badge: nil, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.primaryTextColor)
|
||||
}, iconSource: nil, action: { c, _ in
|
||||
c.dismiss(completion: {
|
||||
var replaceImpl: ((ViewController) -> Void)?
|
||||
@ -543,61 +511,131 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
||||
controllerInteraction.navigationController()?.pushViewController(controller)
|
||||
})
|
||||
})))
|
||||
}
|
||||
|
||||
actions.append(.separator)
|
||||
|
||||
if chatPresentationInterfaceState.copyProtectionEnabled {
|
||||
} else {
|
||||
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuCopy, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.actionSheet.primaryTextColor)
|
||||
}, action: { _, f in
|
||||
var messageEntities: [MessageTextEntity]?
|
||||
var restrictedText: String?
|
||||
for attribute in message.attributes {
|
||||
if let attribute = attribute as? TextEntitiesMessageAttribute {
|
||||
messageEntities = attribute.entities
|
||||
if adAttribute.sponsorInfo != nil || adAttribute.additionalInfo != nil {
|
||||
actions.append(.action(ContextMenuActionItem(text: presentationData.strings.Chat_ContextMenu_AdSponsorInfo, textColor: .primary, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Channels"), color: theme.actionSheet.primaryTextColor)
|
||||
}, iconSource: nil, action: { c, _ in
|
||||
var subItems: [ContextMenuItem] = []
|
||||
|
||||
subItems.append(.action(ContextMenuActionItem(text: presentationData.strings.Common_Back, textColor: .primary, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor)
|
||||
}, iconSource: nil, iconPosition: .left, action: { c, _ in
|
||||
c.popItems()
|
||||
})))
|
||||
|
||||
subItems.append(.separator)
|
||||
|
||||
if let sponsorInfo = adAttribute.sponsorInfo {
|
||||
subItems.append(.action(ContextMenuActionItem(text: sponsorInfo, textColor: .primary, textLayout: .multiline, textFont: .custom(font: Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 0.8)), height: nil, verticalOffset: nil), badge: nil, icon: { theme in
|
||||
return nil
|
||||
}, iconSource: nil, action: { [weak controllerInteraction] c, _ in
|
||||
c.dismiss(completion: {
|
||||
UIPasteboard.general.string = sponsorInfo
|
||||
|
||||
let content: UndoOverlayContent = .copy(text: presentationData.strings.Chat_ContextMenu_AdSponsorInfoCopied)
|
||||
controllerInteraction?.displayUndo(content)
|
||||
})
|
||||
})))
|
||||
}
|
||||
if let attribute = attribute as? RestrictedContentMessageAttribute {
|
||||
restrictedText = attribute.platformText(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) ?? ""
|
||||
if let additionalInfo = adAttribute.additionalInfo {
|
||||
subItems.append(.action(ContextMenuActionItem(text: additionalInfo, textColor: .primary, textLayout: .multiline, textFont: .custom(font: Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 0.8)), height: nil, verticalOffset: nil), badge: nil, icon: { theme in
|
||||
return nil
|
||||
}, iconSource: nil, action: { [weak controllerInteraction] c, _ in
|
||||
c.dismiss(completion: {
|
||||
UIPasteboard.general.string = additionalInfo
|
||||
|
||||
let content: UndoOverlayContent = .copy(text: presentationData.strings.Chat_ContextMenu_AdSponsorInfoCopied)
|
||||
controllerInteraction?.displayUndo(content)
|
||||
})
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
if let restrictedText = restrictedText {
|
||||
storeMessageTextInPasteboard(restrictedText, entities: nil)
|
||||
} else {
|
||||
if let translationState = chatPresentationInterfaceState.translationState, translationState.isEnabled,
|
||||
let translation = message.attributes.first(where: { ($0 as? TranslationMessageAttribute)?.toLang == translationState.toLang }) as? TranslationMessageAttribute, !translation.text.isEmpty {
|
||||
storeMessageTextInPasteboard(translation.text, entities: translation.entities)
|
||||
|
||||
c.pushItems(items: .single(ContextController.Items(content: .list(subItems))))
|
||||
})))
|
||||
actions.append(.separator)
|
||||
}
|
||||
|
||||
actions.append(.action(ContextMenuActionItem(text: presentationData.strings.SponsoredMessageMenu_Info, textColor: .primary, textLayout: .twoLinesMax, textFont: .custom(font: Font.regular(presentationData.listsFontSize.baseDisplaySize - 1.0), height: nil, verticalOffset: nil), badge: nil, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Info"), color: theme.actionSheet.primaryTextColor)
|
||||
}, iconSource: nil, action: { _, f in
|
||||
f(.dismissWithoutContent)
|
||||
controllerInteraction.navigationController()?.pushViewController(AdInfoScreen(context: context))
|
||||
})))
|
||||
|
||||
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
|
||||
if !chatPresentationInterfaceState.isPremium && !premiumConfiguration.isPremiumDisabled {
|
||||
actions.append(.action(ContextMenuActionItem(text: presentationData.strings.SponsoredMessageMenu_Hide, textColor: .primary, textLayout: .twoLinesMax, textFont: .custom(font: Font.regular(presentationData.listsFontSize.baseDisplaySize - 1.0), height: nil, verticalOffset: nil), badge: nil, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.primaryTextColor)
|
||||
}, iconSource: nil, action: { c, _ in
|
||||
c.dismiss(completion: {
|
||||
var replaceImpl: ((ViewController) -> Void)?
|
||||
let controller = PremiumDemoScreen(context: context, subject: .noAds, action: {
|
||||
let controller = PremiumIntroScreen(context: context, source: .ads)
|
||||
replaceImpl?(controller)
|
||||
})
|
||||
replaceImpl = { [weak controller] c in
|
||||
controller?.replace(with: c)
|
||||
}
|
||||
controllerInteraction.navigationController()?.pushViewController(controller)
|
||||
})
|
||||
})))
|
||||
}
|
||||
|
||||
actions.append(.separator)
|
||||
|
||||
if chatPresentationInterfaceState.copyProtectionEnabled {
|
||||
} else {
|
||||
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuCopy, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.actionSheet.primaryTextColor)
|
||||
}, action: { _, f in
|
||||
var messageEntities: [MessageTextEntity]?
|
||||
var restrictedText: String?
|
||||
for attribute in message.attributes {
|
||||
if let attribute = attribute as? TextEntitiesMessageAttribute {
|
||||
messageEntities = attribute.entities
|
||||
}
|
||||
if let attribute = attribute as? RestrictedContentMessageAttribute {
|
||||
restrictedText = attribute.platformText(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) ?? ""
|
||||
}
|
||||
}
|
||||
|
||||
if let restrictedText = restrictedText {
|
||||
storeMessageTextInPasteboard(restrictedText, entities: nil)
|
||||
} else {
|
||||
storeMessageTextInPasteboard(message.text, entities: messageEntities)
|
||||
if let translationState = chatPresentationInterfaceState.translationState, translationState.isEnabled,
|
||||
let translation = message.attributes.first(where: { ($0 as? TranslationMessageAttribute)?.toLang == translationState.toLang }) as? TranslationMessageAttribute, !translation.text.isEmpty {
|
||||
storeMessageTextInPasteboard(translation.text, entities: translation.entities)
|
||||
} else {
|
||||
storeMessageTextInPasteboard(message.text, entities: messageEntities)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Queue.mainQueue().after(0.2, {
|
||||
let content: UndoOverlayContent = .copy(text: chatPresentationInterfaceState.strings.Conversation_MessageCopied)
|
||||
controllerInteraction.displayUndo(content)
|
||||
})
|
||||
|
||||
f(.default)
|
||||
})))
|
||||
}
|
||||
|
||||
if let author = message.author, let addressName = author.addressName {
|
||||
let link = "https://t.me/\(addressName)"
|
||||
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuCopyLink, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.actionSheet.primaryTextColor)
|
||||
}, action: { _, f in
|
||||
UIPasteboard.general.string = link
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
Queue.mainQueue().after(0.2, {
|
||||
controllerInteraction.displayUndo(.linkCopied(text: presentationData.strings.Conversation_LinkCopied))
|
||||
})
|
||||
|
||||
f(.default)
|
||||
})))
|
||||
|
||||
Queue.mainQueue().after(0.2, {
|
||||
let content: UndoOverlayContent = .copy(text: chatPresentationInterfaceState.strings.Conversation_MessageCopied)
|
||||
controllerInteraction.displayUndo(content)
|
||||
})
|
||||
|
||||
f(.default)
|
||||
})))
|
||||
}
|
||||
|
||||
if let author = message.author, let addressName = author.addressName {
|
||||
let link = "https://t.me/\(addressName)"
|
||||
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuCopyLink, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.actionSheet.primaryTextColor)
|
||||
}, action: { _, f in
|
||||
UIPasteboard.general.string = link
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
Queue.mainQueue().after(0.2, {
|
||||
controllerInteraction.displayUndo(.linkCopied(text: presentationData.strings.Conversation_LinkCopied))
|
||||
})
|
||||
|
||||
f(.default)
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
return .single(ContextController.Items(content: .list(actions)))
|
||||
|
@ -1845,6 +1845,10 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
public func makePrivacyAndSecurityController(context: AccountContext) -> ViewController {
|
||||
return SettingsUI.makePrivacyAndSecurityController(context: context)
|
||||
}
|
||||
|
||||
public func makeBioPrivacyController(context: AccountContext, settings: Promise<AccountPrivacySettings?>, present: @escaping (ViewController) -> Void) {
|
||||
SettingsUI.makeBioPrivacyController(context: context, settings: settings, present: present)
|
||||
}
|
||||
|
||||
public func makeBirthdayPrivacyController(context: AccountContext, settings: Promise<AccountPrivacySettings?>, openedFromBirthdayScreen: Bool, present: @escaping (ViewController) -> Void) {
|
||||
SettingsUI.makeBirthdayPrivacyController(context: context, settings: settings, openedFromBirthdayScreen: openedFromBirthdayScreen, present: present)
|
||||
|
Loading…
x
Reference in New Issue
Block a user