Merge commit '755b3dbd48c58c866b564cf3ce4fe7dc905c22d1'

This commit is contained in:
Isaac 2024-03-18 19:24:01 +04:00
commit 0fc64aa505
30 changed files with 1237 additions and 273 deletions

View File

@ -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

View File

@ -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!

View File

@ -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 {

View File

@ -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 {

View File

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

View File

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

View File

@ -193,6 +193,9 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
public var customSelection: ((MediaPickerScreen, Any) -> Void)? = nil
public var 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)
}

View File

@ -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!

View File

@ -1076,15 +1076,17 @@ private final class StickerPackContainer: ASDisplayNode {
}
})))
if let (info, _, _) = self.currentStickerPack, info.flags.contains(.isCreator) {
if let (info, packItems, _) = self.currentStickerPack, info.flags.contains(.isCreator) {
//TODO:localize
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(

View File

@ -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
}
}

View File

@ -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) }
}

View File

@ -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 {

View File

@ -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

View File

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

View File

@ -1525,9 +1525,9 @@ func targetSize(cropSize: CGSize, rotateSideward: Bool = false) -> CGSize {
return CGSize(width: renderWidth, height: renderHeight)
}
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,

View File

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

View File

@ -43,162 +43,6 @@ protocol MediaEditorVideoExportWriter {
var error: Error? { get }
}
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
}

View File

@ -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, _, _, _):

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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

View File

@ -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
)
}
}

View File

@ -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)
}
}
}
)

View File

@ -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)
}

View File

@ -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."

View File

@ -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,

View File

@ -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))
}

View File

@ -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)

View File

@ -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)
}

View File

@ -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 \
"