mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-08-08 08:31:13 +00:00
[WIP] Stickers
This commit is contained in:
parent
db1d4422cb
commit
bb1b425217
@ -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
|
||||||
|
@ -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 {
|
||||||
|
@ -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 {
|
||||||
|
14
submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegVideoWriter.h
Executable file
14
submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegVideoWriter.h
Executable 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
|
147
submodules/FFMpegBinding/Sources/FFMpegVideoWriter.m
Executable file
147
submodules/FFMpegBinding/Sources/FFMpegVideoWriter.m
Executable 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
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
items.append(.action(ContextMenuActionItem(text: "Reorder", icon: { theme in
|
if packItems.count > 0 {
|
||||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor)
|
items.append(.action(ContextMenuActionItem(text: "Reorder", icon: { theme in
|
||||||
}, action: { [weak self] _, f in
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor)
|
||||||
f(.default)
|
}, action: { [weak self] _, f in
|
||||||
self?.updateIsEditing(true)
|
f(.default)
|
||||||
})))
|
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(
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -2795,35 +2795,36 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior {
|
|||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
menuItems.append(
|
|
||||||
.action(ContextMenuActionItem(text: presentationData.strings.StickerPack_ViewPack, icon: { theme in
|
loop: for attribute in file.attributes {
|
||||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Sticker"), color: theme.actionSheet.primaryTextColor)
|
switch attribute {
|
||||||
}, action: { _, f in
|
case let .CustomEmoji(_, _, _, packReference), let .Sticker(_, packReference, _):
|
||||||
f(.default)
|
if let packReference = packReference {
|
||||||
|
menuItems.append(
|
||||||
guard let strongSelf = self else {
|
.action(ContextMenuActionItem(text: presentationData.strings.StickerPack_ViewPack, icon: { theme in
|
||||||
return
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Sticker"), color: theme.actionSheet.primaryTextColor)
|
||||||
}
|
}, action: { _, f in
|
||||||
|
f(.default)
|
||||||
loop: for attribute in file.attributes {
|
|
||||||
switch attribute {
|
guard let strongSelf = self else {
|
||||||
case let .CustomEmoji(_, _, _, packReference), let .Sticker(_, packReference, _):
|
return
|
||||||
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
|
|
||||||
sendSticker(file, false, false, nil, false, sourceView, sourceRect, nil)
|
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
|
||||||
return true
|
sendSticker(file, false, false, nil, false, sourceView, sourceRect, nil)
|
||||||
})
|
return true
|
||||||
|
})
|
||||||
interaction.navigationController()?.view.window?.endEditing(true)
|
|
||||||
interaction.presentController(controller, nil)
|
interaction.navigationController()?.view.window?.endEditing(true)
|
||||||
}
|
interaction.presentController(controller, nil)
|
||||||
break loop
|
}))
|
||||||
default:
|
)
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
break loop
|
||||||
|
default:
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}))
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let view = view else {
|
guard let view = view else {
|
||||||
|
@ -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
|
||||||
|
@ -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?
|
||||||
|
}
|
@ -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]
|
||||||
AVFormatIDKey: kAudioFormatMPEG4AAC,
|
if isSticker {
|
||||||
AVSampleRateKey: 44100,
|
audioSettings = [:]
|
||||||
AVEncoderBitRateKey: audioBitrate * 1000,
|
} else {
|
||||||
AVNumberOfChannelsKey: audioNumberOfChannels
|
audioSettings = [
|
||||||
]
|
AVFormatIDKey: kAudioFormatMPEG4AAC,
|
||||||
|
AVSampleRateKey: 44100,
|
||||||
|
AVEncoderBitRateKey: audioBitrate * 1000,
|
||||||
|
AVNumberOfChannelsKey: audioNumberOfChannels
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
return MediaEditorVideoExport.Configuration(
|
return MediaEditorVideoExport.Configuration(
|
||||||
videoSettings: videoSettings,
|
videoSettings: videoSettings,
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.writer = MediaEditorVideoAVAssetWriter()
|
if let codec = self.configuration.videoSettings[AVVideoCodecKey] as? String, codec == "VP9" {
|
||||||
|
self.writer = MediaEditorFFMpegWriter()
|
||||||
|
} else {
|
||||||
|
self.writer = MediaEditorVideoAVAssetWriter()
|
||||||
|
}
|
||||||
|
|
||||||
guard let writer = self.writer else {
|
guard let writer = self.writer else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -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, _, _, _):
|
||||||
|
@ -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))
|
||||||
Queue.concurrentDefaultQueue().async {
|
|
||||||
if let data = try? WebP.convert(toWebP: image, quality: 97.0) {
|
var isVideo = false
|
||||||
self.context.account.postbox.mediaBox.storeResourceData(resource.id, data: data)
|
if mediaEditor.resultIsVideo {
|
||||||
|
isVideo = true
|
||||||
|
self.performSave(toStickerResource: resource)
|
||||||
|
} else {
|
||||||
|
Queue.concurrentDefaultQueue().async {
|
||||||
|
if let data = try? WebP.convert(toWebP: image, quality: 97.0) {
|
||||||
|
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
|
||||||
|
|
||||||
self.previousSavedValues = mediaEditor.values
|
if !isSticker {
|
||||||
self.isSavingAvailable = false
|
self.previousSavedValues = mediaEditor.values
|
||||||
self.requestLayout(transition: .animated(duration: 0.25, curve: .easeInOut))
|
self.isSavingAvailable = false
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -287,8 +287,9 @@ private final class ImportStickerPackTitleInputFieldNode: ASDisplayNode, UITextF
|
|||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
self.addSubnode(self.backgroundNode)
|
self.addSubnode(self.backgroundNode)
|
||||||
self.addSubnode(self.clearButton)
|
if hasClearButton {
|
||||||
|
self.addSubnode(self.clearButton)
|
||||||
|
}
|
||||||
self.clearButton.addTarget(self, action: #selector(self.clearPressed), forControlEvents: .touchUpInside)
|
self.clearButton.addTarget(self, action: #selector(self.clearPressed), forControlEvents: .touchUpInside)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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."
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 \
|
||||||
"
|
"
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user