mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-08-08 08:31:13 +00:00
Merge commit '8659a5d6eab0db8063bb9ec097d89560e18ac7b0'
# Conflicts: # Telegram/Telegram-iOS/en.lproj/Localizable.strings
This commit is contained in:
commit
6ef84f3735
@ -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$@";
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
|
@ -101,6 +101,7 @@ public final class HashtagSearchController: TelegramBaseController {
|
||||
}, openChatFolderUpdates: {
|
||||
}, hideChatFolderUpdates: {
|
||||
}, openStories: { _, _ in
|
||||
}, dismissNotice: { _ in
|
||||
})
|
||||
|
||||
let previousSearchItems = Atomic<[ChatListSearchEntry]?>(value: nil)
|
||||
|
@ -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))
|
||||
|
@ -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)
|
||||
|
@ -375,6 +375,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
}, performActiveSessionAction: { _, _ in
|
||||
}, openChatFolderUpdates: {}, hideChatFolderUpdates: {
|
||||
}, openStories: { _, _ in
|
||||
}, dismissNotice: { _ in
|
||||
})
|
||||
|
||||
func makeChatListItem(
|
||||
|
@ -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(
|
||||
|
@ -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),
|
||||
|
@ -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
|
||||
|
@ -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),
|
||||
|
@ -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,
|
||||
|
@ -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),
|
||||
|
@ -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()
|
||||
|
@ -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),
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -272,6 +272,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDe
|
||||
}, openChatFolderUpdates: {
|
||||
}, hideChatFolderUpdates: {
|
||||
}, openStories: { _, _ in
|
||||
}, dismissNotice: { _ in
|
||||
})
|
||||
interaction.searchTextHighightState = searchQuery
|
||||
self.interaction = interaction
|
||||
|
Loading…
x
Reference in New Issue
Block a user