diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index cdc2d67764..842523c323 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -10796,3 +10796,9 @@ Sorry for the inconvenience."; "Channel.Appearance.UnsavedChangesAlertText" = "You have changed the channel appearance settings. Apply changes?"; "Channel.Appearance.UnsavedChangesAlertDiscard" = "Discard"; "Channel.Appearance.UnsavedChangesAlertApply" = "Apply"; + +"ChatList.PremiumGiftInSettingsInfo" = "You can gift **Telegram Premium** to a friend later in **Settings**."; + +"ChannelAppearance.BoostLevel" = "Level %@"; + +"Message.FullDateFormat" = "%1$@, %2$@"; diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 9efaf4cd31..ebcb16827f 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -2251,6 +2251,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { if let sourceNode = sourceNode as? ChatListItemNode { self.interaction.openStories?(id, sourceNode.avatarNode) } + }, dismissNotice: { _ in }) chatListInteraction.isSearchMode = true @@ -3561,6 +3562,7 @@ public final class ChatListSearchShimmerNode: ASDisplayNode { }, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: { }, openStories: { _, _ in + }, dismissNotice: { _ in }) var isInlineMode = false if case .topics = key { diff --git a/submodules/ChatListUI/Sources/ChatListShimmerNode.swift b/submodules/ChatListUI/Sources/ChatListShimmerNode.swift index f9a989c274..d07e4174d9 100644 --- a/submodules/ChatListUI/Sources/ChatListShimmerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListShimmerNode.swift @@ -156,7 +156,8 @@ final class ChatListShimmerNode: ASDisplayNode { let interaction = ChatListNodeInteraction(context: context, animationCache: animationCache, animationRenderer: animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() - }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: {}, openActiveSessions: {}, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in }) + }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: {}, openActiveSessions: {}, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in }, dismissNotice: { _ in + }) interaction.isInlineMode = isInlineMode let items = (0 ..< 2).map { _ -> ChatListItem in diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index d78f60b438..9f7a21fb85 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -106,6 +106,7 @@ public final class ChatListNodeInteraction { let openChatFolderUpdates: () -> Void let hideChatFolderUpdates: () -> Void let openStories: (ChatListNode.OpenStoriesSubject, ASDisplayNode?) -> Void + let dismissNotice: (ChatListNotice) -> Void public var searchTextHighightState: String? var highlightedChatLocation: ChatListHighlightedLocation? @@ -156,7 +157,8 @@ public final class ChatListNodeInteraction { performActiveSessionAction: @escaping (NewSessionReview, Bool) -> Void, openChatFolderUpdates: @escaping () -> Void, hideChatFolderUpdates: @escaping () -> Void, - openStories: @escaping (ChatListNode.OpenStoriesSubject, ASDisplayNode?) -> Void + openStories: @escaping (ChatListNode.OpenStoriesSubject, ASDisplayNode?) -> Void, + dismissNotice: @escaping (ChatListNotice) -> Void ) { self.activateSearch = activateSearch self.peerSelected = peerSelected @@ -195,6 +197,7 @@ public final class ChatListNodeInteraction { self.openChatFolderUpdates = openChatFolderUpdates self.hideChatFolderUpdates = hideChatFolderUpdates self.openStories = openStories + self.dismissNotice = dismissNotice } } @@ -701,7 +704,7 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL hideChatListContacts(context: context) } : nil), directionHint: entry.directionHint) case let .Notice(presentationData, notice): - return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListStorageInfoItem(theme: presentationData.theme, strings: presentationData.strings, notice: notice, action: { [weak nodeInteraction] action in + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListStorageInfoItem(context: context, theme: presentationData.theme, strings: presentationData.strings, notice: notice, action: { [weak nodeInteraction] action in switch action { case .activate: switch notice { @@ -717,10 +720,7 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL break } case .hide: - switch notice { - default: - break - } + nodeInteraction?.dismissNotice(notice) case let .buttonChoice(isPositive): switch notice { case let .reviewLogin(newSessionReview, _): @@ -1023,7 +1023,7 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL hideChatListContacts(context: context) } : nil), directionHint: entry.directionHint) case let .Notice(presentationData, notice): - return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListStorageInfoItem(theme: presentationData.theme, strings: presentationData.strings, notice: notice, action: { [weak nodeInteraction] action in + return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListStorageInfoItem(context: context, theme: presentationData.theme, strings: presentationData.strings, notice: notice, action: { [weak nodeInteraction] action in switch action { case .activate: switch notice { @@ -1039,10 +1039,7 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL break } case .hide: - switch notice { - default: - break - } + nodeInteraction?.dismissNotice(notice) case let .buttonChoice(isPositive): switch notice { case let .reviewLogin(newSessionReview, _): @@ -1703,6 +1700,20 @@ public final class ChatListNode: ListView { return } self.openStories?(subject, itemNode) + }, dismissNotice: { [weak self] notice in + guard let self else { + return + } + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + switch notice { + case .xmasPremiumGift: + let _ = dismissServerProvidedSuggestion(account: self.context.account, suggestion: .xmasPremiumGift).startStandalone() + self.present?(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.ChatList_PremiumGiftInSettingsInfo, timeout: 5.0, customUndoText: nil), elevatedLayout: false, action: { _ in + return true + })) + default: + break + } }) nodeInteraction.isInlineMode = isInlineMode diff --git a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift index bd2ee5f620..b8007ffcae 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift @@ -79,7 +79,7 @@ public enum ChatListNodeEntryPromoInfo: Equatable { case psa(type: String, message: String?) } -enum ChatListNotice: Equatable { +public enum ChatListNotice: Equatable { case clearStorage(sizeFraction: Double) case setupPassword case premiumUpgrade(discount: Int32) diff --git a/submodules/ChatListUI/Sources/Node/ChatListStorageInfoItem.swift b/submodules/ChatListUI/Sources/Node/ChatListStorageInfoItem.swift index 246ac15a9c..5d4f82c091 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListStorageInfoItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListStorageInfoItem.swift @@ -8,6 +8,8 @@ import ListSectionHeaderNode import AppBundle import ItemListUI import Markdown +import AccountContext +import TelegramCore class ChatListStorageInfoItem: ListViewItem { enum Action { @@ -16,6 +18,7 @@ class ChatListStorageInfoItem: ListViewItem { case buttonChoice(isPositive: Bool) } + let context: AccountContext let theme: PresentationTheme let strings: PresentationStrings let notice: ChatListNotice @@ -23,7 +26,8 @@ class ChatListStorageInfoItem: ListViewItem { let selectable: Bool = true - init(theme: PresentationTheme, strings: PresentationStrings, notice: ChatListNotice, action: @escaping (Action) -> Void) { + init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, notice: ChatListNotice, action: @escaping (Action) -> Void) { + self.context = context self.theme = theme self.strings = strings self.notice = notice @@ -86,6 +90,8 @@ class ChatListStorageInfoItemNode: ItemListRevealOptionsItemNode { private let arrowNode: ASImageNode private let separatorNode: ASDisplayNode + private var closeButton: HighlightableButtonNode? + private var okButtonText: TextNode? private var cancelButtonText: TextNode? private var okButton: HighlightableButtonNode? @@ -127,6 +133,13 @@ class ChatListStorageInfoItemNode: ItemListRevealOptionsItemNode { super.didLoad() } + @objc private func closePressed() { + guard let item = self.item else { + return + } + item.action(.hide) + } + override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { let layout = self.asyncLayout() let (_, apply) = layout(item as! ChatListStorageInfoItem, params, nextItem == nil) @@ -205,7 +218,7 @@ class ChatListStorageInfoItemNode: ItemListRevealOptionsItemNode { textString = NSAttributedString(string: item.strings.ChatList_PremiumRestoreDiscountText, font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor) case .xmasPremiumGift: - titleString = parseMarkdownIntoAttributedString(item.strings.ChatList_PremiumXmasGiftTitle, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor), bold: MarkdownAttributeSet(font: titleFont, textColor: item.theme.rootController.navigationBar.accentTextColor), link: MarkdownAttributeSet(font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor), linkAttribute: { _ in return nil })) + titleString = parseMarkdownIntoAttributedString(item.strings.ChatList_PremiumXmasGiftTitle, 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: item.strings.ChatList_PremiumXmasGiftText, font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor) case let .reviewLogin(newSessionReview, totalCount): spacing = 2.0 @@ -263,7 +276,7 @@ class ChatListStorageInfoItemNode: ItemListRevealOptionsItemNode { if let image = strongSelf.arrowNode.image { strongSelf.arrowNode.frame = CGRect(origin: CGPoint(x: layout.size.width - sideInset - image.size.width + 8.0, y: floor((layout.size.height - image.size.height) / 2.0)), size: image.size) } - + if let okButtonLayout, let cancelButtonLayout { strongSelf.arrowNode.isHidden = true @@ -334,6 +347,31 @@ class ChatListStorageInfoItemNode: ItemListRevealOptionsItemNode { } } + let arrowIsHidden = strongSelf.arrowNode.isHidden + if case .xmasPremiumGift = item.notice { + strongSelf.arrowNode.isHidden = true + + let closeButton: HighlightableButtonNode + if let current = strongSelf.closeButton { + closeButton = current + } else { + closeButton = HighlightableButtonNode() + closeButton.hitTestSlop = UIEdgeInsets(top: -8.0, left: -8.0, bottom: -8.0, right: -8.0) + closeButton.addTarget(self, action: #selector(strongSelf.closePressed), forControlEvents: [.touchUpInside]) + strongSelf.contentContainer.addSubnode(closeButton) + strongSelf.closeButton = closeButton + } + + if themeUpdated { + closeButton.setImage(PresentationResourcesItemList.itemListCloseIconImage(item.theme), for: .normal) + } + + let closeButtonSize = closeButton.measure(CGSize(width: 100.0, height: 100.0)) + closeButton.frame = CGRect(origin: CGPoint(x: layout.size.width - sideInset - closeButtonSize.width, y: floor((layout.size.height - closeButtonSize.height) / 2.0)), size: closeButtonSize) + } else { + strongSelf.arrowNode.isHidden = arrowIsHidden + } + strongSelf.contentSize = layout.contentSize strongSelf.insets = layout.insets diff --git a/submodules/Display/Source/TextNode.swift b/submodules/Display/Source/TextNode.swift index 387a9ff316..cabd9cb729 100644 --- a/submodules/Display/Source/TextNode.swift +++ b/submodules/Display/Source/TextNode.swift @@ -1485,7 +1485,7 @@ open class TextNode: ASDisplayNode { var blockQuotes: [TextNodeBlockQuote] = [] - loop: for i in 0 ..< calculatedSegments.count { + for i in 0 ..< calculatedSegments.count { let segment = calculatedSegments[i] if i != 0 { if segment.blockQuote != nil { @@ -1510,10 +1510,6 @@ open class TextNode: ASDisplayNode { } for line in segment.lines { - var isLastLine = false - if maximumNumberOfLines > 0 && lines.count == maximumNumberOfLines - 1 { - isLastLine = true - } line.frame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: -insets.bottom + size.height + line.frame.size.height), size: line.frame.size) line.frame.size.width += max(0.0, segment.additionalWidth - 2.0) //line.frame.size.width = max(blockWidth, line.frame.size.width) @@ -1579,10 +1575,6 @@ open class TextNode: ASDisplayNode { } lines.append(line) - - if isLastLine { - break loop - } } let blockMaxY = size.height - insets.bottom diff --git a/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift b/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift index 71bd36285f..9d0ce67744 100644 --- a/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift +++ b/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift @@ -276,21 +276,28 @@ public class DrawingStickerEntityView: DrawingEntityView { self.animatedImageView = imageView self.addSubview(imageView) self.setNeedsLayout() - } else if case let .message(_, innerFile, _) = self.stickerEntity.content { - let imageView = UIImageView() - imageView.contentMode = .scaleAspectFit - imageView.image = self.stickerEntity.renderImage - self.animatedImageView = imageView - self.addSubview(imageView) - self.setNeedsLayout() - - let _ = innerFile -// if let innerFile, innerFile.isAnimated { -// self.setupWithVideo(innerFile) -// } + } else if case .message = self.stickerEntity.content { + if let image = self.stickerEntity.renderImage { + self.setupWithImage(image) + } } } + private func setupWithImage(_ image: UIImage) { + let imageView: UIImageView + if let current = self.animatedImageView { + imageView = current + } else { + imageView = UIImageView() + imageView.contentMode = .scaleAspectFit + self.addSubview(imageView) + self.animatedImageView = imageView + } + imageView.image = image + self.currentSize = nil + self.setNeedsLayout() + } + private func setupWithVideo(_ file: TelegramMediaFile) { let videoNode = UniversalVideoNode( postbox: self.context.account.postbox, @@ -366,10 +373,10 @@ public class DrawingStickerEntityView: DrawingEntityView { self.applyVisibility() } - private var isNight = false - public func toggleNightTheme() { - self.isNight = !self.isNight - self.animatedImageView?.image = self.isNight ? self.stickerEntity.secondaryRenderImage : self.stickerEntity.renderImage + public var isNightTheme = false { + didSet { + self.animatedImageView?.image = self.isNightTheme ? self.stickerEntity.secondaryRenderImage : self.stickerEntity.renderImage + } } func applyVisibility() { @@ -615,6 +622,13 @@ public class DrawingStickerEntityView: DrawingEntityView { self.transform = CGAffineTransformScale(CGAffineTransformMakeRotation(self.stickerEntity.rotation), self.stickerEntity.scale, self.stickerEntity.scale) self.updateAnimationColor() + + if case .message = self.stickerEntity.content, self.animatedImageView == nil { + let image = self.isNightTheme ? self.stickerEntity.secondaryRenderImage : self.stickerEntity.renderImage + if let image { + self.setupWithImage(image) + } + } self.updateMirroring(animated: animated) @@ -983,9 +997,9 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView { var count = 12 if case .message = entity.content { cornerRadius *= 2.1 - count = 20 + count = 24 } else if case .image = entity.content { - count = 20 + count = 24 } let perimeter: CGFloat = 2.0 * (width + height - cornerRadius * (4.0 - .pi)) diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift index 2b60d8e325..2ed45cdf2c 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift @@ -101,6 +101,7 @@ public final class HashtagSearchController: TelegramBaseController { }, openChatFolderUpdates: { }, hideChatFolderUpdates: { }, openStories: { _, _ in + }, dismissNotice: { _ in }) let previousSearchItems = Atomic<[ChatListSearchEntry]?>(value: nil) diff --git a/submodules/PremiumUI/Sources/CreateGiveawayController.swift b/submodules/PremiumUI/Sources/CreateGiveawayController.swift index fadc5de53d..e4d93d125e 100644 --- a/submodules/PremiumUI/Sources/CreateGiveawayController.swift +++ b/submodules/PremiumUI/Sources/CreateGiveawayController.swift @@ -1013,7 +1013,7 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio let controller = ItemListController(context: context, state: signal) controller.navigationPresentation = .modal controller.beganInteractiveDragging = { - dismissInputImpl?() +// dismissInputImpl?() } presentControllerImpl = { [weak controller] c in controller?.present(c, in: .window(.root)) diff --git a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift index 4c4560408d..39b8e37855 100644 --- a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift +++ b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift @@ -226,6 +226,7 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, UIScrollView }, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: { }, openStories: { _, _ in + }, dismissNotice: { _ in }) let chatListPresentationData = ChatListPresentationData(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true) diff --git a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift index d2f4e8a75e..9dd1e3c964 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift @@ -375,6 +375,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { }, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: { }, openStories: { _, _ in + }, dismissNotice: { _ in }) func makeChatListItem( diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift index 14c750a2bd..f006a5ca4d 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift @@ -491,9 +491,12 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { if remainingCutoutHeight > 0.0 { cutout = TextNodeCutout(topRight: CGSize(width: cutoutWidth, height: remainingCutoutHeight)) } - + var maximumNumberOfLines: Int = 12 + if isPreview { + maximumNumberOfLines = mediaAndFlags != nil ? 4 : 6 + } let textString = stringWithAppliedEntities(text, entities: entities ?? [], baseColor: messageTheme.primaryTextColor, linkColor: incoming ? mainColor : messageTheme.linkTextColor, baseFont: textFont, linkFont: textFont, boldFont: textBoldFont, italicFont: textItalicFont, boldItalicFont: textBoldItalicFont, fixedFont: textFixedFont, blockQuoteFont: textBlockQuoteFont, message: nil, adjustQuoteFontSize: true) - let textLayoutAndApplyValue = makeTextLayout(TextNodeLayoutArguments(attributedString: textString, backgroundColor: nil, maximumNumberOfLines: 12, truncationType: .end, constrainedSize: CGSize(width: maxContentsWidth, height: 10000.0), alignment: .natural, lineSpacing: textLineSpacing, cutout: cutout, insets: UIEdgeInsets())) + let textLayoutAndApplyValue = makeTextLayout(TextNodeLayoutArguments(attributedString: textString, backgroundColor: nil, maximumNumberOfLines: maximumNumberOfLines, truncationType: .end, constrainedSize: CGSize(width: maxContentsWidth, height: 10000.0), alignment: .natural, lineSpacing: textLineSpacing, cutout: cutout, insets: UIEdgeInsets())) textLayoutAndApply = textLayoutAndApplyValue remainingCutoutHeight -= textLayoutAndApplyValue.0.size.height @@ -631,8 +634,8 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { let statusLayoutAndContinueValue = makeStatusLayout(ChatMessageDateAndStatusNode.Arguments( context: context, presentationData: presentationData, - edited: edited, - impressionCount: viewCount, + edited: edited && !isPreview, + impressionCount: !isPreview ? viewCount : nil, dateText: dateText, type: statusType, layoutInput: .trailingContent( diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index ebc87a3363..3404f827fe 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -1887,7 +1887,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } else { bubbleReactions = ReactionsMessageAttribute(canViewList: false, reactions: [], recentPeers: []) } - if !bubbleReactions.reactions.isEmpty { + if !bubbleReactions.reactions.isEmpty && !item.presentationData.isPreview { bottomNodeMergeStatus = .Both } @@ -2093,7 +2093,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI context: item.context, presentationData: item.presentationData, edited: edited && !item.presentationData.isPreview, - impressionCount: viewCount, + impressionCount: !item.presentationData.isPreview ? viewCount : nil, dateText: dateText, type: statusType, layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium, forceInline: item.associatedData.forceInlineReactions) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil), diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/StringForMessageTimestampStatus.swift b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/StringForMessageTimestampStatus.swift index 7ffe411fda..e1de4b5dc9 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/StringForMessageTimestampStatus.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/StringForMessageTimestampStatus.swift @@ -111,9 +111,7 @@ public func stringForMessageTimestampStatus(accountPeerId: PeerId, message: Mess } else { dayText = strings.Date_ChatDateHeaderYear(monthAtIndex(Int(timeinfo.tm_mon), strings: strings), "\(timeinfo.tm_mday)", "\(1900 + timeinfo.tm_year)").string } - - //TODO:localize - dateText = "\(dayText), \(stringForMessageTimestamp(timestamp: timestamp, dateTimeFormat: dateTimeFormat))" + dateText = strings.Message_FullDateFormat(dayText, stringForMessageTimestamp(timestamp: timestamp, dateTimeFormat: dateTimeFormat)).string } else if let forwardInfo = message.forwardInfo, forwardInfo.flags.contains(.isImported) { dateText = strings.Message_ImportedDateFormat(dateStringForDay(strings: strings, dateTimeFormat: dateTimeFormat, timestamp: forwardInfo.date), stringForMessageTimestamp(timestamp: forwardInfo.date, dateTimeFormat: dateTimeFormat), dateText).string diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift index 9908db1c35..c6fdb43aaa 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift @@ -508,7 +508,7 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode, context: item.context, presentationData: item.presentationData, edited: edited, - impressionCount: viewCount, + impressionCount: !item.presentationData.isPreview ? viewCount : nil, dateText: dateText, type: statusType, layoutInput: .trailingContent(contentWidth: 1000.0, reactionSettings: shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium, forceInline: item.associatedData.forceInlineReactions) ? ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: true, preferAdditionalInset: false) : nil), diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift index 661123bb1d..5cfd8befe7 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift @@ -919,8 +919,8 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { statusSuggestedWidthAndContinue = statusLayout(ChatMessageDateAndStatusNode.Arguments( context: arguments.context, presentationData: arguments.presentationData, - edited: edited, - impressionCount: viewCount, + edited: edited && !arguments.presentationData.isPreview, + impressionCount: !arguments.presentationData.isPreview ? viewCount : nil, dateText: dateText, type: statusType, layoutInput: statusLayoutInput, diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift index ea633f8bc0..46440364f0 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift @@ -554,7 +554,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { context: item.context, presentationData: item.presentationData, edited: edited && !sentViaBot && !item.presentationData.isPreview, - impressionCount: item.presentationData.isPreview ? nil : viewCount, + impressionCount: !item.presentationData.isPreview ? viewCount : nil, dateText: dateText, type: statusType, layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium, forceInline: item.associatedData.forceInlineReactions) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil), diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift index 6f87d78134..43e7208f66 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift @@ -862,7 +862,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr context: context, presentationData: presentationData, edited: dateAndStatus.edited && !presentationData.isPreview, - impressionCount: dateAndStatus.viewCount, + impressionCount: !presentationData.isPreview ? dateAndStatus.viewCount : nil, dateText: dateAndStatus.dateText, type: dateAndStatus.type, layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: message, isPremium: associatedData.isPremium, forceInline: associatedData.forceInlineReactions) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil), @@ -1862,6 +1862,9 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr badgeContent = .text(inset: 0.0, backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, text: string, iconName: nil) } } + + let gifTitle = game != nil ? strings.Message_Game.uppercased() : strings.Message_Animation.uppercased() + var animated = animated if let updatingMedia = attributes.updatingMedia, case .update = updatingMedia.media { state = .progress(color: messageTheme.mediaOverlayControlColors.foregroundColor, lineWidth: nil, value: CGFloat(updatingMedia.progress), cancelEnabled: true, animateRotation: true) @@ -1915,9 +1918,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } } } - - let gifTitle = game != nil ? strings.Message_Game.uppercased() : strings.Message_Animation.uppercased() - + let formatting = DataSizeStringFormatting(strings: strings, decimalSeparator: decimalSeparator) var media = self.media @@ -2079,7 +2080,11 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } } } - if isPreview, let _ = media as? TelegramMediaFile { + if isPreview, let file = media as? TelegramMediaFile { + if let duration = file.duration, !file.isVideoSticker { + let durationString = file.isAnimated ? gifTitle : stringForDuration(Int32(duration), position: nil) + badgeContent = .mediaDownload(backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, duration: durationString, size: nil, muted: false, active: false) + } state = .play(messageTheme.mediaOverlayControlColors.foregroundColor) } @@ -2125,6 +2130,13 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr animated = true } + if isPreview { + if case .play = state { + } else { + state = .none + } + } + statusNode.transitionToState(state, animated: animated, completion: { [weak statusNode] in if removeStatusNode { statusNode?.removeFromSupernode() diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift index 80a2f405c0..64e2a050f1 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift @@ -267,7 +267,7 @@ public class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { context: item.context, presentationData: item.presentationData, edited: edited, - impressionCount: viewCount, + impressionCount: !item.presentationData.isPreview ? viewCount : nil, dateText: dateText, type: statusType, layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium, forceInline: item.associatedData.forceInlineReactions) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil), diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift index 2fc6aa4beb..8a98990b74 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift @@ -521,6 +521,8 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } else { maximumNumberOfLines = 6 } + } else if let _ = item.message.media.first(where: { $0 is TelegramMediaWebpage }) as? TelegramMediaWebpage { + maximumNumberOfLines = 9 } else { maximumNumberOfLines = 12 } @@ -562,7 +564,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { context: item.context, presentationData: item.presentationData, edited: edited && !item.presentationData.isPreview, - impressionCount: viewCount, + impressionCount: !item.presentationData.isPreview ? viewCount : nil, dateText: dateText, type: statusType, layoutInput: dateLayoutInput, diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageWallpaperBubbleContentNode/Sources/ChatMessageWallpaperBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageWallpaperBubbleContentNode/Sources/ChatMessageWallpaperBubbleContentNode.swift index 39101581ce..5792f86080 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageWallpaperBubbleContentNode/Sources/ChatMessageWallpaperBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageWallpaperBubbleContentNode/Sources/ChatMessageWallpaperBubbleContentNode.swift @@ -337,8 +337,17 @@ public class ChatMessageWallpaperBubbleContentNode: ChatMessageBubbleContentNode var patternArguments: PatternWallpaperArguments? var mediaContent = media.content - if case let .emoticon(emoticon) = mediaContent, let theme = item.associatedData.chatThemes.first(where: { $0.emoticon?.strippedEmoji == emoticon.strippedEmoji }), let themeWallpaper = theme.settings?.first?.wallpaper, let themeWallpaperContent = WallpaperPreviewMedia(wallpaper: themeWallpaper)?.content { - mediaContent = themeWallpaperContent + if case let .emoticon(emoticon) = mediaContent, let theme = item.associatedData.chatThemes.first(where: { $0.emoticon?.strippedEmoji == emoticon.strippedEmoji }) { + let themeSettings: TelegramThemeSettings? + if let matching = theme.settings?.first(where: { $0.baseTheme == item.presentationData.theme.theme.referenceTheme.baseTheme }) { + themeSettings = matching + } else { + themeSettings = theme.settings?.first + } + + if let themeWallpaper = themeSettings?.wallpaper, let themeWallpaperContent = WallpaperPreviewMedia(wallpaper: themeWallpaper)?.content { + mediaContent = themeWallpaperContent + } } switch mediaContent { diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/DrawingMessageRenderer.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/DrawingMessageRenderer.swift index cd3b903b46..095f03133a 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/DrawingMessageRenderer.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/DrawingMessageRenderer.swift @@ -11,26 +11,28 @@ import WallpaperBackgroundNode public final class DrawingWallpaperRenderer { private let context: AccountContext - private let customWallpaper: TelegramWallpaper? + private let customDayWallpaper: TelegramWallpaper? + private let customNightWallpaper: TelegramWallpaper? private let wallpaperBackgroundNode: WallpaperBackgroundNode private let darkWallpaperBackgroundNode: WallpaperBackgroundNode - public init (context: AccountContext, customWallpaper: TelegramWallpaper?) { + public init (context: AccountContext, customDayWallpaper: TelegramWallpaper?, customNightWallpaper: TelegramWallpaper?) { self.context = context - self.customWallpaper = customWallpaper + self.customDayWallpaper = customDayWallpaper + self.customNightWallpaper = customNightWallpaper self.wallpaperBackgroundNode = createWallpaperBackgroundNode(context: context, forChatDisplay: true, useSharedAnimationPhase: false) self.wallpaperBackgroundNode.displaysAsynchronously = false - let wallpaper = self.customWallpaper ?? context.sharedContext.currentPresentationData.with { $0 }.chatWallpaper + let wallpaper = self.customDayWallpaper ?? context.sharedContext.currentPresentationData.with { $0 }.chatWallpaper self.wallpaperBackgroundNode.update(wallpaper: wallpaper, animated: false) self.darkWallpaperBackgroundNode = createWallpaperBackgroundNode(context: context, forChatDisplay: true, useSharedAnimationPhase: false) self.darkWallpaperBackgroundNode.displaysAsynchronously = false let darkTheme = defaultDarkColorPresentationTheme - let darkWallpaper = darkTheme.chat.defaultWallpaper + let darkWallpaper = self.customNightWallpaper ?? darkTheme.chat.defaultWallpaper self.darkWallpaperBackgroundNode.update(wallpaper: darkWallpaper, animated: false) } @@ -39,7 +41,7 @@ public final class DrawingWallpaperRenderer { let resultSize = CGSize(width: 1080, height: 1920) self.generate(view: self.wallpaperBackgroundNode.view) { dayImage in - if self.customWallpaper != nil { + if self.customDayWallpaper != nil && self.customNightWallpaper == nil { completion(resultSize, dayImage, nil, nil) } else { Queue.mainQueue().justDispatch { @@ -77,180 +79,217 @@ public final class DrawingWallpaperRenderer { } public final class DrawingMessageRenderer { + class ContainerNode: ASDisplayNode { + private let context: AccountContext + private let messages: [Message] + private let isNight: Bool + + private let messagesContainerNode: ASDisplayNode + private var avatarHeaderNode: ListViewItemHeaderNode? + private var messageNodes: [ListViewItemNode]? + + init(context: AccountContext, messages: [Message], isNight: Bool = false) { + self.context = context + self.messages = messages + self.isNight = isNight + + self.messagesContainerNode = ASDisplayNode() + self.messagesContainerNode.clipsToBounds = true + self.messagesContainerNode.transform = CATransform3DMakeScale(1.0, -1.0, 1.0) + + super.init() + + self.addSubnode(self.messagesContainerNode) + } + + public func render(completion: @escaping (CGSize, UIImage?) -> Void) { + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + let defaultPresentationData = defaultPresentationData() + + var mockPresentationData = PresentationData( + strings: presentationData.strings, + theme: defaultPresentationTheme, + autoNightModeTriggered: false, + chatWallpaper: presentationData.chatWallpaper, + chatFontSize: defaultPresentationData.chatFontSize, + chatBubbleCorners: defaultPresentationData.chatBubbleCorners, + listsFontSize: defaultPresentationData.listsFontSize, + dateTimeFormat: presentationData.dateTimeFormat, + nameDisplayOrder: presentationData.nameDisplayOrder, + nameSortOrder: presentationData.nameSortOrder, + reduceMotion: false, + largeEmoji: true + ) + + if self.isNight { + let darkTheme = defaultDarkColorPresentationTheme + mockPresentationData = mockPresentationData.withUpdated(theme: darkTheme).withUpdated(chatWallpaper: darkTheme.chat.defaultWallpaper) + } + + let layout = ContainerViewLayout(size: CGSize(width: 360.0, height: 640.0), metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: .portrait), deviceMetrics: .iPhoneX, intrinsicInsets: .zero, safeInsets: .zero, additionalInsets: .zero, statusBarHeight: 0.0, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false) + let size = self.updateMessagesLayout(layout: layout, presentationData: mockPresentationData) + + Queue.mainQueue().after(0.03, { + self.generate(size: size) { image in + completion(size, image) + } + }) + } + + private func generate(size: CGSize, completion: @escaping (UIImage) -> Void) { + UIGraphicsBeginImageContextWithOptions(size, false, 3.0) + self.view.drawHierarchy(in: CGRect(origin: CGPoint(), size: size), afterScreenUpdates: true) + let img = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + + let finalImage = generateImage(CGSize(width: size.width * 3.0, height: size.height * 3.0), contextGenerator: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + if let cgImage = img?.cgImage { + context.draw(cgImage, in: CGRect(origin: .zero, size: size), byTiling: false) + } + }, opaque: false, scale: 1.0) + if let finalImage { + completion(finalImage) + } + } + + private func updateMessagesLayout(layout: ContainerViewLayout, presentationData: PresentationData) -> CGSize { + let size = layout.size + + let theme = presentationData.theme.withUpdated(preview: true) + + let avatarHeaderItem = self.context.sharedContext.makeChatMessageAvatarHeaderItem(context: self.context, timestamp: self.messages.first?.timestamp ?? 0, peer: self.messages.first!.peers[self.messages.first!.author!.id]!, message: self.messages.first!, theme: theme, strings: presentationData.strings, wallpaper: presentationData.chatWallpaper, fontSize: presentationData.chatFontSize, chatBubbleCorners: presentationData.chatBubbleCorners, dateTimeFormat: presentationData.dateTimeFormat, nameOrder: presentationData.nameDisplayOrder) + + let items: [ListViewItem] = [self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: self.messages, theme: theme, strings: presentationData.strings, wallpaper: presentationData.theme.chat.defaultWallpaper, fontSize: presentationData.chatFontSize, chatBubbleCorners: presentationData.chatBubbleCorners, dateTimeFormat: presentationData.dateTimeFormat, nameOrder: presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: nil, availableReactions: nil, accountPeer: nil, isCentered: false)] + + let inset: CGFloat = 16.0 + let leftInset: CGFloat = 37.0 + let containerWidth = layout.size.width - inset * 2.0 + let params = ListViewItemLayoutParams(width: containerWidth, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, availableHeight: layout.size.height) + + var width: CGFloat = containerWidth + var height: CGFloat = size.height + if let messageNodes = self.messageNodes { + for i in 0 ..< items.count { + let itemNode = messageNodes[i] + items[i].updateNode(async: { $0() }, node: { + return itemNode + }, params: params, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], animation: .None, completion: { (layout, apply) in + let nodeFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: containerWidth, height: layout.size.height)) + + itemNode.contentSize = layout.contentSize + itemNode.insets = layout.insets + itemNode.frame = nodeFrame + itemNode.isUserInteractionEnabled = false + + apply(ListViewItemApply(isOnScreen: true)) + }) + } + } else { + var messageNodes: [ListViewItemNode] = [] + for i in 0 ..< items.count { + var itemNode: ListViewItemNode? + items[i].nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: true, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], completion: { node, apply in + itemNode = node + apply().1(ListViewItemApply(isOnScreen: true)) + }) + itemNode!.subnodeTransform = CATransform3DMakeScale(-1.0, 1.0, 1.0) + itemNode!.isUserInteractionEnabled = false + messageNodes.append(itemNode!) + self.messagesContainerNode.addSubnode(itemNode!) + } + self.messageNodes = messageNodes + } + + if let messageNodes = self.messageNodes { + var minX: CGFloat = .greatestFiniteMagnitude + var maxX: CGFloat = -.greatestFiniteMagnitude + var minY: CGFloat = .greatestFiniteMagnitude + var maxY: CGFloat = -.greatestFiniteMagnitude + for node in messageNodes { + if node.frame.minY < minY { + minY = node.frame.minY + } + if node.frame.maxY > maxY { + maxY = node.frame.maxY + } + if let areaNode = node.subnodes?.last { + if areaNode.frame.minX < minX { + minX = areaNode.frame.minX + } + if areaNode.frame.maxX > maxX { + maxX = areaNode.frame.maxX + } + } + } + width = abs(maxX - minX) + height = abs(maxY - minY) + } + + var bottomOffset: CGFloat = 0.0 + if let messageNodes = self.messageNodes { + for itemNode in messageNodes { + itemNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: itemNode.frame.size) + bottomOffset += itemNode.frame.maxY + itemNode.updateFrame(itemNode.frame, within: layout.size) + } + } + + let avatarHeaderNode: ListViewItemHeaderNode + if let currentAvatarHeaderNode = self.avatarHeaderNode { + avatarHeaderNode = currentAvatarHeaderNode + avatarHeaderItem.updateNode(avatarHeaderNode, previous: nil, next: avatarHeaderItem) + } else { + avatarHeaderNode = avatarHeaderItem.node(synchronousLoad: true) + avatarHeaderNode.subnodeTransform = CATransform3DMakeScale(-1.0, 1.0, 1.0) + self.messagesContainerNode.addSubnode(avatarHeaderNode) + self.avatarHeaderNode = avatarHeaderNode + } + + avatarHeaderNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 3.0), size: CGSize(width: layout.size.width, height: avatarHeaderItem.height)) + avatarHeaderNode.updateLayout(size: size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right) + + let containerSize = CGSize(width: width + leftInset + 6.0, height: height) + self.frame = CGRect(origin: CGPoint(), size: containerSize) + self.messagesContainerNode.frame = CGRect(origin: CGPoint(), size: containerSize) + + return containerSize + } + } + private let context: AccountContext private let messages: [Message] - private let containerNode: ASDisplayNode - - private let messagesContainerNode: ASDisplayNode - private var avatarHeaderNode: ListViewItemHeaderNode? - private var messageNodes: [ListViewItemNode]? + private let dayContainerNode: ContainerNode + private let nightContainerNode: ContainerNode public init(context: AccountContext, messages: [Message]) { self.context = context self.messages = messages - - self.containerNode = ASDisplayNode() - - self.messagesContainerNode = ASDisplayNode() - self.messagesContainerNode.clipsToBounds = true - self.messagesContainerNode.transform = CATransform3DMakeScale(1.0, -1.0, 1.0) - - self.containerNode.addSubnode(self.messagesContainerNode) + + self.dayContainerNode = ContainerNode(context: context, messages: messages) + self.nightContainerNode = ContainerNode(context: context, messages: messages, isNight: true) } public func render(completion: @escaping (CGSize, UIImage?, UIImage?) -> Void) { - let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } - let defaultPresentationData = defaultPresentationData() + var finalSize: CGSize = .zero + var dayImage: UIImage? + var nightImage: UIImage? - let mockPresentationData = PresentationData( - strings: presentationData.strings, - theme: defaultPresentationTheme, - autoNightModeTriggered: false, - chatWallpaper: presentationData.chatWallpaper, - chatFontSize: defaultPresentationData.chatFontSize, - chatBubbleCorners: defaultPresentationData.chatBubbleCorners, - listsFontSize: defaultPresentationData.listsFontSize, - dateTimeFormat: presentationData.dateTimeFormat, - nameDisplayOrder: presentationData.nameDisplayOrder, - nameSortOrder: presentationData.nameSortOrder, - reduceMotion: false, - largeEmoji: true - ) - - let layout = ContainerViewLayout(size: CGSize(width: 360.0, height: 640.0), metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: .portrait), deviceMetrics: .iPhoneX, intrinsicInsets: .zero, safeInsets: .zero, additionalInsets: .zero, statusBarHeight: 0.0, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false) - let size = self.updateMessagesLayout(layout: layout, presentationData: mockPresentationData) - - Queue.mainQueue().after(0.01, { - self.generate(size: size) { dayImage in - let darkTheme = defaultDarkColorPresentationTheme - let darkPresentationData = mockPresentationData.withUpdated(theme: darkTheme) - - let _ = self.updateMessagesLayout(layout: layout, presentationData: darkPresentationData) - self.generate(size: size) { nightImage in - completion(size, dayImage, nightImage) - } - } - }) - } - - private func generate(size: CGSize, completion: @escaping (UIImage) -> Void) { - UIGraphicsBeginImageContextWithOptions(size, false, 3.0) - self.containerNode.view.drawHierarchy(in: CGRect(origin: CGPoint(), size: size), afterScreenUpdates: true) - let img = UIGraphicsGetImageFromCurrentImageContext() - UIGraphicsEndImageContext() - - let finalImage = generateImage(CGSize(width: size.width * 3.0, height: size.height * 3.0), contextGenerator: { size, context in - context.clear(CGRect(origin: .zero, size: size)) - if let cgImage = img?.cgImage { - context.draw(cgImage, in: CGRect(origin: .zero, size: size), byTiling: false) - } - }, opaque: false, scale: 1.0) - if let finalImage { - completion(finalImage) - } - } - - private func updateMessagesLayout(layout: ContainerViewLayout, presentationData: PresentationData) -> CGSize { - let size = layout.size - - let theme = presentationData.theme.withUpdated(preview: true) - - let avatarHeaderItem = self.context.sharedContext.makeChatMessageAvatarHeaderItem(context: self.context, timestamp: self.messages.first?.timestamp ?? 0, peer: self.messages.first!.peers[self.messages.first!.author!.id]!, message: self.messages.first!, theme: theme, strings: presentationData.strings, wallpaper: presentationData.chatWallpaper, fontSize: presentationData.chatFontSize, chatBubbleCorners: presentationData.chatBubbleCorners, dateTimeFormat: presentationData.dateTimeFormat, nameOrder: presentationData.nameDisplayOrder) - - let items: [ListViewItem] = [self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: self.messages, theme: theme, strings: presentationData.strings, wallpaper: presentationData.theme.chat.defaultWallpaper, fontSize: presentationData.chatFontSize, chatBubbleCorners: presentationData.chatBubbleCorners, dateTimeFormat: presentationData.dateTimeFormat, nameOrder: presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: nil, availableReactions: nil, accountPeer: nil, isCentered: false)] - - let inset: CGFloat = 16.0 - let leftInset: CGFloat = 37.0 - let containerWidth = layout.size.width - inset * 2.0 - let params = ListViewItemLayoutParams(width: containerWidth, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, availableHeight: layout.size.height) - - var width: CGFloat = containerWidth - var height: CGFloat = size.height - if let messageNodes = self.messageNodes { - for i in 0 ..< items.count { - let itemNode = messageNodes[i] - items[i].updateNode(async: { $0() }, node: { - return itemNode - }, params: params, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], animation: .None, completion: { (layout, apply) in - let nodeFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: containerWidth, height: layout.size.height)) - - itemNode.contentSize = layout.contentSize - itemNode.insets = layout.insets - itemNode.frame = nodeFrame - itemNode.isUserInteractionEnabled = false - - apply(ListViewItemApply(isOnScreen: true)) - }) - } - } else { - var messageNodes: [ListViewItemNode] = [] - for i in 0 ..< items.count { - var itemNode: ListViewItemNode? - items[i].nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: true, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], completion: { node, apply in - itemNode = node - apply().1(ListViewItemApply(isOnScreen: true)) - }) - itemNode!.subnodeTransform = CATransform3DMakeScale(-1.0, 1.0, 1.0) - itemNode!.isUserInteractionEnabled = false - messageNodes.append(itemNode!) - self.messagesContainerNode.addSubnode(itemNode!) - } - self.messageNodes = messageNodes - } - - if let messageNodes = self.messageNodes { - var minX: CGFloat = .greatestFiniteMagnitude - var maxX: CGFloat = -.greatestFiniteMagnitude - var minY: CGFloat = .greatestFiniteMagnitude - var maxY: CGFloat = -.greatestFiniteMagnitude - for node in messageNodes { - if node.frame.minY < minY { - minY = node.frame.minY - } - if node.frame.maxY > maxY { - maxY = node.frame.maxY - } - if let areaNode = node.subnodes?.last { - if areaNode.frame.minX < minX { - minX = areaNode.frame.minX - } - if areaNode.frame.maxX > maxX { - maxX = areaNode.frame.maxX - } - } - } - width = abs(maxX - minX) - height = abs(maxY - minY) - } - - var bottomOffset: CGFloat = 0.0 - if let messageNodes = self.messageNodes { - for itemNode in messageNodes { - itemNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: itemNode.frame.size) - bottomOffset += itemNode.frame.maxY - itemNode.updateFrame(itemNode.frame, within: layout.size) + let completeIfReady = { + if let dayImage, let nightImage { + completion(finalSize, dayImage, nightImage) } } - - let avatarHeaderNode: ListViewItemHeaderNode - if let currentAvatarHeaderNode = self.avatarHeaderNode { - avatarHeaderNode = currentAvatarHeaderNode - avatarHeaderItem.updateNode(avatarHeaderNode, previous: nil, next: avatarHeaderItem) - } else { - avatarHeaderNode = avatarHeaderItem.node(synchronousLoad: true) - avatarHeaderNode.subnodeTransform = CATransform3DMakeScale(-1.0, 1.0, 1.0) - self.messagesContainerNode.addSubnode(avatarHeaderNode) - self.avatarHeaderNode = avatarHeaderNode + self.dayContainerNode.render { size, image in + finalSize = size + dayImage = image + completeIfReady() + } + self.nightContainerNode.render { size, image in + finalSize = size + nightImage = image + completeIfReady() } - - avatarHeaderNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 3.0), size: CGSize(width: layout.size.width, height: avatarHeaderItem.height)) - avatarHeaderNode.updateLayout(size: size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right) - - let containerSize = CGSize(width: width + leftInset + 6.0, height: height) - self.containerNode.frame = CGRect(origin: CGPoint(), size: containerSize) - self.messagesContainerNode.frame = CGRect(origin: CGPoint(), size: containerSize) - - return containerSize } } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorDraft.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorDraft.swift index 5d39ae1961..8fadb76212 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorDraft.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorDraft.swift @@ -73,6 +73,7 @@ public final class MediaEditorDraft: Codable, Equatable { case values case caption case privacy + case forwardInfo case timestamp case locationLatitude case locationLongitude @@ -87,11 +88,12 @@ public final class MediaEditorDraft: Codable, Equatable { public let values: MediaEditorValues public let caption: NSAttributedString public let privacy: MediaEditorResultPrivacy? + public let forwardInfo: StoryId? public let timestamp: Int32 public let location: CLLocationCoordinate2D? public let expiresOn: Int32? - public init(path: String, isVideo: Bool, thumbnail: UIImage, dimensions: PixelDimensions, duration: Double?, values: MediaEditorValues, caption: NSAttributedString, privacy: MediaEditorResultPrivacy?, timestamp: Int32, location: CLLocationCoordinate2D?, expiresOn: Int32?) { + public init(path: String, isVideo: Bool, thumbnail: UIImage, dimensions: PixelDimensions, duration: Double?, values: MediaEditorValues, caption: NSAttributedString, privacy: MediaEditorResultPrivacy?, forwardInfo: StoryId?, timestamp: Int32, location: CLLocationCoordinate2D?, expiresOn: Int32?) { self.path = path self.isVideo = isVideo self.thumbnail = thumbnail @@ -100,6 +102,7 @@ public final class MediaEditorDraft: Codable, Equatable { self.values = values self.caption = caption self.privacy = privacy + self.forwardInfo = forwardInfo self.timestamp = timestamp self.location = location self.expiresOn = expiresOn @@ -135,6 +138,8 @@ public final class MediaEditorDraft: Codable, Equatable { self.privacy = nil } + self.forwardInfo = try container.decodeIfPresent(StoryId.self, forKey: .forwardInfo) + self.timestamp = try container.decodeIfPresent(Int32.self, forKey: .timestamp) ?? 1688909663 if let latitude = try container.decodeIfPresent(Double.self, forKey: .locationLatitude), let longitude = try container.decodeIfPresent(Double.self, forKey: .locationLongitude) { @@ -172,6 +177,8 @@ public final class MediaEditorDraft: Codable, Equatable { } else { try container.encodeNil(forKey: .privacy) } + try container.encodeIfPresent(self.forwardInfo, forKey: .forwardInfo) + try container.encode(self.timestamp, forKey: .timestamp) if let location = self.location { diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorUtils.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorUtils.swift index 9251a6a872..aa265ace01 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorUtils.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorUtils.swift @@ -136,10 +136,36 @@ public func getChatWallpaperImage(context: AccountContext, messageId: EngineMess return context.account.postbox.transaction { transaction -> TelegramWallpaper? in return (transaction.getPeerCachedData(peerId: messageId.peerId) as? CachedChannelData)?.wallpaper } - |> mapToSignal { customWallpaper -> Signal<(CGSize, UIImage?, UIImage?), NoError> in + |> mapToSignal { wallpaper -> Signal<(TelegramWallpaper?, TelegramWallpaper?), NoError> in + if let wallpaper, case let .emoticon(emoticon) = wallpaper { + return context.engine.themes.getChatThemes(accountManager: context.sharedContext.accountManager) + |> map { themes -> (TelegramWallpaper?, TelegramWallpaper?) in + if let theme = themes.first(where: { $0.emoticon?.strippedEmoji == emoticon.strippedEmoji }) { + if let dayMatch = theme.settings?.first(where: { $0.baseTheme == .classic || $0.baseTheme == .day }) { + if let dayWallpaper = dayMatch.wallpaper { + var nightWallpaper: TelegramWallpaper? + if let nightMatch = theme.settings?.first(where: { $0.baseTheme == .night || $0.baseTheme == .tinted }) { + nightWallpaper = nightMatch.wallpaper + } + return (dayWallpaper, nightWallpaper) + } else { + return (nil, nil) + } + } else { + return (nil, nil) + } + } else { + return (nil, nil) + } + } + } else { + return .single((wallpaper, nil)) + } + } + |> mapToSignal { customDayWallpaper, customNightWallpaper -> Signal<(CGSize, UIImage?, UIImage?), NoError> in return Signal { subscriber in Queue.mainQueue().async { - let wallpaperRenderer = DrawingWallpaperRenderer(context: context, customWallpaper: customWallpaper) + let wallpaperRenderer = DrawingWallpaperRenderer(context: context, customDayWallpaper: customDayWallpaper, customNightWallpaper: customNightWallpaper) wallpaperRenderer.render { size, image, darkImage, mediaRect in subscriber.putNext((size, image, darkImage)) subscriber.putCompletion() diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorDrafts.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorDrafts.swift index a99f6ba293..ac834f1ed8 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorDrafts.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorDrafts.swift @@ -45,13 +45,14 @@ extension MediaEditorScreen { } func saveDraft(id: Int64?) { - guard let subject = self.node.subject, let mediaEditor = self.node.mediaEditor else { + guard let subject = self.node.subject, let actualSubject = self.node.actualSubject, let mediaEditor = self.node.mediaEditor else { return } try? FileManager.default.createDirectory(atPath: draftPath(engine: self.context.engine), withIntermediateDirectories: true) let values = mediaEditor.values let privacy = self.state.privacy + let forwardSource = self.forwardSource let caption = self.getCaption() let duration = mediaEditor.duration ?? 0.0 @@ -59,7 +60,7 @@ extension MediaEditorScreen { var timestamp: Int32 var location: CLLocationCoordinate2D? let expiresOn: Int32 - if case let .draft(draft, _) = subject { + if case let .draft(draft, _) = actualSubject { timestamp = draft.timestamp location = draft.location if let _ = id { @@ -85,29 +86,74 @@ extension MediaEditorScreen { guard let resultImage else { return } - let fittedSize = resultImage.size.aspectFitted(CGSize(width: 128.0, height: 128.0)) - - let context = self.context - let saveImageDraft: (UIImage, PixelDimensions) -> Void = { image, dimensions in - if let thumbnailImage = generateScaledImage(image: resultImage, size: fittedSize) { - let path = "\(Int64.random(in: .min ... .max)).jpg" - if let data = image.jpegData(compressionQuality: 0.87) { - let draft = MediaEditorDraft(path: path, isVideo: false, thumbnail: thumbnailImage, dimensions: dimensions, duration: nil, values: values, caption: caption, privacy: privacy, timestamp: timestamp, location: location, expiresOn: expiresOn) - try? data.write(to: URL(fileURLWithPath: draft.fullPath(engine: context.engine))) - if let id { - saveStorySource(engine: context.engine, item: draft, peerId: context.account.peerId, id: id) - } else { - addStoryDraft(engine: context.engine, item: draft) - } + enum MediaInput { + case image(image: UIImage, dimensions: PixelDimensions) + case video(path: String, dimensions: PixelDimensions, duration: Double) + + var isVideo: Bool { + switch self { + case .video: + return true + case .image: + return false + } + } + + var dimensions: PixelDimensions { + switch self { + case let .image(_, dimensions): + return dimensions + case let .video(_, dimensions, _): + return dimensions + } + } + + var duration: Double? { + switch self { + case .image: + return nil + case let .video(_, _, duration): + return duration + } + } + + var fileExtension: String { + switch self { + case .image: + return "jpg" + case .video: + return "mp4" } } } - let saveVideoDraft: (String, PixelDimensions, Double) -> Void = { videoPath, dimensions, duration in + let context = self.context + func innerSaveDraft(media: MediaInput) { + let fittedSize = resultImage.size.aspectFitted(CGSize(width: 128.0, height: 128.0)) if let thumbnailImage = generateScaledImage(image: resultImage, size: fittedSize) { - let path = "\(Int64.random(in: .min ... .max)).mp4" - let draft = MediaEditorDraft(path: path, isVideo: true, thumbnail: thumbnailImage, dimensions: dimensions, duration: duration, values: values, caption: caption, privacy: privacy, timestamp: timestamp, location: location, expiresOn: expiresOn) - try? FileManager.default.copyItem(atPath: videoPath, toPath: draft.fullPath(engine: context.engine)) + let path = "\(Int64.random(in: .min ... .max)).\(media.fileExtension)" + let draft = MediaEditorDraft( + path: path, + isVideo: media.isVideo, + thumbnail: thumbnailImage, + dimensions: media.dimensions, + duration: media.duration, + values: values, + caption: caption, + privacy: privacy, + forwardInfo: forwardSource.flatMap { StoryId(peerId: $0.0.id, id: $0.1.id) }, + timestamp: timestamp, + location: location, + expiresOn: expiresOn + ) + switch media { + case let .image(image, _): + if let data = image.jpegData(compressionQuality: 0.87) { + try? data.write(to: URL(fileURLWithPath: draft.fullPath(engine: context.engine))) + } + case let .video(path, _, _): + try? FileManager.default.copyItem(atPath: path, toPath: draft.fullPath(engine: context.engine)) + } if let id { saveStorySource(engine: context.engine, item: draft, peerId: context.account.peerId, id: id) } else { @@ -118,14 +164,14 @@ extension MediaEditorScreen { switch subject { case let .image(image, dimensions, _, _): - saveImageDraft(image, dimensions) + innerSaveDraft(media: .image(image: image, dimensions: dimensions)) case let .video(path, _, _, _, _, dimensions, _, _, _): - saveVideoDraft(path, dimensions, duration) + innerSaveDraft(media: .video(path: path, dimensions: dimensions, duration: duration)) case let .asset(asset): if asset.mediaType == .video { PHImageManager.default().requestAVAsset(forVideo: asset, options: nil) { avAsset, _, _ in if let urlAsset = avAsset as? AVURLAsset { - saveVideoDraft(urlAsset.url.relativePath, PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight)), duration) + innerSaveDraft(media: .video(path: urlAsset.url.relativePath, dimensions: PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight)), duration: duration)) } } } else { @@ -133,19 +179,24 @@ extension MediaEditorScreen { options.deliveryMode = .highQualityFormat PHImageManager.default().requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .default, options: options) { image, _ in if let image { - saveImageDraft(image, PixelDimensions(image.size)) + innerSaveDraft(media: .image(image: image, dimensions: PixelDimensions(image.size))) } } } case let .draft(draft, _): if draft.isVideo { - saveVideoDraft(draft.fullPath(engine: context.engine), draft.dimensions, draft.duration ?? 0.0) + innerSaveDraft(media: .video(path: draft.fullPath(engine: context.engine), dimensions: draft.dimensions, duration: draft.duration ?? 0.0)) } else if let image = UIImage(contentsOfFile: draft.fullPath(engine: context.engine)) { - saveImageDraft(image, draft.dimensions) + innerSaveDraft(media: .image(image: image, dimensions: draft.dimensions)) } + case .message: + if let pixel = generateSingleColorImage(size: CGSize(width: 1, height: 1), color: .black) { + innerSaveDraft(media: .image(image: pixel, dimensions: PixelDimensions(width: 1080, height: 1920))) + } + } + + if case let .draft(draft, _) = actualSubject { removeStoryDraft(engine: self.context.engine, path: draft.path, delete: false) - case let .message(messageId): - let _ = messageId } }) } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 9d9ea09c53..990144081d 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -668,7 +668,7 @@ final class MediaEditorScreenComponent: Component { if self.component == nil { if let initialCaption = controller.initialCaption { self.inputPanelExternalState.initialText = initialCaption - } else if case let .draft(draft, _) = controller.node.subject { + } else if case let .draft(draft, _) = controller.node.actualSubject { self.inputPanelExternalState.initialText = draft.caption } } @@ -1633,7 +1633,7 @@ final class MediaEditorScreenComponent: Component { mediaEditor.toggleNightTheme() controller.node.entitiesView.eachView { view in if let stickerEntityView = view as? DrawingStickerEntityView { - stickerEntityView.toggleNightTheme() + stickerEntityView.isNightTheme = mediaEditor.values.nightTheme } } } @@ -2036,6 +2036,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate private let initializationTimestamp = CACurrentMediaTime() var subject: MediaEditorScreen.Subject? + var actualSubject: MediaEditorScreen.Subject? + private var subjectDisposable: Disposable? private var appInForegroundDisposable: Disposable? @@ -2270,7 +2272,19 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } private func setup(with subject: MediaEditorScreen.Subject) { - self.subject = subject + self.actualSubject = subject + + var effectiveSubject = subject + if case let .draft(draft, _ ) = subject { + for entity in draft.values.entities { + if case let .sticker(sticker) = entity, case let .message(ids, _, _) = sticker.content { + effectiveSubject = .message(ids) + break + } + } + } + self.subject = effectiveSubject + guard let controller = self.controller else { return } @@ -2299,7 +2313,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate controller.isSavingAvailable = isSavingAvailable controller.requestLayout(transition: .immediate) - let mediaDimensions = subject.dimensions + let mediaDimensions = effectiveSubject.dimensions let maxSide: CGFloat = 1920.0 / UIScreen.main.scale let fittedSize = mediaDimensions.cgSize.fitted(CGSize(width: maxSide, height: maxSide)) let mediaEntity = DrawingMediaEntity(size: fittedSize) @@ -2354,7 +2368,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } } - let mediaEditor = MediaEditor(context: self.context, subject: subject.editorSubject, values: initialValues, hasHistogram: true) + let mediaEditor = MediaEditor(context: self.context, subject: effectiveSubject.editorSubject, values: initialValues, hasHistogram: true) if let initialVideoPosition = controller.initialVideoPosition { mediaEditor.seek(initialVideoPosition, andPlay: true) } @@ -2372,12 +2386,12 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } } - if case .message = subject { + if case .message = effectiveSubject { } else { self.readyValue.set(.single(true)) } - if case let .image(_, _, additionalImage, position) = subject, let additionalImage { + if case let .image(_, _, additionalImage, position) = effectiveSubject, let additionalImage { let image = generateImage(CGSize(width: additionalImage.size.width, height: additionalImage.size.width), contextGenerator: { size, context in let bounds = CGRect(origin: .zero, size: size) context.clear(bounds) @@ -2393,7 +2407,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate imageEntity.scale = 1.625 imageEntity.position = position.getPosition(storyDimensions) self.entitiesView.add(imageEntity, announce: false) - } else if case let .video(_, _, mirror, additionalVideoPath, _, _, _, changes, position) = subject { + } else if case let .video(_, _, mirror, additionalVideoPath, _, _, _, changes, position) = effectiveSubject { mediaEditor.setVideoIsMirrored(mirror) if let additionalVideoPath { let videoEntity = DrawingStickerEntity(content: .dualVideoReference(false)) @@ -2412,7 +2426,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } } } - } else if case let .message(messageIds) = subject { + } else if case let .message(messageIds) = effectiveSubject { + let isNightTheme = mediaEditor.values.nightTheme let _ = ((self.context.engine.data.get( EngineDataMap(messageIds.map(TelegramEngine.EngineData.Item.Messages.Message.init(id:))) )) @@ -2431,16 +2446,30 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate let renderer = DrawingMessageRenderer(context: self.context, messages: messages) renderer.render(completion: { size, dayImage, nightImage in - let messageEntity = DrawingStickerEntity(content: .message(messageIds, maybeFile?.isVideo == true ? maybeFile : nil, size)) - messageEntity.renderImage = dayImage - messageEntity.secondaryRenderImage = nightImage - messageEntity.referenceDrawingSize = storyDimensions - messageEntity.position = CGPoint(x: storyDimensions.width / 2.0, y: storyDimensions.height / 2.0) - - let fraction = max(size.width, size.height) / 353.0 - messageEntity.scale = min(6.0, 3.3 * fraction) - - self.entitiesView.add(messageEntity, announce: false) + if case .draft = subject, let existingEntityView = self.entitiesView.getView(where: { entityView in + if let stickerEntityView = entityView as? DrawingStickerEntityView, case .message = (stickerEntityView.entity as! DrawingStickerEntity).content { + return true + } else { + return false + } + }) as? DrawingStickerEntityView { + existingEntityView.isNightTheme = isNightTheme + let messageEntity = existingEntityView.entity as! DrawingStickerEntity + messageEntity.renderImage = dayImage + messageEntity.secondaryRenderImage = nightImage + existingEntityView.update(animated: false) + } else { + let messageEntity = DrawingStickerEntity(content: .message(messageIds, maybeFile?.isVideo == true ? maybeFile : nil, size)) + messageEntity.renderImage = dayImage + messageEntity.secondaryRenderImage = nightImage + messageEntity.referenceDrawingSize = storyDimensions + messageEntity.position = CGPoint(x: storyDimensions.width / 2.0, y: storyDimensions.height / 2.0) + + let fraction = max(size.width, size.height) / 353.0 + messageEntity.scale = min(6.0, 3.3 * fraction) + + self.entitiesView.add(messageEntity, announce: false) + } self.readyValue.set(.single(true)) }) @@ -2477,7 +2506,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate if controller.isEmbeddedEditor == true { mediaEditor.onFirstDisplay = { [weak self] in if let self { - if subject.isPhoto { + if effectiveSubject.isPhoto { self.previewContainerView.layer.allowsGroupOpacity = true self.previewContainerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, completion: { _ in self.previewContainerView.layer.allowsGroupOpacity = false @@ -2955,7 +2984,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } } } else { - if case .message = self.subject, let layout = self.validLayout { + 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) completion() } else if let view = self.componentHost.view as? MediaEditorScreenComponent.View { @@ -2981,7 +3010,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.backgroundDimView.layer.animateAlpha(from: previousDimAlpha, to: 0.0, duration: 0.15) var isNew: Bool? = false - if let subject = self.subject { + if let subject = self.actualSubject { if saveDraft { isNew = true } @@ -3368,7 +3397,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } var location: CLLocationCoordinate2D? - if let subject = self.subject { + if let subject = self.actualSubject { if case let .asset(asset) = subject { location = asset.location?.coordinate } else if case let .draft(draft, _) = subject { @@ -4862,7 +4891,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } let title: String let save: String - if case .draft = self.node.subject { + if case .draft = self.node.actualSubject { title = presentationData.strings.Story_Editor_DraftDiscardDraft save = presentationData.strings.Story_Editor_DraftKeepDraft } else { @@ -4898,13 +4927,13 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.dismissAllTooltips() var showDraftTooltip = saveDraft - if let subject = self.node.subject, case .draft = subject { + if let subject = self.node.actualSubject, case .draft = subject { showDraftTooltip = false } if saveDraft { self.saveDraft(id: nil) } else { - if case let .draft(draft, id) = self.node.subject, id == nil { + if case let .draft(draft, id) = self.node.actualSubject, id == nil { removeStoryDraft(engine: self.context.engine, path: draft.path, delete: true) } } @@ -4957,7 +4986,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate private var didComplete = false func requestCompletion(animated: Bool) { - guard let mediaEditor = self.node.mediaEditor, let subject = self.node.subject, !self.didComplete else { + guard let mediaEditor = self.node.mediaEditor, let subject = self.node.subject, let actualSubject = self.node.actualSubject, !self.didComplete else { return } @@ -4983,14 +5012,14 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate var hasEntityChanges = false let randomId: Int64 - if case let .draft(_, id) = subject, let id { + if case let .draft(_, id) = actualSubject, let id { randomId = id } else { randomId = Int64.random(in: .min ... .max) } var mediaAreas: [MediaArea] = [] - if case let .draft(draft, _) = subject { + if case let .draft(draft, _) = actualSubject { if draft.values.entities != codableEntities { hasEntityChanges = true } @@ -5281,7 +5310,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } }) - if case let .draft(draft, id) = subject, id == nil { + if case let .draft(draft, id) = actualSubject, id == nil { removeStoryDraft(engine: self.context.engine, path: draft.path, delete: false) } } else { @@ -5299,7 +5328,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } }) }) - if case let .draft(draft, id) = subject, id == nil { + if case let .draft(draft, id) = actualSubject, id == nil { removeStoryDraft(engine: self.context.engine, path: draft.path, delete: true) } } diff --git a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift index e1028d3402..a23fef5c55 100644 --- a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift +++ b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift @@ -869,6 +869,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate }, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: { }, openStories: { _, _ in + }, dismissNotice: { _ in }) let chatListPresentationData = ChatListPresentationData(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true) diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridControllerNode.swift b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridControllerNode.swift index 65d5b031c0..b8743f6b10 100644 --- a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridControllerNode.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridControllerNode.swift @@ -258,8 +258,8 @@ final class ThemeGridControllerNode: ASDisplayNode { if case let .peer(_, _, _, _, customLevel) = mode { requiredCustomWallpaperLevel = customLevel } - //TODO:localize - self.galleryItem = ItemListPeerActionItem(presentationData: ItemListPresentationData(presentationData), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat/Attach Menu/Image"), color: presentationData.theme.list.itemAccentColor), title: presentationData.strings.Wallpaper_SetCustomBackground, additionalBadgeIcon: requiredCustomWallpaperLevel.flatMap { generateDisclosureActionBoostLevelBadgeImage(text: "Level \($0)") }, alwaysPlain: false, hasSeparator: true, sectionId: 0, height: .generic, color: .accent, editing: false, action: { + + self.galleryItem = ItemListPeerActionItem(presentationData: ItemListPresentationData(presentationData), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat/Attach Menu/Image"), color: presentationData.theme.list.itemAccentColor), title: presentationData.strings.Wallpaper_SetCustomBackground, additionalBadgeIcon: requiredCustomWallpaperLevel.flatMap { generateDisclosureActionBoostLevelBadgeImage(text: presentationData.strings.ChannelAppearance_BoostLevel("\($0)").string) }, alwaysPlain: false, hasSeparator: true, sectionId: 0, height: .generic, color: .accent, editing: false, action: { presentGallery() }) self.galleryItemNode = ItemListPeerActionItemNode() @@ -329,7 +329,7 @@ final class ThemeGridControllerNode: ASDisplayNode { if let strongSelf = self, !strongSelf.currentState.editing { let entries = previousEntries.with { $0 } if let entries = entries, !entries.isEmpty { - let wallpapers = entries.map { $0.wallpaper } + let wallpapers = entries.map { $0.wallpaper }.filter { !$0.isColorOrGradient } var options = WallpaperPresentationOptions() if wallpaper == strongSelf.presentationData.chatWallpaper, let settings = wallpaper.settings { @@ -684,7 +684,7 @@ final class ThemeGridControllerNode: ASDisplayNode { if case let .peer(_, _, _, _, customLevel) = mode { requiredCustomWallpaperLevel = customLevel } - self.galleryItem = ItemListPeerActionItem(presentationData: ItemListPresentationData(presentationData), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat/Attach Menu/Image"), color: presentationData.theme.list.itemAccentColor), title: presentationData.strings.Wallpaper_SetCustomBackground, additionalBadgeIcon: requiredCustomWallpaperLevel.flatMap { generateDisclosureActionBoostLevelBadgeImage(text: "Level \($0)") }, alwaysPlain: false, hasSeparator: true, sectionId: 0, height: .generic, color: .accent, editing: false, action: { [weak self] in + self.galleryItem = ItemListPeerActionItem(presentationData: ItemListPresentationData(presentationData), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat/Attach Menu/Image"), color: presentationData.theme.list.itemAccentColor), title: presentationData.strings.Wallpaper_SetCustomBackground, additionalBadgeIcon: requiredCustomWallpaperLevel.flatMap { generateDisclosureActionBoostLevelBadgeImage(text: presentationData.strings.ChannelAppearance_BoostLevel("\($0)").string) }, alwaysPlain: false, hasSeparator: true, sectionId: 0, height: .generic, color: .accent, editing: false, action: { [weak self] in self?.presentGallery() }) self.removeItem = ItemListPeerActionItem(presentationData: ItemListPresentationData(presentationData), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: presentationData.theme.list.itemDestructiveColor), title: presentationData.strings.Wallpaper_ChannelRemoveBackground, alwaysPlain: false, hasSeparator: true, sectionId: 0, height: .generic, color: .destructive, editing: false, action: { [weak self] in @@ -805,7 +805,7 @@ final class ThemeGridControllerNode: ASDisplayNode { var hasCustomWallpaper = false if case let .peer(_, _, wallpaper, _, _) = self.mode { isChannel = true - if let wallpaper, !wallpaper.isPattern { + if let wallpaper, !wallpaper.isEmoticon { hasCustomWallpaper = true } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index ad8c16d3fb..88a5109fbc 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -979,7 +979,7 @@ public final class StoryItemSetContainerComponent: Component { return abs(rotatedX) <= area.coordinates.width / 100.0 * referenceSize.width / 2.0 * 1.1 && abs(rotatedY) <= area.coordinates.height / 100.0 * referenceSize.height / 2.0 * 1.1 } - for area in component.slice.item.storyItem.mediaAreas { + for area in component.slice.item.storyItem.mediaAreas.reversed() { if case .reaction = area { continue } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index bc026afad1..98ae78ce9d 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -2415,14 +2415,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let strongSelf = self, let messages = strongSelf.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(id), let message = messages.first { let chatPresentationInterfaceState = strongSelf.presentationInterfaceState var warnAboutPrivate = false + var canShareToStory = false if case .peer = chatPresentationInterfaceState.chatLocation, let channel = message.peers[message.id.peerId] as? TelegramChannel { + canShareToStory = true if channel.addressName == nil { warnAboutPrivate = true } } let shareController = ShareController(context: strongSelf.context, subject: .messages(messages), updatedPresentationData: strongSelf.updatedPresentationData, shareAsLink: true) - var canShareToStory = true if let message = messages.first, message.media.contains(where: { media in if media is TelegramMediaContact || media is TelegramMediaPoll { return true @@ -6376,7 +6377,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var useDarkAppearance = presentationData.theme.overallDarkAppearance if let wallpaper = chatWallpaper, case let .emoticon(wallpaperEmoticon) = wallpaper, let theme = chatThemes.first(where: { $0.emoticon?.strippedEmoji == wallpaperEmoticon.strippedEmoji }) { - if let themeWallpaper = theme.settings?.first?.wallpaper { + let themeSettings: TelegramThemeSettings? + if let matching = theme.settings?.first(where: { $0.baseTheme == presentationData.theme.referenceTheme.baseTheme }) { + themeSettings = matching + } else { + themeSettings = theme.settings?.first + } + if let themeWallpaper = themeSettings?.wallpaper { chatWallpaper = themeWallpaper } } diff --git a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift index 6039167ad2..5b124d6689 100644 --- a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift +++ b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift @@ -272,6 +272,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDe }, openChatFolderUpdates: { }, hideChatFolderUpdates: { }, openStories: { _, _ in + }, dismissNotice: { _ in }) interaction.searchTextHighightState = searchQuery self.interaction = interaction