[WIP] Stickers

This commit is contained in:
Ilya Laktyushin 2024-03-16 15:36:51 +04:00
parent db1d4422cb
commit bb1b425217
32 changed files with 1272 additions and 282 deletions

View File

@ -997,9 +997,9 @@ public protocol SharedAccountContext: AnyObject {
func makeMediaPickerScreen(context: AccountContext, hasSearch: Bool, completion: @escaping (Any) -> Void) -> ViewController func makeMediaPickerScreen(context: AccountContext, hasSearch: Bool, completion: @escaping (Any) -> Void) -> ViewController
func makeStickerEditorScreen(context: AccountContext, source: Any, transitionArguments: (UIView, CGRect, UIImage?)?, completion: @escaping (TelegramMediaFile, @escaping () -> Void) -> Void) -> ViewController func makeStickerEditorScreen(context: AccountContext, source: Any?, transitionArguments: (UIView, CGRect, UIImage?)?, completion: @escaping (TelegramMediaFile, @escaping () -> Void) -> Void) -> ViewController
func makeStickerMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> ViewController func makeStickerMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any?, UIView?, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> ViewController
func makeStoryMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void, groupsPresented: @escaping () -> Void) -> ViewController func makeStoryMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void, groupsPresented: @escaping () -> Void) -> ViewController
func makeStickerPickerScreen(context: AccountContext, inputData: Promise<StickerPickerInput>, completion: @escaping (TelegramMediaFile) -> Void) -> ViewController func makeStickerPickerScreen(context: AccountContext, inputData: Promise<StickerPickerInput>, completion: @escaping (TelegramMediaFile) -> Void) -> ViewController

View File

