diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index c1eb7bce93..585db597dc 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -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, completion: @escaping (TelegramMediaFile) -> Void) -> ViewController diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift index b1e1230c53..9b868ee2f3 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift @@ -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! diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index 0bc6ae5aa3..df0d87512c 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -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 { diff --git a/submodules/ChatListUI/Sources/Node/ChatListStorageInfoItem.swift b/submodules/ChatListUI/Sources/Node/ChatListStorageInfoItem.swift index 62e391c799..7d8bd0f2f2 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListStorageInfoItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListStorageInfoItem.swift @@ -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 { diff --git a/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegVideoWriter.h b/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegVideoWriter.h new file mode 100755 index 0000000000..84b36cfa0d --- /dev/null +++ b/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegVideoWriter.h @@ -0,0 +1,14 @@ +#import +#import + +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 diff --git a/submodules/FFMpegBinding/Sources/FFMpegVideoWriter.m b/submodules/FFMpegBinding/Sources/FFMpegVideoWriter.m new file mode 100755 index 0000000000..61cb174de2 --- /dev/null +++ b/submodules/FFMpegBinding/Sources/FFMpegVideoWriter.m @@ -0,0 +1,147 @@ +#import + +#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 diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index d04d19a559..f332202541 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -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) } diff --git a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift index 3ab98d4d93..e6efe3d882 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift @@ -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! diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift index cf1eb65ab7..57226f3a74 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift @@ -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( diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 607f780fcb..4e21ae467b 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -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 } } diff --git a/submodules/TelegramCore/Sources/Suggestions.swift b/submodules/TelegramCore/Sources/Suggestions.swift index 8a405838d3..63f0f2c739 100644 --- a/submodules/TelegramCore/Sources/Suggestions.swift +++ b/submodules/TelegramCore/Sources/Suggestions.swift @@ -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) } } diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift index be05c8b6a8..de524211ea 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift @@ -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 { diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index b9c9081609..9ce449b6b3 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -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 diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorFFMpegWriter.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorFFMpegWriter.swift new file mode 100644 index 0000000000..eb0b300fb5 --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorFFMpegWriter.swift @@ -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? +} diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift index fcf114e4df..4cd2a1c424 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift @@ -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, diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoAVAssetWriter.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoAVAssetWriter.swift new file mode 100644 index 0000000000..d5d7c9df15 --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoAVAssetWriter.swift @@ -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 + } +} diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift index d4e7c8512c..255c4bf711 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift @@ -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 } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorDrafts.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorDrafts.swift index e1318c1b3d..4435dd2956 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorDrafts.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorDrafts.swift @@ -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, _, _, _): diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 890f4e5d5f..a8dc2403cc 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -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 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) } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenBirthdatePickerItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenBirthdatePickerItem.swift index 11c8cee150..9b8cb6850a 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenBirthdatePickerItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenBirthdatePickerItem.swift @@ -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() + + 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, 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, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenDisclosureItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenDisclosureItem.swift index 63c1d698cf..d9d6ad497c 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenDisclosureItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenDisclosureItem.swift @@ -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 diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift index 77b84a1a44..905bc9016a 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift @@ -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 ) } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 62f73db0c4..67b15dc80d 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -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?) -> 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(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) + } + } } ) diff --git a/submodules/TelegramUI/Components/Stickers/StickerPackEditTitleController/Sources/StickerPackEditTitleController.swift b/submodules/TelegramUI/Components/Stickers/StickerPackEditTitleController/Sources/StickerPackEditTitleController.swift index 12339fecda..e4b9e0746f 100644 --- a/submodules/TelegramUI/Components/Stickers/StickerPackEditTitleController/Sources/StickerPackEditTitleController.swift +++ b/submodules/TelegramUI/Components/Stickers/StickerPackEditTitleController/Sources/StickerPackEditTitleController.swift @@ -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) } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index dd005cd94b..53551f799d 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -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." diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift index fd8e11c61a..8cfaea2ba0 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift @@ -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 + 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, diff --git a/submodules/TelegramUI/Sources/ContactMultiselectionController.swift b/submodules/TelegramUI/Sources/ContactMultiselectionController.swift index ba05f5d38c..a5cc74ff9f 100644 --- a/submodules/TelegramUI/Sources/ContactMultiselectionController.swift +++ b/submodules/TelegramUI/Sources/ContactMultiselectionController.swift @@ -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)) } diff --git a/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift b/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift index ed094a4bb9..75da214999 100644 --- a/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift @@ -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) diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index a7d9ac3d04..a2c0cc7964 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -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) } diff --git a/submodules/ffmpeg/Sources/FFMpeg/build-ffmpeg-bazel.sh b/submodules/ffmpeg/Sources/FFMpeg/build-ffmpeg-bazel.sh index 95404823f2..a9cfe22522 100755 --- a/submodules/ffmpeg/Sources/FFMpeg/build-ffmpeg-bazel.sh +++ b/submodules/ffmpeg/Sources/FFMpeg/build-ffmpeg-bazel.sh @@ -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 \ "