mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 21:45:19 +00:00
Merge commit '755b3dbd48c58c866b564cf3ce4fe7dc905c22d1'
This commit is contained in:
commit
0fc64aa505
@ -1001,9 +1001,9 @@ public protocol SharedAccountContext: AnyObject {
|
||||
|
||||
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 makeStickerPickerScreen(context: AccountContext, inputData: Promise<StickerPickerInput>, completion: @escaping (TelegramMediaFile) -> Void) -> ViewController
|
||||
|
@ -801,6 +801,14 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
public func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
|
||||
guard let lastController = self.viewControllers.last as? ViewController else {
|
||||
return
|
||||
}
|
||||
lastController.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: error.localizedDescription, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
}
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
public func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
|
||||
return self.view.window!
|
||||
|
@ -1919,7 +1919,15 @@ public final class ChatListNode: ListView {
|
||||
}
|
||||
}
|
||||
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) {
|
||||
return .single(.xmasPremiumGift)
|
||||
} else if suggestions.contains(.annualPremium) || suggestions.contains(.upgradePremium) || suggestions.contains(.restorePremium), let inAppPurchaseManager = context.inAppPurchaseManager {
|
||||
|
@ -3,13 +3,14 @@ import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import ListSectionHeaderNode
|
||||
import AppBundle
|
||||
import ItemListUI
|
||||
import Markdown
|
||||
import AccountContext
|
||||
import TelegramCore
|
||||
import MergedAvatarsNode
|
||||
|
||||
class ChatListStorageInfoItem: ListViewItem {
|
||||
enum Action {
|
||||
@ -90,6 +91,8 @@ class ChatListStorageInfoItemNode: ItemListRevealOptionsItemNode {
|
||||
private let arrowNode: ASImageNode
|
||||
private let separatorNode: ASDisplayNode
|
||||
|
||||
private var avatarsNode: MergedAvatarsNode?
|
||||
|
||||
private var closeButton: HighlightableButtonNode?
|
||||
|
||||
private var okButtonText: TextNode?
|
||||
@ -168,6 +171,7 @@ class ChatListStorageInfoItemNode: ItemListRevealOptionsItemNode {
|
||||
|
||||
let titleString: NSAttributedString
|
||||
let textString: NSAttributedString
|
||||
var avatarPeers: [EnginePeer] = []
|
||||
|
||||
var okButtonLayout: (TextNodeLayout, () -> TextNode)?
|
||||
var cancelButtonLayout: (TextNodeLayout, () -> TextNode)?
|
||||
@ -229,14 +233,15 @@ class ChatListStorageInfoItemNode: ItemListRevealOptionsItemNode {
|
||||
let title: String
|
||||
let text: String
|
||||
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."
|
||||
} else {
|
||||
title = "\(peers.count) contacts have [birthdays]() today! 🎂"
|
||||
title = "\(peers.count) contacts have **birthdays** today! 🎂"
|
||||
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 }))
|
||||
textString = NSAttributedString(string: text, font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor)
|
||||
avatarPeers = Array(peers.prefix(3))
|
||||
case let .reviewLogin(newSessionReview, totalCount):
|
||||
spacing = 2.0
|
||||
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)))
|
||||
}
|
||||
|
||||
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)
|
||||
if let okButtonLayout {
|
||||
@ -279,7 +290,7 @@ class ChatListStorageInfoItemNode: ItemListRevealOptionsItemNode {
|
||||
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)
|
||||
} 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()
|
||||
@ -287,7 +298,26 @@ class ChatListStorageInfoItemNode: ItemListRevealOptionsItemNode {
|
||||
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)
|
||||
} 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 {
|
||||
|
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 createFromScratch: () -> Void = {}
|
||||
public var presentFilePicker: () -> Void = {}
|
||||
|
||||
private var completed = false
|
||||
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 {
|
||||
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 {
|
||||
// self.navigationItem.rightBarButtonItem = UIBarButtonItem(customDisplayNode: self.moreButtonNode)
|
||||
// self.navigationItem.rightBarButtonItem?.action = #selector(self.rightButtonPressed)
|
||||
@ -1993,7 +2001,9 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
self.selectionCount = count
|
||||
|
||||
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.segmentsHidden = true
|
||||
moreIsVisible = true
|
||||
@ -2181,6 +2191,33 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
}
|
||||
|
||||
@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 {
|
||||
case .search:
|
||||
// self.presentSearch(activateOnDisplay: true)
|
||||
@ -2611,7 +2648,7 @@ public func storyMediaPickerController(
|
||||
public func stickerMediaPickerController(
|
||||
context: AccountContext,
|
||||
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
|
||||
) -> ViewController {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with({ $0 })
|
||||
@ -2621,7 +2658,7 @@ public func stickerMediaPickerController(
|
||||
})
|
||||
controller.forceSourceRect = true
|
||||
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)
|
||||
mediaPickerController.customSelection = { controller, result in
|
||||
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)
|
||||
}
|
||||
|
@ -679,6 +679,11 @@ class PrivacyAndSecurityControllerImpl: ItemListController, ASAuthorizationContr
|
||||
self.authorizationCompletion?(authorization.credential)
|
||||
}
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
public func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
|
||||
Logger.shared.log("AppleSignIn", "Failed with error: \(error.localizedDescription)")
|
||||
}
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
public func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
|
||||
return self.view.window!
|
||||
|
@ -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
|
||||
items.append(.separator)
|
||||
items.append(.action(ContextMenuActionItem(text: "Reorder", icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] _, f in
|
||||
f(.default)
|
||||
self?.updateIsEditing(true)
|
||||
})))
|
||||
if packItems.count > 0 {
|
||||
items.append(.action(ContextMenuActionItem(text: "Reorder", icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] _, f in
|
||||
f(.default)
|
||||
self?.updateIsEditing(true)
|
||||
})))
|
||||
}
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: "Edit Name", icon: { theme in
|
||||
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(
|
||||
context: context,
|
||||
source: result,
|
||||
transitionArguments: (transitionView, transitionRect, transitionImage),
|
||||
transitionArguments: transitionView.flatMap { ($0, transitionRect, transitionImage) },
|
||||
completion: { file, commit in
|
||||
dismissImpl?()
|
||||
let sticker = ImportSticker(
|
||||
|
@ -1325,7 +1325,7 @@ public extension Api {
|
||||
return parser(reader)
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -38,9 +38,10 @@ public func getServerProvidedSuggestions(account: Account) -> Signal<[ServerProv
|
||||
return []
|
||||
}
|
||||
|
||||
let list = listItems
|
||||
// var list = listItems
|
||||
// list.append(ServerProvidedSuggestion.setupBirthday.rawValue)
|
||||
|
||||
return list.compactMap { item -> ServerProvidedSuggestion? in
|
||||
return listItems.compactMap { item -> ServerProvidedSuggestion? in
|
||||
return ServerProvidedSuggestion(rawValue: item)
|
||||
}.filter { !dismissedSuggestions.contains($0) }
|
||||
}
|
||||
|
@ -2795,35 +2795,36 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior {
|
||||
})
|
||||
}))
|
||||
)
|
||||
menuItems.append(
|
||||
.action(ContextMenuActionItem(text: presentationData.strings.StickerPack_ViewPack, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Sticker"), color: theme.actionSheet.primaryTextColor)
|
||||
}, action: { _, f in
|
||||
f(.default)
|
||||
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
loop: for attribute in file.attributes {
|
||||
switch attribute {
|
||||
case let .CustomEmoji(_, _, _, packReference), let .Sticker(_, packReference, _):
|
||||
if let packReference = packReference {
|
||||
let controller = strongSelf.context.sharedContext.makeStickerPackScreen(context: context, updatedPresentationData: nil, mainStickerPack: packReference, stickerPacks: [packReference], loadedStickerPacks: [], isEditing: false, parentNavigationController: interaction.navigationController(), sendSticker: { file, sourceView, sourceRect in
|
||||
sendSticker(file, false, false, nil, false, sourceView, sourceRect, nil)
|
||||
return true
|
||||
})
|
||||
|
||||
interaction.navigationController()?.view.window?.endEditing(true)
|
||||
interaction.presentController(controller, nil)
|
||||
}
|
||||
break loop
|
||||
default:
|
||||
break
|
||||
|
||||
loop: for attribute in file.attributes {
|
||||
switch attribute {
|
||||
case let .CustomEmoji(_, _, _, packReference), let .Sticker(_, packReference, _):
|
||||
if let packReference = packReference {
|
||||
menuItems.append(
|
||||
.action(ContextMenuActionItem(text: presentationData.strings.StickerPack_ViewPack, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Sticker"), color: theme.actionSheet.primaryTextColor)
|
||||
}, action: { _, f in
|
||||
f(.default)
|
||||
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
return true
|
||||
})
|
||||
|
||||
interaction.navigationController()?.view.window?.endEditing(true)
|
||||
interaction.presentController(controller, nil)
|
||||
}))
|
||||
)
|
||||
}
|
||||
break loop
|
||||
default:
|
||||
break
|
||||
}
|
||||
}))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
guard let view = view else {
|
||||
|
@ -986,7 +986,11 @@ private final class GroupHeaderLayer: UIView {
|
||||
|
||||
var textConstrainedWidth = constrainedSize.width - titleHorizontalOffset - 10.0
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
||||
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 codecType: AVVideoCodecType
|
||||
let codecType: Any
|
||||
|
||||
var videoBitrate: Int = 3700
|
||||
var audioBitrate: Int = 64
|
||||
@ -1548,6 +1548,7 @@ public func recommendedVideoExportConfiguration(values: MediaEditorValues, durat
|
||||
let height: Int
|
||||
|
||||
var useHEVC = hasHEVCHardwareEncoder
|
||||
var useVP9 = false
|
||||
if let qualityPreset = values.qualityPreset {
|
||||
let maxSize = CGSize(width: qualityPreset.maximumDimensions, height: qualityPreset.maximumDimensions)
|
||||
var resultSize = values.originalDimensions.cgSize
|
||||
@ -1566,7 +1567,11 @@ public func recommendedVideoExportConfiguration(values: MediaEditorValues, durat
|
||||
|
||||
useHEVC = false
|
||||
} else {
|
||||
if values.videoIsFullHd {
|
||||
if isSticker {
|
||||
width = 512
|
||||
height = 512
|
||||
useVP9 = true
|
||||
} else if values.videoIsFullHd {
|
||||
width = 1080
|
||||
height = 1920
|
||||
} else {
|
||||
@ -1575,7 +1580,10 @@ public func recommendedVideoExportConfiguration(values: MediaEditorValues, durat
|
||||
}
|
||||
}
|
||||
|
||||
if useHEVC {
|
||||
if useVP9 {
|
||||
codecType = "VP9"
|
||||
compressionProperties = [:]
|
||||
} else if useHEVC {
|
||||
codecType = AVVideoCodecType.hevc
|
||||
compressionProperties = [
|
||||
AVVideoAverageBitRateKey: videoBitrate * 1000,
|
||||
@ -1597,12 +1605,17 @@ public func recommendedVideoExportConfiguration(values: MediaEditorValues, durat
|
||||
AVVideoHeightKey: height
|
||||
]
|
||||
|
||||
let audioSettings: [String: Any] = [
|
||||
AVFormatIDKey: kAudioFormatMPEG4AAC,
|
||||
AVSampleRateKey: 44100,
|
||||
AVEncoderBitRateKey: audioBitrate * 1000,
|
||||
AVNumberOfChannelsKey: audioNumberOfChannels
|
||||
]
|
||||
let audioSettings: [String: Any]
|
||||
if isSticker {
|
||||
audioSettings = [:]
|
||||
} else {
|
||||
audioSettings = [
|
||||
AVFormatIDKey: kAudioFormatMPEG4AAC,
|
||||
AVSampleRateKey: 44100,
|
||||
AVEncoderBitRateKey: audioBitrate * 1000,
|
||||
AVNumberOfChannelsKey: audioNumberOfChannels
|
||||
]
|
||||
}
|
||||
|
||||
return MediaEditorVideoExport.Configuration(
|
||||
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 }
|
||||
}
|
||||
|
||||
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 enum Subject {
|
||||
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 {
|
||||
return
|
||||
}
|
||||
|
@ -163,6 +163,8 @@ extension MediaEditorScreen {
|
||||
}
|
||||
|
||||
switch subject {
|
||||
case .empty:
|
||||
break
|
||||
case let .image(image, dimensions, _, _):
|
||||
innerSaveDraft(media: .image(image: image, dimensions: dimensions))
|
||||
case let .video(path, _, _, _, _, dimensions, _, _, _):
|
||||
|
@ -3184,8 +3184,17 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if case .message = self.actualSubject, let layout = self.validLayout {
|
||||
self.layer.animatePosition(from: CGPoint(x: 0.0, y: layout.size.height), to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||
var animateIn = false
|
||||
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()
|
||||
} else if let view = self.componentHost.view as? MediaEditorScreenComponent.View {
|
||||
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 {
|
||||
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
|
||||
if let self {
|
||||
if let content {
|
||||
@ -4447,6 +4456,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
}
|
||||
|
||||
public enum Subject {
|
||||
case empty(PixelDimensions)
|
||||
case image(UIImage, PixelDimensions, UIImage?, PIPPosition)
|
||||
case video(String, UIImage?, Bool, String?, UIImage?, PixelDimensions, Double, [(Bool, Double)], PIPPosition)
|
||||
case asset(PHAsset)
|
||||
@ -4456,6 +4466,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
|
||||
var dimensions: PixelDimensions {
|
||||
switch self {
|
||||
case let .empty(dimensions):
|
||||
return dimensions
|
||||
case let .image(_, dimensions, _, _), let .video(_, _, _, _, _, dimensions, _, _, _):
|
||||
return dimensions
|
||||
case let .asset(asset):
|
||||
@ -4471,6 +4483,11 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
|
||||
var editorSubject: MediaEditor.Subject {
|
||||
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, _, _):
|
||||
return .image(image, dimensions)
|
||||
case let .video(videoPath, transitionImage, mirror, additionalVideoPath, _, dimensions, duration, _, _):
|
||||
@ -4492,6 +4509,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
|
||||
var isVideo: Bool {
|
||||
switch self {
|
||||
case .empty:
|
||||
return false
|
||||
case .image:
|
||||
return false
|
||||
case .video:
|
||||
@ -5401,6 +5420,18 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
var videoIsMirrored = false
|
||||
let duration: Double
|
||||
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, _, _, _):
|
||||
let tempImagePath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).jpg"
|
||||
if let data = image.jpegData(compressionQuality: 0.85) {
|
||||
@ -5684,8 +5715,6 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
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
|
||||
if let self, let resultImage {
|
||||
Logger.shared.log("MediaEditor", "Completed with image \(resultImage)")
|
||||
|
||||
let dimensions = CGSize(width: 512, height: 512)
|
||||
let scaledImage = generateImage(dimensions, contextGenerator: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
@ -5703,16 +5732,29 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func presentStickerPreview(image: UIImage) {
|
||||
guard let mediaEditor = self.node.mediaEditor else {
|
||||
return
|
||||
}
|
||||
|
||||
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) {
|
||||
self.context.account.postbox.mediaBox.storeResourceData(resource.id, data: data)
|
||||
|
||||
var isVideo = false
|
||||
if mediaEditor.resultIsVideo {
|
||||
isVideo = true
|
||||
self.performSave(toStickerResource: resource)
|
||||
} else {
|
||||
Queue.concurrentDefaultQueue().async {
|
||||
if let data = try? WebP.convert(toWebP: image, quality: 97.0) {
|
||||
self.context.account.postbox.mediaBox.storeResourceData(resource.id, data: data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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] = []
|
||||
if case let .stickerEditor(mode) = self.mode {
|
||||
@ -5931,7 +5973,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
case .progress:
|
||||
return .single(status)
|
||||
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 {
|
||||
case .addToFavorites:
|
||||
return context.engine.stickers.toggleStickerSaved(file: file, saved: true)
|
||||
@ -5997,7 +6039,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
|
||||
let result: MediaEditorScreen.Result
|
||||
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(
|
||||
media: .sticker(file: file),
|
||||
mediaAreas: [],
|
||||
@ -6061,12 +6103,13 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
if !authorized {
|
||||
return
|
||||
}
|
||||
self?.hapticFeedback.impact(.light)
|
||||
self?.performSave()
|
||||
})
|
||||
}
|
||||
|
||||
private func performSave() {
|
||||
guard let mediaEditor = self.node.mediaEditor, let subject = self.node.subject, self.isSavingAvailable else {
|
||||
private func performSave(toStickerResource: MediaResource? = nil) {
|
||||
guard let mediaEditor = self.node.mediaEditor, let subject = self.node.subject else {
|
||||
return
|
||||
}
|
||||
|
||||
@ -6076,14 +6119,17 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.node.entitiesView)
|
||||
mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities)
|
||||
|
||||
self.hapticFeedback.impact(.light)
|
||||
let isSticker = toStickerResource != nil
|
||||
|
||||
self.previousSavedValues = mediaEditor.values
|
||||
self.isSavingAvailable = false
|
||||
self.requestLayout(transition: .animated(duration: 0.25, curve: .easeInOut))
|
||||
if !isSticker {
|
||||
self.previousSavedValues = mediaEditor.values
|
||||
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 tempVideoPath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).\(fileExtension)"
|
||||
PHPhotoLibrary.shared().performChanges({
|
||||
if isVideo {
|
||||
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>
|
||||
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, _, _, _, _, _, _, _, _):
|
||||
let asset = AVURLAsset(url: NSURL(fileURLWithPath: path) as URL)
|
||||
exportSubject = .single(.video(asset: asset, isStory: true))
|
||||
@ -6171,8 +6222,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
if case let .video(video, _) = exportSubject {
|
||||
duration = video.duration.seconds
|
||||
}
|
||||
let configuration = recommendedVideoExportConfiguration(values: mediaEditor.values, duration: duration, forceFullHd: true, frameRate: 60.0)
|
||||
let outputPath = NSTemporaryDirectory() + "\(Int64.random(in: 0 ..< .max)).mp4"
|
||||
let configuration = recommendedVideoExportConfiguration(values: mediaEditor.values, duration: duration, forceFullHd: true, frameRate: 60.0, isSticker: isSticker)
|
||||
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)
|
||||
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] = []
|
||||
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(.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)
|
||||
}
|
||||
|
@ -1,8 +1,338 @@
|
||||
//
|
||||
// PeerInfoScreenBirthdatePickerItem.swift
|
||||
// MediaEditorScreen
|
||||
//
|
||||
// Created by Ilya Laktyushin on 15.03.2024.
|
||||
//
|
||||
|
||||
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 {
|
||||
enum Label {
|
||||
enum LabelColor {
|
||||
case generic
|
||||
case accent
|
||||
}
|
||||
|
||||
case none
|
||||
case text(String)
|
||||
case coloredText(String, LabelColor)
|
||||
case badge(String, UIColor)
|
||||
case semitransparentBadge(String, UIColor)
|
||||
case titleBadge(String, UIColor)
|
||||
@ -16,14 +22,14 @@ final class PeerInfoScreenDisclosureItem: PeerInfoScreenItem {
|
||||
switch self {
|
||||
case .none, .image:
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
var badgeColor: UIColor? {
|
||||
switch self {
|
||||
case .none, .text, .image:
|
||||
case .none, .text, .coloredText, .image:
|
||||
return nil
|
||||
case let .badge(_, color), let .semitransparentBadge(_, color), let .titleBadge(_, color):
|
||||
return color
|
||||
@ -159,6 +165,14 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode {
|
||||
} else if case .titleBadge = item.label {
|
||||
labelColorValue = presentationData.theme.list.itemCheckColors.foregroundColor
|
||||
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 {
|
||||
labelColorValue = presentationData.theme.list.itemSecondaryTextColor
|
||||
labelFont = titleFont
|
||||
|
@ -33,6 +33,8 @@ final class PeerInfoState {
|
||||
let updatingBio: String?
|
||||
let avatarUploadProgress: AvatarUploadProgress?
|
||||
let highlightedButton: PeerInfoHeaderButtonKey?
|
||||
let isEditingBirthDate: Bool
|
||||
let updatingBirthDate: BirthdayPickerComponent.BirthDate?
|
||||
|
||||
init(
|
||||
isEditing: Bool,
|
||||
@ -40,7 +42,9 @@ final class PeerInfoState {
|
||||
updatingAvatar: PeerInfoUpdatingAvatar?,
|
||||
updatingBio: String?,
|
||||
avatarUploadProgress: AvatarUploadProgress?,
|
||||
highlightedButton: PeerInfoHeaderButtonKey?
|
||||
highlightedButton: PeerInfoHeaderButtonKey?,
|
||||
isEditingBirthDate: Bool,
|
||||
updatingBirthDate: BirthdayPickerComponent.BirthDate?
|
||||
) {
|
||||
self.isEditing = isEditing
|
||||
self.selectedMessageIds = selectedMessageIds
|
||||
@ -48,6 +52,8 @@ final class PeerInfoState {
|
||||
self.updatingBio = updatingBio
|
||||
self.avatarUploadProgress = avatarUploadProgress
|
||||
self.highlightedButton = highlightedButton
|
||||
self.isEditingBirthDate = isEditingBirthDate
|
||||
self.updatingBirthDate = updatingBirthDate
|
||||
}
|
||||
|
||||
func withIsEditing(_ isEditing: Bool) -> PeerInfoState {
|
||||
@ -57,7 +63,9 @@ final class PeerInfoState {
|
||||
updatingAvatar: self.updatingAvatar,
|
||||
updatingBio: self.updatingBio,
|
||||
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,
|
||||
updatingBio: self.updatingBio,
|
||||
avatarUploadProgress: self.avatarUploadProgress,
|
||||
highlightedButton: self.highlightedButton
|
||||
highlightedButton: self.highlightedButton,
|
||||
isEditingBirthDate: self.isEditingBirthDate,
|
||||
updatingBirthDate: self.updatingBirthDate
|
||||
)
|
||||
}
|
||||
|
||||
@ -79,7 +89,9 @@ final class PeerInfoState {
|
||||
updatingAvatar: updatingAvatar,
|
||||
updatingBio: self.updatingBio,
|
||||
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,
|
||||
updatingBio: updatingBio,
|
||||
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,
|
||||
updatingBio: self.updatingBio,
|
||||
avatarUploadProgress: avatarUploadProgress,
|
||||
highlightedButton: self.highlightedButton
|
||||
highlightedButton: self.highlightedButton,
|
||||
isEditingBirthDate: self.isEditingBirthDate,
|
||||
updatingBirthDate: self.updatingBirthDate
|
||||
)
|
||||
}
|
||||
|
||||
@ -112,7 +128,35 @@ final class PeerInfoState {
|
||||
updatingAvatar: self.updatingAvatar,
|
||||
updatingBio: self.updatingBio,
|
||||
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 openBotApp: (AttachMenuBot) -> Void
|
||||
let openEditing: () -> Void
|
||||
let updateBirthDate: (BirthdayPickerComponent.BirthDate?) -> Void
|
||||
let updateIsEditingBirthdate: (Bool) -> Void
|
||||
|
||||
init(
|
||||
openUsername: @escaping (String, Bool, Promise<Bool>?) -> Void,
|
||||
@ -637,7 +639,9 @@ private final class PeerInfoInteraction {
|
||||
displayTopicsLimited: @escaping (TopicsLimitedReason) -> Void,
|
||||
openPeerMention: @escaping (String, ChatControllerInteractionNavigateToPeer) -> Void,
|
||||
openBotApp: @escaping (AttachMenuBot) -> Void,
|
||||
openEditing: @escaping () -> Void
|
||||
openEditing: @escaping () -> Void,
|
||||
updateBirthDate: @escaping (BirthdayPickerComponent.BirthDate?) -> Void,
|
||||
updateIsEditingBirthdate: @escaping (Bool) -> Void
|
||||
) {
|
||||
self.openUsername = openUsername
|
||||
self.openPhone = openPhone
|
||||
@ -692,6 +696,8 @@ private final class PeerInfoInteraction {
|
||||
self.openPeerMention = openPeerMention
|
||||
self.openBotApp = openBotApp
|
||||
self.openEditing = openEditing
|
||||
self.updateBirthDate = updateBirthDate
|
||||
self.updateIsEditingBirthdate = updateIsEditingBirthdate
|
||||
}
|
||||
}
|
||||
|
||||
@ -979,6 +985,7 @@ private func settingsEditingItems(data: PeerInfoScreenData?, state: PeerInfoStat
|
||||
enum Section: Int, CaseIterable {
|
||||
case help
|
||||
case bio
|
||||
case birthday
|
||||
case info
|
||||
case account
|
||||
case logout
|
||||
@ -998,6 +1005,10 @@ private func settingsEditingItems(data: PeerInfoScreenData?, state: PeerInfoStat
|
||||
let ItemAddAccountHelp = 6
|
||||
let ItemLogout = 7
|
||||
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))
|
||||
|
||||
@ -1010,6 +1021,71 @@ private func settingsEditingItems(data: PeerInfoScreenData?, state: PeerInfoStat
|
||||
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 {
|
||||
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)
|
||||
@ -2297,7 +2373,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
updatingAvatar: nil,
|
||||
updatingBio: nil,
|
||||
avatarUploadProgress: nil,
|
||||
highlightedButton: nil
|
||||
highlightedButton: nil,
|
||||
isEditingBirthDate: false,
|
||||
updatingBirthDate: nil
|
||||
)
|
||||
private var forceIsContactPromise = ValuePromise<Bool>(false)
|
||||
private let nearbyPeerDistance: Int32?
|
||||
@ -2566,6 +2644,22 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
},
|
||||
openEditing: { [weak self] in
|
||||
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()
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
self.addSubnode(self.clearButton)
|
||||
|
||||
if hasClearButton {
|
||||
self.addSubnode(self.clearButton)
|
||||
}
|
||||
self.clearButton.addTarget(self, action: #selector(self.clearPressed), forControlEvents: .touchUpInside)
|
||||
}
|
||||
|
||||
|
@ -16074,6 +16074,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
return
|
||||
}
|
||||
|
||||
//TODO:localize
|
||||
let peerName = peer.compactDisplayTitle
|
||||
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,
|
||||
getSourceRect: { return .zero },
|
||||
completion: { [weak self] result, transitionView, transitionRect, transitionImage, transitionOut, dismissed in
|
||||
guard let self, let asset = result as? PHAsset else {
|
||||
guard let self else {
|
||||
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(
|
||||
context: self.context,
|
||||
mode: .stickerEditor(mode: .generic),
|
||||
subject: .single(.asset(asset)),
|
||||
transitionIn: .gallery(
|
||||
subject: subject,
|
||||
transitionIn: transitionView.flatMap({ .gallery(
|
||||
MediaEditorScreen.TransitionIn.GalleryTransitionIn(
|
||||
sourceView: transitionView,
|
||||
sourceView: $0,
|
||||
sourceRect: transitionRect,
|
||||
sourceImage: transitionImage
|
||||
)
|
||||
),
|
||||
) }),
|
||||
transitionOut: { finished, isNew in
|
||||
if !finished {
|
||||
if !finished, let transitionView {
|
||||
return MediaEditorScreen.TransitionOut(
|
||||
destinationView: transitionView,
|
||||
destinationRect: transitionView.bounds,
|
||||
|
@ -164,6 +164,29 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
|
||||
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:
|
||||
self._peersReady.set(.single(true))
|
||||
}
|
||||
|
@ -182,9 +182,11 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
|
||||
self.contentNode = .chats(chatListNode)
|
||||
} else {
|
||||
let displayTopPeers: ContactListPresentation.TopPeers
|
||||
var selectedPeers: [EnginePeer.Id] = []
|
||||
if case let .premiumGifting(topSectionTitle, topSectionPeers) = mode {
|
||||
if let topSectionTitle {
|
||||
if let topSectionTitle, !topSectionPeers.isEmpty {
|
||||
displayTopPeers = .custom(title: topSectionTitle, peerIds: topSectionPeers)
|
||||
selectedPeers = topSectionPeers
|
||||
} else {
|
||||
displayTopPeers = .recent
|
||||
}
|
||||
@ -195,6 +197,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())
|
||||
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)
|
||||
|
@ -2118,6 +2118,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
|
||||
let mode: ContactMultiselectionControllerMode
|
||||
if case let .chatList(peerIds) = source {
|
||||
//TODO:localize
|
||||
mode = .premiumGifting(topSectionTitle: "🎂 BIRTHDAY TODAY", topSectionPeers: peerIds)
|
||||
} else {
|
||||
mode = .premiumGifting(topSectionTitle: nil, topSectionPeers: [])
|
||||
@ -2322,7 +2323,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
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 mode: MediaEditorScreen.Mode.StickerEditorMode
|
||||
if let file = source as? TelegramMediaFile {
|
||||
@ -2331,8 +2332,12 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
} else if let asset = source as? PHAsset {
|
||||
subject = .asset(asset)
|
||||
mode = .addingToPack
|
||||
} else if let image = source as? UIImage {
|
||||
subject = .image(image, PixelDimensions(image.size), nil, .bottomRight)
|
||||
mode = .addingToPack
|
||||
} else {
|
||||
fatalError()
|
||||
subject = .empty(PixelDimensions(width: 1080, height: 1920))
|
||||
mode = .addingToPack
|
||||
}
|
||||
let controller = MediaEditorScreen(
|
||||
context: context,
|
||||
@ -2373,7 +2378,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -49,10 +49,11 @@ CONFIGURE_FLAGS="--enable-cross-compile --disable-programs \
|
||||
--enable-audiotoolbox \
|
||||
--enable-bsf=aac_adtstoasc \
|
||||
--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-parser=aac,h264,mp3,libopus \
|
||||
--enable-protocol=file \
|
||||
--enable-muxer=mp4 \
|
||||
--enable-muxer=mp4,matroska \
|
||||
"
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user