@ -1919,7 +1919,15 @@ public final class ChatListNode: ListView {
} }
} }
if suggestions.contains(.setupBirthday) { if suggestions.contains(.setupBirthday) {
return .single(.setupBirthday) return context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|> map { peer in
if let peer {
return .birthdayPremiumGift(peers: [peer])
} else {
return .setupBirthday
}
}
//return .single(.setupBirthday)
} else if suggestions.contains(.xmasPremiumGift) { } else if suggestions.contains(.xmasPremiumGift) {
return .single(.xmasPremiumGift) return .single(.xmasPremiumGift)
} else if suggestions.contains(.annualPremium) || suggestions.contains(.upgradePremium) || suggestions.contains(.restorePremium), let inAppPurchaseManager = context.inAppPurchaseManager { } else if suggestions.contains(.annualPremium) || suggestions.contains(.upgradePremium) || suggestions.contains(.restorePremium), let inAppPurchaseManager = context.inAppPurchaseManager {

View File

@ -3,13 +3,14 @@ import UIKit
import AsyncDisplayKit import AsyncDisplayKit
import Display import Display
import SwiftSignalKit import SwiftSignalKit
import TelegramCore
import TelegramPresentationData import TelegramPresentationData
import ListSectionHeaderNode import ListSectionHeaderNode
import AppBundle import AppBundle
import ItemListUI import ItemListUI
import Markdown import Markdown
import AccountContext import AccountContext
import TelegramCore import MergedAvatarsNode
class ChatListStorageInfoItem: ListViewItem { class ChatListStorageInfoItem: ListViewItem {
enum Action { enum Action {
@ -90,6 +91,8 @@ class ChatListStorageInfoItemNode: ItemListRevealOptionsItemNode {
private let arrowNode: ASImageNode private let arrowNode: ASImageNode
private let separatorNode: ASDisplayNode private let separatorNode: ASDisplayNode
private var avatarsNode: MergedAvatarsNode?
private var closeButton: HighlightableButtonNode? private var closeButton: HighlightableButtonNode?
private var okButtonText: TextNode? private var okButtonText: TextNode?
@ -168,6 +171,7 @@ class ChatListStorageInfoItemNode: ItemListRevealOptionsItemNode {
let titleString: NSAttributedString let titleString: NSAttributedString
let textString: NSAttributedString let textString: NSAttributedString
var avatarPeers: [EnginePeer] = []
var okButtonLayout: (TextNodeLayout, () -> TextNode)? var okButtonLayout: (TextNodeLayout, () -> TextNode)?
var cancelButtonLayout: (TextNodeLayout, () -> TextNode)? var cancelButtonLayout: (TextNodeLayout, () -> TextNode)?
@ -229,14 +233,15 @@ class ChatListStorageInfoItemNode: ItemListRevealOptionsItemNode {
let title: String let title: String
let text: String let text: String
if peers.count == 1, let peer = peers.first { if peers.count == 1, let peer = peers.first {
title = "It's \(peer.compactDisplayTitle)'s [birthday]() today! 🎂" title = "It's \(peer.compactDisplayTitle)'s **birthday** today! 🎂"
text = "Gift them Telegram Premium." text = "Gift them Telegram Premium."
} else { } else {
title = "\(peers.count) contacts have [birthdays]() today! 🎂" title = "\(peers.count) contacts have **birthdays** today! 🎂"
text = "Gift them Telegram Premium." text = "Gift them Telegram Premium."
} }
titleString = parseMarkdownIntoAttributedString(title, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor), bold: MarkdownAttributeSet(font: titleFont, textColor: item.theme.rootController.navigationBar.accentTextColor), link: MarkdownAttributeSet(font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor), linkAttribute: { _ in return nil })) titleString = parseMarkdownIntoAttributedString(title, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor), bold: MarkdownAttributeSet(font: titleFont, textColor: item.theme.rootController.navigationBar.accentTextColor), link: MarkdownAttributeSet(font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor), linkAttribute: { _ in return nil }))
textString = NSAttributedString(string: text, font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor) textString = NSAttributedString(string: text, font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor)
avatarPeers = Array(peers.prefix(3))
case let .reviewLogin(newSessionReview, totalCount): case let .reviewLogin(newSessionReview, totalCount):
spacing = 2.0 spacing = 2.0
alignment = .center alignment = .center
@ -254,9 +259,15 @@ class ChatListStorageInfoItemNode: ItemListRevealOptionsItemNode {
cancelButtonLayout = makeCancelButtonTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.ChatList_SessionReview_PanelReject, font: titleFont, textColor: item.theme.list.itemDestructiveColor), maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - sideInset - rightInset, height: 100.0))) cancelButtonLayout = makeCancelButtonTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.ChatList_SessionReview_PanelReject, font: titleFont, textColor: item.theme.list.itemDestructiveColor), maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - sideInset - rightInset, height: 100.0)))
} }
let titleLayout = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleString, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - sideInset - rightInset, height: 100.0), alignment: alignment, lineSpacing: 0.18)) var leftInset: CGFloat = sideInset
if !avatarPeers.isEmpty {
let avatarsWidth = 30.0 + CGFloat(avatarPeers.count - 1) * 16.0
leftInset += avatarsWidth + 4.0
}
let textLayout = makeTextLayout(TextNodeLayoutArguments(attributedString: textString, maximumNumberOfLines: 10, truncationType: .end, constrainedSize: CGSize(width: params.width - sideInset - rightInset, height: 100.0), alignment: alignment, lineSpacing: 0.18)) let titleLayout = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleString, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: 100.0), alignment: alignment, lineSpacing: 0.18))
let textLayout = makeTextLayout(TextNodeLayoutArguments(attributedString: textString, maximumNumberOfLines: 10, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: 100.0), alignment: alignment, lineSpacing: 0.18))
var contentSize = CGSize(width: params.width, height: verticalInset * 2.0 + titleLayout.0.size.height + textLayout.0.size.height) var contentSize = CGSize(width: params.width, height: verticalInset * 2.0 + titleLayout.0.size.height + textLayout.0.size.height)
if let okButtonLayout { if let okButtonLayout {
@ -279,7 +290,7 @@ class ChatListStorageInfoItemNode: ItemListRevealOptionsItemNode {
if case .center = alignment { if case .center = alignment {
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: floor((params.width - titleLayout.0.size.width) * 0.5), y: verticalInset), size: titleLayout.0.size) strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: floor((params.width - titleLayout.0.size.width) * 0.5), y: verticalInset), size: titleLayout.0.size)
} else { } else {
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: sideInset, y: verticalInset), size: titleLayout.0.size) strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: titleLayout.0.size)
} }
let _ = textLayout.1() let _ = textLayout.1()
@ -287,7 +298,26 @@ class ChatListStorageInfoItemNode: ItemListRevealOptionsItemNode {
if case .center = alignment { if case .center = alignment {
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: floor((params.width - textLayout.0.size.width) * 0.5), y: strongSelf.titleNode.frame.maxY + spacing), size: textLayout.0.size) strongSelf.textNode.frame = CGRect(origin: CGPoint(x: floor((params.width - textLayout.0.size.width) * 0.5), y: strongSelf.titleNode.frame.maxY + spacing), size: textLayout.0.size)
} else { } else {
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: sideInset, y: strongSelf.titleNode.frame.maxY + spacing), size: textLayout.0.size) strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: strongSelf.titleNode.frame.maxY + spacing), size: textLayout.0.size)
}
if !avatarPeers.isEmpty {
let avatarsNode: MergedAvatarsNode
if let current = strongSelf.avatarsNode {
avatarsNode = current
} else {
avatarsNode = MergedAvatarsNode()
strongSelf.addSubnode(avatarsNode)
strongSelf.avatarsNode = avatarsNode
}
let avatarSize = CGSize(width: 30.0, height: 30.0)
avatarsNode.update(context: item.context, peers: avatarPeers.map { $0._asPeer() }, synchronousLoad: false, imageSize: avatarSize.width, imageSpacing: 16.0, borderWidth: 2.0 - UIScreenPixel, avatarFontSize: 10.0)
let avatarsSize = CGSize(width: avatarSize.width + 16.0 * CGFloat(avatarPeers.count - 1), height: avatarSize.height)
avatarsNode.updateLayout(size: avatarsSize)
avatarsNode.frame = CGRect(origin: CGPoint(x: sideInset - 6.0, y: floor((layout.size.height - avatarsSize.height) / 2.0)), size: avatarsSize)
} else if let avatarsNode = strongSelf.avatarsNode {
avatarsNode.removeFromSupernode()
strongSelf.avatarsNode = nil
} }
if let image = strongSelf.arrowNode.image { if let image = strongSelf.arrowNode.image {

View File

@ -0,0 +1,14 @@
#import <Foundation/Foundation.h>
#import <CoreVideo/CoreVideo.h>
NS_ASSUME_NONNULL_BEGIN
@interface FFMpegVideoWriter : NSObject
- (void)setupWithOutputPath:(NSString *)outputPath width:(int)width height:(int)height;
- (void)encodeFrame:(CVPixelBufferRef)pixelBuffer;
- (void)finalizeVideo;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,147 @@
#import <FFMpegBinding/FFMpegVideoWriter.h>
#include "libavformat/avformat.h"
#include "libavcodec/avcodec.h"
@interface FFMpegVideoWriter ()
@property (nonatomic) AVFormatContext *formatContext;
@property (nonatomic) AVCodecContext *codecContext;
@property (nonatomic) AVStream *stream;
@property (nonatomic) int64_t framePts;
@end
@implementation FFMpegVideoWriter
- (instancetype)init {
self = [super init];
if (self) {
_framePts = 0;
}
return self;
}
- (void)setupWithOutputPath:(NSString *)outputPath width:(int)width height:(int)height {
AVOutputFormat *outFmt = av_guess_format("webm", NULL, NULL);
const AVOutputFormat *oformat;
void *opaque = NULL;
while ((oformat = av_muxer_iterate(&opaque))) {
NSLog(@"%s", oformat->long_name);
}
int error = avformat_alloc_output_context2(&_formatContext, outFmt, NULL, [outputPath UTF8String]);
NSLog(@"%d", error);
if (!_formatContext) return;
AVCodec *codec = avcodec_find_encoder_by_name("libvpx-vp9");
if (!codec) return;
_stream = avformat_new_stream(_formatContext, codec);
if (!_stream) return;
_codecContext = avcodec_alloc_context3(codec);
if (!_codecContext) return;
_codecContext->bit_rate = 400000;
_codecContext->width = width;
_codecContext->height = height;
_codecContext->time_base = (AVRational){1, 30};
_codecContext->gop_size = 10;
_codecContext->max_b_frames = 1;
_codecContext->pix_fmt = AV_PIX_FMT_YUVA420P;
if (_formatContext->oformat->flags & AVFMT_GLOBALHEADER) {
_codecContext->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
}
avcodec_open2(_codecContext, codec, NULL);
av_dump_format(_formatContext, 0, [outputPath UTF8String], 1);
if (!(_formatContext->oformat->flags & AVFMT_NOFILE)) {
avio_open(&_formatContext->pb, [outputPath UTF8String], AVIO_FLAG_WRITE);
}
__unused int result = avformat_write_header(_formatContext, NULL);
}
- (void)encodeFrame:(CVPixelBufferRef)pixelBuffer {
if (!_codecContext || !_stream) return;
AVFrame *frame = av_frame_alloc();
if (!frame) return;
int width = (int)CVPixelBufferGetWidth(pixelBuffer);
int height = (int)CVPixelBufferGetHeight(pixelBuffer);
frame->format = AV_PIX_FMT_YUV420P;
frame->width = width;
frame->height = height;
av_frame_get_buffer(frame, 0);
// CVPixelBufferLockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
// uint8_t *baseAddress = (uint8_t *)CVPixelBufferGetBaseAddress(pixelBuffer);
// AVPixelFormat srcPixFmt = AV_PIX_FMT_BGRA;
// AVPixelFormat dstPixFmt = AV_PIX_FMT_YUV420P;
// struct SwsContext *swsCtx = sws_getContext(width, height, srcPixFmt,
// width, height, dstPixFmt,
// SWS_BICUBIC, NULL, NULL, NULL);
// if (swsCtx) {
// const uint8_t *const srcSlice[] = { baseAddress };
// int srcStride[] = { CVPixelBufferGetBytesPerRow(pixelBuffer) };
// sws_scale(swsCtx, srcSlice, srcStride, 0, height, frame->data, frame->linesize);
// sws_freeContext(swsCtx);
// }
// CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
frame->pts = self.framePts++;
int sendRet = avcodec_send_frame(_codecContext, frame);
if (sendRet < 0) {
// Error sending frame
av_frame_free(&frame);
return;
}
AVPacket pkt;
av_init_packet(&pkt);
pkt.data = NULL;
pkt.size = 0;
while (sendRet >= 0) {
int recvRet = avcodec_receive_packet(_codecContext, &pkt);
if (recvRet == AVERROR(EAGAIN) || recvRet == AVERROR_EOF) {
break;
} else if (recvRet < 0) {
break;
}
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_frame_free(&frame);
}
- (void)finalizeVideo {
av_write_trailer(_formatContext);
if (!(_formatContext->oformat->flags & AVFMT_NOFILE)) {
avio_closep(&_formatContext->pb);
}
avcodec_free_context(&_codecContext);
avformat_free_context(_formatContext);
}
@end

View File

@ -193,6 +193,9 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
public var customSelection: ((MediaPickerScreen, Any) -> Void)? = nil public var customSelection: ((MediaPickerScreen, Any) -> Void)? = nil
public var createFromScratch: () -> Void = {}
public var presentFilePicker: () -> Void = {}
private var completed = false private var completed = false
public var legacyCompletion: (_ signals: [Any], _ silently: Bool, _ scheduleTime: Int32?, @escaping (String) -> UIView?, @escaping () -> Void) -> Void = { _, _, _, _, _ in } public var legacyCompletion: (_ signals: [Any], _ silently: Bool, _ scheduleTime: Int32?, @escaping (String) -> UIView?, @escaping () -> Void) -> Void = { _, _, _, _, _ in }
@ -1673,6 +1676,11 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
} else if collection == nil { } else if collection == nil {
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed))
if [.story, .createSticker].contains(mode) {
self.navigationItem.rightBarButtonItem = UIBarButtonItem(customDisplayNode: self.moreButtonNode)
self.navigationItem.rightBarButtonItem?.action = #selector(self.rightButtonPressed)
self.navigationItem.rightBarButtonItem?.target = self
}
// if mode == .story || mode == .addImage { // if mode == .story || mode == .addImage {
// self.navigationItem.rightBarButtonItem = UIBarButtonItem(customDisplayNode: self.moreButtonNode) // self.navigationItem.rightBarButtonItem = UIBarButtonItem(customDisplayNode: self.moreButtonNode)
// self.navigationItem.rightBarButtonItem?.action = #selector(self.rightButtonPressed) // self.navigationItem.rightBarButtonItem?.action = #selector(self.rightButtonPressed)
@ -1993,7 +2001,9 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
self.selectionCount = count self.selectionCount = count
var moreIsVisible = false var moreIsVisible = false
if case let .media(media) = self.subject { if case let .assets(_, mode) = self.subject, [.story, .createSticker].contains(mode) {
moreIsVisible = true
} else if case let .media(media) = self.subject {
self.titleView.title = media.count == 1 ? self.presentationData.strings.Attachment_Pasteboard : self.presentationData.strings.Attachment_SelectedMedia(count) self.titleView.title = media.count == 1 ? self.presentationData.strings.Attachment_Pasteboard : self.presentationData.strings.Attachment_SelectedMedia(count)
self.titleView.segmentsHidden = true self.titleView.segmentsHidden = true
moreIsVisible = true moreIsVisible = true
@ -2181,6 +2191,33 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
} }
@objc private func searchOrMorePressed(node: ContextReferenceContentNode, gesture: ContextGesture?) { @objc private func searchOrMorePressed(node: ContextReferenceContentNode, gesture: ContextGesture?) {
//TODO:localize
if case let .assets(_, mode) = self.subject, [.story, .addImage, .createSticker].contains(mode) {
var items: [ContextMenuItem] = []
if mode != .addImage {
items.append(.action(ContextMenuActionItem(text: "Create", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Draw"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.default)
self?.createFromScratch()
})))
}
items.append(.action(ContextMenuActionItem(text: "Select from Files", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/File"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.default)
self?.presentFilePicker()
})))
let contextController = ContextController(presentationData: self.presentationData, source: .reference(MediaPickerContextReferenceContentSource(controller: self, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
self.presentInGlobalOverlay(contextController)
return
}
switch self.moreButtonNode.iconNode.iconState { switch self.moreButtonNode.iconNode.iconState {
case .search: case .search:
// self.presentSearch(activateOnDisplay: true) // self.presentSearch(activateOnDisplay: true)
@ -2611,7 +2648,7 @@ public func storyMediaPickerController(
public func stickerMediaPickerController( public func stickerMediaPickerController(
context: AccountContext, context: AccountContext,
getSourceRect: @escaping () -> CGRect, getSourceRect: @escaping () -> CGRect,
completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, completion: @escaping (Any?, UIView?, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void,
dismissed: @escaping () -> Void dismissed: @escaping () -> Void
) -> ViewController { ) -> ViewController {
let presentationData = context.sharedContext.currentPresentationData.with({ $0 }) let presentationData = context.sharedContext.currentPresentationData.with({ $0 })
@ -2621,7 +2658,7 @@ public func stickerMediaPickerController(
}) })
controller.forceSourceRect = true controller.forceSourceRect = true
controller.getSourceRect = getSourceRect controller.getSourceRect = getSourceRect
controller.requestController = { _, present in controller.requestController = { [weak controller] _, present in
let mediaPickerController = MediaPickerScreen(context: context, updatedPresentationData: updatedPresentationData, peer: nil, threadTitle: nil, chatLocation: nil, bannedSendPhotos: nil, bannedSendVideos: nil, subject: .assets(nil, .createSticker), mainButtonState: nil, mainButtonAction: nil) let mediaPickerController = MediaPickerScreen(context: context, updatedPresentationData: updatedPresentationData, peer: nil, threadTitle: nil, chatLocation: nil, bannedSendPhotos: nil, bannedSendVideos: nil, subject: .assets(nil, .createSticker), mainButtonState: nil, mainButtonAction: nil)
mediaPickerController.customSelection = { controller, result in mediaPickerController.customSelection = { controller, result in
if let result = result as? PHAsset { if let result = result as? PHAsset {
@ -2645,6 +2682,14 @@ public func stickerMediaPickerController(
}) })
} }
} }
}
mediaPickerController.createFromScratch = { [weak controller] in
completion(nil, nil, .zero, nil, { _ in return nil }, { [weak controller] in
controller?.dismiss(animated: true)
})
}
mediaPickerController.presentFilePicker = {
} }
present(mediaPickerController, mediaPickerController.mediaPickerContext) present(mediaPickerController, mediaPickerController.mediaPickerContext)
} }

View File

@ -1076,15 +1076,17 @@ private final class StickerPackContainer: ASDisplayNode {
} }
}))) })))
if let (info, _, _) = self.currentStickerPack, info.flags.contains(.isCreator) { if let (info, packItems, _) = self.currentStickerPack, info.flags.contains(.isCreator) {
//TODO:localize //TODO:localize
items.append(.separator) items.append(.separator)
if packItems.count > 0 {
items.append(.action(ContextMenuActionItem(text: "Reorder", icon: { theme in items.append(.action(ContextMenuActionItem(text: "Reorder", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor) return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in }, action: { [weak self] _, f in
f(.default) f(.default)
self?.updateIsEditing(true) self?.updateIsEditing(true)
}))) })))
}
items.append(.action(ContextMenuActionItem(text: "Edit Name", icon: { theme in items.append(.action(ContextMenuActionItem(text: "Edit Name", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor)
@ -1206,7 +1208,7 @@ private final class StickerPackContainer: ASDisplayNode {
let editorController = context.sharedContext.makeStickerEditorScreen( let editorController = context.sharedContext.makeStickerEditorScreen(
context: context, context: context,
source: result, source: result,
transitionArguments: (transitionView, transitionRect, transitionImage), transitionArguments: transitionView.flatMap { ($0, transitionRect, transitionImage) },
completion: { file, commit in completion: { file, commit in
dismissImpl?() dismissImpl?()
let sticker = ImportSticker( let sticker = ImportSticker(

View File

@ -671,7 +671,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-901375139] = { return Api.PeerLocated.parse_peerLocated($0) } dict[-901375139] = { return Api.PeerLocated.parse_peerLocated($0) }
dict[-118740917] = { return Api.PeerLocated.parse_peerSelfLocated($0) } dict[-118740917] = { return Api.PeerLocated.parse_peerSelfLocated($0) }
dict[-1721619444] = { return Api.PeerNotifySettings.parse_peerNotifySettings($0) } dict[-1721619444] = { return Api.PeerNotifySettings.parse_peerNotifySettings($0) }
dict[-1525149427] = { return Api.PeerSettings.parse_peerSettings($0) } dict[-1395233698] = { return Api.PeerSettings.parse_peerSettings($0) }
dict[-1707742823] = { return Api.PeerStories.parse_peerStories($0) } dict[-1707742823] = { return Api.PeerStories.parse_peerStories($0) }
dict[-1770029977] = { return Api.PhoneCall.parse_phoneCall($0) } dict[-1770029977] = { return Api.PhoneCall.parse_phoneCall($0) }
dict[912311057] = { return Api.PhoneCall.parse_phoneCallAccepted($0) } dict[912311057] = { return Api.PhoneCall.parse_phoneCallAccepted($0) }
@ -1321,7 +1321,7 @@ public extension Api {
return parser(reader) return parser(reader)
} }
else { else {
telegramApiLog("Type constructor \(String(UInt32(bitPattern: signature), radix: 16, uppercase: false)) not found") telegramApiLog("Type constructor \(String(signature, radix: 16, uppercase: false)) not found")
return nil return nil
} }
} }

View File

@ -898,26 +898,28 @@ public extension Api {
} }
public extension Api { public extension Api {
enum PeerSettings: TypeConstructorDescription { enum PeerSettings: TypeConstructorDescription {
case peerSettings(flags: Int32, geoDistance: Int32?, requestChatTitle: String?, requestChatDate: Int32?) case peerSettings(flags: Int32, geoDistance: Int32?, requestChatTitle: String?, requestChatDate: Int32?, businessBotId: Int64?, businessBotManageUrl: String?)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self { switch self {
case .peerSettings(let flags, let geoDistance, let requestChatTitle, let requestChatDate): case .peerSettings(let flags, let geoDistance, let requestChatTitle, let requestChatDate, let businessBotId, let businessBotManageUrl):
if boxed { if boxed {
buffer.appendInt32(-1525149427) buffer.appendInt32(-1395233698)
} }
serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(flags, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 6) != 0 {serializeInt32(geoDistance!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 6) != 0 {serializeInt32(geoDistance!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 9) != 0 {serializeString(requestChatTitle!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 9) != 0 {serializeString(requestChatTitle!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 9) != 0 {serializeInt32(requestChatDate!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 9) != 0 {serializeInt32(requestChatDate!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 13) != 0 {serializeInt64(businessBotId!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 13) != 0 {serializeString(businessBotManageUrl!, buffer: buffer, boxed: false)}
break break
} }
} }
public func descriptionFields() -> (String, [(String, Any)]) { public func descriptionFields() -> (String, [(String, Any)]) {
switch self { switch self {
case .peerSettings(let flags, let geoDistance, let requestChatTitle, let requestChatDate): case .peerSettings(let flags, let geoDistance, let requestChatTitle, let requestChatDate, let businessBotId, let businessBotManageUrl):
return ("peerSettings", [("flags", flags as Any), ("geoDistance", geoDistance as Any), ("requestChatTitle", requestChatTitle as Any), ("requestChatDate", requestChatDate as Any)]) return ("peerSettings", [("flags", flags as Any), ("geoDistance", geoDistance as Any), ("requestChatTitle", requestChatTitle as Any), ("requestChatDate", requestChatDate as Any), ("businessBotId", businessBotId as Any), ("businessBotManageUrl", businessBotManageUrl as Any)])
} }
} }
@ -930,12 +932,18 @@ public extension Api {
if Int(_1!) & Int(1 << 9) != 0 {_3 = parseString(reader) } if Int(_1!) & Int(1 << 9) != 0 {_3 = parseString(reader) }
var _4: Int32? var _4: Int32?
if Int(_1!) & Int(1 << 9) != 0 {_4 = reader.readInt32() } if Int(_1!) & Int(1 << 9) != 0 {_4 = reader.readInt32() }
var _5: Int64?
if Int(_1!) & Int(1 << 13) != 0 {_5 = reader.readInt64() }
var _6: String?
if Int(_1!) & Int(1 << 13) != 0 {_6 = parseString(reader) }
let _c1 = _1 != nil let _c1 = _1 != nil
let _c2 = (Int(_1!) & Int(1 << 6) == 0) || _2 != nil let _c2 = (Int(_1!) & Int(1 << 6) == 0) || _2 != nil
let _c3 = (Int(_1!) & Int(1 << 9) == 0) || _3 != nil let _c3 = (Int(_1!) & Int(1 << 9) == 0) || _3 != nil
let _c4 = (Int(_1!) & Int(1 << 9) == 0) || _4 != nil let _c4 = (Int(_1!) & Int(1 << 9) == 0) || _4 != nil
if _c1 && _c2 && _c3 && _c4 { let _c5 = (Int(_1!) & Int(1 << 13) == 0) || _5 != nil
return Api.PeerSettings.peerSettings(flags: _1!, geoDistance: _2, requestChatTitle: _3, requestChatDate: _4) let _c6 = (Int(_1!) & Int(1 << 13) == 0) || _6 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 {
return Api.PeerSettings.peerSettings(flags: _1!, geoDistance: _2, requestChatTitle: _3, requestChatDate: _4, businessBotId: _5, businessBotManageUrl: _6)
} }
else { else {
return nil return nil

View File

@ -221,6 +221,21 @@ public extension Api.functions.account {
}) })
} }
} }
public extension Api.functions.account {
static func disablePeerConnectedBot(peer: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
let buffer = Buffer()
buffer.appendInt32(1581481689)
peer.serialize(buffer, true)
return (FunctionDescription(name: "account.disablePeerConnectedBot", parameters: [("peer", String(describing: peer))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
let reader = BufferReader(buffer)
var result: Api.Bool?
if let signature = reader.readInt32() {
result = Api.parse(reader, signature: signature) as? Api.Bool
}
return result
})
}
}
public extension Api.functions.account { public extension Api.functions.account {
static func finishTakeoutSession(flags: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) { static func finishTakeoutSession(flags: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
let buffer = Buffer() let buffer = Buffer()
@ -1254,6 +1269,22 @@ public extension Api.functions.account {
}) })
} }
} }
public extension Api.functions.account {
static func toggleConnectedBotPaused(peer: Api.InputPeer, paused: Api.Bool) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
let buffer = Buffer()
buffer.appendInt32(1684934807)
peer.serialize(buffer, true)
paused.serialize(buffer, true)
return (FunctionDescription(name: "account.toggleConnectedBotPaused", parameters: [("peer", String(describing: peer)), ("paused", String(describing: paused))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
let reader = BufferReader(buffer)
var result: Api.Bool?
if let signature = reader.readInt32() {
result = Api.parse(reader, signature: signature) as? Api.Bool
}
return result
})
}
}
public extension Api.functions.account { public extension Api.functions.account {
static func toggleUsername(username: String, active: Api.Bool) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) { static func toggleUsername(username: String, active: Api.Bool) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
let buffer = Buffer() let buffer = Buffer()

View File

@ -6,7 +6,7 @@ import SwiftSignalKit
extension PeerStatusSettings { extension PeerStatusSettings {
init(apiSettings: Api.PeerSettings) { init(apiSettings: Api.PeerSettings) {
switch apiSettings { switch apiSettings {
case let .peerSettings(flags, geoDistance, requestChatTitle, requestChatDate): case let .peerSettings(flags, geoDistance, requestChatTitle, requestChatDate, _, _):
var result = PeerStatusSettings.Flags() var result = PeerStatusSettings.Flags()
if (flags & (1 << 1)) != 0 { if (flags & (1 << 1)) != 0 {
result.insert(.canAddContact) result.insert(.canAddContact)

View File

@ -38,7 +38,8 @@ public func getServerProvidedSuggestions(account: Account) -> Signal<[ServerProv
return [] return []
} }
let list = listItems var list = listItems
list.append(ServerProvidedSuggestion.setupBirthday.rawValue)
return list.compactMap { item -> ServerProvidedSuggestion? in return list.compactMap { item -> ServerProvidedSuggestion? in
return ServerProvidedSuggestion(rawValue: item) return ServerProvidedSuggestion(rawValue: item)

View File

@ -2795,6 +2795,11 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior {
}) })
})) }))
) )
loop: for attribute in file.attributes {
switch attribute {
case let .CustomEmoji(_, _, _, packReference), let .Sticker(_, packReference, _):
if let packReference = packReference {
menuItems.append( menuItems.append(
.action(ContextMenuActionItem(text: presentationData.strings.StickerPack_ViewPack, icon: { theme in .action(ContextMenuActionItem(text: presentationData.strings.StickerPack_ViewPack, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Sticker"), color: theme.actionSheet.primaryTextColor) return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Sticker"), color: theme.actionSheet.primaryTextColor)
@ -2805,10 +2810,6 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior {
return return
} }
loop: for attribute in file.attributes {
switch attribute {
case let .CustomEmoji(_, _, _, packReference), let .Sticker(_, packReference, _):
if let packReference = packReference {
let controller = strongSelf.context.sharedContext.makeStickerPackScreen(context: context, updatedPresentationData: nil, mainStickerPack: packReference, stickerPacks: [packReference], loadedStickerPacks: [], isEditing: false, parentNavigationController: interaction.navigationController(), sendSticker: { file, sourceView, sourceRect in let controller = strongSelf.context.sharedContext.makeStickerPackScreen(context: context, updatedPresentationData: nil, mainStickerPack: packReference, stickerPacks: [packReference], loadedStickerPacks: [], isEditing: false, parentNavigationController: interaction.navigationController(), sendSticker: { file, sourceView, sourceRect in
sendSticker(file, false, false, nil, false, sourceView, sourceRect, nil) sendSticker(file, false, false, nil, false, sourceView, sourceRect, nil)
return true return true
@ -2816,14 +2817,14 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior {
interaction.navigationController()?.view.window?.endEditing(true) interaction.navigationController()?.view.window?.endEditing(true)
interaction.presentController(controller, nil) interaction.presentController(controller, nil)
}))
)
} }
break loop break loop
default: default:
break break
} }
} }
}))
)
} }
guard let view = view else { guard let view = view else {

View File

@ -986,7 +986,11 @@ private final class GroupHeaderLayer: UIView {
var textConstrainedWidth = constrainedSize.width - titleHorizontalOffset - 10.0 var textConstrainedWidth = constrainedSize.width - titleHorizontalOffset - 10.0
if let actionButtonSize = actionButtonSize { if let actionButtonSize = actionButtonSize {
textConstrainedWidth -= actionButtonSize.width - 10.0 if actionButtonIsCompact {
textConstrainedWidth -= actionButtonSize.width * 2.0 + 10.0
} else {
textConstrainedWidth -= actionButtonSize.width + 10.0
}
} }
if clearWidth > 0.0 { if clearWidth > 0.0 {
textConstrainedWidth -= clearWidth + 8.0 textConstrainedWidth -= clearWidth + 8.0

View File

@ -0,0 +1,90 @@
import Foundation
import CoreMedia
import FFMpegBinding
final class MediaEditorFFMpegWriter: MediaEditorVideoExportWriter {
public static let registerFFMpegGlobals: Void = {
FFMpegGlobals.initializeGlobals()
return
}()
let ffmpegWriter = FFMpegVideoWriter()
func setup(configuration: MediaEditorVideoExport.Configuration, outputPath: String) {
let _ = MediaEditorFFMpegWriter.registerFFMpegGlobals
self.ffmpegWriter.setup(
withOutputPath: outputPath,
width: Int32(configuration.dimensions.width),
height: Int32(configuration.dimensions.height)
)
}
func setupVideoInput(configuration: MediaEditorVideoExport.Configuration, preferredTransform: CGAffineTransform?, sourceFrameRate: Float) {
}
func setupAudioInput(configuration: MediaEditorVideoExport.Configuration) {
}
func startWriting() -> Bool {
return false
}
func startSession(atSourceTime time: CMTime) {
}
func finishWriting(completion: @escaping () -> Void) {
self.ffmpegWriter.finalizeVideo()
completion()
}
func cancelWriting() {
}
func requestVideoDataWhenReady(on queue: DispatchQueue, using block: @escaping () -> Void) {
}
func requestAudioDataWhenReady(on queue: DispatchQueue, using block: @escaping () -> Void) {
}
var isReadyForMoreVideoData: Bool {
return true
}
func appendVideoBuffer(_ buffer: CMSampleBuffer) -> Bool {
return false
}
func appendPixelBuffer(_ buffer: CVPixelBuffer, at time: CMTime) -> Bool {
return false
}
func markVideoAsFinished() {
}
var pixelBufferPool: CVPixelBufferPool?
var isReadyForMoreAudioData: Bool {
return false
}
func appendAudioBuffer(_ buffer: CMSampleBuffer) -> Bool {
return false
}
func markAudioAsFinished() {
}
var status: ExportWriterStatus = .unknown
var error: Error?
}

View File

@ -1525,9 +1525,9 @@ func targetSize(cropSize: CGSize, rotateSideward: Bool = false) -> CGSize {
return CGSize(width: renderWidth, height: renderHeight) return CGSize(width: renderWidth, height: renderHeight)
} }
public func recommendedVideoExportConfiguration(values: MediaEditorValues, duration: Double, image: Bool = false, forceFullHd: Bool = false, frameRate: Float) -> MediaEditorVideoExport.Configuration { public func recommendedVideoExportConfiguration(values: MediaEditorValues, duration: Double, image: Bool = false, forceFullHd: Bool = false, frameRate: Float, isSticker: Bool = false) -> MediaEditorVideoExport.Configuration {
let compressionProperties: [String: Any] let compressionProperties: [String: Any]
let codecType: AVVideoCodecType let codecType: Any
var videoBitrate: Int = 3700 var videoBitrate: Int = 3700
var audioBitrate: Int = 64 var audioBitrate: Int = 64
@ -1548,6 +1548,7 @@ public func recommendedVideoExportConfiguration(values: MediaEditorValues, durat
let height: Int let height: Int
var useHEVC = hasHEVCHardwareEncoder var useHEVC = hasHEVCHardwareEncoder
var useVP9 = false
if let qualityPreset = values.qualityPreset { if let qualityPreset = values.qualityPreset {
let maxSize = CGSize(width: qualityPreset.maximumDimensions, height: qualityPreset.maximumDimensions) let maxSize = CGSize(width: qualityPreset.maximumDimensions, height: qualityPreset.maximumDimensions)
var resultSize = values.originalDimensions.cgSize var resultSize = values.originalDimensions.cgSize
@ -1566,7 +1567,11 @@ public func recommendedVideoExportConfiguration(values: MediaEditorValues, durat
useHEVC = false useHEVC = false
} else { } else {
if values.videoIsFullHd { if isSticker {
width = 512
height = 512
useVP9 = true
} else if values.videoIsFullHd {
width = 1080 width = 1080
height = 1920 height = 1920
} else { } else {
@ -1575,7 +1580,10 @@ public func recommendedVideoExportConfiguration(values: MediaEditorValues, durat
} }
} }
if useHEVC { if useVP9 {
codecType = "VP9"
compressionProperties = [:]
} else if useHEVC {
codecType = AVVideoCodecType.hevc codecType = AVVideoCodecType.hevc
compressionProperties = [ compressionProperties = [
AVVideoAverageBitRateKey: videoBitrate * 1000, AVVideoAverageBitRateKey: videoBitrate * 1000,
@ -1597,12 +1605,17 @@ public func recommendedVideoExportConfiguration(values: MediaEditorValues, durat
AVVideoHeightKey: height AVVideoHeightKey: height
] ]
let audioSettings: [String: Any] = [ let audioSettings: [String: Any]
if isSticker {
audioSettings = [:]
} else {
audioSettings = [
AVFormatIDKey: kAudioFormatMPEG4AAC, AVFormatIDKey: kAudioFormatMPEG4AAC,
AVSampleRateKey: 44100, AVSampleRateKey: 44100,
AVEncoderBitRateKey: audioBitrate * 1000, AVEncoderBitRateKey: audioBitrate * 1000,
AVNumberOfChannelsKey: audioNumberOfChannels AVNumberOfChannelsKey: audioNumberOfChannels
] ]
}
return MediaEditorVideoExport.Configuration( return MediaEditorVideoExport.Configuration(
videoSettings: videoSettings, videoSettings: videoSettings,

View File

@ -0,0 +1,160 @@
import Foundation
import AVFoundation
import TelegramCore
import FFMpegBinding
final class MediaEditorVideoAVAssetWriter: MediaEditorVideoExportWriter {
private var writer: AVAssetWriter?
private var videoInput: AVAssetWriterInput?
private var audioInput: AVAssetWriterInput?
private var adaptor: AVAssetWriterInputPixelBufferAdaptor!
func setup(configuration: MediaEditorVideoExport.Configuration, outputPath: String) {
Logger.shared.log("VideoExport", "Will setup asset writer")
let url = URL(fileURLWithPath: outputPath)
self.writer = try? AVAssetWriter(url: url, fileType: .mp4)
guard let writer = self.writer else {
return
}
writer.shouldOptimizeForNetworkUse = configuration.shouldOptimizeForNetworkUse
Logger.shared.log("VideoExport", "Did setup asset writer")
}
func setupVideoInput(configuration: MediaEditorVideoExport.Configuration, preferredTransform: CGAffineTransform?, sourceFrameRate: Float) {
guard let writer = self.writer else {
return
}
Logger.shared.log("VideoExport", "Will setup video input")
var dimensions = configuration.dimensions
var videoSettings = configuration.videoSettings
if var compressionSettings = videoSettings[AVVideoCompressionPropertiesKey] as? [String: Any] {
compressionSettings[AVVideoExpectedSourceFrameRateKey] = sourceFrameRate
videoSettings[AVVideoCompressionPropertiesKey] = compressionSettings
}
if let preferredTransform {
if (preferredTransform.b == -1 && preferredTransform.c == 1) || (preferredTransform.b == 1 && preferredTransform.c == -1) {
dimensions = CGSize(width: dimensions.height, height: dimensions.width)
}
videoSettings[AVVideoWidthKey] = Int(dimensions.width)
videoSettings[AVVideoHeightKey] = Int(dimensions.height)
}
let videoInput = AVAssetWriterInput(mediaType: .video, outputSettings: videoSettings)
if let preferredTransform {
videoInput.transform = preferredTransform
}
videoInput.expectsMediaDataInRealTime = false
let sourcePixelBufferAttributes = [
kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA,
kCVPixelBufferWidthKey as String: UInt32(dimensions.width),
kCVPixelBufferHeightKey as String: UInt32(dimensions.height)
]
self.adaptor = AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: videoInput, sourcePixelBufferAttributes: sourcePixelBufferAttributes)
if writer.canAdd(videoInput) {
writer.add(videoInput)
} else {
Logger.shared.log("VideoExport", "Failed to add video input")
}
self.videoInput = videoInput
}
func setupAudioInput(configuration: MediaEditorVideoExport.Configuration) {
guard let writer = self.writer else {
return
}
let audioInput = AVAssetWriterInput(mediaType: .audio, outputSettings: configuration.audioSettings)
audioInput.expectsMediaDataInRealTime = false
if writer.canAdd(audioInput) {
writer.add(audioInput)
}
self.audioInput = audioInput
}
func startWriting() -> Bool {
return self.writer?.startWriting() ?? false
}
func startSession(atSourceTime time: CMTime) {
self.writer?.startSession(atSourceTime: time)
}
func finishWriting(completion: @escaping () -> Void) {
self.writer?.finishWriting(completionHandler: completion)
}
func cancelWriting() {
self.writer?.cancelWriting()
}
func requestVideoDataWhenReady(on queue: DispatchQueue, using block: @escaping () -> Void) {
self.videoInput?.requestMediaDataWhenReady(on: queue, using: block)
}
func requestAudioDataWhenReady(on queue: DispatchQueue, using block: @escaping () -> Void) {
self.audioInput?.requestMediaDataWhenReady(on: queue, using: block)
}
var isReadyForMoreVideoData: Bool {
return self.videoInput?.isReadyForMoreMediaData ?? false
}
func appendVideoBuffer(_ buffer: CMSampleBuffer) -> Bool {
return self.videoInput?.append(buffer) ?? false
}
func appendPixelBuffer(_ pixelBuffer: CVPixelBuffer, at time: CMTime) -> Bool {
return self.adaptor.append(pixelBuffer, withPresentationTime: time)
}
var pixelBufferPool: CVPixelBufferPool? {
return self.adaptor.pixelBufferPool
}
func markVideoAsFinished() {
self.videoInput?.markAsFinished()
}
var isReadyForMoreAudioData: Bool {
return self.audioInput?.isReadyForMoreMediaData ?? false
}
func appendAudioBuffer(_ buffer: CMSampleBuffer) -> Bool {
return self.audioInput?.append(buffer) ?? false
}
func markAudioAsFinished() {
self.audioInput?.markAsFinished()
}
var status: ExportWriterStatus {
if let writer = self.writer {
switch writer.status {
case .unknown:
return .unknown
case .writing:
return .writing
case .completed:
return .completed
case .failed:
return .failed
case .cancelled:
return .cancelled
@unknown default:
fatalError()
}
} else {
return .unknown
}
}
var error: Error? {
return self.writer?.error
}
}

View File

@ -43,162 +43,6 @@ protocol MediaEditorVideoExportWriter {
var error: Error? { get } var error: Error? { get }
} }
public final class MediaEditorVideoAVAssetWriter: MediaEditorVideoExportWriter {
private var writer: AVAssetWriter?
private var videoInput: AVAssetWriterInput?
private var audioInput: AVAssetWriterInput?
private var adaptor: AVAssetWriterInputPixelBufferAdaptor!
func setup(configuration: MediaEditorVideoExport.Configuration, outputPath: String) {
Logger.shared.log("VideoExport", "Will setup asset writer")
let url = URL(fileURLWithPath: outputPath)
self.writer = try? AVAssetWriter(url: url, fileType: .mp4)
guard let writer = self.writer else {
return
}
writer.shouldOptimizeForNetworkUse = configuration.shouldOptimizeForNetworkUse
Logger.shared.log("VideoExport", "Did setup asset writer")
}
func setupVideoInput(configuration: MediaEditorVideoExport.Configuration, preferredTransform: CGAffineTransform?, sourceFrameRate: Float) {
guard let writer = self.writer else {
return
}
Logger.shared.log("VideoExport", "Will setup video input")
var dimensions = configuration.dimensions
var videoSettings = configuration.videoSettings
if var compressionSettings = videoSettings[AVVideoCompressionPropertiesKey] as? [String: Any] {
compressionSettings[AVVideoExpectedSourceFrameRateKey] = sourceFrameRate
videoSettings[AVVideoCompressionPropertiesKey] = compressionSettings
}
if let preferredTransform {
if (preferredTransform.b == -1 && preferredTransform.c == 1) || (preferredTransform.b == 1 && preferredTransform.c == -1) {
dimensions = CGSize(width: dimensions.height, height: dimensions.width)
}
videoSettings[AVVideoWidthKey] = Int(dimensions.width)
videoSettings[AVVideoHeightKey] = Int(dimensions.height)
}
let videoInput = AVAssetWriterInput(mediaType: .video, outputSettings: videoSettings)
if let preferredTransform {
videoInput.transform = preferredTransform
}
videoInput.expectsMediaDataInRealTime = false
let sourcePixelBufferAttributes = [
kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA,
kCVPixelBufferWidthKey as String: UInt32(dimensions.width),
kCVPixelBufferHeightKey as String: UInt32(dimensions.height)
]
self.adaptor = AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: videoInput, sourcePixelBufferAttributes: sourcePixelBufferAttributes)
if writer.canAdd(videoInput) {
writer.add(videoInput)
} else {
Logger.shared.log("VideoExport", "Failed to add video input")
}
self.videoInput = videoInput
}
func setupAudioInput(configuration: MediaEditorVideoExport.Configuration) {
guard let writer = self.writer else {
return
}
let audioInput = AVAssetWriterInput(mediaType: .audio, outputSettings: configuration.audioSettings)
audioInput.expectsMediaDataInRealTime = false
if writer.canAdd(audioInput) {
writer.add(audioInput)
}
self.audioInput = audioInput
}
func startWriting() -> Bool {
return self.writer?.startWriting() ?? false
}
func startSession(atSourceTime time: CMTime) {
self.writer?.startSession(atSourceTime: time)
}
func finishWriting(completion: @escaping () -> Void) {
self.writer?.finishWriting(completionHandler: completion)
}
func cancelWriting() {
self.writer?.cancelWriting()
}
func requestVideoDataWhenReady(on queue: DispatchQueue, using block: @escaping () -> Void) {
self.videoInput?.requestMediaDataWhenReady(on: queue, using: block)
}
func requestAudioDataWhenReady(on queue: DispatchQueue, using block: @escaping () -> Void) {
self.audioInput?.requestMediaDataWhenReady(on: queue, using: block)
}
var isReadyForMoreVideoData: Bool {
return self.videoInput?.isReadyForMoreMediaData ?? false
}
func appendVideoBuffer(_ buffer: CMSampleBuffer) -> Bool {
return self.videoInput?.append(buffer) ?? false
}
func appendPixelBuffer(_ pixelBuffer: CVPixelBuffer, at time: CMTime) -> Bool {
return self.adaptor.append(pixelBuffer, withPresentationTime: time)
}
var pixelBufferPool: CVPixelBufferPool? {
return self.adaptor.pixelBufferPool
}
func markVideoAsFinished() {
self.videoInput?.markAsFinished()
}
var isReadyForMoreAudioData: Bool {
return self.audioInput?.isReadyForMoreMediaData ?? false
}
func appendAudioBuffer(_ buffer: CMSampleBuffer) -> Bool {
return self.audioInput?.append(buffer) ?? false
}
func markAudioAsFinished() {
self.audioInput?.markAsFinished()
}
var status: ExportWriterStatus {
if let writer = self.writer {
switch writer.status {
case .unknown:
return .unknown
case .writing:
return .writing
case .completed:
return .completed
case .failed:
return .failed
case .cancelled:
return .cancelled
@unknown default:
fatalError()
}
} else {
return .unknown
}
}
var error: Error? {
return self.writer?.error
}
}
public final class MediaEditorVideoExport { public final class MediaEditorVideoExport {
public enum Subject { public enum Subject {
case image(image: UIImage) case image(image: UIImage)
@ -607,7 +451,12 @@ public final class MediaEditorVideoExport {
} }
} }
if let codec = self.configuration.videoSettings[AVVideoCodecKey] as? String, codec == "VP9" {
self.writer = MediaEditorFFMpegWriter()
} else {
self.writer = MediaEditorVideoAVAssetWriter() self.writer = MediaEditorVideoAVAssetWriter()
}
guard let writer = self.writer else { guard let writer = self.writer else {
return return
} }

View File

@ -163,6 +163,8 @@ extension MediaEditorScreen {
} }
switch subject { switch subject {
case .empty:
break
case let .image(image, dimensions, _, _): case let .image(image, dimensions, _, _):
innerSaveDraft(media: .image(image: image, dimensions: dimensions)) innerSaveDraft(media: .image(image: image, dimensions: dimensions))
case let .video(path, _, _, _, _, dimensions, _, _, _): case let .video(path, _, _, _, _, dimensions, _, _, _):

View File

@ -3184,8 +3184,17 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
} }
} }
} else { } else {
if case .message = self.actualSubject, let layout = self.validLayout { var animateIn = false
self.layer.animatePosition(from: CGPoint(x: 0.0, y: layout.size.height), to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true) if let subject {
switch subject {
case .empty, .message, .sticker:
animateIn = true
default:
break
}
}
if animateIn, let layout = self.validLayout {
self.layer.animatePosition(from: CGPoint(x: 0.0, y: layout.size.height), to: .zero, duration: 0.35, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
completion() completion()
} else if let view = self.componentHost.view as? MediaEditorScreenComponent.View { } else if let view = self.componentHost.view as? MediaEditorScreenComponent.View {
view.animateIn(from: .camera, completion: completion) view.animateIn(from: .camera, completion: completion)
@ -4089,7 +4098,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
if let controller = self.controller, case .stickerEditor = controller.mode { if let controller = self.controller, case .stickerEditor = controller.mode {
hasInteractiveStickers = false hasInteractiveStickers = false
} }
let controller = StickerPickerScreen(context: self.context, inputData: self.stickerPickerInputData.get(), forceDark: true, defaultToEmoji: self.defaultToEmoji, hasGifs: hasInteractiveStickers, hasInteractiveStickers: hasInteractiveStickers) let controller = StickerPickerScreen(context: self.context, inputData: self.stickerPickerInputData.get(), forceDark: true, defaultToEmoji: self.defaultToEmoji, hasGifs: true, hasInteractiveStickers: hasInteractiveStickers)
controller.completion = { [weak self] content in controller.completion = { [weak self] content in
if let self { if let self {
if let content { if let content {
@ -4447,6 +4456,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
} }
public enum Subject { public enum Subject {
case empty(PixelDimensions)
case image(UIImage, PixelDimensions, UIImage?, PIPPosition) case image(UIImage, PixelDimensions, UIImage?, PIPPosition)
case video(String, UIImage?, Bool, String?, UIImage?, PixelDimensions, Double, [(Bool, Double)], PIPPosition) case video(String, UIImage?, Bool, String?, UIImage?, PixelDimensions, Double, [(Bool, Double)], PIPPosition)
case asset(PHAsset) case asset(PHAsset)
@ -4456,6 +4466,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
var dimensions: PixelDimensions { var dimensions: PixelDimensions {
switch self { switch self {
case let .empty(dimensions):
return dimensions
case let .image(_, dimensions, _, _), let .video(_, _, _, _, _, dimensions, _, _, _): case let .image(_, dimensions, _, _), let .video(_, _, _, _, _, dimensions, _, _, _):
return dimensions return dimensions
case let .asset(asset): case let .asset(asset):
@ -4471,6 +4483,11 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
var editorSubject: MediaEditor.Subject { var editorSubject: MediaEditor.Subject {
switch self { switch self {
case let .empty(dimensions):
let image = generateImage(dimensions.cgSize, opaque: false, scale: 1.0, rotatedContext: { size, context in
context.clear(CGRect(origin: .zero, size: size))
})!
return .image(image, dimensions)
case let .image(image, dimensions, _, _): case let .image(image, dimensions, _, _):
return .image(image, dimensions) return .image(image, dimensions)
case let .video(videoPath, transitionImage, mirror, additionalVideoPath, _, dimensions, duration, _, _): case let .video(videoPath, transitionImage, mirror, additionalVideoPath, _, dimensions, duration, _, _):
@ -4492,6 +4509,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
var isVideo: Bool { var isVideo: Bool {
switch self { switch self {
case .empty:
return false
case .image: case .image:
return false return false
case .video: case .video:
@ -5401,6 +5420,18 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
var videoIsMirrored = false var videoIsMirrored = false
let duration: Double let duration: Double
switch subject { switch subject {
case let .empty(dimensions):
let image = generateImage(dimensions.cgSize, opaque: false, scale: 1.0, rotatedContext: { size, context in
context.clear(CGRect(origin: .zero, size: size))
})!
let tempImagePath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).jpg"
if let data = image.jpegData(compressionQuality: 0.85) {
try? data.write(to: URL(fileURLWithPath: tempImagePath))
}
videoResult = .single(.imageFile(path: tempImagePath))
duration = 3.0
firstFrame = .single((image, nil))
case let .image(image, _, _, _): case let .image(image, _, _, _):
let tempImagePath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).jpg" let tempImagePath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).jpg"
if let data = image.jpegData(compressionQuality: 0.85) { if let data = image.jpegData(compressionQuality: 0.85) {
@ -5684,8 +5715,6 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
if let image = mediaEditor.resultImage { if let image = mediaEditor.resultImage {
makeEditorImageComposition(context: self.node.ciContext, postbox: self.context.account.postbox, inputImage: image, dimensions: storyDimensions, values: mediaEditor.values, time: .zero, textScale: 2.0, completion: { [weak self] resultImage in makeEditorImageComposition(context: self.node.ciContext, postbox: self.context.account.postbox, inputImage: image, dimensions: storyDimensions, values: mediaEditor.values, time: .zero, textScale: 2.0, completion: { [weak self] resultImage in
if let self, let resultImage { if let self, let resultImage {
Logger.shared.log("MediaEditor", "Completed with image \(resultImage)")
let dimensions = CGSize(width: 512, height: 512) let dimensions = CGSize(width: 512, height: 512)
let scaledImage = generateImage(dimensions, contextGenerator: { size, context in let scaledImage = generateImage(dimensions, contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size)) context.clear(CGRect(origin: CGPoint(), size: size))
@ -5703,16 +5732,29 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
} }
} }
func presentStickerPreview(image: UIImage) { func presentStickerPreview(image: UIImage) {
guard let mediaEditor = self.node.mediaEditor else {
return
}
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
var isVideo = false
if mediaEditor.resultIsVideo {
isVideo = true
self.performSave(toStickerResource: resource)
} else {
Queue.concurrentDefaultQueue().async { Queue.concurrentDefaultQueue().async {
if let data = try? WebP.convert(toWebP: image, quality: 97.0) { if let data = try? WebP.convert(toWebP: image, quality: 97.0) {
self.context.account.postbox.mediaBox.storeResourceData(resource.id, data: data) self.context.account.postbox.mediaBox.storeResourceData(resource.id, data: data)
} }
} }
}
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkColorPresentationTheme) let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkColorPresentationTheme)
let file = stickerFile(resource: resource, size: Int64(0), dimensions: PixelDimensions(image.size)) let file = stickerFile(resource: resource, size: Int64(0), dimensions: PixelDimensions(image.size), isVideo: isVideo)
var menuItems: [ContextMenuItem] = [] var menuItems: [ContextMenuItem] = []
if case let .stickerEditor(mode) = self.mode { if case let .stickerEditor(mode) = self.mode {
@ -5931,7 +5973,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
case .progress: case .progress:
return .single(status) return .single(status)
case let .complete(resource, _): case let .complete(resource, _):
let file = stickerFile(resource: resource, size: file.size ?? 0, dimensions: dimensions) let file = stickerFile(resource: resource, size: file.size ?? 0, dimensions: dimensions, isVideo: file.mimeType == "video/webm")
switch action { switch action {
case .addToFavorites: case .addToFavorites:
return context.engine.stickers.toggleStickerSaved(file: file, saved: true) return context.engine.stickers.toggleStickerSaved(file: file, saved: true)
@ -5997,7 +6039,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
let result: MediaEditorScreen.Result let result: MediaEditorScreen.Result
if case .upload = action { if case .upload = action {
let file = stickerFile(resource: resource, size: resource.size ?? 0, dimensions: dimensions) let file = stickerFile(resource: resource, size: resource.size ?? 0, dimensions: dimensions, isVideo: file.mimeType == "video/webm")
result = MediaEditorScreen.Result( result = MediaEditorScreen.Result(
media: .sticker(file: file), media: .sticker(file: file),
mediaAreas: [], mediaAreas: [],
@ -6061,12 +6103,13 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
if !authorized { if !authorized {
return return
} }
self?.hapticFeedback.impact(.light)
self?.performSave() self?.performSave()
}) })
} }
private func performSave() { private func performSave(toStickerResource: MediaResource? = nil) {
guard let mediaEditor = self.node.mediaEditor, let subject = self.node.subject, self.isSavingAvailable else { guard let mediaEditor = self.node.mediaEditor, let subject = self.node.subject else {
return return
} }
@ -6076,14 +6119,17 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.node.entitiesView) let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.node.entitiesView)
mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities) mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities)
self.hapticFeedback.impact(.light) let isSticker = toStickerResource != nil
if !isSticker {
self.previousSavedValues = mediaEditor.values self.previousSavedValues = mediaEditor.values
self.isSavingAvailable = false self.isSavingAvailable = false
self.requestLayout(transition: .animated(duration: 0.25, curve: .easeInOut)) self.requestLayout(transition: .animated(duration: 0.25, curve: .easeInOut))
}
let tempVideoPath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).mp4" let fileExtension = isSticker ? "webm" : "mp4"
let saveToPhotos: (String, Bool) -> Void = { path, isVideo in let saveToPhotos: (String, Bool) -> Void = { path, isVideo in
let tempVideoPath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).\(fileExtension)"
PHPhotoLibrary.shared().performChanges({ PHPhotoLibrary.shared().performChanges({
if isVideo { if isVideo {
if let _ = try? FileManager.default.copyItem(atPath: path, toPath: tempVideoPath) { if let _ = try? FileManager.default.copyItem(atPath: path, toPath: tempVideoPath) {
@ -6108,6 +6154,11 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
let exportSubject: Signal<MediaEditorVideoExport.Subject, NoError> let exportSubject: Signal<MediaEditorVideoExport.Subject, NoError>
switch subject { switch subject {
case let .empty(dimensions):
let image = generateImage(dimensions.cgSize, opaque: false, scale: 1.0, rotatedContext: { size, context in
context.clear(CGRect(origin: .zero, size: size))
})!
exportSubject = .single(.image(image: image))
case let .video(path, _, _, _, _, _, _, _, _): case let .video(path, _, _, _, _, _, _, _, _):
let asset = AVURLAsset(url: NSURL(fileURLWithPath: path) as URL) let asset = AVURLAsset(url: NSURL(fileURLWithPath: path) as URL)
exportSubject = .single(.video(asset: asset, isStory: true)) exportSubject = .single(.video(asset: asset, isStory: true))
@ -6171,8 +6222,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
if case let .video(video, _) = exportSubject { if case let .video(video, _) = exportSubject {
duration = video.duration.seconds duration = video.duration.seconds
} }
let configuration = recommendedVideoExportConfiguration(values: mediaEditor.values, duration: duration, forceFullHd: true, frameRate: 60.0) let configuration = recommendedVideoExportConfiguration(values: mediaEditor.values, duration: duration, forceFullHd: true, frameRate: 60.0, isSticker: isSticker)
let outputPath = NSTemporaryDirectory() + "\(Int64.random(in: 0 ..< .max)).mp4" let outputPath = NSTemporaryDirectory() + "\(Int64.random(in: 0 ..< .max)).\(fileExtension)"
let videoExport = MediaEditorVideoExport(postbox: self.context.account.postbox, subject: exportSubject, configuration: configuration, outputPath: outputPath, textScale: 2.0) let videoExport = MediaEditorVideoExport(postbox: self.context.account.postbox, subject: exportSubject, configuration: configuration, outputPath: outputPath, textScale: 2.0)
self.videoExport = videoExport self.videoExport = videoExport
@ -6879,11 +6930,11 @@ extension MediaScrubberComponent.Track {
} }
} }
private func stickerFile(resource: TelegramMediaResource, size: Int64, dimensions: PixelDimensions) -> TelegramMediaFile { private func stickerFile(resource: TelegramMediaResource, size: Int64, dimensions: PixelDimensions, isVideo: Bool) -> TelegramMediaFile {
var fileAttributes: [TelegramMediaFileAttribute] = [] var fileAttributes: [TelegramMediaFileAttribute] = []
fileAttributes.append(.FileName(fileName: "sticker.webp")) fileAttributes.append(.FileName(fileName: isVideo ? "sticker.webm" : "sticker.webp"))
fileAttributes.append(.Sticker(displayText: "", packReference: nil, maskData: nil)) fileAttributes.append(.Sticker(displayText: "", packReference: nil, maskData: nil))
fileAttributes.append(.ImageSize(size: dimensions)) fileAttributes.append(.ImageSize(size: dimensions))
return TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "image/webp", size: size, attributes: fileAttributes) return TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: isVideo ? "video/webm" : "image/webp", size: size, attributes: fileAttributes)
} }

View File

@ -78,7 +78,7 @@ private final class StickerPackListContextItemNode: ASDisplayNode, ContextMenuCu
}) })
let actionNode = ContextControllerActionsListActionItemNode(getController: getController, requestDismiss: actionSelected, requestUpdateAction: { _, _ in }, item: action) let actionNode = ContextControllerActionsListActionItemNode(getController: getController, requestDismiss: actionSelected, requestUpdateAction: { _, _ in }, item: action)
actionNodes.append(actionNode) actionNodes.append(actionNode)
if actionNodes.count != item.packs.count { if actionNodes.count != packs.count {
let separatorNode = ASDisplayNode() let separatorNode = ASDisplayNode()
separatorNode.backgroundColor = presentationData.theme.contextMenu.itemSeparatorColor separatorNode.backgroundColor = presentationData.theme.contextMenu.itemSeparatorColor
separatorNodes.append(separatorNode) separatorNodes.append(separatorNode)

View File

@ -1,8 +1,338 @@
//
// PeerInfoScreenBirthdatePickerItem.swift
// MediaEditorScreen
//
// Created by Ilya Laktyushin on 15.03.2024.
//
import Foundation import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import TelegramCore
import AccountContext
import ComponentFlow
final class PeerInfoScreenBirthdatePickerItem: PeerInfoScreenItem {
let id: AnyHashable
let value: BirthdayPickerComponent.BirthDate
let valueUpdated: (BirthdayPickerComponent.BirthDate) -> Void
init(
id: AnyHashable,
value: BirthdayPickerComponent.BirthDate,
valueUpdated: @escaping (BirthdayPickerComponent.BirthDate) -> Void
) {
self.id = id
self.value = value
self.valueUpdated = valueUpdated
}
func node() -> PeerInfoScreenItemNode {
return PeerInfoScreenBirthdatePickerItemNode()
}
}
private final class PeerInfoScreenBirthdatePickerItemNode: PeerInfoScreenItemNode {
private let maskNode: ASImageNode
private let picker = ComponentView<Empty>()
private let bottomSeparatorNode: ASDisplayNode
private var item: PeerInfoScreenBirthdatePickerItem?
private var presentationData: PresentationData?
private var theme: PresentationTheme?
override init() {
self.maskNode = ASImageNode()
self.maskNode.isUserInteractionEnabled = false
self.bottomSeparatorNode = ASDisplayNode()
self.bottomSeparatorNode.isLayerBacked = true
super.init()
self.addSubnode(self.bottomSeparatorNode)
self.addSubnode(self.maskNode)
}
override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
guard let item = item as? PeerInfoScreenBirthdatePickerItem else {
return 10.0
}
self.item = item
self.presentationData = presentationData
self.theme = presentationData.theme
let sideInset: CGFloat = 16.0 + safeInsets.left
let height: CGFloat = 226.0
self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
let pickerSize = self.picker.update(
transition: .immediate,
component: AnyComponent(BirthdayPickerComponent(
theme: BirthdayPickerComponent.Theme(presentationTheme: presentationData.theme),
strings: presentationData.strings,
value: item.value,
valueUpdated: item.valueUpdated
)),
environment: {},
containerSize: CGSize(width: width - sideInset * 2.0, height: height)
)
let pickerFrame = CGRect(origin: CGPoint(x: sideInset, y: 0.0), size: pickerSize)
if let pickerView = self.picker.view {
if pickerView.superview == nil {
self.view.addSubview(pickerView)
}
transition.updateFrame(view: pickerView, frame: pickerFrame)
}
transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: sideInset, y: height - UIScreenPixel), size: CGSize(width: width - sideInset, height: UIScreenPixel)))
transition.updateAlpha(node: self.bottomSeparatorNode, alpha: bottomItem == nil ? 0.0 : 1.0)
let hasCorners = hasCorners && (topItem == nil || bottomItem == nil)
let hasTopCorners = hasCorners && topItem == nil
let hasBottomCorners = hasCorners && bottomItem == nil
self.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
self.maskNode.frame = CGRect(origin: CGPoint(x: safeInsets.left, y: 0.0), size: CGSize(width: width - safeInsets.left - safeInsets.right, height: height))
self.bottomSeparatorNode.isHidden = hasBottomCorners
return height
}
}
public final class BirthdayPickerComponent: Component {
public struct Theme: Equatable {
let backgroundColor: UIColor
let textColor: UIColor
let selectionColor: UIColor
init(presentationTheme: PresentationTheme) {
self.backgroundColor = presentationTheme.list.itemBlocksBackgroundColor
self.textColor = presentationTheme.list.itemPrimaryTextColor
self.selectionColor = presentationTheme.list.itemHighlightedBackgroundColor
}
}
public struct BirthDate: Equatable {
let year: Int?
let month: Int
let day: Int
init(year: Int?, month: Int, day: Int) {
self.year = year
self.month = month
self.day = day
}
func withUpdated(year: Int?) -> BirthDate {
return BirthDate(year: year, month: self.month, day: self.day)
}
func withUpdated(month: Int) -> BirthDate {
return BirthDate(year: self.year, month: month, day: self.day)
}
func withUpdated(day: Int) -> BirthDate {
return BirthDate(year: self.year, month: self.month, day: day)
}
}
public let theme: Theme
public let strings: PresentationStrings
public let value: BirthDate
public let valueUpdated: (BirthDate) -> Void
public init(
theme: Theme,
strings: PresentationStrings,
value: BirthDate,
valueUpdated: @escaping (BirthDate) -> Void
) {
self.theme = theme
self.strings = strings
self.value = value
self.valueUpdated = valueUpdated
}
public static func ==(lhs: BirthdayPickerComponent, rhs: BirthdayPickerComponent) -> Bool {
if lhs.theme != rhs.theme {
return false
}
if lhs.value != rhs.value {
return false
}
return true
}
public final class View: UIView, UIPickerViewDelegate, UIPickerViewDataSource {
private var component: BirthdayPickerComponent?
private weak var componentState: EmptyComponentState?
private let pickerView = UIPickerView()
enum PickerComponent: Int {
case day = 0
case month = 1
case year = 2
}
private let calendar = Calendar(identifier: .gregorian)
private var value = BirthdayPickerComponent.BirthDate(year: nil, month: 1, day: 1)
private let minYear = 1900
private let maxYear: Int
override init(frame: CGRect) {
self.maxYear = self.calendar.component(.year, from: Date())
super.init(frame: frame)
self.pickerView.delegate = self
self.pickerView.dataSource = self
self.addSubview(self.pickerView)
}
required init(coder: NSCoder) {
preconditionFailure()
}
func update(component: BirthdayPickerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
let isFirstTime = self.component == nil
self.component = component
self.componentState = state
self.pickerView.frame = CGRect(origin: .zero, size: availableSize)
if isFirstTime || self.value != component.value {
self.value = component.value
self.pickerView.reloadAllComponents()
if let year = component.value.year {
self.pickerView.selectRow(year - self.minYear, inComponent: PickerComponent.year.rawValue, animated: false)
} else {
self.pickerView.selectRow(self.maxYear - self.minYear + 1, inComponent: PickerComponent.year.rawValue, animated: false)
}
self.pickerView.selectRow(component.value.month - 1, inComponent: PickerComponent.month.rawValue, animated: false)
self.pickerView.selectRow(component.value.day - 1, inComponent: PickerComponent.day.rawValue, animated: false)
}
return availableSize
}
public func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 3
}
public func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
switch component {
case PickerComponent.day.rawValue:
let year = self.value.year ?? 2024
let month = self.value.month
let range = Calendar.current.range(of: .day, in: .month, for: Calendar.current.date(from: DateComponents(year: year, month: month))!)!
return range.upperBound - range.lowerBound
case PickerComponent.month.rawValue:
return 12
case PickerComponent.year.rawValue:
return self.maxYear - self.minYear + 2
default:
return 0
}
}
public func pickerView(_ pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? {
var string = ""
switch component {
case PickerComponent.day.rawValue:
string = "\(row + 1)"
case PickerComponent.month.rawValue:
guard let strings = self.component?.strings else {
break
}
switch row {
case 0:
string = strings.Month_GenJanuary
case 1:
string = strings.Month_GenFebruary
case 2:
string = strings.Month_GenMarch
case 3:
string = strings.Month_GenApril
case 4:
string = strings.Month_GenMay
case 5:
string = strings.Month_GenJune
case 6:
string = strings.Month_GenJuly
case 7:
string = strings.Month_GenAugust
case 8:
string = strings.Month_GenSeptember
case 9:
string = strings.Month_GenOctober
case 10:
string = strings.Month_GenNovember
case 11:
string = strings.Month_GenDecember
default:
break
}
case PickerComponent.year.rawValue:
if row == self.maxYear - self.minYear + 1 {
string = ""
} else {
string = "\(self.minYear + row)"
}
default:
break
}
let textColor = self.component?.theme.textColor ?? .black
return NSAttributedString(string: string, attributes: [NSAttributedString.Key.foregroundColor: textColor])
}
public func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
switch component {
case PickerComponent.day.rawValue:
self.value = self.value.withUpdated(day: row + 1)
case PickerComponent.month.rawValue:
self.value = self.value.withUpdated(month: row + 1)
case PickerComponent.year.rawValue:
if row == self.maxYear - self.minYear + 1 {
self.value = self.value.withUpdated(year: nil)
} else {
self.value = self.value.withUpdated(year: self.minYear + row)
}
default:
break
}
if [PickerComponent.month.rawValue, PickerComponent.year.rawValue].contains(component) {
self.pickerView.reloadComponent(PickerComponent.day.rawValue)
}
self.component?.valueUpdated(self.value)
}
public func pickerView(_ pickerView: UIPickerView, widthForComponent component: Int) -> CGFloat {
switch component {
case PickerComponent.day.rawValue:
return 50.0
case PickerComponent.month.rawValue:
return 145.0
case PickerComponent.year.rawValue:
return 75.0
default:
return 0
}
}
public func pickerView(_ pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat {
return 40.0
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -5,8 +5,14 @@ import TelegramPresentationData
final class PeerInfoScreenDisclosureItem: PeerInfoScreenItem { final class PeerInfoScreenDisclosureItem: PeerInfoScreenItem {
enum Label { enum Label {
enum LabelColor {
case generic
case accent
}
case none case none
case text(String) case text(String)
case coloredText(String, LabelColor)
case badge(String, UIColor) case badge(String, UIColor)
case semitransparentBadge(String, UIColor) case semitransparentBadge(String, UIColor)
case titleBadge(String, UIColor) case titleBadge(String, UIColor)
@ -16,14 +22,14 @@ final class PeerInfoScreenDisclosureItem: PeerInfoScreenItem {
switch self { switch self {
case .none, .image: case .none, .image:
return "" return ""
case let .text(text), let .badge(text, _), let .semitransparentBadge(text, _), let .titleBadge(text, _): case let .text(text), let .coloredText(text, _), let .badge(text, _), let .semitransparentBadge(text, _), let .titleBadge(text, _):
return text return text
} }
} }
var badgeColor: UIColor? { var badgeColor: UIColor? {
switch self { switch self {
case .none, .text, .image: case .none, .text, .coloredText, .image:
return nil return nil
case let .badge(_, color), let .semitransparentBadge(_, color), let .titleBadge(_, color): case let .badge(_, color), let .semitransparentBadge(_, color), let .titleBadge(_, color):
return color return color
@ -159,6 +165,14 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode {
} else if case .titleBadge = item.label { } else if case .titleBadge = item.label {
labelColorValue = presentationData.theme.list.itemCheckColors.foregroundColor labelColorValue = presentationData.theme.list.itemCheckColors.foregroundColor
labelFont = Font.medium(11.0) labelFont = Font.medium(11.0)
} else if case let .coloredText(_, color) = item.label {
switch color {
case .generic:
labelColorValue = presentationData.theme.list.itemSecondaryTextColor
case .accent:
labelColorValue = presentationData.theme.list.itemAccentColor
}
labelFont = titleFont
} else { } else {
labelColorValue = presentationData.theme.list.itemSecondaryTextColor labelColorValue = presentationData.theme.list.itemSecondaryTextColor
labelFont = titleFont labelFont = titleFont

View File

@ -33,6 +33,8 @@ final class PeerInfoState {
let updatingBio: String? let updatingBio: String?
let avatarUploadProgress: AvatarUploadProgress? let avatarUploadProgress: AvatarUploadProgress?
let highlightedButton: PeerInfoHeaderButtonKey? let highlightedButton: PeerInfoHeaderButtonKey?
let isEditingBirthDate: Bool
let updatingBirthDate: BirthdayPickerComponent.BirthDate?
init( init(
isEditing: Bool, isEditing: Bool,
@ -40,7 +42,9 @@ final class PeerInfoState {
updatingAvatar: PeerInfoUpdatingAvatar?, updatingAvatar: PeerInfoUpdatingAvatar?,
updatingBio: String?, updatingBio: String?,
avatarUploadProgress: AvatarUploadProgress?, avatarUploadProgress: AvatarUploadProgress?,
highlightedButton: PeerInfoHeaderButtonKey? highlightedButton: PeerInfoHeaderButtonKey?,
isEditingBirthDate: Bool,
updatingBirthDate: BirthdayPickerComponent.BirthDate?
) { ) {
self.isEditing = isEditing self.isEditing = isEditing
self.selectedMessageIds = selectedMessageIds self.selectedMessageIds = selectedMessageIds
@ -48,6 +52,8 @@ final class PeerInfoState {
self.updatingBio = updatingBio self.updatingBio = updatingBio
self.avatarUploadProgress = avatarUploadProgress self.avatarUploadProgress = avatarUploadProgress
self.highlightedButton = highlightedButton self.highlightedButton = highlightedButton
self.isEditingBirthDate = isEditingBirthDate
self.updatingBirthDate = updatingBirthDate
} }
func withIsEditing(_ isEditing: Bool) -> PeerInfoState { func withIsEditing(_ isEditing: Bool) -> PeerInfoState {
@ -57,7 +63,9 @@ final class PeerInfoState {
updatingAvatar: self.updatingAvatar, updatingAvatar: self.updatingAvatar,
updatingBio: self.updatingBio, updatingBio: self.updatingBio,
avatarUploadProgress: self.avatarUploadProgress, avatarUploadProgress: self.avatarUploadProgress,
highlightedButton: self.highlightedButton highlightedButton: self.highlightedButton,
isEditingBirthDate: self.isEditingBirthDate,
updatingBirthDate: self.updatingBirthDate
) )
} }
@ -68,7 +76,9 @@ final class PeerInfoState {
updatingAvatar: self.updatingAvatar, updatingAvatar: self.updatingAvatar,
updatingBio: self.updatingBio, updatingBio: self.updatingBio,
avatarUploadProgress: self.avatarUploadProgress, avatarUploadProgress: self.avatarUploadProgress,
highlightedButton: self.highlightedButton highlightedButton: self.highlightedButton,
isEditingBirthDate: self.isEditingBirthDate,
updatingBirthDate: self.updatingBirthDate
) )
} }
@ -79,7 +89,9 @@ final class PeerInfoState {
updatingAvatar: updatingAvatar, updatingAvatar: updatingAvatar,
updatingBio: self.updatingBio, updatingBio: self.updatingBio,
avatarUploadProgress: self.avatarUploadProgress, avatarUploadProgress: self.avatarUploadProgress,
highlightedButton: self.highlightedButton highlightedButton: self.highlightedButton,
isEditingBirthDate: self.isEditingBirthDate,
updatingBirthDate: self.updatingBirthDate
) )
} }
@ -90,7 +102,9 @@ final class PeerInfoState {
updatingAvatar: self.updatingAvatar, updatingAvatar: self.updatingAvatar,
updatingBio: updatingBio, updatingBio: updatingBio,
avatarUploadProgress: self.avatarUploadProgress, avatarUploadProgress: self.avatarUploadProgress,
highlightedButton: self.highlightedButton highlightedButton: self.highlightedButton,
isEditingBirthDate: self.isEditingBirthDate,
updatingBirthDate: self.updatingBirthDate
) )
} }
@ -101,7 +115,9 @@ final class PeerInfoState {
updatingAvatar: self.updatingAvatar, updatingAvatar: self.updatingAvatar,
updatingBio: self.updatingBio, updatingBio: self.updatingBio,
avatarUploadProgress: avatarUploadProgress, avatarUploadProgress: avatarUploadProgress,
highlightedButton: self.highlightedButton highlightedButton: self.highlightedButton,
isEditingBirthDate: self.isEditingBirthDate,
updatingBirthDate: self.updatingBirthDate
) )
} }
@ -112,7 +128,35 @@ final class PeerInfoState {
updatingAvatar: self.updatingAvatar, updatingAvatar: self.updatingAvatar,
updatingBio: self.updatingBio, updatingBio: self.updatingBio,
avatarUploadProgress: self.avatarUploadProgress, avatarUploadProgress: self.avatarUploadProgress,
highlightedButton: highlightedButton highlightedButton: highlightedButton,
isEditingBirthDate: self.isEditingBirthDate,
updatingBirthDate: self.updatingBirthDate
)
}
func withIsEditingBirthDate(_ isEditingBirthDate: Bool) -> PeerInfoState {
return PeerInfoState(
isEditing: self.isEditing,
selectedMessageIds: self.selectedMessageIds,
updatingAvatar: self.updatingAvatar,
updatingBio: self.updatingBio,
avatarUploadProgress: self.avatarUploadProgress,
highlightedButton: self.highlightedButton,
isEditingBirthDate: isEditingBirthDate,
updatingBirthDate: self.updatingBirthDate
)
}
func withUpdatingBirthDate(_ updatingBirthDate: BirthdayPickerComponent.BirthDate?) -> PeerInfoState {
return PeerInfoState(
isEditing: self.isEditing,
selectedMessageIds: self.selectedMessageIds,
updatingAvatar: self.updatingAvatar,
updatingBio: self.updatingBio,
avatarUploadProgress: self.avatarUploadProgress,
highlightedButton: self.highlightedButton,
isEditingBirthDate: self.isEditingBirthDate,
updatingBirthDate: updatingBirthDate
) )
} }
} }

View File

@ -583,6 +583,8 @@ private final class PeerInfoInteraction {
let openPeerMention: (String, ChatControllerInteractionNavigateToPeer) -> Void let openPeerMention: (String, ChatControllerInteractionNavigateToPeer) -> Void
let openBotApp: (AttachMenuBot) -> Void let openBotApp: (AttachMenuBot) -> Void
let openEditing: () -> Void let openEditing: () -> Void
let updateBirthDate: (BirthdayPickerComponent.BirthDate?) -> Void
let updateIsEditingBirthdate: (Bool) -> Void
init( init(
openUsername: @escaping (String, Bool, Promise<Bool>?) -> Void, openUsername: @escaping (String, Bool, Promise<Bool>?) -> Void,
@ -637,7 +639,9 @@ private final class PeerInfoInteraction {
displayTopicsLimited: @escaping (TopicsLimitedReason) -> Void, displayTopicsLimited: @escaping (TopicsLimitedReason) -> Void,
openPeerMention: @escaping (String, ChatControllerInteractionNavigateToPeer) -> Void, openPeerMention: @escaping (String, ChatControllerInteractionNavigateToPeer) -> Void,
openBotApp: @escaping (AttachMenuBot) -> Void, openBotApp: @escaping (AttachMenuBot) -> Void,
openEditing: @escaping () -> Void openEditing: @escaping () -> Void,
updateBirthDate: @escaping (BirthdayPickerComponent.BirthDate?) -> Void,
updateIsEditingBirthdate: @escaping (Bool) -> Void
) { ) {
self.openUsername = openUsername self.openUsername = openUsername
self.openPhone = openPhone self.openPhone = openPhone
@ -692,6 +696,8 @@ private final class PeerInfoInteraction {
self.openPeerMention = openPeerMention self.openPeerMention = openPeerMention
self.openBotApp = openBotApp self.openBotApp = openBotApp
self.openEditing = openEditing self.openEditing = openEditing
self.updateBirthDate = updateBirthDate
self.updateIsEditingBirthdate = updateIsEditingBirthdate
} }
} }
@ -979,6 +985,7 @@ private func settingsEditingItems(data: PeerInfoScreenData?, state: PeerInfoStat
enum Section: Int, CaseIterable { enum Section: Int, CaseIterable {
case help case help
case bio case bio
case birthday
case info case info
case account case account
case logout case logout
@ -998,6 +1005,10 @@ private func settingsEditingItems(data: PeerInfoScreenData?, state: PeerInfoStat
let ItemAddAccountHelp = 6 let ItemAddAccountHelp = 6
let ItemLogout = 7 let ItemLogout = 7
let ItemPeerColor = 8 let ItemPeerColor = 8
let ItemBirthday = 9
let ItemBirthdayPicker = 10
let ItemBirthdayRemove = 11
let ItemBirthdayHelp = 12
items[.help]!.append(PeerInfoScreenCommentItem(id: ItemNameHelp, text: presentationData.strings.EditProfile_NameAndPhotoOrVideoHelp)) items[.help]!.append(PeerInfoScreenCommentItem(id: ItemNameHelp, text: presentationData.strings.EditProfile_NameAndPhotoOrVideoHelp))
@ -1010,6 +1021,71 @@ private func settingsEditingItems(data: PeerInfoScreenData?, state: PeerInfoStat
items[.bio]!.append(PeerInfoScreenCommentItem(id: ItemBioHelp, text: presentationData.strings.Settings_About_Help)) items[.bio]!.append(PeerInfoScreenCommentItem(id: ItemBioHelp, text: presentationData.strings.Settings_About_Help))
} }
//TODO:localize
var birthDateString: String
if let updatingBirthDate = state.updatingBirthDate {
var components: [String] = []
components.append("\(updatingBirthDate.day)")
let month: String
switch updatingBirthDate.month {
case 1:
month = "Jan"
case 2:
month = "Feb"
case 3:
month = "Mar"
case 4:
month = "Apr"
case 5:
month = "May"
case 6:
month = "Jun"
case 7:
month = "Jul"
case 8:
month = "Aug"
case 9:
month = "Sep"
case 10:
month = "Oct"
case 11:
month = "Nov"
case 12:
month = "Dec"
default:
month = ""
}
components.append(month)
if let year = updatingBirthDate.year {
components.append("\(year)")
}
birthDateString = components.joined(separator: " ")
} else {
birthDateString = "Add"
}
let isEditingBirthDate = state.isEditingBirthDate
items[.birthday]!.append(PeerInfoScreenDisclosureItem(id: ItemBirthday, label: .coloredText(birthDateString, isEditingBirthDate ? .accent : .generic), text: "Date of Birth", icon: nil, hasArrow: false, action: {
if !isEditingBirthDate {
interaction.updateBirthDate(BirthdayPickerComponent.BirthDate(year: nil, month: 1, day: 1))
}
interaction.updateIsEditingBirthdate(!isEditingBirthDate)
}))
if isEditingBirthDate, let birthDate = state.updatingBirthDate {
items[.birthday]!.append(PeerInfoScreenBirthdatePickerItem(id: ItemBirthdayPicker, value: birthDate, valueUpdated: { value in
interaction.updateBirthDate(value)
}))
items[.birthday]!.append(PeerInfoScreenActionItem(id: ItemBirthdayRemove, text: "Remove Date of Birth", alignment: .natural, action: {
interaction.updateBirthDate(nil)
interaction.updateIsEditingBirthdate(false)
}))
}
items[.birthday]!.append(PeerInfoScreenCommentItem(id: ItemBirthdayHelp, text: "Date of birth is only visible to your contacts."))
if let user = data.peer as? TelegramUser { if let user = data.peer as? TelegramUser {
items[.info]!.append(PeerInfoScreenDisclosureItem(id: ItemPhoneNumber, label: .text(user.phone.flatMap({ formatPhoneNumber(context: context, number: $0) }) ?? ""), text: presentationData.strings.Settings_PhoneNumber, action: { items[.info]!.append(PeerInfoScreenDisclosureItem(id: ItemPhoneNumber, label: .text(user.phone.flatMap({ formatPhoneNumber(context: context, number: $0) }) ?? ""), text: presentationData.strings.Settings_PhoneNumber, action: {
interaction.openSettings(.phoneNumber) interaction.openSettings(.phoneNumber)
@ -2297,7 +2373,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
updatingAvatar: nil, updatingAvatar: nil,
updatingBio: nil, updatingBio: nil,
avatarUploadProgress: nil, avatarUploadProgress: nil,
highlightedButton: nil highlightedButton: nil,
isEditingBirthDate: false,
updatingBirthDate: nil
) )
private var forceIsContactPromise = ValuePromise<Bool>(false) private var forceIsContactPromise = ValuePromise<Bool>(false)
private let nearbyPeerDistance: Int32? private let nearbyPeerDistance: Int32?
@ -2566,6 +2644,22 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}, },
openEditing: { [weak self] in openEditing: { [weak self] in
self?.headerNode.navigationButtonContainer.performAction?(.edit, nil, nil) self?.headerNode.navigationButtonContainer.performAction?(.edit, nil, nil)
},
updateBirthDate: { [weak self] birthDate in
if let self {
self.state = self.state.withUpdatingBirthDate(birthDate)
if let (layout, navigationHeight) = self.validLayout {
self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false)
}
}
},
updateIsEditingBirthdate: { [weak self] value in
if let self {
self.state = self.state.withIsEditingBirthDate(value)
if let (layout, navigationHeight) = self.validLayout {
self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.2, curve: .easeInOut), additive: false)
}
}
} }
) )

View File

@ -287,8 +287,9 @@ private final class ImportStickerPackTitleInputFieldNode: ASDisplayNode, UITextF
super.init() super.init()
self.addSubnode(self.backgroundNode) self.addSubnode(self.backgroundNode)
if hasClearButton {
self.addSubnode(self.clearButton) self.addSubnode(self.clearButton)
}
self.clearButton.addTarget(self, action: #selector(self.clearPressed), forControlEvents: .touchUpInside) self.clearButton.addTarget(self, action: #selector(self.clearPressed), forControlEvents: .touchUpInside)
} }

View File

@ -15979,6 +15979,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return return
} }
//TODO:localize
let peerName = peer.compactDisplayTitle let peerName = peer.compactDisplayTitle
let text = "🎂 \(peerName) is having a birthday today. You can give \(peerName) **Telegram Premium** as a birthday gift." let text = "🎂 \(peerName) is having a birthday today. You can give \(peerName) **Telegram Premium** as a birthday gift."

View File

@ -1720,22 +1720,31 @@ extension ChatControllerImpl {
context: self.context, context: self.context,
getSourceRect: { return .zero }, getSourceRect: { return .zero },
completion: { [weak self] result, transitionView, transitionRect, transitionImage, transitionOut, dismissed in completion: { [weak self] result, transitionView, transitionRect, transitionImage, transitionOut, dismissed in
guard let self, let asset = result as? PHAsset else { guard let self else {
return return
} }
let subject: Signal<MediaEditorScreen.Subject?, NoError>
if let asset = result as? PHAsset {
subject = .single(.asset(asset))
} else if let image = result as? UIImage {
subject = .single(.image(image, PixelDimensions(image.size), nil, .bottomRight))
} else {
subject = .single(.empty(PixelDimensions(width: 1080, height: 1920)))
}
let editorController = MediaEditorScreen( let editorController = MediaEditorScreen(
context: self.context, context: self.context,
mode: .stickerEditor(mode: .generic), mode: .stickerEditor(mode: .generic),
subject: .single(.asset(asset)), subject: subject,
transitionIn: .gallery( transitionIn: transitionView.flatMap({ .gallery(
MediaEditorScreen.TransitionIn.GalleryTransitionIn( MediaEditorScreen.TransitionIn.GalleryTransitionIn(
sourceView: transitionView, sourceView: $0,
sourceRect: transitionRect, sourceRect: transitionRect,
sourceImage: transitionImage sourceImage: transitionImage
) )
), ) }),
transitionOut: { finished, isNew in transitionOut: { finished, isNew in
if !finished { if !finished, let transitionView {
return MediaEditorScreen.TransitionOut( return MediaEditorScreen.TransitionOut(
destinationView: transitionView, destinationView: transitionView,
destinationRect: transitionView.bounds, destinationRect: transitionView.bounds,

View File

@ -164,6 +164,29 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
strongSelf.requestLayout(transition: .immediate) strongSelf.requestLayout(transition: .immediate)
} }
}) })
case let .premiumGifting(_, topSectionPeers):
if !topSectionPeers.isEmpty {
let _ = (self.context.engine.data.get(
EngineDataList(
topSectionPeers.map(TelegramEngine.EngineData.Item.Peer.Peer.init)
)
)
|> deliverOnMainQueue).startStandalone(next: { [weak self] peerList in
guard let strongSelf = self else {
return
}
let peers = peerList.compactMap { $0 }
strongSelf.contactsNode.editableTokens.append(contentsOf: peers.map { peer -> EditableTokenListToken in
return EditableTokenListToken(id: peer.id, title: peerTokenTitle(accountPeerId: params.context.account.peerId, peer: peer._asPeer(), strings: strongSelf.presentationData.strings, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder), fixedPosition: nil, subject: .peer(peer))
})
strongSelf._peersReady.set(.single(true))
if strongSelf.isNodeLoaded {
strongSelf.requestLayout(transition: .immediate)
}
})
} else {
self._peersReady.set(.single(true))
}
default: default:
self._peersReady.set(.single(true)) self._peersReady.set(.single(true))
} }

View File

@ -170,9 +170,11 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
self.contentNode = .chats(chatListNode) self.contentNode = .chats(chatListNode)
} else { } else {
let displayTopPeers: ContactListPresentation.TopPeers let displayTopPeers: ContactListPresentation.TopPeers
var selectedPeers: [EnginePeer.Id] = []
if case let .premiumGifting(topSectionTitle, topSectionPeers) = mode { if case let .premiumGifting(topSectionTitle, topSectionPeers) = mode {
if let topSectionTitle { if let topSectionTitle, !topSectionPeers.isEmpty {
displayTopPeers = .custom(title: topSectionTitle, peerIds: topSectionPeers) displayTopPeers = .custom(title: topSectionTitle, peerIds: topSectionPeers)
selectedPeers = topSectionPeers
} else { } else {
displayTopPeers = .recent displayTopPeers = .recent
} }
@ -183,6 +185,16 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
} }
let contactListNode = ContactListNode(context: context, presentation: .single(.natural(options: options, includeChatList: includeChatList, topPeers: displayTopPeers)), filters: filters, onlyWriteable: onlyWriteable, selectionState: ContactListNodeGroupSelectionState()) let contactListNode = ContactListNode(context: context, presentation: .single(.natural(options: options, includeChatList: includeChatList, topPeers: displayTopPeers)), filters: filters, onlyWriteable: onlyWriteable, selectionState: ContactListNodeGroupSelectionState())
self.contentNode = .contacts(contactListNode) self.contentNode = .contacts(contactListNode)
if !selectedPeers.isEmpty {
contactListNode.updateSelectionState { state in
var state = state ?? ContactListNodeGroupSelectionState()
for peerId in selectedPeers {
state = state.withToggledPeerId(.peer(peerId))
}
return state
}
}
} }
self.tokenListNode = EditableTokenListNode(context: self.context, presentationTheme: self.presentationData.theme, theme: EditableTokenListNodeTheme(backgroundColor: .clear, separatorColor: self.presentationData.theme.rootController.navigationBar.separatorColor, placeholderTextColor: self.presentationData.theme.list.itemPlaceholderTextColor, primaryTextColor: self.presentationData.theme.list.itemPrimaryTextColor, tokenBackgroundColor: self.presentationData.theme.list.itemCheckColors.strokeColor.withAlphaComponent(0.25), selectedTextColor: self.presentationData.theme.list.itemCheckColors.foregroundColor, selectedBackgroundColor: self.presentationData.theme.list.itemCheckColors.fillColor, accentColor: self.presentationData.theme.list.itemAccentColor, keyboardColor: self.presentationData.theme.rootController.keyboardColor), placeholder: placeholder, shortPlaceholder: shortPlaceholder) self.tokenListNode = EditableTokenListNode(context: self.context, presentationTheme: self.presentationData.theme, theme: EditableTokenListNodeTheme(backgroundColor: .clear, separatorColor: self.presentationData.theme.rootController.navigationBar.separatorColor, placeholderTextColor: self.presentationData.theme.list.itemPlaceholderTextColor, primaryTextColor: self.presentationData.theme.list.itemPrimaryTextColor, tokenBackgroundColor: self.presentationData.theme.list.itemCheckColors.strokeColor.withAlphaComponent(0.25), selectedTextColor: self.presentationData.theme.list.itemCheckColors.foregroundColor, selectedBackgroundColor: self.presentationData.theme.list.itemCheckColors.fillColor, accentColor: self.presentationData.theme.list.itemAccentColor, keyboardColor: self.presentationData.theme.rootController.keyboardColor), placeholder: placeholder, shortPlaceholder: shortPlaceholder)

View File

@ -2113,6 +2113,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
let mode: ContactMultiselectionControllerMode let mode: ContactMultiselectionControllerMode
if case let .chatList(peerIds) = source { if case let .chatList(peerIds) = source {
//TODO:localize
mode = .premiumGifting(topSectionTitle: "🎂 BIRTHDAY TODAY", topSectionPeers: peerIds) mode = .premiumGifting(topSectionTitle: "🎂 BIRTHDAY TODAY", topSectionPeers: peerIds)
} else { } else {
mode = .premiumGifting(topSectionTitle: nil, topSectionPeers: []) mode = .premiumGifting(topSectionTitle: nil, topSectionPeers: [])
@ -2317,7 +2318,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return StickerPackScreen(context: context, updatedPresentationData: updatedPresentationData, mainStickerPack: mainStickerPack, stickerPacks: stickerPacks, loadedStickerPacks: loadedStickerPacks, isEditing: isEditing, parentNavigationController: parentNavigationController, sendSticker: sendSticker) return StickerPackScreen(context: context, updatedPresentationData: updatedPresentationData, mainStickerPack: mainStickerPack, stickerPacks: stickerPacks, loadedStickerPacks: loadedStickerPacks, isEditing: isEditing, parentNavigationController: parentNavigationController, sendSticker: sendSticker)
} }
public func makeStickerEditorScreen(context: AccountContext, source: Any, transitionArguments: (UIView, CGRect, UIImage?)?, completion: @escaping (TelegramMediaFile, @escaping () -> Void) -> Void) -> ViewController { public func makeStickerEditorScreen(context: AccountContext, source: Any?, transitionArguments: (UIView, CGRect, UIImage?)?, completion: @escaping (TelegramMediaFile, @escaping () -> Void) -> Void) -> ViewController {
let subject: MediaEditorScreen.Subject let subject: MediaEditorScreen.Subject
let mode: MediaEditorScreen.Mode.StickerEditorMode let mode: MediaEditorScreen.Mode.StickerEditorMode
if let file = source as? TelegramMediaFile { if let file = source as? TelegramMediaFile {
@ -2326,8 +2327,12 @@ public final class SharedAccountContextImpl: SharedAccountContext {
} else if let asset = source as? PHAsset { } else if let asset = source as? PHAsset {
subject = .asset(asset) subject = .asset(asset)
mode = .addingToPack mode = .addingToPack
} else if let image = source as? UIImage {
subject = .image(image, PixelDimensions(image.size), nil, .bottomRight)
mode = .addingToPack
} else { } else {
fatalError() subject = .empty(PixelDimensions(width: 1080, height: 1920))
mode = .addingToPack
} }
let controller = MediaEditorScreen( let controller = MediaEditorScreen(
context: context, context: context,
@ -2368,7 +2373,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return storyMediaPickerController(context: context, getSourceRect: getSourceRect, completion: completion, dismissed: dismissed, groupsPresented: groupsPresented) return storyMediaPickerController(context: context, getSourceRect: getSourceRect, completion: completion, dismissed: dismissed, groupsPresented: groupsPresented)
} }
public func makeStickerMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> ViewController { public func makeStickerMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any?, UIView?, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> ViewController {
return stickerMediaPickerController(context: context, getSourceRect: getSourceRect, completion: completion, dismissed: dismissed) return stickerMediaPickerController(context: context, getSourceRect: getSourceRect, completion: completion, dismissed: dismissed)
} }

View File

@ -49,10 +49,11 @@ CONFIGURE_FLAGS="--enable-cross-compile --disable-programs \
--enable-audiotoolbox \ --enable-audiotoolbox \
--enable-bsf=aac_adtstoasc \ --enable-bsf=aac_adtstoasc \
--enable-decoder=h264,libvpx_vp9,hevc,libopus,mp3,aac,flac,alac_at,pcm_s16le,pcm_s24le,gsm_ms_at \ --enable-decoder=h264,libvpx_vp9,hevc,libopus,mp3,aac,flac,alac_at,pcm_s16le,pcm_s24le,gsm_ms_at \
--enable-encoder=libvpx_vp9 \
--enable-demuxer=aac,mov,m4v,mp3,ogg,libopus,flac,wav,aiff,matroska \ --enable-demuxer=aac,mov,m4v,mp3,ogg,libopus,flac,wav,aiff,matroska \
--enable-parser=aac,h264,mp3,libopus \ --enable-parser=aac,h264,mp3,libopus \
--enable-protocol=file \ --enable-protocol=file \
--enable-muxer=mp4 \ --enable-muxer=mp4,matroska \
" "