diff --git a/Telegram/NotificationService/Sources/NotificationService.swift b/Telegram/NotificationService/Sources/NotificationService.swift index 3e50ecf6e1..2ba4482b8c 100644 --- a/Telegram/NotificationService/Sources/NotificationService.swift +++ b/Telegram/NotificationService/Sources/NotificationService.swift @@ -613,7 +613,7 @@ private final class NotificationServiceHandler { let appVersion = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "unknown" - self.accountManager = AccountManager(basePath: rootPath + "/accounts-metadata", isTemporary: true, isReadOnly: false, useCaches: false) + self.accountManager = AccountManager(basePath: rootPath + "/accounts-metadata", isTemporary: true, isReadOnly: false, useCaches: false, removeDatabaseOnError: false) let deviceSpecificEncryptionParameters = BuildConfig.deviceSpecificEncryptionParameters(rootPath, baseAppBundleId: baseAppBundleId) self.encryptionParameters = ValueBoxEncryptionParameters(forceEncryptionIfNoSet: false, key: ValueBoxEncryptionParameters.Key(data: deviceSpecificEncryptionParameters.key)!, salt: ValueBoxEncryptionParameters.Salt(data: deviceSpecificEncryptionParameters.salt)!) diff --git a/Telegram/SiriIntents/IntentHandler.swift b/Telegram/SiriIntents/IntentHandler.swift index 075cd25ffd..b9e62875fb 100644 --- a/Telegram/SiriIntents/IntentHandler.swift +++ b/Telegram/SiriIntents/IntentHandler.swift @@ -121,7 +121,7 @@ class DefaultIntentHandler: INExtension, INSendMessageIntentHandling, INSearchFo let appVersion = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "unknown" initializeAccountManagement() - let accountManager = AccountManager(basePath: rootPath + "/accounts-metadata", isTemporary: true, isReadOnly: false, useCaches: false) + let accountManager = AccountManager(basePath: rootPath + "/accounts-metadata", isTemporary: true, isReadOnly: false, useCaches: false, removeDatabaseOnError: false) self.accountManager = accountManager let deviceSpecificEncryptionParameters = BuildConfig.deviceSpecificEncryptionParameters(rootPath, baseAppBundleId: baseAppBundleId) diff --git a/Telegram/Telegram-iOS/Resources/PlaneLogoPlain.tgs b/Telegram/Telegram-iOS/Resources/PlaneLogoPlain.tgs new file mode 100644 index 0000000000..1bfbf196d7 Binary files /dev/null and b/Telegram/Telegram-iOS/Resources/PlaneLogoPlain.tgs differ diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 21c11250d4..08524f9e6c 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -217,6 +217,40 @@ "PUSH_CHAT_MESSAGE_GAME_SCORE" = "%1$@ scored %4$@ in game %3$@ in the group %2$@"; "PUSH_CHAT_MESSAGE_VIDEOS" = "%1$@ sent %3$@ videos to the group %2$@"; +"PUSH_REACT_TEXT" = "%1$@|%2$@ to your %3$@"; +"PUSH_REACT_NOTEXT" = "%1$@|%2$@ to your message"; +"PUSH_REACT_PHOTO" = "%1$@|%2$@ to your photo"; +"PUSH_REACT_VIDEO" = "%1$@|%2$@ to your video"; +"PUSH_REACT_ROUND" = "%1$@|%2$@ to your video message"; +"PUSH_REACT_DOC" = "%1$@|%2$@ to your file"; +"PUSH_REACT_STICKER" = "%1$@|%2$@ to your %3$@sticker"; +"PUSH_REACT_AUDIO" = "%1$@|%2$@ to your voice message"; +"PUSH_REACT_CONTACT" = "%1$@|%2$@ to your contact %3$@"; +"PUSH_REACT_GEO" = "%1$@|%2$@ to your map"; +"PUSH_REACT_GEOLIVE" = "%1$@|%2$@ to your live location"; +"PUSH_REACT_POLL" = "%1$@|%2$@ to your poll %3$@"; +"PUSH_REACT_QUIZ" = "%1$@|%2$@ to your quiz %3$@"; +"PUSH_REACT_GAME" = "%1$@|%2$@ to your game"; +"PUSH_REACT_INVOICE" = "%1$@|%2$@ to your invoice"; +"PUSH_REACT_GIF" = "%1$@|%2$@ to your GIF"; + +"PUSH_CHAT_REACT_TEXT" = "%2$@|%1$@ %3$@ to your %4$@"; +"PUSH_CHAT_REACT_NOTEXT" = "%2$@|%1$@ %3$@ to your message"; +"PUSH_CHAT_REACT_PHOTO" = "%2$@|%1$@ %3$@ to your photo"; +"PUSH_CHAT_REACT_VIDEO" = "%2$@|%1$@ %3$@ to your video"; +"PUSH_CHAT_REACT_ROUND" = "%2$@|%1$@ %3$@ to your video message"; +"PUSH_CHAT_REACT_DOC" = "%2$@|%1$@ %3$@ to your file"; +"PUSH_CHAT_REACT_STICKER" = "%2$@|%1$@ %3$@ to your %4$@sticker"; +"PUSH_CHAT_REACT_AUDIO" = "%2$@|%1$@ %3$@ to your voice message"; +"PUSH_CHAT_REACT_CONTACT" = "%2$@|%1$@ %3$@ to your contact %4$@"; +"PUSH_CHAT_REACT_GEO" = "%2$@|%1$@ %3$@ to your map"; +"PUSH_CHAT_REACT_GEOLIVE" = "%2$@|%1$@ %3$@ to your live location"; +"PUSH_CHAT_REACT_POLL" = "%2$@|%1$@ %3$@ to your poll %4$@"; +"PUSH_CHAT_REACT_QUIZ" = "%2$@|%1$@ %3$@ to your quiz %4$@"; +"PUSH_CHAT_REACT_GAME" = "%2$@|%1$@ %3$@ to your game"; +"PUSH_CHAT_REACT_INVOICE" = "%2$@|%1$@ %3$@ to your invoice"; +"PUSH_CHAT_REACT_GIF" = "%2$@|%1$@ %3$@ to your GIF"; + "PUSH_REMINDER_TITLE" = "🗓 Reminder"; "PUSH_SENDER_YOU" = "📅 You"; @@ -7145,3 +7179,19 @@ Sorry for the inconvenience."; "ChatList.Archive" = "Archive"; "TextFormat.Spoiler" = "Spoiler"; + +"Conversation.ContextMenuTranslate" = "Translate"; + +"ClearCache.ClearDescription" = "All media will stay in the Telegram cloud and can be re-downloaded if you need it again."; + +"ChatSettings.StickersAndReactions" = "Stickers and Emoji"; + +"Localization.TranslateMessages" = "Translate Messages"; +"Localization.ShowTranslate" = "Show Translate Button"; +"Localization.ShowTranslateInfo" = "Show 'Translate' button in the message action menu."; +"Localization.DoNotTranslate" = "Do Not Translate"; +"Localization.DoNotTranslateInfo" = "Do not show 'Translate' button in the message action menu for this language"; +"Localization.DoNotTranslateManyInfo" = "Do not show 'Translate' button in the message action menu for this languages"; +"Localization.InterfaceLanguage" = "Interface Language"; + +"DoNotTranslate.Title" = "Do Not Translate"; diff --git a/buildbox/deploy-appcenter.sh b/buildbox/deploy-appcenter.sh index 141a43bf48..0c5195fe3f 100644 --- a/buildbox/deploy-appcenter.sh +++ b/buildbox/deploy-appcenter.sh @@ -9,5 +9,11 @@ DSYM_PATH="build/artifacts/Telegram.DSYMs.zip" APPCENTER="/usr/local/bin/appcenter" $APPCENTER login --token "$API_TOKEN" -$APPCENTER distribute release --app "$API_USER_NAME/$API_APP_NAME" -f "$IPA_PATH" -g Internal + +NEXT_WAIT_TIME=0 +until [ $NEXT_WAIT_TIME -eq 5 ] || $APPCENTER distribute release --app "$API_USER_NAME/$API_APP_NAME" -f "$IPA_PATH" -g Internal; do + sleep $(( NEXT_WAIT_TIME++ )) +done +[ $NEXT_WAIT_TIME -lt 10 ] + $APPCENTER crashes upload-symbols --app "$API_USER_NAME/$API_APP_NAME" --symbol "$DSYM_PATH" diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 3771943896..7065a4f7e4 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -602,7 +602,7 @@ public protocol SharedAccountContext: AnyObject { func makeComposeController(context: AccountContext) -> ViewController func makeChatListController(context: AccountContext, groupId: PeerGroupId, controlsHistoryPreload: Bool, hideNetworkActivityStatus: Bool, previewing: Bool, enableDebugActions: Bool) -> ChatListController func makeChatController(context: AccountContext, chatLocation: ChatLocation, subject: ChatControllerSubject?, botStart: ChatControllerInitialBotStart?, mode: ChatControllerPresentationMode) -> ChatController - func makeChatMessagePreviewItem(context: AccountContext, messages: [Message], theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder, forcedResourceStatus: FileMediaResourceStatus?, tapMessage: ((Message) -> Void)?, clickThroughMessage: (() -> Void)?, backgroundNode: ASDisplayNode?) -> ListViewItem + func makeChatMessagePreviewItem(context: AccountContext, messages: [Message], theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder, forcedResourceStatus: FileMediaResourceStatus?, tapMessage: ((Message) -> Void)?, clickThroughMessage: (() -> Void)?, backgroundNode: ASDisplayNode?, availableReactions: AvailableReactions?) -> ListViewItem func makeChatMessageDateHeaderItem(context: AccountContext, timestamp: Int32, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder) -> ListViewItemHeader func makePeerSharedMediaController(context: AccountContext, peerId: PeerId) -> ViewController? func makeContactSelectionController(_ params: ContactSelectionControllerParams) -> ContactSelectionController diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index ea22dc9a76..f82c3d695e 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -29,7 +29,7 @@ public final class ChatMessageItemAssociatedData: Equatable { public let availableReactions: AvailableReactions? public let defaultReaction: String? - public init(automaticDownloadPeerType: MediaAutoDownloadPeerType, automaticDownloadNetworkType: MediaAutoDownloadNetworkType, isRecentActions: Bool = false, subject: ChatControllerSubject? = nil, contactsPeerIds: Set = Set(), channelDiscussionGroup: ChannelDiscussionGroupStatus = .unknown, animatedEmojiStickers: [String: [StickerPackItem]] = [:], additionalAnimatedEmojiStickers: [String: [Int: StickerPackItem]] = [:], forcedResourceStatus: FileMediaResourceStatus? = nil, currentlyPlayingMessageId: EngineMessage.Index? = nil, isCopyProtectionEnabled: Bool = false, availableReactions: AvailableReactions?) { + public init(automaticDownloadPeerType: MediaAutoDownloadPeerType, automaticDownloadNetworkType: MediaAutoDownloadNetworkType, isRecentActions: Bool = false, subject: ChatControllerSubject? = nil, contactsPeerIds: Set = Set(), channelDiscussionGroup: ChannelDiscussionGroupStatus = .unknown, animatedEmojiStickers: [String: [StickerPackItem]] = [:], additionalAnimatedEmojiStickers: [String: [Int: StickerPackItem]] = [:], forcedResourceStatus: FileMediaResourceStatus? = nil, currentlyPlayingMessageId: EngineMessage.Index? = nil, isCopyProtectionEnabled: Bool = false, availableReactions: AvailableReactions?, defaultReaction: String?) { self.automaticDownloadPeerType = automaticDownloadPeerType self.automaticDownloadNetworkType = automaticDownloadNetworkType self.isRecentActions = isRecentActions @@ -42,15 +42,6 @@ public final class ChatMessageItemAssociatedData: Equatable { self.currentlyPlayingMessageId = currentlyPlayingMessageId self.isCopyProtectionEnabled = isCopyProtectionEnabled self.availableReactions = availableReactions - - var defaultReaction: String? - if let availableReactions = availableReactions { - for reaction in availableReactions.reactions { - if reaction.title.lowercased().contains("thumbs up") { - defaultReaction = reaction.value - } - } - } self.defaultReaction = defaultReaction } @@ -560,3 +551,7 @@ public enum FileMediaResourceMediaStatus: Equatable { case fetchStatus(MediaResourceStatus) case playbackStatus(FileMediaResourcePlaybackStatus) } + +public protocol ChatMessageItemNodeProtocol: ListViewItemNode { + func targetReactionView(value: String) -> UIView? +} diff --git a/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift b/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift index 9680abb33b..dee5e42818 100644 --- a/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift +++ b/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift @@ -13,22 +13,6 @@ import DirectMediaImageCache import TelegramStringFormatting import TooltipUI -private final class NullActionClass: NSObject, CAAction { - @objc func run(forKey event: String, object anObject: Any, arguments dict: [AnyHashable : Any]?) { - } -} - -private let nullAction = NullActionClass() - -private class SimpleLayer: CALayer { - override func action(forKey event: String) -> CAAction? { - return nullAction - } - - func update(size: CGSize) { - } -} - private enum SelectionTransition { case begin case change diff --git a/submodules/CallListUI/Sources/CallListController.swift b/submodules/CallListUI/Sources/CallListController.swift index f8750eeecb..0941143254 100644 --- a/submodules/CallListUI/Sources/CallListController.swift +++ b/submodules/CallListUI/Sources/CallListController.swift @@ -491,6 +491,7 @@ private final class CallListTabBarContextExtractedContentSource: ContextExtracte let keepInPlace: Bool = true let ignoreContentTouches: Bool = true let blurBackground: Bool = true + let centerActionsHorizontally: Bool = true private let controller: ViewController private let sourceNode: ContextExtractedContentContainingNode diff --git a/submodules/ChatListUI/BUILD b/submodules/ChatListUI/BUILD index 5123ae2fd3..652f74211d 100644 --- a/submodules/ChatListUI/BUILD +++ b/submodules/ChatListUI/BUILD @@ -61,6 +61,7 @@ swift_library( "//submodules/TelegramStringFormatting:TelegramStringFormatting", "//submodules/TelegramCallsUI:TelegramCallsUI", "//submodules/StickerResources:StickerResources", + "//submodules/TextFormat:TextFormat", ], visibility = [ "//visibility:public", diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 00fe2f8e1a..ffdbc52d92 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -2900,6 +2900,7 @@ private final class ChatListTabBarContextExtractedContentSource: ContextExtracte let keepInPlace: Bool = true let ignoreContentTouches: Bool = true let blurBackground: Bool = true + let centerActionsHorizontally: Bool = true private let controller: ChatListController private let sourceNode: ContextExtractedContentContainingNode diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 5b4b00a598..245220e46c 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -866,7 +866,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { let foundRemotePeers: Signal<([FoundPeer], [FoundPeer], Bool), NoError> let currentRemotePeersValue: ([FoundPeer], [FoundPeer]) = currentRemotePeers.with { $0 } ?? ([], []) - if let query = query { + if let query = query, tagMask == nil { foundRemotePeers = ( .single((currentRemotePeersValue.0, currentRemotePeersValue.1, true)) |> then( diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 17b5c9cf6e..72502f1f39 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -17,6 +17,8 @@ import PhotoResources import ChatListSearchItemNode import ContextUI import ChatInterfaceState +import TextFormat +import InvisibleInkDustNode public enum ChatListItemContent { public final class DraftState: Equatable { @@ -427,6 +429,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let measureNode: TextNode private var currentItemHeight: CGFloat? let textNode: TextNode + var dustNode: InvisibleInkDustNode? let inputActivitiesNode: ChatListInputActivitiesNode let dateNode: TextNode let separatorNode: ASDisplayNode @@ -1049,12 +1052,26 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { attributedText = NSAttributedString(string: foldLineBreaks(draftText.replacingOccurrences(of: "\n\n", with: " ")), font: textFont, textColor: theme.messageTextColor) } else if let message = messages.last { var composedString: NSMutableAttributedString + + let entities = (message._asMessage().textEntitiesAttribute?.entities ?? []).filter { entity in + if case .Spoiler = entity.type { + return true + } else { + return false + } + } + let messageString: NSAttributedString + if !message.text.isEmpty { + messageString = stringWithAppliedEntities(message.text, entities: entities, baseColor: theme.messageTextColor, linkColor: theme.messageTextColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false) + } else { + messageString = NSAttributedString(string: messageText, font: textFont, textColor: theme.messageTextColor) + } if let inlineAuthorPrefix = inlineAuthorPrefix { composedString = NSMutableAttributedString() composedString.append(NSAttributedString(string: "\(inlineAuthorPrefix): ", font: textFont, textColor: theme.titleColor)) - composedString.append(NSAttributedString(string: messageText, font: textFont, textColor: theme.messageTextColor)) + composedString.append(messageString) } else { - composedString = NSMutableAttributedString(string: messageText, font: textFont, textColor: theme.messageTextColor) + composedString = NSMutableAttributedString(attributedString: messageString) } if let searchQuery = item.interaction.searchTextHighightState { @@ -1395,7 +1412,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { textCutout = TextNodeCutout(topLeft: CGSize(width: textLeftCutout, height: 10.0), topRight: nil, bottomRight: nil) } let (textLayout, textApply) = textLayout(TextNodeLayoutArguments(attributedString: textAttributedString, backgroundColor: nil, maximumNumberOfLines: authorAttributedString == nil ? 2 : 1, truncationType: .end, constrainedSize: CGSize(width: rawContentWidth - badgeSize, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: textCutout, insets: UIEdgeInsets(top: 2.0, left: 1.0, bottom: 2.0, right: 1.0))) - + let titleRectWidth = rawContentWidth - dateLayout.size.width - 10.0 - statusWidth - titleIconsWidth let (titleLayout, titleApply) = titleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: titleRectWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) @@ -1730,6 +1747,24 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let textNodeFrame = CGRect(origin: CGPoint(x: contentRect.origin.x, y: contentRect.minY + titleLayout.size.height - 1.0 + UIScreenPixel + (authorLayout.size.height.isZero ? 0.0 : (authorLayout.size.height - 3.0))), size: textLayout.size) strongSelf.textNode.frame = textNodeFrame + if !textLayout.spoilers.isEmpty { + let dustNode: InvisibleInkDustNode + if let current = strongSelf.dustNode { + dustNode = current + } else { + dustNode = InvisibleInkDustNode(textNode: nil) + dustNode.isUserInteractionEnabled = false + strongSelf.dustNode = dustNode + strongSelf.contextContainer.insertSubnode(dustNode, aboveSubnode: strongSelf.textNode) + } + dustNode.update(size: textNodeFrame.size, color: theme.messageTextColor, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 0.0, dy: 1.0) }, wordRects: textLayout.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 0.0, dy: 1.0) }) + dustNode.frame = textNodeFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0) + + } else if let dustNode = strongSelf.dustNode { + strongSelf.dustNode = nil + dustNode.removeFromSupernode() + } + var animateInputActivitiesFrame = false let inputActivities = inputActivities?.filter({ switch $0.1 { @@ -1751,11 +1786,13 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { strongSelf.inputActivitiesNode.alpha = 1.0 strongSelf.textNode.alpha = 0.0 strongSelf.authorNode.alpha = 0.0 + strongSelf.dustNode?.alpha = 0.0 if animated || animateContent { strongSelf.inputActivitiesNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) strongSelf.textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15) strongSelf.authorNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15) + strongSelf.dustNode?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15) } } } else { @@ -1763,6 +1800,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { strongSelf.inputActivitiesNode.alpha = 0.0 strongSelf.textNode.alpha = 1.0 strongSelf.authorNode.alpha = 1.0 + strongSelf.dustNode?.alpha = 1.0 if animated || animateContent { strongSelf.inputActivitiesNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, completion: { value in if let strongSelf = self, value { @@ -1771,6 +1809,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { }) strongSelf.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) strongSelf.authorNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + strongSelf.dustNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) } else { strongSelf.inputActivitiesNode.removeFromSupernode() } @@ -1981,6 +2020,10 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { textFrame.origin.x = contentRect.origin.x transition.updateFrameAdditive(node: self.textNode, frame: textFrame) + if let dustNode = self.dustNode { + transition.updateFrameAdditive(node: dustNode, frame: textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0)) + } + var mediaPreviewOffsetX = textFrame.origin.x + 1.0 let contentImageSpacing: CGFloat = 2.0 for (_, media, mediaSize) in self.currentMediaPreviewSpecs { diff --git a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift index 846ee21db2..a98a6f642c 100644 --- a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift +++ b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift @@ -11,32 +11,7 @@ import UIKit import WebPBinding import AnimatedAvatarSetNode -private final class NullActionClass: NSObject, CAAction { - @objc func run(forKey event: String, object anObject: Any, arguments dict: [AnyHashable : Any]?) { - } -} - -private let nullAction = NullActionClass() - -private final class SimpleLayer: CALayer { - override func action(forKey event: String) -> CAAction? { - return nullAction - } - - override init() { - super.init() - } - - override init(layer: Any) { - super.init(layer: layer) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} - -fileprivate final class CounterLayer: CALayer { +fileprivate final class CounterLayer: SimpleLayer { fileprivate final class Layout { struct Spec: Equatable { let clippingHeight: CGFloat @@ -106,10 +81,6 @@ fileprivate final class CounterLayer: CALayer { fatalError("init(coder:) has not been implemented") } - override func action(forKey event: String) -> CAAction? { - return nullAction - } - func apply(layout: Layout, animation: ListViewItemUpdateAnimation) { /*if animation.isAnimated, let previousContents = self.contents { self.animate(from: previousContents as! CGImage, to: layout.image.cgImage!, keyPath: "contents", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.2) @@ -121,7 +92,7 @@ fileprivate final class CounterLayer: CALayer { } } -public final class ReactionButtonAsyncView: UIButton { +public final class ReactionButtonAsyncNode: ContextControllerSourceNode { fileprivate final class Layout { struct Spec: Equatable { var component: ReactionButtonComponent @@ -139,6 +110,7 @@ public final class ReactionButtonAsyncView: UIButton { let counterFrame: CGRect? let backgroundImage: UIImage + let extractedBackgroundImage: UIImage let size: CGSize @@ -151,6 +123,7 @@ public final class ReactionButtonAsyncView: UIButton { counter: CounterLayer.Layout?, counterFrame: CGRect?, backgroundImage: UIImage, + extractedBackgroundImage: UIImage, size: CGSize ) { self.spec = spec @@ -161,6 +134,7 @@ public final class ReactionButtonAsyncView: UIButton { self.counter = counter self.counterFrame = counterFrame self.backgroundImage = backgroundImage + self.extractedBackgroundImage = extractedBackgroundImage self.size = size } @@ -179,7 +153,7 @@ public final class ReactionButtonAsyncView: UIButton { } var counterComponents: [String] = [] - for character in "\(spec.component.count)" { + for character in countString(Int64(spec.component.count)) { counterComponents.append(String(character)) } @@ -190,17 +164,19 @@ public final class ReactionButtonAsyncView: UIButton { var previousDisplayCounter: String? if let currentLayout = currentLayout { if currentLayout.spec.component.avatarPeers.isEmpty { - previousDisplayCounter = "\(spec.component.count)" + previousDisplayCounter = countString(Int64(spec.component.count)) } } var currentDisplayCounter: String? if spec.component.avatarPeers.isEmpty { - currentDisplayCounter = "\(spec.component.count)" + currentDisplayCounter = countString(Int64(spec.component.count)) } let backgroundImage: UIImage + let extractedBackgroundImage: UIImage if let currentLayout = currentLayout, currentLayout.spec.component.isSelected == spec.component.isSelected, currentLayout.spec.component.colors == spec.component.colors, previousDisplayCounter == currentDisplayCounter { backgroundImage = currentLayout.backgroundImage + extractedBackgroundImage = currentLayout.extractedBackgroundImage } else { backgroundImage = generateImage(CGSize(width: height + 18.0, height: height), rotatedContext: { size, context in UIGraphicsPushContext(context) @@ -225,6 +201,31 @@ public final class ReactionButtonAsyncView: UIButton { string.draw(at: CGPoint(x: size.width - sideInsets - boundingRect.width, y: (size.height - boundingRect.height) / 2.0)) } + UIGraphicsPopContext() + })!.stretchableImage(withLeftCapWidth: Int(height / 2.0), topCapHeight: Int(height / 2.0)) + extractedBackgroundImage = generateImage(CGSize(width: height + 18.0, height: height), rotatedContext: { size, context in + UIGraphicsPushContext(context) + + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setBlendMode(.copy) + + context.setFillColor(UIColor(argb: spec.component.colors.extractedBackground).cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: height, height: height))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - height, y: 0.0), size: CGSize(width: height, height: size.height))) + context.fill(CGRect(origin: CGPoint(x: height / 2.0, y: 0.0), size: CGSize(width: size.width - height, height: size.height))) + + context.setBlendMode(.normal) + + if let currentDisplayCounter = currentDisplayCounter { + let textColor = UIColor(argb: spec.component.colors.extractedForeground) + let string = NSAttributedString(string: currentDisplayCounter, font: Font.medium(11.0), textColor: textColor) + let boundingRect = string.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil) + if textColor.alpha < 1.0 { + context.setBlendMode(.copy) + } + string.draw(at: CGPoint(x: size.width - sideInsets - boundingRect.width, y: (size.height - boundingRect.height) / 2.0)) + } + UIGraphicsPopContext() })!.stretchableImage(withLeftCapWidth: Int(height / 2.0), topCapHeight: Int(height / 2.0)) } @@ -270,6 +271,7 @@ public final class ReactionButtonAsyncView: UIButton { counter: counter, counterFrame: counterFrame, backgroundImage: backgroundImage, + extractedBackgroundImage: extractedBackgroundImage, size: size ) } @@ -277,21 +279,64 @@ public final class ReactionButtonAsyncView: UIButton { private var layout: Layout? + public let containerNode: ContextExtractedContentContainingNode + private let buttonNode: HighlightTrackingButtonNode public let iconView: UIImageView private var counterLayer: CounterLayer? private var avatarsView: AnimatedAvatarSetView? private let iconImageDisposable = MetaDisposable() - override init(frame: CGRect) { + override init() { + self.containerNode = ContextExtractedContentContainingNode() + self.buttonNode = HighlightTrackingButtonNode() + self.iconView = UIImageView() self.iconView.isUserInteractionEnabled = false - super.init(frame: CGRect()) + super.init() - self.addSubview(self.iconView) + self.targetNodeForActivationProgress = self.containerNode.contentNode - self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + self.addSubnode(self.containerNode) + self.containerNode.contentNode.addSubnode(self.buttonNode) + self.buttonNode.view.addSubview(self.iconView) + + self.buttonNode.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside) + + self.buttonNode.highligthedChanged = { [weak self] highlighted in + guard let strongSelf = self else { + return + } + let _ = strongSelf + if highlighted { + } else { + } + } + + self.isGestureEnabled = true + + self.containerNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, _ in + guard let strongSelf = self, let layout = strongSelf.layout else { + return + } + + let backgroundImage = isExtracted ? layout.extractedBackgroundImage : layout.backgroundImage + + let previousContents = strongSelf.buttonNode.layer.contents + + let backgroundCapInsets = backgroundImage.capInsets + if backgroundCapInsets.left.isZero && backgroundCapInsets.top.isZero { + strongSelf.buttonNode.layer.contentsScale = backgroundImage.scale + strongSelf.buttonNode.layer.contents = backgroundImage.cgImage + } else { + ASDisplayNodeSetResizableContents(strongSelf.buttonNode.layer, backgroundImage) + } + + if let previousContents = previousContents { + strongSelf.buttonNode.layer.animate(from: previousContents as! CGImage, to: backgroundImage.cgImage!, keyPath: "contents", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.2) + } + } } required init?(coder aDecoder: NSCoder) { @@ -310,12 +355,17 @@ public final class ReactionButtonAsyncView: UIButton { } fileprivate func apply(layout: Layout, animation: ListViewItemUpdateAnimation) { + self.containerNode.frame = CGRect(origin: CGPoint(), size: layout.size) + self.containerNode.contentNode.frame = CGRect(origin: CGPoint(), size: layout.size) + self.containerNode.contentRect = CGRect(origin: CGPoint(), size: layout.size) + animation.animator.updateFrame(layer: self.buttonNode.layer, frame: CGRect(origin: CGPoint(), size: layout.size), completion: nil) + let backgroundCapInsets = layout.backgroundImage.capInsets if backgroundCapInsets.left.isZero && backgroundCapInsets.top.isZero { - self.layer.contentsScale = layout.backgroundImage.scale - self.layer.contents = layout.backgroundImage.cgImage + self.buttonNode.layer.contentsScale = layout.backgroundImage.scale + self.buttonNode.layer.contents = layout.backgroundImage.cgImage } else { - ASDisplayNodeSetResizableContents(self.layer, layout.backgroundImage) + ASDisplayNodeSetResizableContents(self.buttonNode.layer, layout.backgroundImage) } animation.animator.updateFrame(layer: self.iconView.layer, frame: layout.imageFrame, completion: nil) @@ -372,7 +422,7 @@ public final class ReactionButtonAsyncView: UIButton { avatarsView = AnimatedAvatarSetView() avatarsView.isUserInteractionEnabled = false self.avatarsView = avatarsView - self.addSubview(avatarsView) + self.buttonNode.view.addSubview(avatarsView) } let content = AnimatedAvatarSetContext().update(peers: layout.spec.component.avatarPeers, animated: false) let avatarsSize = avatarsView.update( @@ -399,7 +449,7 @@ public final class ReactionButtonAsyncView: UIButton { self.layout = layout } - public static func asyncLayout(_ view: ReactionButtonAsyncView?) -> (ReactionButtonComponent) -> (size: CGSize, apply: (_ animation: ListViewItemUpdateAnimation) -> ReactionButtonAsyncView) { + public static func asyncLayout(_ view: ReactionButtonAsyncNode?) -> (ReactionButtonComponent) -> (size: CGSize, apply: (_ animation: ListViewItemUpdateAnimation) -> ReactionButtonAsyncNode) { let currentLayout = view?.layout return { component in @@ -414,11 +464,11 @@ public final class ReactionButtonAsyncView: UIButton { return (size: layout.size, apply: { animation in var animation = animation - let updatedView: ReactionButtonAsyncView + let updatedView: ReactionButtonAsyncNode if let view = view { updatedView = view } else { - updatedView = ReactionButtonAsyncView() + updatedView = ReactionButtonAsyncNode() animation = .None } @@ -464,17 +514,23 @@ public final class ReactionButtonComponent: Component { public var selectedBackground: UInt32 public var deselectedForeground: UInt32 public var selectedForeground: UInt32 + public var extractedBackground: UInt32 + public var extractedForeground: UInt32 public init( deselectedBackground: UInt32, selectedBackground: UInt32, deselectedForeground: UInt32, - selectedForeground: UInt32 + selectedForeground: UInt32, + extractedBackground: UInt32, + extractedForeground: UInt32 ) { self.deselectedBackground = deselectedBackground self.selectedBackground = selectedBackground self.deselectedForeground = deselectedForeground self.selectedForeground = selectedForeground + self.extractedBackground = extractedBackground + self.extractedForeground = extractedForeground } } @@ -613,7 +669,7 @@ public final class ReactionButtonComponent: Component { self.iconView.frame = CGRect(origin: CGPoint(x: sideInsets, y: floorToScreenPixels((height - imageSize.height) / 2.0)), size: imageSize) - let text = "\(component.count)" + let text = countString(Int64(component.count)) var measureText = "" for _ in 0 ..< text.count { measureText.append("0") @@ -675,6 +731,25 @@ public final class ReactionButtonComponent: Component { } public final class ReactionButtonsAsyncLayoutContainer { + public struct Reaction { + public var reaction: ReactionButtonComponent.Reaction + public var count: Int + public var peers: [EnginePeer] + public var isSelected: Bool + + public init( + reaction: ReactionButtonComponent.Reaction, + count: Int, + peers: [EnginePeer], + isSelected: Bool + ) { + self.reaction = reaction + self.count = count + self.peers = peers + self.isSelected = isSelected + } + } + public struct Result { public struct Item { public var size: CGSize @@ -687,15 +762,15 @@ public final class ReactionButtonsAsyncLayoutContainer { public struct ApplyResult { public struct Item { public var value: String - public var view: ReactionButtonAsyncView + public var node: ReactionButtonAsyncNode public var size: CGSize } public var items: [Item] - public var removedViews: [ReactionButtonAsyncView] + public var removedNodes: [ReactionButtonAsyncNode] } - public private(set) var buttons: [String: ReactionButtonAsyncView] = [:] + public private(set) var buttons: [String: ReactionButtonAsyncNode] = [:] public init() { } @@ -703,12 +778,12 @@ public final class ReactionButtonsAsyncLayoutContainer { public func update( context: AccountContext, action: @escaping (String) -> Void, - reactions: [ReactionButtonsLayoutContainer.Reaction], + reactions: [ReactionButtonsAsyncLayoutContainer.Reaction], colors: ReactionButtonComponent.Colors, constrainedWidth: CGFloat ) -> Result { var items: [Result.Item] = [] - var applyItems: [(key: String, size: CGSize, apply: (_ animation: ListViewItemUpdateAnimation) -> ReactionButtonAsyncView)] = [] + var applyItems: [(key: String, size: CGSize, apply: (_ animation: ListViewItemUpdateAnimation) -> ReactionButtonAsyncNode)] = [] var validIds = Set() for reaction in reactions.sorted(by: { lhs, rhs in @@ -737,7 +812,7 @@ public final class ReactionButtonsAsyncLayoutContainer { } } - let viewLayout = ReactionButtonAsyncView.asyncLayout(self.buttons[reaction.reaction.value]) + let viewLayout = ReactionButtonAsyncNode.asyncLayout(self.buttons[reaction.reaction.value]) let (size, apply) = viewLayout(ReactionButtonComponent( context: context, colors: colors, @@ -760,10 +835,10 @@ public final class ReactionButtonsAsyncLayoutContainer { removeIds.append(id) } } - var removedViews: [ReactionButtonAsyncView] = [] + var removedNodes: [ReactionButtonAsyncNode] = [] for id in removeIds { - if let view = self.buttons.removeValue(forKey: id) { - removedViews.append(view) + if let node = self.buttons.removeValue(forKey: id) { + removedNodes.append(node) } } @@ -772,128 +847,18 @@ public final class ReactionButtonsAsyncLayoutContainer { apply: { animation in var items: [ApplyResult.Item] = [] for (key, size, apply) in applyItems { - let view = apply(animation) - items.append(ApplyResult.Item(value: key, view: view, size: size)) + let node = apply(animation) + items.append(ApplyResult.Item(value: key, node: node, size: size)) if let current = self.buttons[key] { - assert(current === view) + assert(current === node) } else { - self.buttons[key] = view + self.buttons[key] = node } } - return ApplyResult(items: items, removedViews: removedViews) + return ApplyResult(items: items, removedNodes: removedNodes) } ) } } - -public final class ReactionButtonsLayoutContainer { - public struct Reaction { - public var reaction: ReactionButtonComponent.Reaction - public var count: Int - public var peers: [EnginePeer] - public var isSelected: Bool - - public init( - reaction: ReactionButtonComponent.Reaction, - count: Int, - peers: [EnginePeer], - isSelected: Bool - ) { - self.reaction = reaction - self.count = count - self.peers = peers - self.isSelected = isSelected - } - } - - public struct Result { - public struct Item { - public var view: ComponentHostView - public var size: CGSize - } - - public var items: [Item] - public var removedViews: [ComponentHostView] - } - - public private(set) var buttons: [String: ComponentHostView] = [:] - - public init() { - } - - public func update( - context: AccountContext, - action: @escaping (String) -> Void, - reactions: [Reaction], - colors: ReactionButtonComponent.Colors, - constrainedWidth: CGFloat, - transition: Transition - ) -> Result { - var items: [Result.Item] = [] - var removedViews: [ComponentHostView] = [] - - var validIds = Set() - for reaction in reactions.sorted(by: { lhs, rhs in - var lhsCount = lhs.count - if lhs.isSelected { - lhsCount -= 1 - } - var rhsCount = rhs.count - if rhs.isSelected { - rhsCount -= 1 - } - if lhsCount != rhsCount { - return lhsCount > rhsCount - } - return lhs.reaction.value < rhs.reaction.value - }) { - validIds.insert(reaction.reaction.value) - - let view: ComponentHostView - var itemTransition = transition - if let current = self.buttons[reaction.reaction.value] { - itemTransition = .immediate - view = current - } else { - view = ComponentHostView() - self.buttons[reaction.reaction.value] = view - } - let itemSize = view.update( - transition: itemTransition, - component: AnyComponent(ReactionButtonComponent( - context: context, - colors: colors, - reaction: reaction.reaction, - avatarPeers: reaction.peers, - count: reaction.count, - isSelected: reaction.isSelected, - action: action - )), - environment: {}, - containerSize: CGSize(width: constrainedWidth, height: 1000.0) - ) - items.append(Result.Item( - view: view, - size: itemSize - )) - } - - var removeIds: [String] = [] - for (id, view) in self.buttons { - if !validIds.contains(id) { - removeIds.append(id) - removedViews.append(view) - } - } - for id in removeIds { - self.buttons.removeValue(forKey: id) - } - - return Result( - items: items, - removedViews: removedViews - ) - } -} diff --git a/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift b/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift index 5f7ea8334e..0ae1d4ae53 100644 --- a/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift +++ b/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift @@ -187,6 +187,11 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent private let selectionHighlightNode: ASDisplayNode private let itemNodes: [ItemNode] + private struct ScrollToTabReaction { + var value: String? + } + private var scrollToTabReaction: ScrollToTabReaction? + var action: ((String?) -> Void)? init(context: AccountContext, availableReactions: AvailableReactions?, reactions: [(String?, Int)], message: EngineMessage) { @@ -217,6 +222,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent guard let strongSelf = self else { return } + strongSelf.scrollToTabReaction = ScrollToTabReaction(value: reaction) strongSelf.action?(reaction) } } @@ -256,6 +262,16 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent if self.scrollNode.view.contentSize != contentSize { self.scrollNode.view.contentSize = contentSize } + + if let scrollToTabReaction = self.scrollToTabReaction { + self.scrollToTabReaction = nil + for itemNode in self.itemNodes { + if itemNode.reaction == scrollToTabReaction.value { + self.scrollNode.view.scrollRectToVisible(itemNode.frame.insetBy(dx: -sideInset, dy: 0.0), animated: transition.isAnimated) + break + } + } + } } } @@ -316,6 +332,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent let avatarInset: CGFloat = 12.0 let avatarSpacing: CGFloat = 8.0 let avatarSize: CGFloat = 28.0 + let sideInset: CGFloat = 16.0 let reaction: String? = item.reaction if let reaction = reaction { @@ -336,7 +353,6 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent self.avatarNode.frame = CGRect(origin: CGPoint(x: avatarInset, y: floor((size.height - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize)) self.avatarNode.setPeer(context: self.context, theme: presentationData.theme, peer: item.peer, synchronousLoad: true) - let sideInset: CGFloat = 16.0 self.titleLabelNode.attributedText = NSAttributedString(string: item.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), font: Font.regular(17.0), textColor: presentationData.theme.contextMenu.primaryColor) var maxTextWidth: CGFloat = size.width - avatarInset - avatarSize - avatarSpacing - sideInset if reactionIconNode != nil { @@ -355,6 +371,54 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent } } + private struct ItemsState { + let listState: EngineMessageReactionListContext.State + let readStats: MessageReadStats? + + let mergedItems: [EngineMessageReactionListContext.Item] + + init(listState: EngineMessageReactionListContext.State, readStats: MessageReadStats?) { + self.listState = listState + self.readStats = readStats + + var mergedItems: [EngineMessageReactionListContext.Item] = listState.items + if !listState.canLoadMore, let readStats = readStats { + var existingPeers = Set(mergedItems.map(\.peer.id)) + for peer in readStats.peers { + if !existingPeers.contains(peer.id) { + existingPeers.insert(peer.id) + mergedItems.append(EngineMessageReactionListContext.Item(peer: peer, reaction: nil)) + } + } + } + self.mergedItems = mergedItems + } + + var totalCount: Int { + if !self.listState.canLoadMore { + return self.mergedItems.count + } else { + var value = self.listState.totalCount + if let readStats = self.readStats { + value = max(value, readStats.peers.count) + } + return value + } + } + + var canLoadMore: Bool { + return self.listState.canLoadMore + } + + func item(at index: Int) -> EngineMessageReactionListContext.Item? { + if index < self.mergedItems.count { + return self.mergedItems[index] + } else { + return nil + } + } + } + private let context: AccountContext private let availableReactions: AvailableReactions? let reaction: String? @@ -372,16 +436,20 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent private var apparentHeight: CGFloat = 0.0 private let listContext: EngineMessageReactionListContext - private var state: EngineMessageReactionListContext.State + private var state: ItemsState private var stateDisposable: Disposable? private var itemNodes: [Int: ItemNode] = [:] + private var placeholderItemImage: UIImage? + private var placeholderLayers: [Int: SimpleLayer] = [:] + init( context: AccountContext, availableReactions: AvailableReactions?, message: EngineMessage, reaction: String?, + readStats: MessageReadStats?, requestUpdate: @escaping (ReactionsTabNode, ContainedViewLayoutTransition) -> Void, requestUpdateApparentHeight: @escaping (ReactionsTabNode, ContainedViewLayoutTransition) -> Void, openPeer: @escaping (PeerId) -> Void @@ -393,9 +461,8 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent self.requestUpdateApparentHeight = requestUpdateApparentHeight self.openPeer = openPeer - self.presentationData = context.sharedContext.currentPresentationData.with({ $0 }) self.listContext = context.engine.messages.messageReactionList(message: message, reaction: reaction) - self.state = EngineMessageReactionListContext.State(message: message, reaction: reaction) + self.state = ItemsState(listState: EngineMessageReactionListContext.State(message: message, reaction: reaction), readStats: readStats) self.scrollNode = ASScrollNode() self.scrollNode.canCancelAllTouchesInViews = true @@ -418,14 +485,17 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent guard let strongSelf = self else { return } + let updatedState = ItemsState(listState: state, readStats: strongSelf.state.readStats) var animateIn = false - if strongSelf.state.items.isEmpty && !state.items.isEmpty { + if strongSelf.state.item(at: 0) == nil && updatedState.item(at: 0) != nil { animateIn = true } - strongSelf.state = state - strongSelf.requestUpdate(strongSelf, .immediate) + strongSelf.state = updatedState + strongSelf.requestUpdate(strongSelf, animateIn ? .animated(duration: 0.2, curve: .easeInOut) : .immediate) if animateIn { - strongSelf.scrollNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + for (_, itemNode) in strongSelf.itemNodes { + itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } } }) } @@ -438,7 +508,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent if self.ignoreScrolling { return } - self.updateVisibleItems(syncronousLoad: false) + self.updateVisibleItems(animated: false, syncronousLoad: false) if let size = self.currentSize { var apparentHeight = -self.scrollNode.view.contentOffset.y + self.scrollNode.view.contentSize.height @@ -452,7 +522,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent } } - private func updateVisibleItems(syncronousLoad: Bool) { + private func updateVisibleItems(animated: Bool, syncronousLoad: Bool) { guard let size = self.currentSize else { return } @@ -463,34 +533,50 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent let visibleBounds = self.scrollNode.bounds.insetBy(dx: 0.0, dy: -180.0) var validIds = Set() + var validPlaceholderIds = Set() let minVisibleIndex = max(0, Int(floor(visibleBounds.minY / itemHeight))) let maxVisibleIndex = Int(ceil(visibleBounds.maxY / itemHeight)) if minVisibleIndex <= maxVisibleIndex { for index in minVisibleIndex ... maxVisibleIndex { - if index >= self.state.items.count { - break - } - - validIds.insert(index) - - let itemNode: ItemNode - if let current = self.itemNodes[index] { - itemNode = current - } else { - let openPeer = self.openPeer - let peerId = self.state.items[index].peer.id - itemNode = ItemNode(context: self.context, availableReactions: self.availableReactions, action: { - openPeer(peerId) - }) - self.itemNodes[index] = itemNode - self.scrollNode.addSubnode(itemNode) - } - let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: CGFloat(index) * itemHeight), size: CGSize(width: size.width, height: itemHeight)) - itemNode.update(size: itemFrame.size, presentationData: presentationData, item: self.state.items[index], isLast: index == self.state.items.count - 1, syncronousLoad: syncronousLoad) - itemNode.frame = itemFrame + + if let item = self.state.item(at: index) { + validIds.insert(index) + + let itemNode: ItemNode + if let current = self.itemNodes[index] { + itemNode = current + } else { + let openPeer = self.openPeer + let peerId = item.peer.id + itemNode = ItemNode(context: self.context, availableReactions: self.availableReactions, action: { + openPeer(peerId) + }) + self.itemNodes[index] = itemNode + self.scrollNode.addSubnode(itemNode) + } + + itemNode.update(size: itemFrame.size, presentationData: presentationData, item: item, isLast: self.state.item(at: index + 1) == nil, syncronousLoad: syncronousLoad) + itemNode.frame = itemFrame + } else if index < self.state.totalCount { + validPlaceholderIds.insert(index) + + let placeholderLayer: SimpleLayer + if let current = self.placeholderLayers[index] { + placeholderLayer = current + } else { + placeholderLayer = SimpleLayer() + if let placeholderItemImage = self.placeholderItemImage { + ASDisplayNodeSetResizableContents(placeholderLayer, placeholderItemImage) + } + self.placeholderLayers[index] = placeholderLayer + self.scrollNode.layer.addSublayer(placeholderLayer) + } + + placeholderLayer.frame = itemFrame + } } } @@ -501,18 +587,71 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent itemNode.removeFromSupernode() } } - for id in removeIds { self.itemNodes.removeValue(forKey: id) } - if self.state.canLoadMore && maxVisibleIndex >= self.state.items.count - 16 { + var removePlaceholderIds: [Int] = [] + for (id, placeholderLayer) in self.placeholderLayers { + if !validPlaceholderIds.contains(id) { + removePlaceholderIds.append(id) + if animated { + placeholderLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak placeholderLayer] _ in + placeholderLayer?.removeFromSuperlayer() + }) + } else { + placeholderLayer.removeFromSuperlayer() + } + } + } + for id in removePlaceholderIds { + self.placeholderLayers.removeValue(forKey: id) + } + + if self.state.canLoadMore && maxVisibleIndex >= self.state.listState.items.count - 16 { self.listContext.loadMore() } } - func update(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> (size: CGSize, apparentHeight: CGFloat) { + func update(presentationData: PresentationData, constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> (size: CGSize, apparentHeight: CGFloat) { let itemHeight: CGFloat = 44.0 + + if self.presentationData?.theme !== presentationData.theme { + let sideInset: CGFloat = 40.0 + let avatarInset: CGFloat = 12.0 + let avatarSpacing: CGFloat = 8.0 + let avatarSize: CGFloat = 28.0 + let lineHeight: CGFloat = 8.0 + + let shimmeringForegroundColor: UIColor + let shimmeringColor: UIColor + if presentationData.theme.overallDarkAppearance { + let backgroundColor = presentationData.theme.contextMenu.backgroundColor.blitOver(presentationData.theme.list.plainBackgroundColor, alpha: 1.0) + + shimmeringForegroundColor = presentationData.theme.contextMenu.primaryColor.blitOver(backgroundColor, alpha: 0.1) + shimmeringColor = presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.3) + } else { + shimmeringForegroundColor = presentationData.theme.contextMenu.primaryColor.withMultipliedAlpha(0.07) + shimmeringColor = presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.3) + } + let _ = shimmeringColor + + self.placeholderItemImage = generateImage(CGSize(width: avatarInset + avatarSize + avatarSpacing + lineHeight + 2.0 + sideInset, height: itemHeight), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(shimmeringForegroundColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: avatarInset, y: floor((size.height - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize))) + + context.fillEllipse(in: CGRect(origin: CGPoint(x: avatarInset + avatarSize + avatarSpacing, y: floor((size.height - lineHeight) / 2.0)), size: CGSize(width: lineHeight + 2.0, height: lineHeight))) + })?.stretchableImage(withLeftCapWidth: Int(avatarInset + avatarSize + avatarSpacing + lineHeight / 2.0 + 1.0), topCapHeight: 0) + + if let placeholderItemImage = self.placeholderItemImage { + for (_, placeholderLayer) in self.placeholderLayers { + ASDisplayNodeSetResizableContents(placeholderLayer, placeholderItemImage) + } + } + } + self.presentationData = presentationData + let size = CGSize(width: constrainedSize.width, height: CGFloat(self.state.totalCount) * itemHeight) let containerSize = CGSize(width: size.width, height: min(constrainedSize.height, size.height)) @@ -528,7 +667,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent } self.ignoreScrolling = false - self.updateVisibleItems(syncronousLoad: !transition.isAnimated) + self.updateVisibleItems(animated: transition.isAnimated, syncronousLoad: !transition.isAnimated) var apparentHeight = -self.scrollNode.view.contentOffset.y + self.scrollNode.view.contentSize.height apparentHeight = max(apparentHeight, 44.0) @@ -563,9 +702,11 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent context: AccountContext, availableReactions: AvailableReactions?, message: EngineMessage, + reaction: String?, + readStats: MessageReadStats?, requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void, requestUpdateApparentHeight: @escaping (ContainedViewLayoutTransition) -> Void, - back: @escaping () -> Void, + back: (() -> Void)?, openPeer: @escaping (PeerId) -> Void ) { self.context = context @@ -579,34 +720,39 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent var requestUpdateTab: ((ReactionsTabNode, ContainedViewLayoutTransition) -> Void)? var requestUpdateTabApparentHeight: ((ReactionsTabNode, ContainedViewLayoutTransition) -> Void)? - self.backButtonNode = BackButtonNode() - self.backButtonNode?.action = { - back() + if let back = back { + self.backButtonNode = BackButtonNode() + self.backButtonNode?.action = { + back() + } } var reactions: [(String?, Int)] = [] var totalCount: Int = 0 if let reactionsAttribute = message._asMessage().reactionsAttribute { - for reaction in reactionsAttribute.reactions { - totalCount += Int(reaction.count) - reactions.append((reaction.value, Int(reaction.count))) + for listReaction in reactionsAttribute.reactions { + if reaction == nil || listReaction.value == reaction { + totalCount += Int(listReaction.count) + reactions.append((listReaction.value, Int(listReaction.count))) + } } } - reactions.insert((nil, totalCount), at: 0) + if reaction == nil { + reactions.insert((nil, totalCount), at: 0) + } - if reactions.count > 2 { + if reactions.count > 2 && totalCount > 10 { self.tabListNode = ReactionTabListNode(context: context, availableReactions: availableReactions, reactions: reactions, message: message) } self.reactions = reactions - self.separatorNode = ASDisplayNode() - self.currentTabNode = ReactionsTabNode( context: context, availableReactions: availableReactions, message: message, - reaction: nil, + reaction: reaction, + readStats: readStats, requestUpdate: { tab, transition in requestUpdateTab?(tab, transition) }, @@ -620,6 +766,10 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent super.init() + if self.backButtonNode != nil || self.tabListNode != nil { + self.separatorNode = ASDisplayNode() + } + if let backButtonNode = self.backButtonNode { self.addSubnode(backButtonNode) } @@ -642,6 +792,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent availableReactions: availableReactions, message: message, reaction: reaction, + readStats: nil, requestUpdate: { tab, transition in requestUpdateTab?(tab, transition) }, @@ -677,12 +828,12 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent } } - func update(constrainedWidth: CGFloat, maxHeight: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> (cleanSize: CGSize, apparentHeight: CGFloat) { + func update(presentationData: PresentationData, constrainedWidth: CGFloat, maxHeight: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> (cleanSize: CGSize, apparentHeight: CGFloat) { let constrainedSize = CGSize(width: min(260.0, constrainedWidth), height: maxHeight) var topContentHeight: CGFloat = 0.0 if let backButtonNode = self.backButtonNode { - let backButtonFrame = CGRect(origin: CGPoint(x: 0.0, y: topContentHeight), size: CGSize(width: constrainedSize.width, height: 45.0)) + let backButtonFrame = CGRect(origin: CGPoint(x: 0.0, y: topContentHeight), size: CGSize(width: constrainedSize.width, height: 44.0)) backButtonNode.update(size: backButtonFrame.size, presentationData: self.presentationData, isLast: self.tabListNode == nil) transition.updateFrame(node: backButtonNode, frame: backButtonFrame) topContentHeight += backButtonFrame.height @@ -704,7 +855,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent if self.currentTabNode.bounds.isEmpty { currentTabTransition = .immediate } - let currentTabLayout = self.currentTabNode.update(constrainedSize: CGSize(width: constrainedSize.width, height: constrainedSize.height - topContentHeight), transition: currentTabTransition) + let currentTabLayout = self.currentTabNode.update(presentationData: presentationData, constrainedSize: CGSize(width: constrainedSize.width, height: constrainedSize.height - topContentHeight), transition: currentTabTransition) currentTabTransition.updateFrame(node: self.currentTabNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topContentHeight), size: CGSize(width: currentTabLayout.size.width, height: currentTabLayout.size.height + 100.0))) if let dismissedTabNode = self.dismissedTabNode { @@ -734,13 +885,25 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent let context: AccountContext let availableReactions: AvailableReactions? let message: EngineMessage - let back: () -> Void + let reaction: String? + let readStats: MessageReadStats? + let back: (() -> Void)? let openPeer: (PeerId) -> Void - public init(context: AccountContext, availableReactions: AvailableReactions?, message: EngineMessage, back: @escaping () -> Void, openPeer: @escaping (PeerId) -> Void) { + public init( + context: AccountContext, + availableReactions: AvailableReactions?, + message: EngineMessage, + reaction: String?, + readStats: MessageReadStats?, + back: (() -> Void)?, + openPeer: @escaping (PeerId) -> Void + ) { self.context = context self.availableReactions = availableReactions self.message = message + self.reaction = reaction + self.readStats = readStats self.back = back self.openPeer = openPeer } @@ -753,6 +916,8 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent context: self.context, availableReactions: self.availableReactions, message: self.message, + reaction: self.reaction, + readStats: self.readStats, requestUpdate: requestUpdate, requestUpdateApparentHeight: requestUpdateApparentHeight, back: self.back, diff --git a/submodules/ComposePollUI/Sources/CreatePollTextInputItem.swift b/submodules/ComposePollUI/Sources/CreatePollTextInputItem.swift index 430c2a1e9e..3e4812eba5 100644 --- a/submodules/ComposePollUI/Sources/CreatePollTextInputItem.swift +++ b/submodules/ComposePollUI/Sources/CreatePollTextInputItem.swift @@ -316,7 +316,7 @@ public class CreatePollTextInputItemNode: ListViewItemNode, ASEditableTextNodeDe rightInset += inlineAction.icon.size.width + 8.0 } - let itemText = textAttributedStringForStateText(item.text, fontSize: 17.0, textColor: item.presentationData.theme.chat.inputPanel.primaryTextColor, accentTextColor: item.presentationData.theme.chat.inputPanel.panelControlAccentColor, writingDirection: nil) + let itemText = textAttributedStringForStateText(item.text, fontSize: 17.0, textColor: item.presentationData.theme.chat.inputPanel.primaryTextColor, accentTextColor: item.presentationData.theme.chat.inputPanel.panelControlAccentColor, writingDirection: nil, spoilersRevealed: false) let measureText = NSMutableAttributedString(attributedString: itemText) let measureRawString = measureText.string if measureRawString.hasSuffix("\n") || measureRawString.isEmpty { diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index 8a9a1ab98a..0ac7f7abd5 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -148,6 +148,10 @@ public final class ContextMenuActionItem { public protocol ContextMenuCustomNode: ASDisplayNode { func updateLayout(constrainedWidth: CGFloat, constrainedHeight: CGFloat) -> (CGSize, (CGSize, ContainedViewLayoutTransition) -> Void) func updateTheme(presentationData: PresentationData) + + func canBeHighlighted() -> Bool + func updateIsHighlighted(isHighlighted: Bool) + func performAction() } public protocol ContextMenuCustomItem { @@ -355,23 +359,28 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi } } if strongSelf.didMoveFromInitialGesturePoint { - let actionPoint = strongSelf.view.convert(localPoint, to: strongSelf.actionsContainerNode.view) - let actionNode = strongSelf.actionsContainerNode.actionNode(at: actionPoint) - if strongSelf.highlightedActionNode !== actionNode { - strongSelf.highlightedActionNode?.setIsHighlighted(false) - strongSelf.highlightedActionNode = actionNode - if let actionNode = actionNode { - actionNode.setIsHighlighted(true) - strongSelf.hapticFeedback.tap() + if let presentationNode = strongSelf.presentationNode { + let presentationPoint = strongSelf.view.convert(localPoint, to: presentationNode.view) + presentationNode.highlightGestureMoved(location: presentationPoint) + } else { + let actionPoint = strongSelf.view.convert(localPoint, to: strongSelf.actionsContainerNode.view) + let actionNode = strongSelf.actionsContainerNode.actionNode(at: actionPoint) + if strongSelf.highlightedActionNode !== actionNode { + strongSelf.highlightedActionNode?.setIsHighlighted(false) + strongSelf.highlightedActionNode = actionNode + if let actionNode = actionNode { + actionNode.setIsHighlighted(true) + strongSelf.hapticFeedback.tap() + } } - } - - if let reactionContextNode = strongSelf.reactionContextNode { - let reactionPoint = strongSelf.view.convert(localPoint, to: reactionContextNode.view) - let highlightedReaction = reactionContextNode.reaction(at: reactionPoint)?.reaction - if strongSelf.highlightedReaction?.rawValue != highlightedReaction?.rawValue { - strongSelf.highlightedReaction = highlightedReaction - strongSelf.hapticFeedback.tap() + + if let reactionContextNode = strongSelf.reactionContextNode { + let reactionPoint = strongSelf.view.convert(localPoint, to: reactionContextNode.view) + let highlightedReaction = reactionContextNode.reaction(at: reactionPoint)?.reaction + if strongSelf.highlightedReaction?.rawValue != highlightedReaction?.rawValue { + strongSelf.highlightedReaction = highlightedReaction + strongSelf.hapticFeedback.tap() + } } } } @@ -383,18 +392,22 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi } recognizer.externalUpdated = nil if strongSelf.didMoveFromInitialGesturePoint { - if let (_, _) = viewAndPoint { - if let highlightedActionNode = strongSelf.highlightedActionNode { - strongSelf.highlightedActionNode = nil - highlightedActionNode.performAction() - } - if let highlightedReaction = strongSelf.highlightedReaction { - strongSelf.reactionContextNode?.performReactionSelection(reaction: highlightedReaction) - } + if let presentationNode = strongSelf.presentationNode { + presentationNode.highlightGestureFinished(performAction: viewAndPoint != nil) } else { - if let highlightedActionNode = strongSelf.highlightedActionNode { - strongSelf.highlightedActionNode = nil - highlightedActionNode.setIsHighlighted(false) + if let (_, _) = viewAndPoint { + if let highlightedActionNode = strongSelf.highlightedActionNode { + strongSelf.highlightedActionNode = nil + highlightedActionNode.performAction() + } + if let highlightedReaction = strongSelf.highlightedReaction { + strongSelf.reactionContextNode?.performReactionSelection(reaction: highlightedReaction) + } + } else { + if let highlightedActionNode = strongSelf.highlightedActionNode { + strongSelf.highlightedActionNode = nil + highlightedActionNode.setIsHighlighted(false) + } } } } @@ -420,27 +433,32 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi } } if strongSelf.didMoveFromInitialGesturePoint { - let actionPoint = strongSelf.view.convert(localPoint, to: strongSelf.actionsContainerNode.view) - var actionNode = strongSelf.actionsContainerNode.actionNode(at: actionPoint) - if let actionNodeValue = actionNode, !actionNodeValue.isActionEnabled { - actionNode = nil - } - - if strongSelf.highlightedActionNode !== actionNode { - strongSelf.highlightedActionNode?.setIsHighlighted(false) - strongSelf.highlightedActionNode = actionNode - if let actionNode = actionNode { - actionNode.setIsHighlighted(true) - strongSelf.hapticFeedback.tap() + if let presentationNode = strongSelf.presentationNode { + let presentationPoint = strongSelf.view.convert(localPoint, to: presentationNode.view) + presentationNode.highlightGestureMoved(location: presentationPoint) + } else { + let actionPoint = strongSelf.view.convert(localPoint, to: strongSelf.actionsContainerNode.view) + var actionNode = strongSelf.actionsContainerNode.actionNode(at: actionPoint) + if let actionNodeValue = actionNode, !actionNodeValue.isActionEnabled { + actionNode = nil } - } - - if let reactionContextNode = strongSelf.reactionContextNode { - let reactionPoint = strongSelf.view.convert(localPoint, to: reactionContextNode.view) - let highlightedReaction = reactionContextNode.reaction(at: reactionPoint)?.reaction - if strongSelf.highlightedReaction?.rawValue != highlightedReaction?.rawValue { - strongSelf.highlightedReaction = highlightedReaction - strongSelf.hapticFeedback.tap() + + if strongSelf.highlightedActionNode !== actionNode { + strongSelf.highlightedActionNode?.setIsHighlighted(false) + strongSelf.highlightedActionNode = actionNode + if let actionNode = actionNode { + actionNode.setIsHighlighted(true) + strongSelf.hapticFeedback.tap() + } + } + + if let reactionContextNode = strongSelf.reactionContextNode { + let reactionPoint = strongSelf.view.convert(localPoint, to: reactionContextNode.view) + let highlightedReaction = reactionContextNode.reaction(at: reactionPoint)?.reaction + if strongSelf.highlightedReaction?.rawValue != highlightedReaction?.rawValue { + strongSelf.highlightedReaction = highlightedReaction + strongSelf.hapticFeedback.tap() + } } } } @@ -452,19 +470,23 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi } gesture.externalUpdated = nil if strongSelf.didMoveFromInitialGesturePoint { - if let (_, _) = viewAndPoint { - if let highlightedActionNode = strongSelf.highlightedActionNode { - strongSelf.highlightedActionNode = nil - highlightedActionNode.performAction() - } - - if let highlightedReaction = strongSelf.highlightedReaction { - strongSelf.reactionContextNode?.performReactionSelection(reaction: highlightedReaction) - } + if let presentationNode = strongSelf.presentationNode { + presentationNode.highlightGestureFinished(performAction: viewAndPoint != nil) } else { - if let highlightedActionNode = strongSelf.highlightedActionNode { - strongSelf.highlightedActionNode = nil - highlightedActionNode.setIsHighlighted(false) + if let (_, _) = viewAndPoint { + if let highlightedActionNode = strongSelf.highlightedActionNode { + strongSelf.highlightedActionNode = nil + highlightedActionNode.performAction() + } + + if let highlightedReaction = strongSelf.highlightedReaction { + strongSelf.reactionContextNode?.performReactionSelection(reaction: highlightedReaction) + } + } else { + if let highlightedActionNode = strongSelf.highlightedActionNode { + strongSelf.highlightedActionNode = nil + highlightedActionNode.setIsHighlighted(false) + } } } } @@ -1207,12 +1229,21 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi } func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) { + if let presentationNode = self.presentationNode { + presentationNode.addRelativeContentOffset(offset, transition: transition) + } if self.reactionContextNodeIsAnimatingOut, let reactionContextNode = self.reactionContextNode { reactionContextNode.bounds = reactionContextNode.bounds.offsetBy(dx: 0.0, dy: offset.y) transition.animateOffsetAdditive(node: reactionContextNode, offset: -offset.y) } } + func cancelReactionAnimation() { + if let presentationNode = self.presentationNode { + presentationNode.cancelReactionAnimation() + } + } + func animateOutToReaction(value: String, targetView: UIView, hideNode: Bool, completion: @escaping () -> Void) { if let presentationNode = self.presentationNode { presentationNode.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, completion: completion) @@ -2033,6 +2064,7 @@ public protocol ContextExtractedContentSource: AnyObject { var keepInPlace: Bool { get } var ignoreContentTouches: Bool { get } var blurBackground: Bool { get } + var centerActionsHorizontally: Bool { get } var shouldBeDismissed: Signal { get } func takeView() -> ContextControllerTakeViewInfo? @@ -2043,6 +2075,10 @@ public extension ContextExtractedContentSource { var centerVertically: Bool { return false } + + var centerActionsHorizontally: Bool { + return false + } var shouldBeDismissed: Signal { return .single(false) @@ -2076,7 +2112,7 @@ public enum ContextContentSource { } public protocol ContextControllerItemsNode: ASDisplayNode { - func update(constrainedWidth: CGFloat, maxHeight: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> (cleanSize: CGSize, apparentHeight: CGFloat) + func update(presentationData: PresentationData, constrainedWidth: CGFloat, maxHeight: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> (cleanSize: CGSize, apparentHeight: CGFloat) var apparentHeight: CGFloat { get } } @@ -2345,6 +2381,10 @@ public final class ContextController: ViewController, StandalonePresentableContr } } + public func cancelReactionAnimation() { + self.controllerNode.cancelReactionAnimation() + } + public func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) { self.controllerNode.addRelativeContentOffset(offset, transition: transition) } diff --git a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift index 2cc4332b79..c3ae419655 100644 --- a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift @@ -16,6 +16,9 @@ public protocol ContextControllerActionsStackItemNode: ASDisplayNode { standardWidth: CGFloat, transition: ContainedViewLayoutTransition ) -> (size: CGSize, apparentHeight: CGFloat) + + func highlightGestureMoved(location: CGPoint) + func highlightGestureFinished(performAction: Bool) } public protocol ContextControllerActionsStackItem: AnyObject { @@ -31,6 +34,10 @@ public protocol ContextControllerActionsStackItem: AnyObject { protocol ContextControllerActionsListItemNode: ASDisplayNode { func update(presentationData: PresentationData, constrainedSize: CGSize) -> (minSize: CGSize, apply: (_ size: CGSize, _ transition: ContainedViewLayoutTransition) -> Void) + + func canBeHighlighted() -> Bool + func updateIsHighlighted(isHighlighted: Bool) + func performAction() } private final class ContextControllerActionsListActionItemNode: HighlightTrackingButtonNode, ContextControllerActionsListItemNode { @@ -44,6 +51,8 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin private let subtitleNode: ImmediateTextNode private let iconNode: ASImageNode + private var iconDisposable: Disposable? + init( getController: @escaping () -> ContextControllerProtocol?, requestDismiss: @escaping (ContextMenuActionResult) -> Void, @@ -84,15 +93,17 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin if highlighted { strongSelf.highlightBackgroundNode.alpha = 1.0 } else { - let previousAlpha = strongSelf.highlightBackgroundNode.alpha strongSelf.highlightBackgroundNode.alpha = 0.0 - strongSelf.highlightBackgroundNode.layer.animateAlpha(from: previousAlpha, to: 0.0, duration: 0.2) } } self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside) } + deinit { + self.iconDisposable?.dispose() + } + @objc private func pressed() { guard let controller = self.getController() else { return @@ -115,6 +126,18 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin )) } + func canBeHighlighted() -> Bool { + return true + } + + func updateIsHighlighted(isHighlighted: Bool) { + self.highlightBackgroundNode.alpha = isHighlighted ? 1.0 : 0.0 + } + + func performAction() { + self.pressed() + } + func update(presentationData: PresentationData, constrainedSize: CGSize) -> (minSize: CGSize, apply: (_ size: CGSize, _ transition: ContainedViewLayoutTransition) -> Void) { let sideInset: CGFloat = 16.0 let verticalInset: CGFloat = 11.0 @@ -170,12 +193,29 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin ) } - let iconImage = self.iconNode.image ?? self.item.icon(presentationData.theme) + let iconSize: CGSize? + if let iconSource = self.item.iconSource { + iconSize = iconSource.size + if self.iconDisposable == nil { + self.iconDisposable = (iconSource.signal |> deliverOnMainQueue).start(next: { [weak self] image in + guard let strongSelf = self else { + return + } + strongSelf.iconNode.image = image + }) + } + } else if let image = self.iconNode.image { + iconSize = image.size + } else { + let iconImage = self.item.icon(presentationData.theme) + self.iconNode.image = iconImage + iconSize = iconImage?.size + } var maxTextWidth: CGFloat = constrainedSize.width maxTextWidth -= sideInset - if let iconImage = iconImage { - maxTextWidth -= max(standardIconWidth, iconImage.size.width) + if let iconSize = iconSize { + maxTextWidth -= max(standardIconWidth, iconSize.width) } else { maxTextWidth -= sideInset } @@ -187,8 +227,8 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin var minSize = CGSize() minSize.width += sideInset minSize.width += max(titleSize.width, subtitleSize.width) - if let iconImage = iconImage { - minSize.width += max(standardIconWidth, iconImage.size.width) + if let iconSize = iconSize { + minSize.width += max(standardIconWidth, iconSize.width) minSize.width += iconSideInset } else { minSize.width += sideInset @@ -204,23 +244,30 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin let titleFrame = CGRect(origin: CGPoint(x: sideInset, y: verticalInset), size: titleSize) let subtitleFrame = CGRect(origin: CGPoint(x: sideInset, y: titleFrame.maxY + titleSubtitleSpacing), size: subtitleSize) - transition.updateFrame(node: self.highlightBackgroundNode, frame: CGRect(origin: CGPoint(), size: size)) + transition.updateFrame(node: self.highlightBackgroundNode, frame: CGRect(origin: CGPoint(), size: size), beginWithCurrentState: true) transition.updateFrameAdditive(node: self.titleLabelNode, frame: titleFrame) transition.updateFrameAdditive(node: self.subtitleNode, frame: subtitleFrame) - if let iconImage = iconImage { - if self.iconNode.image !== iconImage { - self.iconNode.image = iconImage - } - let iconWidth = max(standardIconWidth, iconImage.size.width) - let iconFrame = CGRect(origin: CGPoint(x: size.width - iconSideInset - iconWidth + floor((iconWidth - iconImage.size.width) / 2.0), y: floor((size.height - iconImage.size.height) / 2.0)), size: iconImage.size) - transition.updateFrame(node: self.iconNode, frame: iconFrame) + if let iconSize = iconSize { + let iconWidth = max(standardIconWidth, iconSize.width) + let iconFrame = CGRect(origin: CGPoint(x: size.width - iconSideInset - iconWidth + floor((iconWidth - iconSize.width) / 2.0), y: floor((size.height - iconSize.height) / 2.0)), size: iconSize) + transition.updateFrame(node: self.iconNode, frame: iconFrame, beginWithCurrentState: true) } }) } } private final class ContextControllerActionsListSeparatorItemNode: ASDisplayNode, ContextControllerActionsListItemNode { + func canBeHighlighted() -> Bool { + return false + } + + func updateIsHighlighted(isHighlighted: Bool) { + } + + func performAction() { + } + override init() { super.init() } @@ -233,6 +280,26 @@ private final class ContextControllerActionsListSeparatorItemNode: ASDisplayNode } private final class ContextControllerActionsListCustomItemNode: ASDisplayNode, ContextControllerActionsListItemNode { + func canBeHighlighted() -> Bool { + if let itemNode = self.itemNode { + return itemNode.canBeHighlighted() + } else { + return false + } + } + + func updateIsHighlighted(isHighlighted: Bool) { + if let itemNode = self.itemNode { + itemNode.updateIsHighlighted(isHighlighted: isHighlighted) + } + } + + func performAction() { + if let itemNode = self.itemNode { + itemNode.performAction() + } + } + private let getController: () -> ContextControllerProtocol? private let item: ContextMenuCustomItem @@ -275,7 +342,7 @@ private final class ContextControllerActionsListCustomItemNode: ASDisplayNode, C let itemLayoutAndApply = itemNode.updateLayout(constrainedWidth: constrainedSize.width, constrainedHeight: constrainedSize.height) return (minSize: itemLayoutAndApply.0, apply: { size, transition in - transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(), size: size)) + transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(), size: size), beginWithCurrentState: true) itemLayoutAndApply.1(size, transition) }) } @@ -297,6 +364,9 @@ final class ContextControllerActionsListStackItem: ContextControllerActionsStack private var items: [ContextMenuItem] private var itemNodes: [Item] + private var hapticFeedback: HapticFeedback? + private var highlightedItemNode: Item? + init( getController: @escaping () -> ContextControllerProtocol?, requestDismiss: @escaping (ContextMenuActionResult) -> Void, @@ -421,10 +491,10 @@ final class ContextControllerActionsListStackItem: ContextControllerActionsStack let itemSize = CGSize(width: combinedSize.width, height: itemNodeLayout.minSize.height) let itemFrame = CGRect(origin: nextItemOrigin, size: itemSize) - itemTransition.updateFrame(node: item.node, frame: itemFrame) + itemTransition.updateFrame(node: item.node, frame: itemFrame, beginWithCurrentState: true) if let separatorNode = item.separatorNode { - itemTransition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: itemFrame.minX, y: itemFrame.maxY), size: CGSize(width: itemFrame.width, height: UIScreenPixel))) + itemTransition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: itemFrame.minX, y: itemFrame.maxY), size: CGSize(width: itemFrame.width, height: UIScreenPixel)), beginWithCurrentState: true) if i != self.itemNodes.count - 1 { switch self.items[i + 1] { case .separator: @@ -445,6 +515,38 @@ final class ContextControllerActionsListStackItem: ContextControllerActionsStack return (combinedSize, combinedSize.height) } + + func highlightGestureMoved(location: CGPoint) { + var highlightedItemNode: Item? + for itemNode in self.itemNodes { + if itemNode.node.frame.contains(location) { + if itemNode.node.canBeHighlighted() { + highlightedItemNode = itemNode + } + break + } + } + if self.highlightedItemNode !== highlightedItemNode { + self.highlightedItemNode?.node.updateIsHighlighted(isHighlighted: false) + highlightedItemNode?.node.updateIsHighlighted(isHighlighted: true) + + self.highlightedItemNode = highlightedItemNode + if self.hapticFeedback == nil { + self.hapticFeedback = HapticFeedback() + } + self.hapticFeedback?.tap() + } + } + + func highlightGestureFinished(performAction: Bool) { + if let highlightedItemNode = self.highlightedItemNode { + self.highlightedItemNode = nil + highlightedItemNode.node.updateIsHighlighted(isHighlighted: false) + if performAction { + highlightedItemNode.node.performAction() + } + } + } } private let items: [ContextMenuItem] @@ -503,15 +605,22 @@ final class ContextControllerActionsCustomStackItem: ContextControllerActionsSta transition: ContainedViewLayoutTransition ) -> (size: CGSize, apparentHeight: CGFloat) { let contentLayout = self.contentNode.update( + presentationData: presentationData, constrainedWidth: constrainedSize.width, maxHeight: constrainedSize.height, bottomInset: 0.0, transition: transition ) - transition.updateFrame(node: self.contentNode, frame: CGRect(origin: CGPoint(), size: contentLayout.cleanSize)) + transition.updateFrame(node: self.contentNode, frame: CGRect(origin: CGPoint(), size: contentLayout.cleanSize), beginWithCurrentState: true) return (contentLayout.cleanSize, contentLayout.apparentHeight) } + + func highlightGestureMoved(location: CGPoint) { + } + + func highlightGestureFinished(performAction: Bool) { + } } private let content: ContextControllerItemsContent @@ -555,18 +664,65 @@ func makeContextControllerActionsStackItem(items: ContextController.Items) -> Co final class ContextControllerActionsStackNode: ASDisplayNode { final class NavigationContainer: ASDisplayNode { + var requestUpdate: ((ContainedViewLayoutTransition) -> Void)? + var requestPop: (() -> Void)? + var transitionFraction: CGFloat = 0.0 + + private var panRecognizer: UIPanGestureRecognizer? + + var isNavigationEnabled: Bool = false { + didSet { + self.panRecognizer?.isEnabled = self.isNavigationEnabled + } + } + override init() { super.init() self.clipsToBounds = true self.cornerRadius = 14.0 + + let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) + self.panRecognizer = panRecognizer + self.view.addGestureRecognizer(panRecognizer) + } + + @objc private func panGesture(_ recognizer: UIPanGestureRecognizer) { + switch recognizer.state { + case .began: + self.transitionFraction = 0.0 + case .changed: + let distanceFactor: CGFloat = recognizer.translation(in: self.view).x / self.bounds.width + let transitionFraction = max(0.0, min(1.0, distanceFactor)) + if self.transitionFraction != transitionFraction { + self.transitionFraction = transitionFraction + self.requestUpdate?(.immediate) + } + case .ended, .cancelled: + let distanceFactor: CGFloat = recognizer.translation(in: self.view).x / self.bounds.width + let transitionFraction = max(0.0, min(1.0, distanceFactor)) + if transitionFraction > 0.2 { + self.transitionFraction = 0.0 + self.requestPop?() + } else { + self.transitionFraction = 0.0 + self.requestUpdate?(.animated(duration: 0.45, curve: .spring)) + } + default: + break + } + } + + func update(presentationData: PresentationData, size: CGSize, transition: ContainedViewLayoutTransition) { } } final class ItemContainer: ASDisplayNode { let requestUpdate: (ContainedViewLayoutTransition) -> Void let node: ContextControllerActionsStackItemNode + let dimNode: ASDisplayNode let reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem])? + var storedScrollingState: CGFloat? let positionLock: CGFloat? init( @@ -586,18 +742,26 @@ final class ContextControllerActionsStackNode: ASDisplayNode { requestUpdateApparentHeight: requestUpdateApparentHeight ) + self.dimNode = ASDisplayNode() + self.dimNode.isUserInteractionEnabled = false + self.dimNode.alpha = 0.0 + self.reactionItems = reactionItems self.positionLock = positionLock super.init() + self.clipsToBounds = true + self.addSubnode(self.node) + self.addSubnode(self.dimNode) } func update( presentationData: PresentationData, constrainedSize: CGSize, standardWidth: CGFloat, + transitionFraction: CGFloat, transition: ContainedViewLayoutTransition ) -> (size: CGSize, apparentHeight: CGFloat) { let (size, apparentHeight) = self.node.update( @@ -606,10 +770,33 @@ final class ContextControllerActionsStackNode: ASDisplayNode { standardWidth: standardWidth, transition: transition ) - transition.updateFrame(node: self.node, frame: CGRect(origin: CGPoint(), size: size)) + + let maxScaleOffset: CGFloat = 10.0 + let scaleOffset: CGFloat = 0.0 * transitionFraction + maxScaleOffset * (1.0 - transitionFraction) + let scale: CGFloat = (size.width - scaleOffset) / size.width + let yOffset: CGFloat = size.height * (1.0 - scale) + let transitionOffset = (1.0 - transitionFraction) * size.width / 2.0 + transition.updatePosition(node: self.node, position: CGPoint(x: size.width / 2.0 + scaleOffset / 2.0 + transitionOffset, y: size.height / 2.0 - yOffset / 2.0), beginWithCurrentState: true) + transition.updateBounds(node: self.node, bounds: CGRect(origin: CGPoint(), size: size), beginWithCurrentState: true) + transition.updateTransformScale(node: self.node, scale: scale, beginWithCurrentState: true) return (size, apparentHeight) } + + func updateDimNode(presentationData: PresentationData, size: CGSize, transitionFraction: CGFloat, transition: ContainedViewLayoutTransition) { + self.dimNode.backgroundColor = presentationData.theme.contextMenu.sectionSeparatorColor + + transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: size), beginWithCurrentState: true) + transition.updateAlpha(node: self.dimNode, alpha: 1.0 - transitionFraction, beginWithCurrentState: true) + } + + func highlightGestureMoved(location: CGPoint) { + self.node.highlightGestureMoved(location: self.view.convert(location, to: self.node.view)) + } + + func highlightGestureFinished(performAction: Bool) { + self.node.highlightGestureFinished(performAction: performAction) + } } private let getController: () -> ContextControllerProtocol? @@ -620,6 +807,8 @@ final class ContextControllerActionsStackNode: ASDisplayNode { private var itemContainers: [ItemContainer] = [] private var dismissingItemContainers: [(container: ItemContainer, isPopped: Bool)] = [] + private var selectionPanGesture: UIPanGestureRecognizer? + var topReactionItems: (context: AccountContext, reactionItems: [ReactionContextItem])? { return self.itemContainers.last?.reactionItems } @@ -628,6 +817,10 @@ final class ContextControllerActionsStackNode: ASDisplayNode { return self.itemContainers.last?.positionLock } + var storedScrollingState: CGFloat? { + return self.itemContainers.last?.storedScrollingState + } + init( getController: @escaping () -> ContextControllerProtocol?, requestDismiss: @escaping (ContextMenuActionResult) -> Void, @@ -642,6 +835,39 @@ final class ContextControllerActionsStackNode: ASDisplayNode { super.init() self.addSubnode(self.navigationContainer) + + self.navigationContainer.requestUpdate = { [weak self] transition in + guard let strongSelf = self else { + return + } + strongSelf.requestUpdate(transition) + } + + self.navigationContainer.requestPop = { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.pop() + } + + let selectionPanGesture = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) + self.selectionPanGesture = selectionPanGesture + self.view.addGestureRecognizer(selectionPanGesture) + selectionPanGesture.isEnabled = false + } + + @objc private func panGesture(_ recognizer: UIPanGestureRecognizer) { + switch recognizer.state { + case .changed: + let location = recognizer.location(in: self.view) + self.highlightGestureMoved(location: location) + case .ended: + self.highlightGestureFinished(performAction: true) + case .cancelled: + self.highlightGestureFinished(performAction: false) + default: + break + } } func replace(item: ContextControllerActionsStackItem, animated: Bool) { @@ -653,11 +879,15 @@ final class ContextControllerActionsStackNode: ASDisplayNode { } } self.itemContainers.removeAll() + self.navigationContainer.isNavigationEnabled = self.itemContainers.count > 1 - self.push(item: item, positionLock: nil, animated: animated) + self.push(item: item, currentScrollingState: nil, positionLock: nil, animated: animated) } - func push(item: ContextControllerActionsStackItem, positionLock: CGFloat?, animated: Bool) { + func push(item: ContextControllerActionsStackItem, currentScrollingState: CGFloat?, positionLock: CGFloat?, animated: Bool) { + if let itemContainer = self.itemContainers.last { + itemContainer.storedScrollingState = currentScrollingState + } let itemContainer = ItemContainer( getController: self.getController, requestDismiss: self.requestDismiss, @@ -674,6 +904,7 @@ final class ContextControllerActionsStackNode: ASDisplayNode { ) self.itemContainers.append(itemContainer) self.navigationContainer.addSubnode(itemContainer) + self.navigationContainer.isNavigationEnabled = self.itemContainers.count > 1 let transition: ContainedViewLayoutTransition if animated { @@ -684,6 +915,10 @@ final class ContextControllerActionsStackNode: ASDisplayNode { self.requestUpdate(transition) } + func clearStoredScrollingState() { + self.itemContainers.last?.storedScrollingState = nil + } + func pop() { if self.itemContainers.count == 1 { //dismiss @@ -693,6 +928,8 @@ final class ContextControllerActionsStackNode: ASDisplayNode { self.dismissingItemContainers.append((itemContainer, true)) } + self.navigationContainer.isNavigationEnabled = self.itemContainers.count > 1 + let transition: ContainedViewLayoutTransition = .animated(duration: 0.45, curve: .spring) self.requestUpdate(transition) } @@ -706,8 +943,17 @@ final class ContextControllerActionsStackNode: ASDisplayNode { let animateAppearingContainers = transition.isAnimated && !self.dismissingItemContainers.isEmpty + struct ItemLayout { + var size: CGSize + var apparentHeight: CGFloat + var transitionFraction: CGFloat + var alphaTransitionFraction: CGFloat + var itemTransition: ContainedViewLayoutTransition + var animateAppearingContainer: Bool + } + var topItemSize = CGSize() - var topItemApparentHeight: CGFloat = 0.0 + var itemLayouts: [ItemLayout] = [] for i in 0 ..< self.itemContainers.count { let itemContainer = self.itemContainers[i] @@ -720,39 +966,113 @@ final class ContextControllerActionsStackNode: ASDisplayNode { let itemConstrainedHeight: CGFloat = constrainedSize.height + let transitionFraction: CGFloat + let alphaTransitionFraction: CGFloat + if i == self.itemContainers.count - 1 { + transitionFraction = self.navigationContainer.transitionFraction + alphaTransitionFraction = 1.0 + } else if i == self.itemContainers.count - 2 { + transitionFraction = self.navigationContainer.transitionFraction - 1.0 + alphaTransitionFraction = self.navigationContainer.transitionFraction + } else { + transitionFraction = 0.0 + alphaTransitionFraction = 0.0 + } + let itemSize = itemContainer.update( presentationData: presentationData, constrainedSize: CGSize(width: constrainedSize.width, height: itemConstrainedHeight), - standardWidth: 260.0, + standardWidth: 250.0, + transitionFraction: alphaTransitionFraction, transition: itemContainerTransition ) if i == self.itemContainers.count - 1 { topItemSize = itemSize.size - topItemApparentHeight = itemSize.apparentHeight } - let itemFrame: CGRect - if i == self.itemContainers.count - 1 { - itemFrame = CGRect(origin: CGPoint(), size: itemSize.size) - } else { - itemFrame = CGRect(origin: CGPoint(x: -itemSize.size.width, y: 0.0), size: itemSize.size) - } - - itemContainerTransition.updateFrame(node: itemContainer, frame: itemFrame) - if animateAppearingContainer { - transition.animatePositionAdditive(node: itemContainer, offset: CGPoint(x: itemContainer.bounds.width, y: 0.0)) - } + itemLayouts.append(ItemLayout( + size: itemSize.size, + apparentHeight: itemSize.apparentHeight, + transitionFraction: transitionFraction, + alphaTransitionFraction: alphaTransitionFraction, + itemTransition: itemContainerTransition, + animateAppearingContainer: animateAppearingContainer + )) } - transition.updateFrame(node: self.navigationContainer, frame: CGRect(origin: CGPoint(), size: CGSize(width: topItemSize.width, height: max(44.0, topItemApparentHeight)))) + let topItemApparentHeight: CGFloat + let topItemWidth: CGFloat + if itemLayouts.isEmpty { + topItemApparentHeight = 0.0 + topItemWidth = 0.0 + } else if itemLayouts.count == 1 { + topItemApparentHeight = itemLayouts[0].apparentHeight + topItemWidth = itemLayouts[0].size.width + } else { + let lastItemLayout = itemLayouts[itemLayouts.count - 1] + let previousItemLayout = itemLayouts[itemLayouts.count - 2] + let transitionFraction = self.navigationContainer.transitionFraction + + topItemApparentHeight = lastItemLayout.apparentHeight * (1.0 - transitionFraction) + previousItemLayout.apparentHeight * transitionFraction + topItemWidth = lastItemLayout.size.width * (1.0 - transitionFraction) + previousItemLayout.size.width * transitionFraction + } + + let navigationContainerFrame = CGRect(origin: CGPoint(), size: CGSize(width: topItemWidth, height: max(14 * 2.0, topItemApparentHeight))) + transition.updateFrame(node: self.navigationContainer, frame: navigationContainerFrame, beginWithCurrentState: true) + self.navigationContainer.update(presentationData: presentationData, size: navigationContainerFrame.size, transition: transition) + + for i in 0 ..< self.itemContainers.count { + let xOffset: CGFloat + if itemLayouts[i].transitionFraction < 0.0 { + xOffset = itemLayouts[i].transitionFraction * itemLayouts[i].size.width + } else { + if i != 0 { + xOffset = itemLayouts[i].transitionFraction * itemLayouts[i - 1].size.width + } else { + xOffset = itemLayouts[i].transitionFraction * topItemWidth + } + } + let itemFrame = CGRect(origin: CGPoint(x: xOffset, y: 0.0), size: CGSize(width: itemLayouts[i].size.width, height: navigationContainerFrame.height)) + + itemLayouts[i].itemTransition.updateFrame(node: self.itemContainers[i], frame: itemFrame, beginWithCurrentState: true) + if itemLayouts[i].animateAppearingContainer { + transition.animatePositionAdditive(node: self.itemContainers[i], offset: CGPoint(x: itemFrame.width, y: 0.0)) + } + + self.itemContainers[i].updateDimNode(presentationData: presentationData, size: CGSize(width: itemLayouts[i].size.width, height: navigationContainerFrame.size.height), transitionFraction: itemLayouts[i].alphaTransitionFraction, transition: transition) + } for (itemContainer, isPopped) in self.dismissingItemContainers { - transition.updatePosition(node: itemContainer, position: CGPoint(x: isPopped ? itemContainer.bounds.width * 3.0 / 2.0 : -itemContainer.bounds.width / 2.0, y: itemContainer.position.y), completion: { [weak itemContainer] _ in + var position = itemContainer.position + if isPopped { + position.x = itemContainer.bounds.width / 2.0 + topItemWidth + } else { + position.x = itemContainer.bounds.width / 2.0 - topItemWidth + } + transition.updatePosition(node: itemContainer, position: position, completion: { [weak itemContainer] _ in itemContainer?.removeFromSupernode() }) } self.dismissingItemContainers.removeAll() - return CGSize(width: topItemSize.width, height: topItemSize.height) + return CGSize(width: topItemWidth, height: topItemSize.height) + } + + func highlightGestureMoved(location: CGPoint) { + if let topItemContainer = self.itemContainers.last { + topItemContainer.highlightGestureMoved(location: self.view.convert(location, to: topItemContainer.view)) + } + } + + func highlightGestureFinished(performAction: Bool) { + if let topItemContainer = self.itemContainers.last { + topItemContainer.highlightGestureFinished(performAction: performAction) + } + } + + func updatePanSelection(isEnabled: Bool) { + if let selectionPanGesture = self.selectionPanGesture { + selectionPanGesture.isEnabled = isEnabled + } } } diff --git a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift index 6f1489864f..b6ccc1f53f 100644 --- a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift @@ -8,7 +8,7 @@ import TelegramCore import SwiftSignalKit import ReactionSelectionNode -final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextControllerPresentationNode { +final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextControllerPresentationNode, UIScrollViewDelegate { private final class ContentNode: ASDisplayNode { let offsetContainerNode: ASDisplayNode let containingNode: ContextExtractedContentContainingNode @@ -23,12 +23,17 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo super.init() self.addSubnode(self.offsetContainerNode) - self.offsetContainerNode.addSubnode(self.containingNode.contentNode) } func update(presentationData: PresentationData, size: CGSize, transition: ContainedViewLayoutTransition) { } + func takeContainingNode() { + if self.containingNode.contentNode.supernode !== self.offsetContainerNode { + self.offsetContainerNode.addSubnode(self.containingNode.contentNode) + } + } + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if !self.bounds.contains(point) { return nil @@ -121,6 +126,8 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo self.scrollNode.addSubnode(self.contentRectDebugNode) #endif*/ + self.scrollNode.view.delegate = self + self.dismissTapNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dismissTapGesture(_:)))) } @@ -137,27 +144,69 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo return result } } + + if !self.source.ignoreContentTouches, let contentNode = self.contentNode { + let contentPoint = self.view.convert(point, to: contentNode.containingNode.contentNode.view) + if let result = contentNode.containingNode.contentNode.customHitTest?(contentPoint) { + return result + } else if let result = contentNode.containingNode.contentNode.hitTest(contentPoint, with: event) { + if result is TextSelectionNodeView { + return result + } else if contentNode.containingNode.contentRect.contains(contentPoint) { + return contentNode.containingNode.contentNode.view + } + } + } + return self.scrollNode.hitTest(self.view.convert(point, to: self.scrollNode.view), with: event) } else { return nil } } + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if let reactionContextNode = self.reactionContextNode { + let isIntersectingContent = scrollView.contentOffset.y >= 10.0 + reactionContextNode.updateIsIntersectingContent(isIntersectingContent: isIntersectingContent, transition: .animated(duration: 0.25, curve: .easeInOut)) + } + } + + func highlightGestureMoved(location: CGPoint) { + self.actionsStackNode.highlightGestureMoved(location: self.view.convert(location, to: self.actionsStackNode.view)) + + if let reactionContextNode = self.reactionContextNode { + reactionContextNode.highlightGestureMoved(location: self.view.convert(location, to: reactionContextNode.view)) + } + } + + func highlightGestureFinished(performAction: Bool) { + self.actionsStackNode.highlightGestureFinished(performAction: performAction) + + if let reactionContextNode = self.reactionContextNode { + reactionContextNode.highlightGestureFinished(performAction: performAction) + } + } + func replaceItems(items: ContextController.Items, animated: Bool) { self.actionsStackNode.replace(item: makeContextControllerActionsStackItem(items: items), animated: animated) } func pushItems(items: ContextController.Items) { + let currentScrollingState = self.getCurrentScrollingState() let positionLock = self.getActionsStackPositionLock() - self.actionsStackNode.push(item: makeContextControllerActionsStackItem(items: items), positionLock: positionLock, animated: true) + self.actionsStackNode.push(item: makeContextControllerActionsStackItem(items: items), currentScrollingState: currentScrollingState, positionLock: positionLock, animated: true) } func popItems() { self.actionsStackNode.pop() } + private func getCurrentScrollingState() -> CGFloat { + return self.scrollNode.view.contentOffset.y + } + private func getActionsStackPositionLock() -> CGFloat? { - return self.actionsStackNode.frame.minY + return self.actionsStackNode.view.convert(CGPoint(), to: self.view).y } func update( @@ -166,7 +215,8 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo transition: ContainedViewLayoutTransition, stateTransition: ContextControllerPresentationNodeStateTransition? ) { - let contentActionsSpacing: CGFloat = 8.0 + let contentActionsSpacing: CGFloat = 7.0 + let actionsEdgeInset: CGFloat = 12.0 let actionsSideInset: CGFloat = 6.0 let topInset: CGFloat = layout.insets(options: .statusBar).top + 8.0 let bottomInset: CGFloat = 10.0 @@ -181,12 +231,12 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo transition: .immediate ) - transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: layout.size), beginWithCurrentState: true) self.backgroundNode.update(size: layout.size, transition: transition) - transition.updateFrame(node: self.clippingNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + transition.updateFrame(node: self.clippingNode, frame: CGRect(origin: CGPoint(), size: layout.size), beginWithCurrentState: true) if self.scrollNode.frame != CGRect(origin: CGPoint(), size: layout.size) { - transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: layout.size), beginWithCurrentState: true) } if let current = self.contentNode { @@ -236,7 +286,9 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo contentNode.storedGlobalFrame = convertFrame(contentNode.containingNode.contentRect, from: contentNode.containingNode.view, to: self.view) } } - //let contentRectGlobalFrame = contentNode.storedGlobalFrame ?? convertFrame(contentNode.containingNode.contentRect, from: contentNode.containingNode.view, to: self.view) + + let contentParentGlobalFrame = convertFrame(contentNode.containingNode.bounds, from: contentNode.containingNode.view, to: self.view) + let contentRectGlobalFrame = CGRect(origin: CGPoint(x: contentNode.containingNode.contentRect.minX, y: (contentNode.storedGlobalFrame?.maxY ?? 0.0) - contentNode.containingNode.contentRect.height), size: contentNode.containingNode.contentRect.size) var contentRect = CGRect(origin: CGPoint(x: contentRectGlobalFrame.minX, y: contentRectGlobalFrame.maxY - contentNode.containingNode.contentRect.size.height), size: contentNode.containingNode.contentRect.size) if case .animateOut = stateTransition { @@ -255,7 +307,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo if let actionsPositionLock = self.actionsStackNode.topPositionLock { actionsConstrainedHeight = layout.size.height - bottomInset - layout.intrinsicInsets.bottom - actionsPositionLock } else { - actionsConstrainedHeight = layout.size.height + actionsConstrainedHeight = layout.size.height - contentTopInset - contentRect.height - contentActionsSpacing - bottomInset - layout.intrinsicInsets.bottom } let actionsSize = self.actionsStackNode.update( @@ -266,18 +318,23 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo if case .animateOut = stateTransition { } else { - if contentRect.minY < contentTopInset { - contentRect.origin.y = contentTopInset + if let topPositionLock = self.actionsStackNode.topPositionLock { + contentRect.origin.y = topPositionLock - contentActionsSpacing - contentRect.height + } else if self.source.keepInPlace { + } else { + if contentRect.minY < contentTopInset { + contentRect.origin.y = contentTopInset + } + var combinedBounds = CGRect(origin: CGPoint(x: 0.0, y: contentRect.minY), size: CGSize(width: layout.size.width, height: contentRect.height + contentActionsSpacing + actionsSize.height)) + if combinedBounds.maxY > layout.size.height - bottomInset - layout.intrinsicInsets.bottom { + combinedBounds.origin.y = layout.size.height - bottomInset - layout.intrinsicInsets.bottom - combinedBounds.height + } + if combinedBounds.minY < contentTopInset { + combinedBounds.origin.y = contentTopInset + } + + contentRect.origin.y = combinedBounds.minY } - var combinedBounds = CGRect(origin: CGPoint(x: 0.0, y: contentRect.minY), size: CGSize(width: layout.size.width, height: contentRect.height + contentActionsSpacing + actionsSize.height)) - if combinedBounds.maxY > layout.size.height - bottomInset - layout.intrinsicInsets.bottom { - combinedBounds.origin.y = layout.size.height - bottomInset - layout.intrinsicInsets.bottom - combinedBounds.height - } - if combinedBounds.minY < contentTopInset { - combinedBounds.origin.y = contentTopInset - } - - contentRect.origin.y = combinedBounds.minY } if let reactionContextNode = self.reactionContextNode { @@ -285,8 +342,8 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo if reactionContextNode.frame.isEmpty { reactionContextNodeTransition = .immediate } - reactionContextNodeTransition.updateFrame(node: reactionContextNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - reactionContextNode.updateLayout(size: layout.size, insets: UIEdgeInsets(top: topInset, left: 0.0, bottom: 0.0, right: 0.0), anchorRect: contentRect, transition: reactionContextNodeTransition) + reactionContextNodeTransition.updateFrame(node: reactionContextNode, frame: CGRect(origin: CGPoint(), size: layout.size), beginWithCurrentState: true) + reactionContextNode.updateLayout(size: layout.size, insets: UIEdgeInsets(top: topInset, left: 0.0, bottom: 0.0, right: 0.0), anchorRect: contentRect.offsetBy(dx: contentParentGlobalFrame.minX, dy: 0.0), transition: reactionContextNodeTransition) } if let removedReactionContextNode = removedReactionContextNode { removedReactionContextNode.animateOut(to: contentRect, animatingOutToReaction: false) @@ -295,30 +352,61 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo }) } - transition.updateFrame(node: self.contentRectDebugNode, frame: contentRect) + transition.updateFrame(node: self.contentRectDebugNode, frame: contentRect, beginWithCurrentState: true) - var actionsFrame = CGRect(origin: CGPoint(x: 0.0, y: contentRect.maxY + contentActionsSpacing), size: actionsSize) - if contentRect.midX < layout.size.width / 2.0 { - actionsFrame.origin.x = contentRect.minX + actionsSideInset - 4.0 - } else { - actionsFrame.origin.x = contentRect.maxX - actionsSideInset - actionsSize.width - 1.0 + var actionsFrame = CGRect(origin: CGPoint(x: actionsSideInset, y: contentRect.maxY + contentActionsSpacing), size: actionsSize) + if self.source.keepInPlace { + actionsFrame.origin.y = contentRect.minY - contentActionsSpacing - actionsFrame.height } - transition.updateFrame(node: self.actionsStackNode, frame: actionsFrame) + if self.source.centerActionsHorizontally { + actionsFrame.origin.x = floor(contentParentGlobalFrame.minX + contentRect.midX - actionsFrame.width / 2.0) + if actionsFrame.maxX > layout.size.width - actionsEdgeInset { + actionsFrame.origin.x = layout.size.width - actionsEdgeInset - actionsFrame.width + } + if actionsFrame.minX < actionsEdgeInset { + actionsFrame.origin.x = actionsEdgeInset + } + } else { + if contentRect.midX < layout.size.width / 2.0 { + actionsFrame.origin.x = contentParentGlobalFrame.minX + contentRect.minX + actionsSideInset - 4.0 + } else { + actionsFrame.origin.x = contentParentGlobalFrame.minX + contentRect.maxX - actionsSideInset - actionsSize.width - 1.0 + } + if actionsFrame.maxX > layout.size.width - actionsEdgeInset { + actionsFrame.origin.x = layout.size.width - actionsEdgeInset - actionsFrame.width + } + if actionsFrame.minX < actionsEdgeInset { + actionsFrame.origin.x = actionsEdgeInset + } + } + transition.updateFrame(node: self.actionsStackNode, frame: actionsFrame, beginWithCurrentState: true) - contentTransition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(x: contentRect.minX - contentNode.containingNode.contentRect.minX, y: contentRect.minY - contentNode.containingNode.contentRect.minY), size: contentNode.containingNode.bounds.size)) + contentTransition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(x: contentParentGlobalFrame.minX + contentRect.minX - contentNode.containingNode.contentRect.minX, y: contentRect.minY - contentNode.containingNode.contentRect.minY), size: contentNode.containingNode.bounds.size), beginWithCurrentState: true) - let contentHeight = actionsFrame.maxY + bottomInset + layout.intrinsicInsets.bottom + let contentHeight: CGFloat + if self.actionsStackNode.topPositionLock != nil { + contentHeight = layout.size.height + } else { + contentHeight = actionsFrame.maxY + bottomInset + layout.intrinsicInsets.bottom + } let contentSize = CGSize(width: layout.size.width, height: contentHeight) if self.scrollNode.view.contentSize != contentSize { let previousContentOffset = self.scrollNode.view.contentOffset self.scrollNode.view.contentSize = contentSize + if let storedScrollingState = self.actionsStackNode.storedScrollingState { + self.actionsStackNode.clearStoredScrollingState() + + self.scrollNode.view.contentOffset = CGPoint(x: 0.0, y: storedScrollingState) + } if case .none = stateTransition, transition.isAnimated { let contentOffset = self.scrollNode.view.contentOffset transition.animateOffsetAdditive(layer: self.scrollNode.layer, offset: previousContentOffset.y - contentOffset.y) } } + self.actionsStackNode.updatePanSelection(isEnabled: contentSize.height <= layout.size.height) + defaultScrollY = contentSize.height - layout.size.height if defaultScrollY < 0.0 { defaultScrollY = 0.0 @@ -329,6 +417,8 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo switch stateTransition { case .animateIn: + contentNode.takeContainingNode() + let duration: Double = 0.42 let springDamping: CGFloat = 104.0 @@ -337,7 +427,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) if let animateClippingFromContentAreaInScreenSpace = contentNode.animateClippingFromContentAreaInScreenSpace { - self.clippingNode.layer.animateFrame(from: animateClippingFromContentAreaInScreenSpace, to: CGRect(origin: CGPoint(), size: layout.size), duration: 0.2) + self.clippingNode.layer.animateFrame(from: CGRect(origin: CGPoint(x: 0.0, y: animateClippingFromContentAreaInScreenSpace.minY), size: CGSize(width: layout.size.width, height: animateClippingFromContentAreaInScreenSpace.height)), to: CGRect(origin: CGPoint(), size: layout.size), duration: 0.2) self.clippingNode.layer.animateBoundsOriginYAdditive(from: animateClippingFromContentAreaInScreenSpace.minY, to: 0.0, duration: 0.2) } @@ -370,7 +460,13 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo let actionsSize = self.actionsStackNode.bounds.size let actionsPositionDeltaXDistance: CGFloat = 0.0 - let actionsPositionDeltaYDistance = -animationInContentDistance - actionsSize.height / 2.0 - contentActionsSpacing + let actionsVerticalTransitionDirection: CGFloat + if contentNode.frame.minY < self.actionsStackNode.frame.minY { + actionsVerticalTransitionDirection = -1.0 + } else { + actionsVerticalTransitionDirection = 1.0 + } + let actionsPositionDeltaYDistance = -animationInContentDistance + actionsVerticalTransitionDirection * actionsSize.height / 2.0 - contentActionsSpacing self.actionsStackNode.layer.animateSpring( from: NSValue(cgPoint: CGPoint(x: actionsPositionDeltaXDistance, y: actionsPositionDeltaYDistance)), to: NSValue(cgPoint: CGPoint()), @@ -409,7 +505,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo animatingOutState.currentContentScreenFrame = updatedContentScreenFrame }*/ } else { - //strongSelf.requestUpdate(animation.transition) + strongSelf.requestUpdate(animation.transition) /*let updatedContentScreenFrame = convertFrame(contentNode.containingNode.contentRect, from: contentNode.containingNode.view, to: strongSelf.view) if let storedGlobalFrame = contentNode.storedGlobalFrame { @@ -436,12 +532,12 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo public var updateDistractionFreeMode: ((Bool) -> Void)? public var requestDismiss: (() -> Void)*/ case let .animateOut(result, completion): - let duration: Double = 0.25 + let duration: Double = self.reactionContextNodeIsAnimatingOut ? 0.25 : 0.2 let putBackInfo = self.source.putBack() if let putBackInfo = putBackInfo { - self.clippingNode.layer.animateFrame(from: CGRect(origin: CGPoint(), size: layout.size), to: putBackInfo.contentAreaInScreenSpace, duration: duration, removeOnCompletion: false) + self.clippingNode.layer.animateFrame(from: CGRect(origin: CGPoint(), size: layout.size), to: CGRect(origin: CGPoint(x: 0.0, y: putBackInfo.contentAreaInScreenSpace.minY), size: CGSize(width: layout.size.width, height: putBackInfo.contentAreaInScreenSpace.height)), duration: duration, removeOnCompletion: false) self.clippingNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: putBackInfo.contentAreaInScreenSpace.minY, duration: duration, removeOnCompletion: false) } @@ -463,11 +559,17 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo contentNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false) } - print("animationInContentDistance: \(animationInContentDistance)") + let actionsVerticalTransitionDirection: CGFloat + if contentNode.frame.minY < self.actionsStackNode.frame.minY { + actionsVerticalTransitionDirection = -1.0 + } else { + actionsVerticalTransitionDirection = 1.0 + } contentNode.containingNode.willUpdateIsExtractedToContextPreview?(false, transition) contentNode.offsetContainerNode.position = contentNode.offsetContainerNode.position.offsetBy(dx: 0.0, dy: -animationInContentDistance) + let reactionContextNodeIsAnimatingOut = self.reactionContextNodeIsAnimatingOut contentNode.offsetContainerNode.layer.animate( from: animationInContentDistance as NSNumber, to: 0.0 as NSNumber, @@ -477,7 +579,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo delay: 0.0, additive: true, completion: { [weak self] _ in - Queue.mainQueue().after(0.2 * UIView.animationDurationFactor(), { + Queue.mainQueue().after(reactionContextNodeIsAnimatingOut ? 0.2 * UIView.animationDurationFactor() : 0.0, { contentNode.containingNode.isExtractedToContextPreview = false contentNode.containingNode.isExtractedToContextPreviewUpdated?(false) @@ -489,16 +591,6 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo }) } ) - /*Queue.mainQueue().after((duration + 0.2) * UIView.animationDurationFactor(), { [weak self] in - contentNode.containingNode.isExtractedToContextPreview = false - contentNode.containingNode.isExtractedToContextPreviewUpdated?(false) - - if let strongSelf = self, let contentNode = strongSelf.contentNode { - contentNode.containingNode.addSubnode(contentNode.containingNode.contentNode) - } - - completion() - })*/ self.actionsStackNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false) self.actionsStackNode.layer.animate( @@ -514,7 +606,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo let actionsSize = self.actionsStackNode.bounds.size let actionsPositionDeltaXDistance: CGFloat = 0.0 - let actionsPositionDeltaYDistance = -animationInContentDistance - actionsSize.height / 2.0 - contentActionsSpacing + let actionsPositionDeltaYDistance = -animationInContentDistance + actionsVerticalTransitionDirection * actionsSize.height / 2.0 - contentActionsSpacing self.actionsStackNode.layer.animate( from: NSValue(cgPoint: CGPoint()), to: NSValue(cgPoint: CGPoint(x: actionsPositionDeltaXDistance, y: actionsPositionDeltaYDistance)), @@ -570,4 +662,15 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo intermediateCompletion() }) } + + func cancelReactionAnimation() { + self.reactionContextNode?.cancelReactionAnimation() + } + + func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) { + if self.reactionContextNodeIsAnimatingOut, let reactionContextNode = self.reactionContextNode { + reactionContextNode.bounds = reactionContextNode.bounds.offsetBy(dx: 0.0, dy: offset.y) + transition.animateOffsetAdditive(node: reactionContextNode, offset: -offset.y) + } + } } diff --git a/submodules/ContextUI/Sources/ContextControllerPresentationNode.swift b/submodules/ContextUI/Sources/ContextControllerPresentationNode.swift index a05fef8cb9..cd27640916 100644 --- a/submodules/ContextUI/Sources/ContextControllerPresentationNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerPresentationNode.swift @@ -25,4 +25,10 @@ protocol ContextControllerPresentationNode: ASDisplayNode { ) func animateOutToReaction(value: String, targetView: UIView, hideNode: Bool, completion: @escaping () -> Void) + func cancelReactionAnimation() + + func highlightGestureMoved(location: CGPoint) + func highlightGestureFinished(performAction: Bool) + + func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) } diff --git a/submodules/DebugSettingsUI/Sources/DebugController.swift b/submodules/DebugSettingsUI/Sources/DebugController.swift index 3ada505314..cd01fdb089 100644 --- a/submodules/DebugSettingsUI/Sources/DebugController.swift +++ b/submodules/DebugSettingsUI/Sources/DebugController.swift @@ -81,6 +81,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { case enableDebugDataDisplay(Bool) case acceleratedStickers(Bool) case experimentalBackground(Bool) + case snow(Bool) case playerEmbedding(Bool) case playlistPlayback(Bool) case voiceConference @@ -102,7 +103,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { return DebugControllerSection.logging.rawValue case .enableRaiseToSpeak, .keepChatNavigationStack, .skipReadHistory, .crashOnSlowQueries: return DebugControllerSection.experiments.rawValue - case .clearTips, .crash, .resetData, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .reindexUnread, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .playerEmbedding, .playlistPlayback, .voiceConference, .experimentalCompatibility, .enableDebugDataDisplay, .acceleratedStickers, .experimentalBackground: + case .clearTips, .crash, .resetData, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .reindexUnread, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .playerEmbedding, .playlistPlayback, .voiceConference, .experimentalCompatibility, .enableDebugDataDisplay, .acceleratedStickers, .experimentalBackground, .snow: return DebugControllerSection.experiments.rawValue case .preferredVideoCodec: return DebugControllerSection.videoExperiments.rawValue @@ -173,14 +174,16 @@ private enum DebugControllerEntry: ItemListNodeEntry { return 29 case .experimentalBackground: return 30 - case .playerEmbedding: + case .snow: return 31 - case .playlistPlayback: + case .playerEmbedding: return 32 - case .voiceConference: + case .playlistPlayback: return 33 + case .voiceConference: + return 34 case let .preferredVideoCodec(index, _, _, _): - return 34 + index + return 35 + index case .disableVideoAspectScaling: return 100 case .enableVoipTcp: @@ -814,6 +817,16 @@ private enum DebugControllerEntry: ItemListNodeEntry { }) }).start() }) + case let .snow(value): + return ItemListSwitchItem(presentationData: presentationData, title: "Snow", value: value, sectionId: self.section, style: .blocks, updated: { value in + let _ = arguments.sharedContext.accountManager.transaction ({ transaction in + transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in + var settings = settings?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings + settings.snow = value + return PreferencesEntry(settings) + }) + }).start() + }) case let .playerEmbedding(value): return ItemListSwitchItem(presentationData: presentationData, title: "Player Embedding", value: value, sectionId: self.section, style: .blocks, updated: { value in let _ = arguments.sharedContext.accountManager.transaction ({ transaction in @@ -927,6 +940,7 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present entries.append(.enableDebugDataDisplay(experimentalSettings.enableDebugDataDisplay)) entries.append(.acceleratedStickers(experimentalSettings.acceleratedStickers)) entries.append(.experimentalBackground(experimentalSettings.experimentalBackground)) + entries.append(.snow(experimentalSettings.snow)) entries.append(.playerEmbedding(experimentalSettings.playerEmbedding)) entries.append(.playlistPlayback(experimentalSettings.playlistPlayback)) } diff --git a/submodules/Display/Source/ContainedViewLayoutTransition.swift b/submodules/Display/Source/ContainedViewLayoutTransition.swift index 0dddbade98..74eec36301 100644 --- a/submodules/Display/Source/ContainedViewLayoutTransition.swift +++ b/submodules/Display/Source/ContainedViewLayoutTransition.swift @@ -261,7 +261,7 @@ public extension ContainedViewLayoutTransition { } } - func updateBounds(node: ASDisplayNode, bounds: CGRect, force: Bool = false, completion: ((Bool) -> Void)? = nil) { + func updateBounds(node: ASDisplayNode, bounds: CGRect, force: Bool = false, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) { if node.bounds.equalTo(bounds) && !force { completion?(true) } else { @@ -272,7 +272,12 @@ public extension ContainedViewLayoutTransition { completion(true) } case let .animated(duration, curve): - let previousBounds = node.bounds + let previousBounds: CGRect + if beginWithCurrentState, let presentation = node.layer.presentation() { + previousBounds = presentation.bounds + } else { + previousBounds = node.bounds + } node.bounds = bounds node.layer.animateBounds(from: previousBounds, to: bounds, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, force: force, completion: { result in if let completion = completion { diff --git a/submodules/Display/Source/ContextControllerSourceNode.swift b/submodules/Display/Source/ContextControllerSourceNode.swift index 08735ca8a9..1cadfe7ba7 100644 --- a/submodules/Display/Source/ContextControllerSourceNode.swift +++ b/submodules/Display/Source/ContextControllerSourceNode.swift @@ -1,7 +1,7 @@ import Foundation import AsyncDisplayKit -public final class ContextControllerSourceNode: ASDisplayNode { +open class ContextControllerSourceNode: ASDisplayNode { private var contextGesture: ContextGesture? public var isGestureEnabled: Bool = true { @@ -14,6 +14,7 @@ public final class ContextControllerSourceNode: ASDisplayNode { public var activated: ((ContextGesture, CGPoint) -> Void)? public var shouldBegin: ((CGPoint) -> Bool)? public var customActivationProgress: ((CGFloat, ContextGestureTransition) -> Void)? + public weak var additionalActivationProgressLayer: CALayer? public var targetNodeForActivationProgress: ASDisplayNode? public var targetNodeForActivationProgressContentRect: CGRect? @@ -23,7 +24,7 @@ public final class ContextControllerSourceNode: ASDisplayNode { self.contextGesture?.isEnabled = self.isGestureEnabled } - override public func didLoad() { + override open func didLoad() { super.didLoad() let contextGesture = ContextGesture(target: self, action: nil) @@ -75,15 +76,27 @@ public final class ContextControllerSourceNode: ASDisplayNode { case .update: let sublayerTransform = CATransform3DTranslate(CATransform3DScale(CATransform3DIdentity, currentScale, currentScale, 1.0), scaleMidX, scaleMidY, 0.0) targetNode.layer.sublayerTransform = sublayerTransform + if let additionalActivationProgressLayer = strongSelf.additionalActivationProgressLayer { + additionalActivationProgressLayer.transform = sublayerTransform + } case .begin: let sublayerTransform = CATransform3DTranslate(CATransform3DScale(CATransform3DIdentity, currentScale, currentScale, 1.0), scaleMidX, scaleMidY, 0.0) targetNode.layer.sublayerTransform = sublayerTransform + if let additionalActivationProgressLayer = strongSelf.additionalActivationProgressLayer { + additionalActivationProgressLayer.transform = sublayerTransform + } case .ended: let sublayerTransform = CATransform3DTranslate(CATransform3DScale(CATransform3DIdentity, currentScale, currentScale, 1.0), scaleMidX, scaleMidY, 0.0) let previousTransform = targetNode.layer.sublayerTransform targetNode.layer.sublayerTransform = sublayerTransform targetNode.layer.animate(from: NSValue(caTransform3D: previousTransform), to: NSValue(caTransform3D: sublayerTransform), keyPath: "sublayerTransform", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.2) + + if let additionalActivationProgressLayer = strongSelf.additionalActivationProgressLayer { + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.2, execute: { + additionalActivationProgressLayer.transform = sublayerTransform + }) + } } } } diff --git a/submodules/Display/Source/SimpleLayer.swift b/submodules/Display/Source/SimpleLayer.swift new file mode 100644 index 0000000000..43f6e3520c --- /dev/null +++ b/submodules/Display/Source/SimpleLayer.swift @@ -0,0 +1,26 @@ +import UIKit + +private final class NullActionClass: NSObject, CAAction { + @objc func run(forKey event: String, object anObject: Any, arguments dict: [AnyHashable : Any]?) { + } +} + +private let nullAction = NullActionClass() + +open class SimpleLayer: CALayer { + override open func action(forKey event: String) -> CAAction? { + return nullAction + } + + override public init() { + super.init() + } + + override public init(layer: Any) { + super.init(layer: layer) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/submodules/Display/Source/TextNode.swift b/submodules/Display/Source/TextNode.swift index 33ca210f87..5cff2c7bbd 100644 --- a/submodules/Display/Source/TextNode.swift +++ b/submodules/Display/Source/TextNode.swift @@ -44,14 +44,16 @@ private final class TextNodeLine { let isRTL: Bool let strikethroughs: [TextNodeStrikethrough] let spoilers: [TextNodeSpoiler] + let spoilerWords: [TextNodeSpoiler] - init(line: CTLine, frame: CGRect, range: NSRange, isRTL: Bool, strikethroughs: [TextNodeStrikethrough], spoilers: [TextNodeSpoiler]) { + init(line: CTLine, frame: CGRect, range: NSRange, isRTL: Bool, strikethroughs: [TextNodeStrikethrough], spoilers: [TextNodeSpoiler], spoilerWords: [TextNodeSpoiler]) { self.line = line self.frame = frame self.range = range self.isRTL = isRTL self.strikethroughs = strikethroughs self.spoilers = spoilers + self.spoilerWords = spoilerWords } } @@ -173,7 +175,8 @@ public final class TextNodeLayout: NSObject { fileprivate let textStroke: (UIColor, CGFloat)? fileprivate let displaySpoilers: Bool public let hasRTL: Bool - public let spoilers: [CGRect] + public let spoilers: [(NSRange, CGRect)] + public let spoilerWords: [(NSRange, CGRect)] fileprivate init(attributedString: NSAttributedString?, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, constrainedSize: CGSize, explicitAlignment: NSTextAlignment, resolvedAlignment: NSTextAlignment, verticalAlignment: TextVerticalAlignment, lineSpacing: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, size: CGSize, rawTextSize: CGSize, truncated: Bool, firstLineOffset: CGFloat, lines: [TextNodeLine], blockQuotes: [TextNodeBlockQuote], backgroundColor: UIColor?, lineColor: UIColor?, textShadowColor: UIColor?, textStroke: (UIColor, CGFloat)?, displaySpoilers: Bool) { self.attributedString = attributedString @@ -198,15 +201,18 @@ public final class TextNodeLayout: NSObject { self.textStroke = textStroke self.displaySpoilers = displaySpoilers var hasRTL = false - var spoilers: [CGRect] = [] + var spoilers: [(NSRange, CGRect)] = [] + var spoilerWords: [(NSRange, CGRect)] = [] for line in lines { if line.isRTL { hasRTL = true } - spoilers.append(contentsOf: line.spoilers.map { $0.frame.offsetBy(dx: line.frame.minX, dy: line.frame.minY) }) + spoilers.append(contentsOf: line.spoilers.map { ( $0.range, $0.frame.offsetBy(dx: line.frame.minX, dy: line.frame.minY)) }) + spoilerWords.append(contentsOf: line.spoilerWords.map { ( $0.range, $0.frame.offsetBy(dx: line.frame.minX, dy: line.frame.minY)) }) } self.hasRTL = hasRTL self.spoilers = spoilers + self.spoilerWords = spoilerWords } public func areLinesEqual(to other: TextNodeLayout) -> Bool { @@ -952,6 +958,7 @@ public class TextNode: ASDisplayNode { while true { var strikethroughs: [TextNodeStrikethrough] = [] var spoilers: [TextNodeSpoiler] = [] + var spoilerWords: [TextNodeSpoiler] = [] var lineConstrainedWidth = constrainedSize.width var lineConstrainedWidthDelta: CGFloat = 0.0 @@ -991,6 +998,24 @@ public class TextNode: ASDisplayNode { spoilers.append(TextNodeSpoiler(range: NSMakeRange(startIndex, endIndex - startIndex + 1), frame: CGRect(x: min(leftOffset, rightOffset), y: descent - (ascent + descent), width: abs(rightOffset - leftOffset), height: ascent + descent))) } + func addSpoilerWord(line: CTLine, ascent: CGFloat, descent: CGFloat, startIndex: Int, endIndex: Int, rightInset: CGFloat = 0.0) { + var secondaryLeftOffset: CGFloat = 0.0 + let rawLeftOffset = CTLineGetOffsetForStringIndex(line, startIndex, &secondaryLeftOffset) + var leftOffset = floor(rawLeftOffset) + if !rawLeftOffset.isEqual(to: secondaryLeftOffset) { + leftOffset = floor(secondaryLeftOffset) + } + + var secondaryRightOffset: CGFloat = 0.0 + let rawRightOffset = CTLineGetOffsetForStringIndex(line, endIndex, &secondaryRightOffset) + var rightOffset = ceil(rawRightOffset) + if !rawRightOffset.isEqual(to: secondaryRightOffset) { + rightOffset = ceil(secondaryRightOffset) + } + + spoilerWords.append(TextNodeSpoiler(range: NSMakeRange(startIndex, endIndex - startIndex + 1), frame: CGRect(x: min(leftOffset, rightOffset), y: descent - (ascent + descent), width: abs(rightOffset - leftOffset) + rightInset, height: ascent + descent))) + } + var isLastLine = false if maximumNumberOfLines != 0 && lines.count == maximumNumberOfLines - 1 && lineCharacterCount > 0 { isLastLine = true @@ -1005,6 +1030,10 @@ public class TextNode: ASDisplayNode { } let lineRange = CFRange(location: lastLineCharacterIndex, length: stringLength - lastLineCharacterIndex) + var brokenLineRange = CFRange(location: lastLineCharacterIndex, length: lineCharacterCount) + if brokenLineRange.location + brokenLineRange.length > attributedString.length { + brokenLineRange.length = attributedString.length - brokenLineRange.location + } if lineRange.length == 0 { break } @@ -1029,11 +1058,15 @@ public class TextNode: ASDisplayNode { let truncationToken = CTLineCreateWithAttributedString(truncatedTokenString) coreTextLine = CTLineCreateTruncatedLine(originalLine, Double(lineConstrainedSize.width), truncationType, truncationToken) ?? truncationToken + brokenLineRange.length = CTLineGetGlyphCount(coreTextLine) - 1 + if brokenLineRange.location + brokenLineRange.length > attributedString.length { + brokenLineRange.length = attributedString.length - brokenLineRange.location + } truncated = true } var headIndent: CGFloat = 0.0 - attributedString.enumerateAttributes(in: NSMakeRange(lineRange.location, lineRange.length), options: []) { attributes, range, _ in + attributedString.enumerateAttributes(in: NSMakeRange(brokenLineRange.location, brokenLineRange.length), options: []) { attributes, range, _ in if let _ = attributes[NSAttributedString.Key.init(rawValue: "TelegramSpoiler")] { var ascent: CGFloat = 0.0 var descent: CGFloat = 0.0 @@ -1048,7 +1081,7 @@ public class TextNode: ASDisplayNode { if let currentStartIndex = startIndex { startIndex = nil let endIndex = range.location - addSpoiler(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex) + addSpoilerWord(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex) } } else if startIndex == nil { startIndex = range.location @@ -1059,8 +1092,10 @@ public class TextNode: ASDisplayNode { if let currentStartIndex = startIndex, let currentIndex = currentIndex { startIndex = nil let endIndex = currentIndex - addSpoiler(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex) + addSpoilerWord(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex, rightInset: truncated ? 12.0 : 0.0) } + + addSpoiler(line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length - 1) } else if let _ = attributes[NSAttributedString.Key.strikethroughStyle] { let lowerX = floor(CTLineGetOffsetForStringIndex(coreTextLine, range.location, nil)) let upperX = ceil(CTLineGetOffsetForStringIndex(coreTextLine, range.location + range.length, nil)) @@ -1090,7 +1125,7 @@ public class TextNode: ASDisplayNode { } } - lines.append(TextNodeLine(line: coreTextLine, frame: lineFrame, range: NSMakeRange(lineRange.location, lineRange.length), isRTL: isRTL, strikethroughs: strikethroughs, spoilers: spoilers)) + lines.append(TextNodeLine(line: coreTextLine, frame: lineFrame, range: NSMakeRange(lineRange.location, lineRange.length), isRTL: isRTL, strikethroughs: strikethroughs, spoilers: spoilers, spoilerWords: spoilerWords)) break } else { if lineCharacterCount > 0 { @@ -1127,7 +1162,7 @@ public class TextNode: ASDisplayNode { if let currentStartIndex = startIndex { startIndex = nil let endIndex = range.location - addSpoiler(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex) + addSpoilerWord(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex) } } else if startIndex == nil { startIndex = range.location @@ -1138,8 +1173,10 @@ public class TextNode: ASDisplayNode { if let currentStartIndex = startIndex, let currentIndex = currentIndex { startIndex = nil let endIndex = currentIndex - addSpoiler(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex) + addSpoilerWord(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex) } + + addSpoiler(line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length - 1) } else if let _ = attributes[NSAttributedString.Key.strikethroughStyle] { let lowerX = floor(CTLineGetOffsetForStringIndex(coreTextLine, range.location, nil)) let upperX = ceil(CTLineGetOffsetForStringIndex(coreTextLine, range.location + range.length, nil)) @@ -1168,7 +1205,7 @@ public class TextNode: ASDisplayNode { } } - lines.append(TextNodeLine(line: coreTextLine, frame: lineFrame, range: NSMakeRange(lineRange.location, lineRange.length), isRTL: isRTL, strikethroughs: strikethroughs, spoilers: spoilers)) + lines.append(TextNodeLine(line: coreTextLine, frame: lineFrame, range: NSMakeRange(lineRange.location, lineRange.length), isRTL: isRTL, strikethroughs: strikethroughs, spoilers: spoilers, spoilerWords: spoilerWords)) } else { if !lines.isEmpty { layoutSize.height += fontLineSpacing @@ -1285,7 +1322,7 @@ public class TextNode: ASDisplayNode { if layout.displaySpoilers && !line.spoilers.isEmpty { context.saveGState() var clipRects: [CGRect] = [] - for spoiler in line.spoilers { + for spoiler in line.spoilerWords { clipRects.append(spoiler.frame.offsetBy(dx: lineFrame.minX, dy: lineFrame.minY)) } context.clip(to: clipRects) @@ -1320,7 +1357,7 @@ public class TextNode: ASDisplayNode { if layout.displaySpoilers { context.restoreGState() } else { - for spoiler in line.spoilers { + for spoiler in line.spoilerWords { clearRects.append(spoiler.frame.offsetBy(dx: lineFrame.minX, dy: lineFrame.minY)) } } diff --git a/submodules/GalleryUI/BUILD b/submodules/GalleryUI/BUILD index 87f0008595..71887c2ba2 100644 --- a/submodules/GalleryUI/BUILD +++ b/submodules/GalleryUI/BUILD @@ -39,6 +39,7 @@ swift_library( "//submodules/Speak:Speak", "//submodules/UndoUI:UndoUI", "//submodules/InvisibleInkDustNode:InvisibleInkDustNode", + "//submodules/Translate:Translate", ], visibility = [ "//visibility:public", diff --git a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift index c3cb3842e9..df4179027e 100644 --- a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift +++ b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift @@ -737,7 +737,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll } if let dustNode = self.dustNode { - dustNode.update(size: textFrame.size, color: .white, rects: textLayout.spoilers.map { $0.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 0.0, dy: 1.0) }) + dustNode.update(size: textFrame.size, color: .white, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 0.0, dy: 1.0) }, wordRects: textLayout.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 0.0, dy: 1.0) }) dustNode.frame = textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0) } } else { diff --git a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift index 824f19c07b..1e32f64027 100644 --- a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift @@ -16,6 +16,7 @@ import PresentationDataUtils import ImageContentAnalysis import TextSelectionNode import Speak +import Translate import ShareController import UndoUI @@ -352,6 +353,8 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { } case .speak: speakText(string) + case .translate: + translateText(context: strongSelf.context, text: string) } }) recognizedContentNode.barcodeAction = { [weak self] payload, rect in diff --git a/submodules/GalleryUI/Sources/RecognizedTextSelectionNode.swift b/submodules/GalleryUI/Sources/RecognizedTextSelectionNode.swift index 64fcc28b6d..8f5713c25c 100644 --- a/submodules/GalleryUI/Sources/RecognizedTextSelectionNode.swift +++ b/submodules/GalleryUI/Sources/RecognizedTextSelectionNode.swift @@ -203,6 +203,7 @@ public enum RecognizedTextSelectionAction { case share case lookup case speak + case translate } public final class RecognizedTextSelectionNode: ASDisplayNode { @@ -509,12 +510,18 @@ public final class RecognizedTextSelectionNode: ASDisplayNode { self?.performAction(selectedText, .lookup) let _ = self?.dismissSelection() })) - if isSpeakSelectionEnabled() { - actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuSpeak, accessibilityLabel: self.strings.Conversation_ContextMenuSpeak), action: { [weak self] in - self?.performAction(selectedText, .speak) + if #available(iOS 15.0, *) { + actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuTranslate, accessibilityLabel: self.strings.Conversation_ContextMenuTranslate), action: { [weak self] in + self?.performAction(selectedText, .translate) let _ = self?.dismissSelection() })) } +// if isSpeakSelectionEnabled() { +// actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuSpeak, accessibilityLabel: self.strings.Conversation_ContextMenuSpeak), action: { [weak self] in +// self?.performAction(selectedText, .speak) +// let _ = self?.dismissSelection() +// })) +// } actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuShare, accessibilityLabel: self.strings.Conversation_ContextMenuShare), action: { [weak self] in self?.performAction(selectedText, .share) let _ = self?.dismissSelection() diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift index 31b9dc9796..a569b6ce2a 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift @@ -84,8 +84,7 @@ public final class HashtagSearchController: TelegramBaseController { let listInteraction = ListMessageItemInteraction(openMessage: { message, mode -> Bool in return true - }, openMessageContextMenu: { message, bool, node, rect, gesture in - + }, openMessageContextMenu: { message, bool, node, rect, gesture in }, toggleMessagesSelection: { messageId, selected in }, openUrl: { url, _, _, message in }, openInstantPage: { message, data in diff --git a/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift b/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift index cdded32c83..db2f20a0b6 100644 --- a/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift +++ b/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift @@ -45,7 +45,7 @@ private let emitterMaskImage: UIImage = { }() public class InvisibleInkDustNode: ASDisplayNode { - private var currentParams: (size: CGSize, color: UIColor, rects: [CGRect])? + private var currentParams: (size: CGSize, color: UIColor, rects: [CGRect], wordRects: [CGRect])? private weak var textNode: TextNode? private let textMaskNode: ASDisplayNode @@ -62,7 +62,7 @@ public class InvisibleInkDustNode: ASDisplayNode { public var isRevealed = false - public init(textNode: TextNode) { + public init(textNode: TextNode?) { self.textNode = textNode self.emitterNode = ASDisplayNode() @@ -143,8 +143,26 @@ public class InvisibleInkDustNode: ASDisplayNode { self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tap(_:)))) } + public func update(revealed: Bool) { + guard self.isRevealed != revealed, let textNode = self.textNode else { + return + } + + self.isRevealed = revealed + + if revealed { + let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear) + transition.updateAlpha(node: self, alpha: 0.0) + transition.updateAlpha(node: textNode, alpha: 1.0) + } else { + let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .linear) + transition.updateAlpha(node: self, alpha: 1.0) + transition.updateAlpha(node: textNode, alpha: 0.0) + } + } + @objc private func tap(_ gestureRecognizer: UITapGestureRecognizer) { - guard let (size, _, _) = self.currentParams, !self.isRevealed else { + guard let (size, _, _, _) = self.currentParams, let textNode = self.textNode, !self.isRevealed else { return } @@ -155,15 +173,15 @@ public class InvisibleInkDustNode: ASDisplayNode { self.emitterLayer?.setValue(position, forKeyPath: "emitterBehaviors.fingerAttractor.position") Queue.mainQueue().after(0.1 * UIView.animationDurationFactor()) { - self.textNode?.view.mask = self.textMaskNode.view - self.textNode?.alpha = 1.0 + textNode.view.mask = self.textMaskNode.view + textNode.alpha = 1.0 let radius = max(size.width, size.height) self.textSpotNode.frame = CGRect(x: position.x - radius / 2.0, y: position.y - radius / 2.0, width: radius, height: radius) self.textSpotNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) - self.textSpotNode.layer.animateScale(from: 0.1, to: 3.5, duration: 0.71, removeOnCompletion: false, completion: { [weak self] _ in - self?.textNode?.view.mask = nil + self.textSpotNode.layer.animateScale(from: 0.1, to: 3.5, duration: 0.71, removeOnCompletion: false, completion: { _ in + textNode.view.mask = nil }) self.emitterNode.view.mask = self.emitterMaskNode.view @@ -174,8 +192,6 @@ public class InvisibleInkDustNode: ASDisplayNode { self?.emitterNode.view.mask = nil }) self.emitterMaskFillNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) - - self.isRevealedUpdated(true) } Queue.mainQueue().after(0.8 * UIView.animationDurationFactor()) { @@ -187,27 +203,24 @@ public class InvisibleInkDustNode: ASDisplayNode { } - let textLength = CGFloat((self.textNode?.cachedLayout?.attributedString?.string ?? "").count) + let textLength = CGFloat((textNode.cachedLayout?.attributedString?.string ?? "").count) let timeToRead = min(45.0, ceil(max(4.0, textLength * 0.04))) Queue.mainQueue().after(timeToRead * UIView.animationDurationFactor()) { self.isRevealed = false - self.isRevealedUpdated(false) let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .linear) transition.updateAlpha(node: self, alpha: 1.0) - if let textNode = self.textNode { - transition.updateAlpha(node: textNode, alpha: 0.0) - } + transition.updateAlpha(node: textNode, alpha: 0.0) } } private func updateEmitter() { - guard let (size, color, rects) = self.currentParams else { + guard let (size, color, _, wordRects) = self.currentParams else { return } self.emitter?.color = color.cgColor - self.emitterLayer?.setValue(rects, forKey: "emitterRects") + self.emitterLayer?.setValue(wordRects, forKey: "emitterRects") self.emitterLayer?.frame = CGRect(origin: CGPoint(), size: size) let radius = max(size.width, size.height) @@ -215,15 +228,15 @@ public class InvisibleInkDustNode: ASDisplayNode { self.emitterLayer?.setValue(radius * -0.5, forKeyPath: "emitterBehaviors.fingerAttractor.falloff") var square: Float = 0.0 - for rect in rects { + for rect in wordRects { square += Float(rect.width * rect.height) } - self.emitter?.birthRate = square * 0.3 + self.emitter?.birthRate = square * 0.4 } - public func update(size: CGSize, color: UIColor, rects: [CGRect]) { - self.currentParams = (size, color, rects) + public func update(size: CGSize, color: UIColor, rects: [CGRect], wordRects: [CGRect]) { + self.currentParams = (size, color, rects, wordRects) self.emitterNode.frame = CGRect(origin: CGPoint(), size: size) self.emitterMaskNode.frame = self.emitterNode.bounds @@ -236,7 +249,7 @@ public class InvisibleInkDustNode: ASDisplayNode { } public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { - if let (_, _, rects) = self.currentParams { + if let (_, _, rects, _) = self.currentParams { for rect in rects { if rect.contains(point) { return true diff --git a/submodules/ItemListUI/Sources/ItemListController.swift b/submodules/ItemListUI/Sources/ItemListController.swift index e504a7d712..88607d6f23 100644 --- a/submodules/ItemListUI/Sources/ItemListController.swift +++ b/submodules/ItemListUI/Sources/ItemListController.swift @@ -206,6 +206,14 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable } } + public var didScrollWithOffset: ((CGFloat, ContainedViewLayoutTransition, ListViewItemNode?) -> Void)? { + didSet { + if self.isNodeLoaded { + (self.displayNode as! ItemListControllerNode).listNode.didScrollWithOffset = self.didScrollWithOffset + } + } + } + public var willScrollToTop: (() -> Void)? public func setReorderEntry(_ f: @escaping (Int, Int, [T]) -> Signal) { @@ -471,6 +479,7 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable displayNode.reorderEntry = self.reorderEntry displayNode.reorderCompleted = self.reorderCompleted displayNode.listNode.experimentalSnapScrollToItem = self.experimentalSnapScrollToItem + displayNode.listNode.didScrollWithOffset = self.didScrollWithOffset displayNode.requestLayout = { [weak self] transition in self?.requestLayout(transition: transition) } diff --git a/submodules/ItemListUI/Sources/Items/ItemListCheckboxItem.swift b/submodules/ItemListUI/Sources/Items/ItemListCheckboxItem.swift index ab833042ec..305d26d051 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListCheckboxItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListCheckboxItem.swift @@ -17,6 +17,8 @@ public enum ItemListCheckboxItemColor { public class ItemListCheckboxItem: ListViewItem, ItemListItem { let presentationData: ItemListPresentationData + let icon: UIImage? + let iconSize: CGSize? let title: String let style: ItemListCheckboxItemStyle let color: ItemListCheckboxItemColor @@ -25,8 +27,10 @@ public class ItemListCheckboxItem: ListViewItem, ItemListItem { public let sectionId: ItemListSectionId let action: () -> Void - public init(presentationData: ItemListPresentationData, title: String, style: ItemListCheckboxItemStyle, color: ItemListCheckboxItemColor = .accent, checked: Bool, zeroSeparatorInsets: Bool, sectionId: ItemListSectionId, action: @escaping () -> Void) { + public init(presentationData: ItemListPresentationData, icon: UIImage? = nil, iconSize: CGSize? = nil, title: String, style: ItemListCheckboxItemStyle, color: ItemListCheckboxItemColor = .accent, checked: Bool, zeroSeparatorInsets: Bool, sectionId: ItemListSectionId, action: @escaping () -> Void) { self.presentationData = presentationData + self.icon = icon + self.iconSize = iconSize self.title = title self.style = style self.color = color @@ -86,6 +90,7 @@ public class ItemListCheckboxItemNode: ListViewItemNode { private let activateArea: AccessibilityAreaNode + private let imageNode: ASImageNode private let iconNode: ASImageNode private let titleNode: TextNode @@ -103,6 +108,11 @@ public class ItemListCheckboxItemNode: ListViewItemNode { self.maskNode = ASImageNode() + self.imageNode = ASImageNode() + self.imageNode.isLayerBacked = true + self.imageNode.displayWithoutProcessing = true + self.imageNode.displaysAsynchronously = false + self.iconNode = ASImageNode() self.iconNode.isLayerBacked = true self.iconNode.displayWithoutProcessing = true @@ -120,6 +130,7 @@ public class ItemListCheckboxItemNode: ListViewItemNode { super.init(layerBacked: false, dynamicBounce: false) + self.addSubnode(self.imageNode) self.addSubnode(self.iconNode) self.addSubnode(self.titleNode) self.addSubnode(self.activateArea) @@ -145,6 +156,11 @@ public class ItemListCheckboxItemNode: ListViewItemNode { leftInset += 16.0 } + let iconInset: CGFloat = 44.0 + if item.icon != nil { + leftInset += iconInset + } + let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize) let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) @@ -253,6 +269,12 @@ public class ItemListCheckboxItemNode: ListViewItemNode { strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 11.0), size: titleLayout.size) + if let icon = item.icon { + let iconSize = item.iconSize ?? icon.size + strongSelf.imageNode.image = icon + strongSelf.imageNode.frame = CGRect(origin: CGPoint(x: params.leftInset + floor((leftInset - params.leftInset - iconSize.width) / 2.0), y: floor((layout.contentSize.height - iconSize.height) / 2.0)), size: iconSize) + } + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: strongSelf.backgroundNode.frame.height + UIScreenPixel + UIScreenPixel)) } }) diff --git a/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift b/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift index cdda64ab67..2857b9f912 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift @@ -23,6 +23,7 @@ public enum ItemListDisclosureLabelStyle { case multilineDetailText case badge(UIColor) case color(UIColor) + case image(image: UIImage, size: CGSize) } public class ItemListDisclosureItem: ListViewItem, ItemListItem { @@ -234,6 +235,9 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { updatedLabelImage = generateFilledCircleImage(diameter: 17.0, color: color) } } + if case let .image(image, _) = item.labelStyle { + updatedLabelImage = image + } let badgeDiameter: CGFloat = 20.0 if currentItem?.presentationData.theme !== item.presentationData.theme { @@ -468,7 +472,16 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { } strongSelf.labelNode.frame = labelFrame - if case .color = item.labelStyle { + if case let .image(_, size) = item.labelStyle { + if let updatedLabelImage = updatedLabelImage { + strongSelf.labelImageNode.image = updatedLabelImage + } + if strongSelf.labelImageNode.supernode == nil { + strongSelf.addSubnode(strongSelf.labelImageNode) + } + + strongSelf.labelImageNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - size.width - 30.0, y: floor((layout.contentSize.height - size.height) / 2.0)), size: size) + } else if case .color = item.labelStyle { if let updatedLabelImage = updatedLabelImage { strongSelf.labelImageNode.image = updatedLabelImage } diff --git a/submodules/LegacyComponents/Sources/TGMediaAssetsModernLibrary.m b/submodules/LegacyComponents/Sources/TGMediaAssetsModernLibrary.m index 09ce790c9e..6cabd8e428 100644 --- a/submodules/LegacyComponents/Sources/TGMediaAssetsModernLibrary.m +++ b/submodules/LegacyComponents/Sources/TGMediaAssetsModernLibrary.m @@ -9,6 +9,8 @@ @interface TGMediaAssetsModernLibrary () { SPipe *_libraryChangePipe; + + bool _registeredChangeObserver; } @end @@ -20,14 +22,21 @@ if (self != nil) { _libraryChangePipe = [[SPipe alloc] init]; - [[PHPhotoLibrary sharedPhotoLibrary] registerChangeObserver:self]; + + PHAuthorizationStatus authorizationStatus = [PHPhotoLibrary authorizationStatus]; + if (authorizationStatus == PHAuthorizationStatusAuthorized) { + _registeredChangeObserver = true; + [[PHPhotoLibrary sharedPhotoLibrary] registerChangeObserver:self]; + } } return self; } - (void)dealloc { - [[PHPhotoLibrary sharedPhotoLibrary] unregisterChangeObserver:self]; + if (_registeredChangeObserver) { + [[PHPhotoLibrary sharedPhotoLibrary] unregisterChangeObserver:self]; + } } - (SSignal *)assetGroups diff --git a/submodules/Postbox/Sources/Coding.swift b/submodules/Postbox/Sources/Coding.swift index 2a2252e00a..00544eb6b2 100644 --- a/submodules/Postbox/Sources/Coding.swift +++ b/submodules/Postbox/Sources/Coding.swift @@ -1806,7 +1806,8 @@ public final class PostboxDecoder { let result = try AdaptedPostboxDecoder().decode(T.self, from: innerData) return result } catch let error { - assertionFailure("Decoding error: \(error)") + postboxLog("Decoding error: \(error)") + //assertionFailure("Decoding error: \(error)") return nil } } else { diff --git a/submodules/Postbox/Sources/Postbox.swift b/submodules/Postbox/Sources/Postbox.swift index 28d04fa502..452cf92738 100644 --- a/submodules/Postbox/Sources/Postbox.swift +++ b/submodules/Postbox/Sources/Postbox.swift @@ -1170,7 +1170,7 @@ func debugRestoreState(basePath:String, name: String) { private let sharedQueue = Queue(name: "org.telegram.postbox.Postbox") -public func openPostbox(basePath: String, seedConfiguration: SeedConfiguration, encryptionParameters: ValueBoxEncryptionParameters, timestampForAbsoluteTimeBasedOperations: Int32, isTemporary: Bool, isReadOnly: Bool, useCopy: Bool, useCaches: Bool) -> Signal { +public func openPostbox(basePath: String, seedConfiguration: SeedConfiguration, encryptionParameters: ValueBoxEncryptionParameters, timestampForAbsoluteTimeBasedOperations: Int32, isTemporary: Bool, isReadOnly: Bool, useCopy: Bool, useCaches: Bool, removeDatabaseOnError: Bool) -> Signal { let queue = sharedQueue return Signal { subscriber in queue.async { @@ -1214,7 +1214,7 @@ public func openPostbox(basePath: String, seedConfiguration: SeedConfiguration, postboxLog("openPostbox, initialize SqliteValueBox") - guard var valueBox = SqliteValueBox(basePath: dbBasePath, queue: queue, isTemporary: isTemporary, isReadOnly: isReadOnly, useCaches: useCaches, encryptionParameters: encryptionParameters, upgradeProgress: { progress in + guard var valueBox = SqliteValueBox(basePath: dbBasePath, queue: queue, isTemporary: isTemporary, isReadOnly: isReadOnly, useCaches: useCaches, removeDatabaseOnError: removeDatabaseOnError, encryptionParameters: encryptionParameters, upgradeProgress: { progress in postboxLog("openPostbox, SqliteValueBox upgrading progress \(progress)") subscriber.putNext(.upgrading(progress)) }) else { @@ -1242,7 +1242,7 @@ public func openPostbox(basePath: String, seedConfiguration: SeedConfiguration, postboxLog("Version \(userVersion) is newer than supported") assertionFailure("Version \(userVersion) is newer than supported") valueBox.drop() - guard let updatedValueBox = SqliteValueBox(basePath: dbBasePath, queue: queue, isTemporary: isTemporary, isReadOnly: isReadOnly, useCaches: useCaches, encryptionParameters: encryptionParameters, upgradeProgress: { progress in + guard let updatedValueBox = SqliteValueBox(basePath: dbBasePath, queue: queue, isTemporary: isTemporary, isReadOnly: isReadOnly, useCaches: useCaches, removeDatabaseOnError: removeDatabaseOnError, encryptionParameters: encryptionParameters, upgradeProgress: { progress in subscriber.putNext(.upgrading(progress)) }) else { subscriber.putNext(.error) @@ -1266,7 +1266,7 @@ public func openPostbox(basePath: String, seedConfiguration: SeedConfiguration, valueBox.internalClose() let _ = try? FileManager.default.removeItem(atPath: dbBasePath) let _ = try? FileManager.default.moveItem(atPath: updatedPath, toPath: dbBasePath) - guard let updatedValueBox = SqliteValueBox(basePath: dbBasePath, queue: queue, isTemporary: isTemporary, isReadOnly: isReadOnly, useCaches: useCaches, encryptionParameters: encryptionParameters, upgradeProgress: { progress in + guard let updatedValueBox = SqliteValueBox(basePath: dbBasePath, queue: queue, isTemporary: isTemporary, isReadOnly: isReadOnly, useCaches: useCaches, removeDatabaseOnError: removeDatabaseOnError, encryptionParameters: encryptionParameters, upgradeProgress: { progress in subscriber.putNext(.upgrading(progress)) }) else { subscriber.putNext(.error) @@ -1280,7 +1280,7 @@ public func openPostbox(basePath: String, seedConfiguration: SeedConfiguration, assertionFailure("Couldn't find any upgrade for \(userVersion)") postboxLog("Couldn't find any upgrade for \(userVersion)") valueBox.drop() - guard let updatedValueBox = SqliteValueBox(basePath: dbBasePath, queue: queue, isTemporary: isTemporary, isReadOnly: isReadOnly, useCaches: useCaches, encryptionParameters: encryptionParameters, upgradeProgress: { progress in + guard let updatedValueBox = SqliteValueBox(basePath: dbBasePath, queue: queue, isTemporary: isTemporary, isReadOnly: isReadOnly, useCaches: useCaches, removeDatabaseOnError: removeDatabaseOnError, encryptionParameters: encryptionParameters, upgradeProgress: { progress in subscriber.putNext(.upgrading(progress)) }) else { subscriber.putNext(.error) diff --git a/submodules/Postbox/Sources/SqliteValueBox.swift b/submodules/Postbox/Sources/SqliteValueBox.swift index 133996cc46..088f89623d 100644 --- a/submodules/Postbox/Sources/SqliteValueBox.swift +++ b/submodules/Postbox/Sources/SqliteValueBox.swift @@ -46,7 +46,7 @@ struct SqlitePreparedStatement { sqlite3_clear_bindings(statement) } - func step(handle: OpaquePointer?, _ initial: Bool = false, path: String?) -> Bool { + func step(handle: OpaquePointer?, _ initial: Bool = false, pathToRemoveOnError: String?) -> Bool { let res = sqlite3_step(statement) if res != SQLITE_ROW && res != SQLITE_DONE { if let error = sqlite3_errmsg(handle), let str = NSString(utf8String: error) { @@ -56,7 +56,7 @@ struct SqlitePreparedStatement { } if res == SQLITE_CORRUPT { - if let path = path { + if let path = pathToRemoveOnError { postboxLog("Corrupted DB at step, dropping") try? FileManager.default.removeItem(atPath: path) preconditionFailure() @@ -70,7 +70,7 @@ struct SqlitePreparedStatement { var code: Int32 } - func tryStep(handle: OpaquePointer?, _ initial: Bool = false, path: String?) -> Result { + func tryStep(handle: OpaquePointer?, _ initial: Bool = false, pathToRemoveOnError: String?) -> Result { let res = sqlite3_step(statement) if res != SQLITE_ROW && res != SQLITE_DONE { if let error = sqlite3_errmsg(handle), let str = NSString(utf8String: error) { @@ -80,7 +80,7 @@ struct SqlitePreparedStatement { } if res == SQLITE_CORRUPT { - if let path = path { + if let path = pathToRemoveOnError { postboxLog("Corrupted DB at step, dropping") try? FileManager.default.removeItem(atPath: path) preconditionFailure() @@ -167,6 +167,7 @@ public final class SqliteValueBox: ValueBox { private let inMemory: Bool private let encryptionParameters: ValueBoxEncryptionParameters? private let databasePath: String + private let removeDatabaseOnError: Bool private var database: Database! private var tables: [Int32: SqliteValueBoxTable] = [:] private var fullTextTables: [Int32: ValueBoxFullTextTable] = [:] @@ -202,11 +203,12 @@ public final class SqliteValueBox: ValueBox { private let queue: Queue - public init?(basePath: String, queue: Queue, isTemporary: Bool, isReadOnly: Bool, useCaches: Bool, encryptionParameters: ValueBoxEncryptionParameters?, upgradeProgress: (Float) -> Void, inMemory: Bool = false) { + public init?(basePath: String, queue: Queue, isTemporary: Bool, isReadOnly: Bool, useCaches: Bool, removeDatabaseOnError: Bool, encryptionParameters: ValueBoxEncryptionParameters?, upgradeProgress: (Float) -> Void, inMemory: Bool = false) { self.basePath = basePath self.isTemporary = isTemporary self.isReadOnly = isReadOnly self.useCaches = useCaches + self.removeDatabaseOnError = removeDatabaseOnError self.inMemory = inMemory self.encryptionParameters = encryptionParameters self.databasePath = basePath + "/db_sqlite" @@ -294,7 +296,9 @@ public final class SqliteValueBox: ValueBox { preconditionFailure("Don't have write access to database folder") } - let _ = try? FileManager.default.removeItem(atPath: path) + if self.removeDatabaseOnError { + let _ = try? FileManager.default.removeItem(atPath: path) + } preconditionFailure("Couldn't open database") } @@ -328,7 +332,7 @@ public final class SqliteValueBox: ValueBox { if self.isEncrypted(database) { postboxLog("Encryption key is invalid") - if isTemporary || isReadOnly { + if isTemporary || isReadOnly || !self.removeDatabaseOnError { return nil } @@ -347,7 +351,7 @@ public final class SqliteValueBox: ValueBox { } } else { postboxLog("Encryption key is required") - if isReadOnly { + if isReadOnly || !self.removeDatabaseOnError { return nil } @@ -548,13 +552,16 @@ public final class SqliteValueBox: ValueBox { postboxLog("isEncrypted prepare...") let allIsOk = Atomic(value: false) + let removeDatabaseOnError = self.removeDatabaseOnError let databasePath = self.databasePath DispatchQueue.global().asyncAfter(deadline: .now() + 5.0, execute: { if allIsOk.with({ $0 }) == false { postboxLog("Timeout reached, discarding database") - try? FileManager.default.removeItem(atPath: databasePath) + if removeDatabaseOnError { + try? FileManager.default.removeItem(atPath: databasePath) + } - exit(0) + preconditionFailure() } }) let status = sqlite3_prepare_v2(database.handle, "SELECT * FROM sqlite_master LIMIT 1", -1, &statement, nil) @@ -569,7 +576,7 @@ public final class SqliteValueBox: ValueBox { return true } let preparedStatement = SqlitePreparedStatement(statement: statement) - switch preparedStatement.tryStep(handle: database.handle, path: self.databasePath) { + switch preparedStatement.tryStep(handle: database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { case .success: break case let .failure(error): @@ -588,7 +595,7 @@ public final class SqliteValueBox: ValueBox { let status = sqlite3_prepare_v2(database.handle, "PRAGMA user_version", -1, &statement, nil) precondition(status == SQLITE_OK) let preparedStatement = SqlitePreparedStatement(statement: statement) - let _ = preparedStatement.step(handle: database.handle, path: self.databasePath) + let _ = preparedStatement.step(handle: database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) let value = preparedStatement.int64At(0) preparedStatement.destroy() return value @@ -601,7 +608,7 @@ public final class SqliteValueBox: ValueBox { precondition(status == SQLITE_OK) let preparedStatement = SqlitePreparedStatement(statement: statement) var result: String? - if preparedStatement.step(handle: database.handle, path: self.databasePath) { + if preparedStatement.step(handle: database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { result = preparedStatement.stringAt(0) } preparedStatement.destroy() @@ -616,7 +623,7 @@ public final class SqliteValueBox: ValueBox { let preparedStatement = SqlitePreparedStatement(statement: statement) var tables: [SqliteValueBoxTable] = [] - while preparedStatement.step(handle: database.handle, true, path: self.databasePath) { + while preparedStatement.step(handle: database.handle, true, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { guard let name = preparedStatement.stringAt(0) else { assertionFailure() continue @@ -663,7 +670,7 @@ public final class SqliteValueBox: ValueBox { let preparedStatement = SqlitePreparedStatement(statement: statement) var tables: [ValueBoxFullTextTable] = [] - while preparedStatement.step(handle: database.handle, true, path: self.databasePath) { + while preparedStatement.step(handle: database.handle, true, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { let value = preparedStatement.int64At(0) tables.append(ValueBoxFullTextTable(id: Int32(value))) } @@ -1475,7 +1482,7 @@ public final class SqliteValueBox: ValueBox { var buffer: ReadBuffer? - while statement.step(handle: self.database.handle, path: self.databasePath) { + while statement.step(handle: self.database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { buffer = statement.valueAt(0) break } @@ -1495,7 +1502,7 @@ public final class SqliteValueBox: ValueBox { if let _ = self.tables[table.id] { let statement = self.getRowIdStatement(table, key: key) - if statement.step(handle: self.database.handle, path: self.databasePath) { + if statement.step(handle: self.database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { let rowId = statement.int64At(0) var blobHandle: OpaquePointer? sqlite3_blob_open(database.handle, "main", "t\(table.id)", "value", rowId, 0, &blobHandle) @@ -1515,7 +1522,7 @@ public final class SqliteValueBox: ValueBox { if let _ = self.tables[table.id] { let statement = self.getRowIdStatement(table, key: key) - if statement.step(handle: self.database.handle, path: self.databasePath) { + if statement.step(handle: self.database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { let rowId = statement.int64At(0) var blobHandle: OpaquePointer? sqlite3_blob_open(database.handle, "main", "t\(table.id)", "value", rowId, 1, &blobHandle) @@ -1566,7 +1573,7 @@ public final class SqliteValueBox: ValueBox { } } - while statement.step(handle: self.database.handle, path: self.databasePath) { + while statement.step(handle: self.database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { let key = statement.keyAt(0) let value = statement.valueAt(1) @@ -1591,7 +1598,7 @@ public final class SqliteValueBox: ValueBox { } } - while statement.step(handle: self.database.handle, path: self.databasePath) { + while statement.step(handle: self.database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { let key = statement.int64KeyAt(0) let value = statement.valueAt(1) @@ -1702,7 +1709,7 @@ public final class SqliteValueBox: ValueBox { } } - while statement.step(handle: self.database.handle, path: self.databasePath) { + while statement.step(handle: self.database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { let key = statement.keyAt(0) if !keys(key) { @@ -1726,7 +1733,7 @@ public final class SqliteValueBox: ValueBox { } } - while statement.step(handle: self.database.handle, path: self.databasePath) { + while statement.step(handle: self.database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { let key = statement.int64KeyAt(0) if !keys(key) { @@ -1748,7 +1755,7 @@ public final class SqliteValueBox: ValueBox { if let _ = self.tables[table.id] { let statement: SqlitePreparedStatement = self.scanStatement(table) - while statement.step(handle: self.database.handle, path: self.databasePath) { + while statement.step(handle: self.database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { let key = statement.keyAt(0) let value = statement.valueAt(1) @@ -1767,7 +1774,7 @@ public final class SqliteValueBox: ValueBox { if let _ = self.tables[table.id] { let statement: SqlitePreparedStatement = self.scanKeysStatement(table) - while statement.step(handle: self.database.handle, path: self.databasePath) { + while statement.step(handle: self.database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { let key = statement.keyAt(0) if !keys(key) { @@ -1785,7 +1792,7 @@ public final class SqliteValueBox: ValueBox { if let _ = self.tables[table.id] { let statement: SqlitePreparedStatement = self.scanStatement(table) - while statement.step(handle: self.database.handle, path: self.databasePath) { + while statement.step(handle: self.database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { let key = statement.int64KeyValueAt(0) let value = statement.valueAt(1) @@ -1804,7 +1811,7 @@ public final class SqliteValueBox: ValueBox { if let _ = self.tables[table.id] { let statement: SqlitePreparedStatement = self.scanKeysStatement(table) - while statement.step(handle: self.database.handle, path: self.databasePath) { + while statement.step(handle: self.database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { let key = statement.int64KeyValueAt(0) if !keys(key) { @@ -1822,18 +1829,18 @@ public final class SqliteValueBox: ValueBox { if sqliteTable.hasPrimaryKey { let statement = self.insertOrReplaceStatement(sqliteTable, key: key, value: value) - while statement.step(handle: self.database.handle, path: self.databasePath) { + while statement.step(handle: self.database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { } statement.reset() } else { if self.exists(table, key: key) { let statement = self.updateStatement(table, key: key, value: value) - while statement.step(handle: self.database.handle, path: self.databasePath) { + while statement.step(handle: self.database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { } statement.reset() } else { let statement = self.insertOrReplaceStatement(sqliteTable, key: key, value: value) - while statement.step(handle: self.database.handle, path: self.databasePath) { + while statement.step(handle: self.database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { } statement.reset() } @@ -1850,7 +1857,7 @@ public final class SqliteValueBox: ValueBox { } let statement = self.deleteStatement(table, key: key) - while statement.step(handle: self.database.handle, path: self.databasePath) { + while statement.step(handle: self.database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { } statement.reset() } @@ -1860,7 +1867,7 @@ public final class SqliteValueBox: ValueBox { precondition(self.queue.isCurrent()) if let _ = self.tables[table.id] { let statement = self.rangeDeleteStatement(table, start: min(start, end), end: max(start, end)) - while statement.step(handle: self.database.handle, path: self.databasePath) { + while statement.step(handle: self.database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { } statement.reset() } @@ -1870,7 +1877,7 @@ public final class SqliteValueBox: ValueBox { precondition(self.queue.isCurrent()) if let _ = self.tables[table.id] { let statement = self.moveStatement(table, from: previousKey, to: updatedKey) - while statement.step(handle: self.database.handle, path: self.databasePath) { + while statement.step(handle: self.database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { } statement.reset() } @@ -1880,7 +1887,7 @@ public final class SqliteValueBox: ValueBox { precondition(self.queue.isCurrent()) if let _ = self.tables[fromTable.id] { let statement = self.copyStatement(fromTable: fromTable, fromKey: fromKey, toTable: toTable, toKey: toKey) - while statement.step(handle: self.database.handle, path: self.databasePath) { + while statement.step(handle: self.database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { } statement.reset() } @@ -1916,7 +1923,7 @@ public final class SqliteValueBox: ValueBox { } if let statement = statement { - while statement.step(handle: self.database.handle, path: self.databasePath) { + while statement.step(handle: self.database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { let resultCollectionId = statement.stringAt(0) let resultItemId = statement.stringAt(1) @@ -1942,7 +1949,7 @@ public final class SqliteValueBox: ValueBox { } let statement = self.fullTextInsertStatement(table, collectionId: collectionIdData, itemId: itemIdData, contents: contentsData, tags: tagsData) - while statement.step(handle: self.database.handle, path: self.databasePath) { + while statement.step(handle: self.database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { } statement.reset() } @@ -1960,7 +1967,7 @@ public final class SqliteValueBox: ValueBox { } let statement = self.fullTextDeleteStatement(table, itemId: itemIdData) - while statement.step(handle: self.database.handle, path: self.databasePath) { + while statement.step(handle: self.database.handle, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { } statement.reset() } @@ -1987,7 +1994,7 @@ public final class SqliteValueBox: ValueBox { } var result = 0 - while statement.step(handle: database.handle, true, path: self.databasePath) { + while statement.step(handle: database.handle, true, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { let value = statement.int32At(0) result = Int(value) } @@ -2005,7 +2012,7 @@ public final class SqliteValueBox: ValueBox { let statement = SqlitePreparedStatement(statement: statementImpl) var result = 0 - while statement.step(handle: database.handle, true, path: self.databasePath) { + while statement.step(handle: database.handle, true, pathToRemoveOnError: self.removeDatabaseOnError ? self.databasePath : nil) { let value = statement.int32At(0) result = Int(value) } diff --git a/submodules/QrCodeUI/Sources/QrCodeScreen.swift b/submodules/QrCodeUI/Sources/QrCodeScreen.swift index 366b0a84ff..b6aadc72ae 100644 --- a/submodules/QrCodeUI/Sources/QrCodeScreen.swift +++ b/submodules/QrCodeUI/Sources/QrCodeScreen.swift @@ -267,8 +267,7 @@ public final class QrCodeScreen: ViewController { self.qrImageNode.clipsToBounds = true self.qrImageNode.cornerRadius = 16.0 - self.qrIconNode = AnimatedStickerNode() - + self.qrIconNode = AnimatedStickerNode() self.qrIconNode.setup(source: AnimatedStickerNodeLocalFileSource(name: "PlaneLogo"), width: 240, height: 240, mode: .direct(cachePathPrefix: nil)) self.qrIconNode.visibility = true @@ -434,7 +433,6 @@ public final class QrCodeScreen: ViewController { let imageSide: CGFloat = 240.0 let imageSize = CGSize(width: imageSide, height: imageSide) let imageApply = makeImageLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: nil)) - let _ = imageApply() let width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: 0.0) diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextBackgroundNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextBackgroundNode.swift new file mode 100644 index 0000000000..6fe7b5158f --- /dev/null +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextBackgroundNode.swift @@ -0,0 +1,195 @@ +import Foundation +import AsyncDisplayKit +import Display +import TelegramPresentationData +import AccountContext + +private func generateBackgroundImage(foreground: UIColor, diameter: CGFloat, sideInset: CGFloat) -> UIImage? { + return generateImage(CGSize(width: diameter + sideInset * 2.0, height: diameter + sideInset * 2.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(foreground.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: sideInset, y: sideInset), size: CGSize(width: diameter, height: diameter))) + })?.stretchableImage(withLeftCapWidth: Int(sideInset + diameter / 2.0), topCapHeight: Int(sideInset + diameter / 2.0)) +} + +private func generateBubbleImage(foreground: UIColor, diameter: CGFloat, sideInset: CGFloat) -> UIImage? { + return generateImage(CGSize(width: diameter + sideInset * 2.0, height: diameter + sideInset * 2.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(foreground.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: sideInset, y: sideInset), size: CGSize(width: diameter, height: diameter))) + })?.stretchableImage(withLeftCapWidth: Int(diameter / 2.0 + sideInset / 2.0), topCapHeight: Int(diameter / 2.0 + sideInset / 2.0)) +} + +private func generateBubbleShadowImage(shadow: UIColor, diameter: CGFloat, shadowBlur: CGFloat) -> UIImage? { + return generateImage(CGSize(width: diameter + shadowBlur * 2.0, height: diameter + shadowBlur * 2.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(shadow.cgColor) + context.setShadow(offset: CGSize(), blur: shadowBlur, color: shadow.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowBlur, y: shadowBlur), size: CGSize(width: diameter, height: diameter))) + context.setShadow(offset: CGSize(), blur: 1.0, color: shadow.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowBlur, y: shadowBlur), size: CGSize(width: diameter, height: diameter))) + context.setFillColor(UIColor.clear.cgColor) + context.setBlendMode(.copy) + context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowBlur, y: shadowBlur), size: CGSize(width: diameter, height: diameter))) + })?.stretchableImage(withLeftCapWidth: Int(shadowBlur + diameter / 2.0), topCapHeight: Int(shadowBlur + diameter / 2.0)) +} + + +final class ReactionContextBackgroundNode: ASDisplayNode { + private let largeCircleSize: CGFloat + private let smallCircleSize: CGFloat + + private let backgroundNode: NavigationBackgroundNode + + private let maskLayer: SimpleLayer + private let backgroundLayer: SimpleLayer + private let backgroundShadowLayer: SimpleLayer + private let largeCircleLayer: SimpleLayer + private let largeCircleShadowLayer: SimpleLayer + private let smallCircleLayer: SimpleLayer + private let smallCircleShadowLayer: SimpleLayer + + private var theme: PresentationTheme? + + init(largeCircleSize: CGFloat, smallCircleSize: CGFloat) { + self.largeCircleSize = largeCircleSize + self.smallCircleSize = smallCircleSize + + self.backgroundNode = NavigationBackgroundNode(color: .clear, enableBlur: true) + + self.maskLayer = SimpleLayer() + self.backgroundLayer = SimpleLayer() + self.backgroundShadowLayer = SimpleLayer() + self.largeCircleLayer = SimpleLayer() + self.largeCircleShadowLayer = SimpleLayer() + self.smallCircleLayer = SimpleLayer() + self.smallCircleShadowLayer = SimpleLayer() + + self.backgroundLayer.backgroundColor = UIColor.black.cgColor + self.backgroundLayer.masksToBounds = true + self.backgroundLayer.cornerRadius = 52.0 / 2.0 + + self.largeCircleLayer.backgroundColor = UIColor.black.cgColor + self.largeCircleLayer.masksToBounds = true + self.largeCircleLayer.cornerRadius = largeCircleSize / 2.0 + + self.smallCircleLayer.backgroundColor = UIColor.black.cgColor + self.smallCircleLayer.masksToBounds = true + self.smallCircleLayer.cornerRadius = smallCircleSize / 2.0 + + if #available(iOS 13.0, *) { + self.backgroundLayer.cornerCurve = .circular + self.largeCircleLayer.cornerCurve = .circular + self.smallCircleLayer.cornerCurve = .circular + } + + super.init() + + self.layer.addSublayer(self.backgroundShadowLayer) + self.layer.addSublayer(self.smallCircleShadowLayer) + self.layer.addSublayer(self.largeCircleShadowLayer) + + self.backgroundShadowLayer.opacity = 0.0 + self.largeCircleShadowLayer.opacity = 0.0 + self.smallCircleShadowLayer.opacity = 0.0 + + self.addSubnode(self.backgroundNode) + + self.maskLayer.addSublayer(self.smallCircleLayer) + self.maskLayer.addSublayer(self.largeCircleLayer) + self.maskLayer.addSublayer(self.backgroundLayer) + + self.backgroundNode.layer.mask = self.maskLayer + } + + func updateIsIntersectingContent(isIntersectingContent: Bool, transition: ContainedViewLayoutTransition) { + let shadowAlpha: CGFloat = isIntersectingContent ? 1.0 : 0.0 + transition.updateAlpha(layer: self.backgroundShadowLayer, alpha: shadowAlpha) + transition.updateAlpha(layer: self.smallCircleShadowLayer, alpha: shadowAlpha) + transition.updateAlpha(layer: self.largeCircleShadowLayer, alpha: shadowAlpha) + } + + func update( + theme: PresentationTheme, + size: CGSize, + cloudSourcePoint: CGFloat, + isLeftAligned: Bool, + transition: ContainedViewLayoutTransition + ) { + let shadowInset: CGFloat = 15.0 + + if self.theme !== theme { + self.theme = theme + + self.backgroundNode.updateColor(color: theme.contextMenu.backgroundColor, transition: .immediate) + + let shadowColor = UIColor(white: 0.0, alpha: 0.4) + + if let image = generateBubbleShadowImage(shadow: shadowColor, diameter: 52.0, shadowBlur: shadowInset) { + ASDisplayNodeSetResizableContents(self.backgroundShadowLayer, image) + } + if let image = generateBubbleShadowImage(shadow: shadowColor, diameter: self.largeCircleSize, shadowBlur: shadowInset) { + ASDisplayNodeSetResizableContents(self.largeCircleShadowLayer, image) + } + if let image = generateBubbleShadowImage(shadow: shadowColor, diameter: self.smallCircleSize, shadowBlur: shadowInset) { + ASDisplayNodeSetResizableContents(self.smallCircleShadowLayer, image) + } + } + + let backgroundFrame = CGRect(origin: CGPoint(), size: size) + + let largeCircleFrame: CGRect + let smallCircleFrame: CGRect + if isLeftAligned { + largeCircleFrame = CGRect(origin: CGPoint(x: cloudSourcePoint - floor(largeCircleSize / 2.0), y: size.height - largeCircleSize / 2.0), size: CGSize(width: largeCircleSize, height: largeCircleSize)) + smallCircleFrame = CGRect(origin: CGPoint(x: largeCircleFrame.maxX - 3.0, y: largeCircleFrame.maxY + 2.0), size: CGSize(width: smallCircleSize, height: smallCircleSize)) + } else { + largeCircleFrame = CGRect(origin: CGPoint(x: cloudSourcePoint - floor(largeCircleSize / 2.0), y: size.height - largeCircleSize / 2.0), size: CGSize(width: largeCircleSize, height: largeCircleSize)) + smallCircleFrame = CGRect(origin: CGPoint(x: largeCircleFrame.minX + 3.0 - smallCircleSize, y: largeCircleFrame.maxY + 2.0), size: CGSize(width: smallCircleSize, height: smallCircleSize)) + } + + let contentBounds = backgroundFrame.insetBy(dx: -10.0, dy: -10.0).union(largeCircleFrame).union(smallCircleFrame) + + transition.updateFrame(layer: self.backgroundLayer, frame: backgroundFrame.offsetBy(dx: -contentBounds.minX, dy: -contentBounds.minY)) + transition.updateFrame(layer: self.largeCircleLayer, frame: largeCircleFrame.offsetBy(dx: -contentBounds.minX, dy: -contentBounds.minY)) + transition.updateFrame(layer: self.smallCircleLayer, frame: smallCircleFrame.offsetBy(dx: -contentBounds.minX, dy: -contentBounds.minY)) + + transition.updateFrame(layer: self.backgroundShadowLayer, frame: backgroundFrame.insetBy(dx: -shadowInset, dy: -shadowInset)) + transition.updateFrame(layer: self.largeCircleShadowLayer, frame: largeCircleFrame.insetBy(dx: -shadowInset, dy: -shadowInset)) + transition.updateFrame(layer: self.smallCircleShadowLayer, frame: smallCircleFrame.insetBy(dx: -shadowInset, dy: -shadowInset)) + + transition.updateFrame(node: self.backgroundNode, frame: contentBounds) + self.backgroundNode.update(size: contentBounds.size, transition: transition) + } + + func animateIn() { + let smallCircleDuration: Double = 0.5 + let largeCircleDuration: Double = 0.5 + let largeCircleDelay: Double = 0.08 + let mainCircleDuration: Double = 0.5 + let mainCircleDelay: Double = 0.1 + + self.smallCircleLayer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: smallCircleDuration) + + self.largeCircleLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: largeCircleDelay) + self.largeCircleLayer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: largeCircleDuration, delay: largeCircleDelay) + + self.backgroundLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: mainCircleDelay) + self.backgroundLayer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: mainCircleDuration, delay: mainCircleDelay) + } + + func animateInFromAnchorRect(size: CGSize, sourceBackgroundFrame: CGRect) { + let springDuration: Double = 0.42 + let springDamping: CGFloat = 104.0 + let springDelay: Double = 0.22 + + self.backgroundLayer.animateSpring(from: NSValue(cgPoint: CGPoint(x: sourceBackgroundFrame.midX - size.width / 2.0, y: 0.0)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping, additive: true) + self.backgroundLayer.animateSpring(from: NSValue(cgRect: CGRect(origin: CGPoint(), size: sourceBackgroundFrame.size)), to: NSValue(cgRect: CGRect(origin: CGPoint(), size: size)), keyPath: "bounds", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping) + } + + func animateOut() { + self.backgroundLayer.animateAlpha(from: CGFloat(self.backgroundLayer.opacity), to: 0.0, duration: 0.2, removeOnCompletion: false) + self.largeCircleLayer.animateAlpha(from: CGFloat(self.largeCircleLayer.opacity), to: 0.0, duration: 0.2, removeOnCompletion: false) + self.smallCircleLayer.animateAlpha(from: CGFloat(self.smallCircleLayer.opacity), to: 0.0, duration: 0.2, removeOnCompletion: false) + } +} diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index 1bcd8f363c..12f421c12a 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -7,11 +7,6 @@ import TelegramPresentationData import AccountContext import TelegramAnimatedStickerNode -public enum ReactionGestureItem { - case like - case unlike -} - public final class ReactionContextItem { public struct Reaction: Equatable { public var rawValue: String @@ -42,68 +37,11 @@ public final class ReactionContextItem { private let largeCircleSize: CGFloat = 16.0 private let smallCircleSize: CGFloat = 8.0 -private func generateBackgroundImage(foreground: UIColor, diameter: CGFloat, shadowBlur: CGFloat) -> UIImage? { - return generateImage(CGSize(width: diameter + shadowBlur * 2.0, height: diameter + shadowBlur * 2.0), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(foreground.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowBlur, y: shadowBlur), size: CGSize(width: diameter, height: diameter))) - })?.stretchableImage(withLeftCapWidth: Int(shadowBlur + diameter / 2.0), topCapHeight: Int(shadowBlur + diameter / 2.0)) -} - -private func generateBackgroundShadowImage(shadow: UIColor, diameter: CGFloat, shadowBlur: CGFloat) -> UIImage? { - return generateImage(CGSize(width: diameter * 2.0 + shadowBlur * 2.0, height: diameter + shadowBlur * 2.0), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(shadow.cgColor) - context.setShadow(offset: CGSize(), blur: shadowBlur, color: shadow.cgColor) - - context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowBlur, y: shadowBlur), size: CGSize(width: diameter, height: diameter))) - context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowBlur + diameter, y: shadowBlur), size: CGSize(width: diameter, height: diameter))) - context.fill(CGRect(origin: CGPoint(x: shadowBlur + diameter / 2.0, y: shadowBlur), size: CGSize(width: diameter, height: diameter))) - - context.setFillColor(UIColor.clear.cgColor) - context.setBlendMode(.copy) - - context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowBlur, y: shadowBlur), size: CGSize(width: diameter, height: diameter))) - context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowBlur + diameter, y: shadowBlur), size: CGSize(width: diameter, height: diameter))) - context.fill(CGRect(origin: CGPoint(x: shadowBlur + diameter / 2.0, y: shadowBlur), size: CGSize(width: diameter, height: diameter))) - })?.stretchableImage(withLeftCapWidth: Int(diameter + shadowBlur / 2.0), topCapHeight: Int(diameter / 2.0 + shadowBlur / 2.0)) -} - -private func generateBubbleImage(foreground: UIColor, diameter: CGFloat, shadowBlur: CGFloat) -> UIImage? { - return generateImage(CGSize(width: diameter + shadowBlur * 2.0, height: diameter + shadowBlur * 2.0), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(foreground.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowBlur, y: shadowBlur), size: CGSize(width: diameter, height: diameter))) - })?.stretchableImage(withLeftCapWidth: Int(diameter / 2.0 + shadowBlur / 2.0), topCapHeight: Int(diameter / 2.0 + shadowBlur / 2.0)) -} - -private func generateBubbleShadowImage(shadow: UIColor, diameter: CGFloat, shadowBlur: CGFloat) -> UIImage? { - return generateImage(CGSize(width: diameter + shadowBlur * 2.0, height: diameter + shadowBlur * 2.0), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(shadow.cgColor) - context.setShadow(offset: CGSize(), blur: shadowBlur, color: shadow.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowBlur, y: shadowBlur), size: CGSize(width: diameter, height: diameter))) - context.setShadow(offset: CGSize(), blur: 1.0, color: shadow.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowBlur, y: shadowBlur), size: CGSize(width: diameter, height: diameter))) - context.setFillColor(UIColor.clear.cgColor) - context.setBlendMode(.copy) - context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowBlur, y: shadowBlur), size: CGSize(width: diameter, height: diameter))) - })?.stretchableImage(withLeftCapWidth: Int(diameter / 2.0 + shadowBlur / 2.0), topCapHeight: Int(diameter / 2.0 + shadowBlur / 2.0)) -} - public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { private let theme: PresentationTheme private let items: [ReactionContextItem] - private let backgroundNode: ASImageNode - private let backgroundShadowNode: ASImageNode - private let backgroundContainerNode: ASDisplayNode - - private let largeCircleNode: ASImageNode - private let largeCircleShadowNode: ASImageNode - - private let smallCircleNode: ASImageNode - private let smallCircleShadowNode: ASImageNode + private let backgroundNode: ReactionContextBackgroundNode private let contentContainer: ASDisplayNode private let contentContainerMask: UIImageView @@ -117,50 +55,17 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { public var reactionSelected: ((ReactionContextItem) -> Void)? - private let hapticFeedback = HapticFeedback() + private var hapticFeedback: HapticFeedback? + private var standaloneReactionAnimation: StandaloneReactionAnimation? + + private weak var animationTargetView: UIView? + private var animationHideNode: Bool = false public init(context: AccountContext, theme: PresentationTheme, items: [ReactionContextItem]) { self.theme = theme self.items = items - let shadowBlur: CGFloat = 5.0 - - self.backgroundNode = ASImageNode() - self.backgroundNode.displayWithoutProcessing = true - self.backgroundNode.displaysAsynchronously = false - - self.backgroundShadowNode = ASImageNode() - self.backgroundShadowNode.displayWithoutProcessing = true - self.backgroundShadowNode.displaysAsynchronously = false - - self.backgroundContainerNode = ASDisplayNode() - self.backgroundContainerNode.allowsGroupOpacity = true - - self.largeCircleNode = ASImageNode() - self.largeCircleNode.displayWithoutProcessing = true - self.largeCircleNode.displaysAsynchronously = false - - self.largeCircleShadowNode = ASImageNode() - self.largeCircleShadowNode.displayWithoutProcessing = true - self.largeCircleShadowNode.displaysAsynchronously = false - - self.smallCircleNode = ASImageNode() - self.smallCircleNode.displayWithoutProcessing = true - self.smallCircleNode.displaysAsynchronously = false - - self.smallCircleShadowNode = ASImageNode() - self.smallCircleShadowNode.displayWithoutProcessing = true - self.smallCircleShadowNode.displaysAsynchronously = false - - self.backgroundNode.image = generateBackgroundImage(foreground: theme.contextMenu.backgroundColor.withAlphaComponent(1.0), diameter: 52.0, shadowBlur: shadowBlur) - - self.backgroundShadowNode.image = generateBackgroundShadowImage(shadow: UIColor(white: 0.0, alpha: 0.2), diameter: 52.0, shadowBlur: shadowBlur) - - self.largeCircleNode.image = generateBubbleImage(foreground: theme.contextMenu.backgroundColor.withAlphaComponent(1.0), diameter: largeCircleSize, shadowBlur: shadowBlur) - self.smallCircleNode.image = generateBubbleImage(foreground: theme.contextMenu.backgroundColor.withAlphaComponent(1.0), diameter: smallCircleSize, shadowBlur: shadowBlur) - - self.largeCircleShadowNode.image = generateBubbleShadowImage(shadow: UIColor(white: 0.0, alpha: 0.2), diameter: largeCircleSize, shadowBlur: shadowBlur) - self.smallCircleShadowNode.image = generateBubbleShadowImage(shadow: UIColor(white: 0.0, alpha: 0.2), diameter: smallCircleSize, shadowBlur: shadowBlur) + self.backgroundNode = ReactionContextBackgroundNode(largeCircleSize: largeCircleSize, smallCircleSize: smallCircleSize) self.scrollNode = ASScrollNode() self.scrollNode.view.disablesInteractiveTransitionGestureRecognizer = true @@ -201,18 +106,10 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { context.fill(CGRect(origin: CGPoint(x: maskGradientWidth, y: 0.0), size: CGSize(width: 1.0, height: size.height))) })?.stretchableImage(withLeftCapWidth: Int(maskGradientWidth), topCapHeight: 0) self.contentContainer.view.mask = self.contentContainerMask - //self.contentContainer.view.addSubview(self.contentContainerMask) super.init() - self.addSubnode(self.smallCircleShadowNode) - self.addSubnode(self.largeCircleShadowNode) - self.addSubnode(self.backgroundShadowNode) - - self.backgroundContainerNode.addSubnode(self.smallCircleNode) - self.backgroundContainerNode.addSubnode(self.largeCircleNode) - self.backgroundContainerNode.addSubnode(self.backgroundNode) - self.addSubnode(self.backgroundContainerNode) + self.addSubnode(self.backgroundNode) self.scrollNode.view.delegate = self @@ -234,17 +131,21 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { self.updateLayout(size: size, insets: insets, anchorRect: anchorRect, transition: transition, animateInFromAnchorRect: nil, animateOutToAnchorRect: nil) } + public func updateIsIntersectingContent(isIntersectingContent: Bool, transition: ContainedViewLayoutTransition) { + self.backgroundNode.updateIsIntersectingContent(isIntersectingContent: isIntersectingContent, transition: transition) + } + private func calculateBackgroundFrame(containerSize: CGSize, insets: UIEdgeInsets, anchorRect: CGRect, contentSize: CGSize) -> (backgroundFrame: CGRect, isLeftAligned: Bool, cloudSourcePoint: CGFloat) { var contentSize = contentSize contentSize.width = max(52.0, contentSize.width) contentSize.height = 52.0 - let sideInset: CGFloat = 12.0 + let sideInset: CGFloat = 11.0 let backgroundOffset: CGPoint = CGPoint(x: 22.0, y: -7.0) var rect: CGRect let isLeftAligned: Bool - if anchorRect.maxX < containerSize.width - backgroundOffset.x - sideInset { + if anchorRect.minX < containerSize.width - anchorRect.maxX { rect = CGRect(origin: CGPoint(x: anchorRect.maxX - contentSize.width + backgroundOffset.x, y: anchorRect.minY - contentSize.height + backgroundOffset.y), size: contentSize) isLeftAligned = true } else { @@ -254,6 +155,12 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { rect.origin.x = max(sideInset, rect.origin.x) rect.origin.y = max(insets.top + sideInset, rect.origin.y) rect.origin.x = min(containerSize.width - contentSize.width - sideInset, rect.origin.x) + if rect.maxX > containerSize.width - sideInset { + rect.origin.x = containerSize.width - sideInset - rect.width + } + if rect.minX < sideInset { + rect.origin.x = sideInset + } let cloudSourcePoint: CGFloat if isLeftAligned { @@ -300,20 +207,26 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { private func updateLayout(size: CGSize, insets: UIEdgeInsets, anchorRect: CGRect, transition: ContainedViewLayoutTransition, animateInFromAnchorRect: CGRect?, animateOutToAnchorRect: CGRect?, animateReactionHighlight: Bool = false) { self.validLayout = (size, insets, anchorRect) - let sideInset: CGFloat = 14.0 + let sideInset: CGFloat = 11.0 let itemSpacing: CGFloat = 9.0 let itemSize: CGFloat = 40.0 - let shadowBlur: CGFloat = 5.0 let verticalInset: CGFloat = 13.0 let rowHeight: CGFloat = 30.0 let completeContentWidth = CGFloat(self.items.count) * itemSize + (CGFloat(self.items.count) - 1.0) * itemSpacing + sideInset * 2.0 let minVisibleItemCount: CGFloat = min(CGFloat(self.items.count), 6.5) - let visibleContentWidth = floor(minVisibleItemCount * itemSize + (minVisibleItemCount - 1.0) * itemSpacing + sideInset * 2.0) + var visibleContentWidth = floor(minVisibleItemCount * itemSize + (minVisibleItemCount - 1.0) * itemSpacing + sideInset * 2.0) + if visibleContentWidth > size.width - sideInset * 2.0 { + visibleContentWidth = size.width - sideInset * 2.0 + } let contentHeight = verticalInset * 2.0 + rowHeight - let (backgroundFrame, isLeftAligned, cloudSourcePoint) = self.calculateBackgroundFrame(containerSize: size, insets: insets, anchorRect: anchorRect, contentSize: CGSize(width: visibleContentWidth, height: contentHeight)) + var backgroundInsets = insets + backgroundInsets.left += sideInset + backgroundInsets.right += sideInset + + let (backgroundFrame, isLeftAligned, cloudSourcePoint) = self.calculateBackgroundFrame(containerSize: CGSize(width: size.width, height: size.height), insets: backgroundInsets, anchorRect: anchorRect, contentSize: CGSize(width: visibleContentWidth, height: contentHeight)) self.isLeftAligned = isLeftAligned transition.updateFrame(node: self.contentContainer, frame: backgroundFrame) @@ -327,60 +240,40 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { let itemOffsetY: CGFloat = -1.0 - let itemFrame = CGRect(origin: CGPoint(x: sideInset + column * (itemSize + itemSpacing), y: verticalInset + floor((rowHeight - itemSize) / 2.0) + itemOffsetY), size: CGSize(width: itemSize, height: itemSize)) + var itemFrame = CGRect(origin: CGPoint(x: sideInset + column * (itemSize + itemSpacing), y: verticalInset + floor((rowHeight - itemSize) / 2.0) + itemOffsetY), size: CGSize(width: itemSize, height: itemSize)) + if self.highlightedReaction == self.items[i].reaction { + itemFrame = itemFrame.insetBy(dx: -6.0, dy: -6.0) + } if !self.itemNodes[i].isExtracted { transition.updateFrame(node: self.itemNodes[i], frame: itemFrame, beginWithCurrentState: true) - self.itemNodes[i].updateLayout(size: CGSize(width: itemSize, height: itemSize), isExpanded: false, transition: transition) + self.itemNodes[i].updateLayout(size: itemFrame.size, isExpanded: false, transition: transition) } } self.updateScrolling(transition: transition) - - let isInOverflow = backgroundFrame.maxY > anchorRect.minY - let backgroundAlpha: CGFloat = isInOverflow ? 1.0 : 0.8 - let shadowAlpha: CGFloat = isInOverflow ? 1.0 : 0.0 - transition.updateAlpha(node: self.backgroundContainerNode, alpha: backgroundAlpha) - transition.updateAlpha(node: self.backgroundShadowNode, alpha: shadowAlpha) - transition.updateAlpha(node: self.largeCircleShadowNode, alpha: shadowAlpha) - transition.updateAlpha(node: self.smallCircleShadowNode, alpha: shadowAlpha) - transition.updateFrame(node: self.backgroundContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))) - - transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame.insetBy(dx: -shadowBlur, dy: -shadowBlur)) - transition.updateFrame(node: self.backgroundShadowNode, frame: backgroundFrame.insetBy(dx: -shadowBlur, dy: -shadowBlur)) - - let largeCircleFrame: CGRect - let smallCircleFrame: CGRect - if isLeftAligned { - largeCircleFrame = CGRect(origin: CGPoint(x: cloudSourcePoint - floor(largeCircleSize / 2.0), y: backgroundFrame.maxY - largeCircleSize / 2.0), size: CGSize(width: largeCircleSize, height: largeCircleSize)) - smallCircleFrame = CGRect(origin: CGPoint(x: largeCircleFrame.maxX - 3.0, y: largeCircleFrame.maxY + 2.0), size: CGSize(width: smallCircleSize, height: smallCircleSize)) - } else { - largeCircleFrame = CGRect(origin: CGPoint(x: cloudSourcePoint - floor(largeCircleSize / 2.0), y: backgroundFrame.maxY - largeCircleSize / 2.0), size: CGSize(width: largeCircleSize, height: largeCircleSize)) - smallCircleFrame = CGRect(origin: CGPoint(x: largeCircleFrame.minX + 3.0 - smallCircleSize, y: largeCircleFrame.maxY + 2.0), size: CGSize(width: smallCircleSize, height: smallCircleSize)) - } - - transition.updateFrame(node: self.largeCircleNode, frame: largeCircleFrame.insetBy(dx: -shadowBlur, dy: -shadowBlur)) - transition.updateFrame(node: self.largeCircleShadowNode, frame: largeCircleFrame.insetBy(dx: -shadowBlur, dy: -shadowBlur)) - transition.updateFrame(node: self.smallCircleNode, frame: smallCircleFrame.insetBy(dx: -shadowBlur, dy: -shadowBlur)) - transition.updateFrame(node: self.smallCircleShadowNode, frame: smallCircleFrame.insetBy(dx: -shadowBlur, dy: -shadowBlur)) + transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) + self.backgroundNode.update( + theme: self.theme, + size: backgroundFrame.size, + cloudSourcePoint: cloudSourcePoint - backgroundFrame.minX, + isLeftAligned: isLeftAligned, + transition: transition + ) if let animateInFromAnchorRect = animateInFromAnchorRect { let springDuration: Double = 0.42 let springDamping: CGFloat = 104.0 let springDelay: Double = 0.22 - let sourceBackgroundFrame = self.calculateBackgroundFrame(containerSize: size, insets: insets, anchorRect: animateInFromAnchorRect, contentSize: CGSize(width: backgroundFrame.height, height: contentHeight)).0 + let sourceBackgroundFrame = self.calculateBackgroundFrame(containerSize: size, insets: backgroundInsets, anchorRect: animateInFromAnchorRect, contentSize: CGSize(width: backgroundFrame.height, height: contentHeight)).0 - self.backgroundNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: sourceBackgroundFrame.midX - backgroundFrame.midX, y: 0.0)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping, additive: true) - self.backgroundNode.layer.animateSpring(from: NSValue(cgRect: CGRect(origin: CGPoint(), size: sourceBackgroundFrame.size).insetBy(dx: -shadowBlur, dy: -shadowBlur)), to: NSValue(cgRect: CGRect(origin: CGPoint(), size: backgroundFrame.size).insetBy(dx: -shadowBlur, dy: -shadowBlur)), keyPath: "bounds", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping) + self.backgroundNode.animateInFromAnchorRect(size: backgroundFrame.size, sourceBackgroundFrame: sourceBackgroundFrame.offsetBy(dx: -backgroundFrame.minX, dy: -backgroundFrame.minY)) self.contentContainer.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: sourceBackgroundFrame.midX - backgroundFrame.midX, y: 0.0)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping, additive: true) self.contentContainer.layer.animateSpring(from: NSValue(cgRect: CGRect(origin: CGPoint(), size: sourceBackgroundFrame.size)), to: NSValue(cgRect: CGRect(origin: CGPoint(), size: backgroundFrame.size)), keyPath: "bounds", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping) - - //self.contentContainerMask.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: sourceBackgroundFrame.midX - backgroundFrame.midX, y: 0.0)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping, additive: true) - //self.contentContainerMask.layer.animateSpring(from: NSValue(cgRect: CGRect(origin: CGPoint(), size: sourceBackgroundFrame.size)), to: NSValue(cgRect: CGRect(origin: CGPoint(), size: backgroundFrame.size)), keyPath: "bounds", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping) } else if let animateOutToAnchorRect = animateOutToAnchorRect { - let targetBackgroundFrame = self.calculateBackgroundFrame(containerSize: size, insets: insets, anchorRect: animateOutToAnchorRect, contentSize: CGSize(width: visibleContentWidth, height: contentHeight)).0 + let targetBackgroundFrame = self.calculateBackgroundFrame(containerSize: size, insets: backgroundInsets, anchorRect: animateOutToAnchorRect, contentSize: CGSize(width: visibleContentWidth, height: contentHeight)).0 let offset = CGPoint(x: -(targetBackgroundFrame.minX - backgroundFrame.minX), y: -(targetBackgroundFrame.minY - backgroundFrame.minY)) self.position = CGPoint(x: self.position.x - offset.x, y: self.position.y - offset.y) @@ -395,22 +288,10 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { self.updateLayout(size: size, insets: insets, anchorRect: anchorRect, transition: .immediate, animateInFromAnchorRect: sourceAnchorRect, animateOutToAnchorRect: nil) } - let smallCircleDuration: Double = 0.5 - let largeCircleDuration: Double = 0.5 - let largeCircleDelay: Double = 0.08 let mainCircleDuration: Double = 0.5 let mainCircleDelay: Double = 0.1 - self.smallCircleNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: smallCircleDuration) - self.smallCircleShadowNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: smallCircleDuration) - - self.largeCircleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: largeCircleDelay) - self.largeCircleNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: largeCircleDuration, delay: largeCircleDelay) - self.largeCircleShadowNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: largeCircleDuration, delay: largeCircleDelay) - - self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: mainCircleDelay) - self.backgroundNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: mainCircleDuration, delay: mainCircleDelay) - self.backgroundShadowNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: mainCircleDuration, delay: mainCircleDelay) + self.backgroundNode.animateIn() for i in 0 ..< self.itemNodes.count { let itemNode = self.itemNodes[i] @@ -418,22 +299,11 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: itemDelay) itemNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: mainCircleDuration, delay: itemDelay, initialVelocity: 0.0) } - - /*if let itemNode = self.itemNodes.first { - itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: mainCircleDelay) - itemNode.didAppear() - itemNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: mainCircleDuration, delay: mainCircleDelay, completion: { _ in - }) - }*/ } public func animateOut(to targetAnchorRect: CGRect?, animatingOutToReaction: Bool) { - self.backgroundNode.layer.animateAlpha(from: self.backgroundNode.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false) - self.backgroundShadowNode.layer.animateAlpha(from: self.backgroundShadowNode.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false) - self.largeCircleNode.layer.animateAlpha(from: self.largeCircleNode.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false) - self.largeCircleShadowNode.layer.animateAlpha(from: self.largeCircleShadowNode.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false) - self.smallCircleNode.layer.animateAlpha(from: self.smallCircleNode.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false) - self.smallCircleShadowNode.layer.animateAlpha(from: self.smallCircleShadowNode.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false) + self.backgroundNode.animateOut() + for itemNode in self.itemNodes { if itemNode.isExtracted { continue @@ -452,6 +322,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { return } + let sourceFrame = itemNode.view.convert(itemNode.bounds, to: self.view) let targetFrame = self.view.convert(targetView.convert(targetView.bounds, to: nil), from: nil) targetSnapshotView.frame = targetFrame @@ -466,15 +337,13 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } let targetPosition = targetFrame.center - let _ = targetPosition let duration: Double = 0.16 itemNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.9, removeOnCompletion: false) + itemNode.layer.animatePosition(from: itemNode.layer.position, to: targetPosition, duration: duration, removeOnCompletion: false) targetSnapshotView.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.8) - targetSnapshotView.layer.animateScale(from: itemNode.bounds.width / targetSnapshotView.bounds.width, to: 1.0, duration: duration, removeOnCompletion: false, completion: { [weak self, weak targetSnapshotView] _ in - if let strongSelf = self { - strongSelf.hapticFeedback.tap() - } + targetSnapshotView.layer.animatePosition(from: sourceFrame.center, to: targetPosition, duration: duration, removeOnCompletion: false) + targetSnapshotView.layer.animateScale(from: itemNode.bounds.width / targetSnapshotView.bounds.width, to: 1.0, duration: duration, removeOnCompletion: false, completion: { [weak targetSnapshotView] _ in completedTarget = true intermediateCompletion() @@ -482,11 +351,9 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { if hideNode { targetView.isHidden = false - /*targetView.layer.animateSpring(from: 0.5 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: duration, initialVelocity: 0.0, damping: 90.0, completion: { _ in*/ - targetSnapshotView?.isHidden = true - targetScaleCompleted = true - intermediateCompletion() - //}) + targetSnapshotView?.isHidden = true + targetScaleCompleted = true + intermediateCompletion() } else { targetScaleCompleted = true intermediateCompletion() @@ -510,6 +377,18 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { if itemNode.item.reaction.rawValue != value { continue } + + self.animationTargetView = targetView + self.animationHideNode = hideNode + + /*let standaloneReactionAnimation = StandaloneReactionAnimation() + self.standaloneReactionAnimation = standaloneReactionAnimation + standaloneReactionAnimation.frame = self.bounds + self.addSubnode(standaloneReactionAnimation) + standaloneReactionAnimation.animateReactionSelection(context: itemNode.context, theme: self.theme, reaction: itemNode.item, targetView: targetView, currentItemNode: itemNode, hideNode: hideNode, completion: completion) + + return*/ + if hideNode { targetView.isHidden = true } @@ -521,7 +400,10 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { let expandedScale: CGFloat = 3.0 let expandedSize = CGSize(width: floor(selfSourceRect.width * expandedScale), height: floor(selfSourceRect.height * expandedScale)) - let expandedFrame = CGRect(origin: CGPoint(x: floor(selfTargetRect.midX - expandedSize.width / 2.0), y: floor(selfTargetRect.midY - expandedSize.height / 2.0)), size: expandedSize) + var expandedFrame = CGRect(origin: CGPoint(x: floor(selfTargetRect.midX - expandedSize.width / 2.0), y: floor(selfTargetRect.midY - expandedSize.height / 2.0)), size: expandedSize) + if expandedFrame.minX < -floor(expandedFrame.width * 0.05) { + expandedFrame.origin.x = -floor(expandedFrame.width * 0.05) + } let transition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .linear) @@ -591,6 +473,34 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } } + public func highlightGestureMoved(location: CGPoint) { + let highlightedReaction = self.reaction(at: location)?.reaction + if self.highlightedReaction != highlightedReaction { + self.highlightedReaction = highlightedReaction + if self.hapticFeedback == nil { + self.hapticFeedback = HapticFeedback() + } + self.hapticFeedback?.tap() + + if let (size, insets, anchorRect) = self.validLayout { + self.updateLayout(size: size, insets: insets, anchorRect: anchorRect, transition: .animated(duration: 0.18, curve: .easeInOut), animateInFromAnchorRect: nil, animateOutToAnchorRect: nil, animateReactionHighlight: true) + } + } + } + + public func highlightGestureFinished(performAction: Bool) { + if let highlightedReaction = self.highlightedReaction { + self.highlightedReaction = nil + if performAction { + self.performReactionSelection(reaction: highlightedReaction) + } else { + if let (size, insets, anchorRect) = self.validLayout { + self.updateLayout(size: size, insets: insets, anchorRect: anchorRect, transition: .animated(duration: 0.18, curve: .easeInOut), animateInFromAnchorRect: nil, animateOutToAnchorRect: nil, animateReactionHighlight: true) + } + } + } + } + public func reaction(at point: CGPoint) -> ReactionContextItem? { for i in 0 ..< 2 { let touchInset: CGFloat = i == 0 ? 0.0 : 8.0 @@ -613,6 +523,14 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } } + public func cancelReactionAnimation() { + self.standaloneReactionAnimation?.cancel() + + if let animationTargetView = self.animationTargetView, self.animationHideNode { + animationTargetView.isHidden = false + } + } + public func setHighlightedReaction(_ value: ReactionContextItem.Reaction?) { self.highlightedReaction = value if let (size, insets, anchorRect) = self.validLayout { @@ -629,36 +547,57 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } public final class StandaloneReactionAnimation: ASDisplayNode { - private let itemNode: ReactionNode + private var itemNode: ReactionNode? = nil private let hapticFeedback = HapticFeedback() + private var isCancelled: Bool = false - public init(context: AccountContext, theme: PresentationTheme, reaction: ReactionContextItem) { - self.itemNode = ReactionNode(context: context, theme: theme, item: reaction) - + private weak var targetView: UIView? + private var hideNode: Bool = false + + override public init() { super.init() self.isUserInteractionEnabled = false - - self.addSubnode(self.itemNode) } - public func animateReactionSelection(targetView: UIView, hideNode: Bool, completion: @escaping () -> Void) { + public func animateReactionSelection(context: AccountContext, theme: PresentationTheme, reaction: ReactionContextItem, targetView: UIView, hideNode: Bool, completion: @escaping () -> Void) { + self.animateReactionSelection(context: context, theme: theme, reaction: reaction, targetView: targetView, currentItemNode: nil, hideNode: hideNode, completion: completion) + } + + func animateReactionSelection(context: AccountContext, theme: PresentationTheme, reaction: ReactionContextItem, targetView: UIView, currentItemNode: ReactionNode?, hideNode: Bool, completion: @escaping () -> Void) { guard let sourceSnapshotView = targetView.snapshotContentTree() else { completion() return } + + self.targetView = targetView + self.hideNode = hideNode + + let itemNode: ReactionNode + if let currentItemNode = currentItemNode { + itemNode = currentItemNode + } else { + itemNode = ReactionNode(context: context, theme: theme, item: reaction) + } + self.itemNode = itemNode + + self.addSubnode(itemNode) + if hideNode { targetView.isHidden = true } - self.itemNode.isExtracted = true + itemNode.isExtracted = true let sourceItemSize: CGFloat = 40.0 let selfTargetRect = self.view.convert(targetView.bounds, from: targetView) let expandedScale: CGFloat = 3.0 let expandedSize = CGSize(width: floor(sourceItemSize * expandedScale), height: floor(sourceItemSize * expandedScale)) - let expandedFrame = CGRect(origin: CGPoint(x: floor(selfTargetRect.midX - expandedSize.width / 2.0), y: floor(selfTargetRect.midY - expandedSize.height / 2.0)), size: expandedSize) + var expandedFrame = CGRect(origin: CGPoint(x: floor(selfTargetRect.midX - expandedSize.width / 2.0), y: floor(selfTargetRect.midY - expandedSize.height / 2.0)), size: expandedSize) + if expandedFrame.minX < -floor(expandedFrame.width * 0.05) { + expandedFrame.origin.x = -floor(expandedFrame.width * 0.05) + } sourceSnapshotView.frame = selfTargetRect self.view.addSubview(sourceSnapshotView) @@ -680,7 +619,7 @@ public final class StandaloneReactionAnimation: ASDisplayNode { let animationFrame = expandedFrame.insetBy(dx: -expandedFrame.width * 0.5, dy: -expandedFrame.height * 0.5) .offsetBy(dx: incomingMessage ? (expandedFrame.width - 50.0) : (-expandedFrame.width + 50.0), dy: 0.0) - additionalAnimationNode.setup(source: AnimatedStickerResourceSource(account: self.itemNode.context.account, resource: self.itemNode.item.applicationAnimation.resource), width: Int(animationFrame.width * 2.0), height: Int(animationFrame.height * 2.0), playbackMode: .once, mode: .direct(cachePathPrefix: self.itemNode.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.itemNode.item.applicationAnimation.resource.id))) + additionalAnimationNode.setup(source: AnimatedStickerResourceSource(account: itemNode.context.account, resource: itemNode.item.applicationAnimation.resource), width: Int(animationFrame.width * 2.0), height: Int(animationFrame.height * 2.0), playbackMode: .once, mode: .direct(cachePathPrefix: itemNode.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(itemNode.item.applicationAnimation.resource.id))) additionalAnimationNode.frame = animationFrame if incomingMessage { additionalAnimationNode.transform = CATransform3DMakeScale(-1.0, 1.0, 1.0) @@ -698,9 +637,28 @@ public final class StandaloneReactionAnimation: ASDisplayNode { } } + var didBeginDismissAnimation = false + let beginDismissAnimation: () -> Void = { [weak self] in + if !didBeginDismissAnimation { + didBeginDismissAnimation = true + + guard let strongSelf = self else { + mainAnimationCompleted = true + intermediateCompletion() + return + } + strongSelf.animateFromItemNodeToReaction(itemNode: itemNode, targetView: targetView, hideNode: hideNode, completion: { + mainAnimationCompleted = true + intermediateCompletion() + }) + } + } + additionalAnimationNode.completed = { _ in additionalAnimationCompleted = true intermediateCompletion() + + beginDismissAnimation() } DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1 * UIView.animationDurationFactor(), execute: { @@ -708,11 +666,9 @@ public final class StandaloneReactionAnimation: ASDisplayNode { }) DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2.0, execute: { - self.animateFromItemNodeToReaction(itemNode: self.itemNode, targetView: targetView, hideNode: hideNode, completion: { - mainAnimationCompleted = true - intermediateCompletion() - }) + beginDismissAnimation() }) + } private func animateFromItemNodeToReaction(itemNode: ReactionNode, targetView: UIView, hideNode: Bool, completion: @escaping () -> Void) { @@ -721,6 +677,7 @@ public final class StandaloneReactionAnimation: ASDisplayNode { return } + let sourceFrame = itemNode.view.convert(itemNode.bounds, to: self.view) let targetFrame = self.view.convert(targetView.convert(targetView.bounds, to: nil), from: nil) targetSnapshotView.frame = targetFrame @@ -735,15 +692,13 @@ public final class StandaloneReactionAnimation: ASDisplayNode { } let targetPosition = targetFrame.center - let _ = targetPosition let duration: Double = 0.16 itemNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.9, removeOnCompletion: false) + itemNode.layer.animatePosition(from: itemNode.layer.position, to: targetPosition, duration: duration, removeOnCompletion: false) targetSnapshotView.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.8) - targetSnapshotView.layer.animateScale(from: itemNode.bounds.width / targetSnapshotView.bounds.width, to: 1.0, duration: duration, removeOnCompletion: false, completion: { [weak self, weak targetSnapshotView] _ in - if let strongSelf = self { - strongSelf.hapticFeedback.tap() - } + targetSnapshotView.layer.animatePosition(from: sourceFrame.center, to: targetPosition, duration: duration, removeOnCompletion: false) + targetSnapshotView.layer.animateScale(from: itemNode.bounds.width / targetSnapshotView.bounds.width, to: 1.0, duration: duration, removeOnCompletion: false, completion: { [weak targetSnapshotView] _ in completedTarget = true intermediateCompletion() @@ -751,11 +706,9 @@ public final class StandaloneReactionAnimation: ASDisplayNode { if hideNode { targetView.isHidden = false - /*targetView.layer.animateSpring(from: 0.5 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: duration, initialVelocity: 0.0, damping: 90.0, completion: { _ in*/ - targetSnapshotView?.isHidden = true - targetScaleCompleted = true - intermediateCompletion() - //}) + targetSnapshotView?.isHidden = true + targetScaleCompleted = true + intermediateCompletion() } else { targetScaleCompleted = true intermediateCompletion() @@ -769,6 +722,14 @@ public final class StandaloneReactionAnimation: ASDisplayNode { self.bounds = self.bounds.offsetBy(dx: 0.0, dy: offset.y) transition.animateOffsetAdditive(node: self, offset: -offset.y) } + + public func cancel() { + self.isCancelled = true + + if let targetView = self.targetView, self.hideNode { + targetView.isHidden = false + } + } } public final class StandaloneDismissReactionAnimation: ASDisplayNode { diff --git a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift index b6644e6dc7..f6c6f2a719 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift @@ -131,9 +131,11 @@ final class ReactionNode: ASDisplayNode { if self.validSize != size { self.validSize = size - self.staticImageNode.setSignal(chatMessageAnimatedSticker(postbox: self.context.account.postbox, file: item.stillAnimation, small: false, size: CGSize(width: animationDisplaySize.width * UIScreenScale, height: animationDisplaySize.height * UIScreenScale), fitzModifier: nil, fetched: false, onlyFullSize: false, thumbnail: false, synchronousLoad: false)) - let imageApply = self.staticImageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: animationDisplaySize, boundingSize: animationDisplaySize, intrinsicInsets: UIEdgeInsets())) - imageApply() + if !self.staticImageNode.isHidden { + self.staticImageNode.setSignal(chatMessageAnimatedSticker(postbox: self.context.account.postbox, file: item.stillAnimation, small: false, size: CGSize(width: animationDisplaySize.width * UIScreenScale, height: animationDisplaySize.height * UIScreenScale), fitzModifier: nil, fetched: false, onlyFullSize: false, thumbnail: false, synchronousLoad: false)) + let imageApply = self.staticImageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: animationDisplaySize, boundingSize: animationDisplaySize, intrinsicInsets: UIEdgeInsets())) + imageApply() + } transition.updateFrame(node: self.staticImageNode, frame: animationFrame) } @@ -141,15 +143,15 @@ final class ReactionNode: ASDisplayNode { if self.animationNode == nil { self.didSetupStillAnimation = true - self.stillAnimationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.stillAnimation.resource), width: Int(animationDisplaySize.width * 2.0), height: Int(animationDisplaySize.height * 2.0), playbackMode: .loop, mode: .direct(cachePathPrefix: self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.item.stillAnimation.resource.id))) + self.stillAnimationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.stillAnimation.resource), width: Int(animationDisplaySize.width * 2.5), height: Int(animationDisplaySize.height * 2.5), playbackMode: .loop, mode: .direct(cachePathPrefix: self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.item.stillAnimation.resource.id))) self.stillAnimationNode.position = animationFrame.center self.stillAnimationNode.bounds = CGRect(origin: CGPoint(), size: animationFrame.size) self.stillAnimationNode.updateLayout(size: animationFrame.size) self.stillAnimationNode.visibility = true } } else { - transition.updatePosition(node: self.stillAnimationNode, position: animationFrame.center) - transition.updateTransformScale(node: self.stillAnimationNode, scale: animationFrame.size.width / self.stillAnimationNode.bounds.width) + transition.updatePosition(node: self.stillAnimationNode, position: animationFrame.center, beginWithCurrentState: true) + transition.updateTransformScale(node: self.stillAnimationNode, scale: animationFrame.size.width / self.stillAnimationNode.bounds.width, beginWithCurrentState: true) } } diff --git a/submodules/SSignalKit/SwiftSignalKit/Source/Signal_Combine.swift b/submodules/SSignalKit/SwiftSignalKit/Source/Signal_Combine.swift index 54590b1f19..6f5db68462 100644 --- a/submodules/SSignalKit/SwiftSignalKit/Source/Signal_Combine.swift +++ b/submodules/SSignalKit/SwiftSignalKit/Source/Signal_Combine.swift @@ -178,6 +178,12 @@ public func combineLatest(queue: Queue? = nil, _ s1: Signal, _ s2: Signal, _ s3: Signal, _ s4: Signal, _ s5: Signal, _ s6: Signal, _ s7: Signal, _ s8: Signal, _ s9: Signal, _ s10: Signal, _ s11: Signal, _ s12: Signal, _ s13: Signal, _ s14: Signal, _ s15: Signal, _ s16: Signal) -> Signal<(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16), E> { + return combineLatestAny([signalOfAny(s1), signalOfAny(s2), signalOfAny(s3), signalOfAny(s4), signalOfAny(s5), signalOfAny(s6), signalOfAny(s7), signalOfAny(s8), signalOfAny(s9), signalOfAny(s10), signalOfAny(s11), signalOfAny(s12), signalOfAny(s13), signalOfAny(s14), signalOfAny(s15), signalOfAny(s16)], combine: { values in + return (values[0] as! T1, values[1] as! T2, values[2] as! T3, values[3] as! T4, values[4] as! T5, values[5] as! T6, values[6] as! T7, values[7] as! T8, values[8] as! T9, values[9] as! T10, values[10] as! T11, values[11] as! T12, values[12] as! T13, values[13] as! T14, values[14] as! T15, values[15] as! T16) + }, initialValues: [:], queue: queue) +} + public func combineLatest(queue: Queue? = nil, _ signals: [Signal]) -> Signal<[T], E> { if signals.count == 0 { return single([T](), E.self) diff --git a/submodules/SettingsUI/BUILD b/submodules/SettingsUI/BUILD index d2036f4f16..aa68c921f7 100644 --- a/submodules/SettingsUI/BUILD +++ b/submodules/SettingsUI/BUILD @@ -91,6 +91,8 @@ swift_library( "//submodules/UIKitRuntimeUtils:UIKitRuntimeUtils", "//submodules/DebugSettingsUI:DebugSettingsUI", "//submodules/WallpaperBackgroundNode:WallpaperBackgroundNode", + "//submodules/WebPBinding:WebPBinding", + "//submodules/Translate:Translate", ], visibility = [ "//visibility:public", diff --git a/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift b/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift index a45d4764e9..12e6053bb8 100644 --- a/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift +++ b/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift @@ -170,20 +170,20 @@ private final class BubbleSettingsControllerNode: ASDisplayNode, UIScrollViewDel messages[replyMessageId] = Message(stableId: 3, stableVersion: 0, id: replyMessageId, globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_1_Text, attributes: [], media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) let message1 = Message(stableId: 4, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 4), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66003, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_3_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []) - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message1], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode)) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message1], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil)) let message2 = Message(stableId: 3, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 3), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66002, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_2_Text, attributes: [ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []) - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message2], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode)) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message2], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil)) let waveformBase64 = "DAAOAAkACQAGAAwADwAMABAADQAPABsAGAALAA0AGAAfABoAHgATABgAGQAYABQADAAVABEAHwANAA0ACQAWABkACQAOAAwACQAfAAAAGQAVAAAAEwATAAAACAAfAAAAHAAAABwAHwAAABcAGQAAABQADgAAABQAHwAAAB8AHwAAAAwADwAAAB8AEwAAABoAFwAAAB8AFAAAAAAAHwAAAAAAHgAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAAAA=" let voiceAttributes: [TelegramMediaFileAttribute] = [.Audio(isVoice: true, duration: 23, title: nil, performer: nil, waveform: Data(base64Encoded: waveformBase64)!)] let voiceMedia = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: 0, attributes: voiceAttributes) let message3 = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66001, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: "", attributes: [], media: [voiceMedia], peers: peers, associatedMessages: messages, associatedMessageIds: []) - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message3], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: FileMediaResourceStatus(mediaStatus: .playbackStatus(.paused), fetchStatus: .Local), tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode)) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message3], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: FileMediaResourceStatus(mediaStatus: .playbackStatus(.paused), fetchStatus: .Local), tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil)) let message4 = Message(stableId: 2, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 2), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66001, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_1_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []) - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message4], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode)) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message4], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil)) let width: CGFloat if case .regular = layout.metrics.widthClass { diff --git a/submodules/SettingsUI/Sources/Data and Storage/StorageUsageController.swift b/submodules/SettingsUI/Sources/Data and Storage/StorageUsageController.swift index b6bd4802fd..893a90de24 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/StorageUsageController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/StorageUsageController.swift @@ -507,7 +507,7 @@ public func storageUsageController(context: AccountContext, cacheUsagePromise: P } else { otherSize = (!otherSize.0, otherSize.1) } - controller?.updateItem(groupIndex: 0, itemIndex: itemIndex, { item in + controller?.updateItem(groupIndex: 0, itemIndex: itemIndex + 1, { item in if let item = item as? ActionSheetCheckboxItem { return ActionSheetCheckboxItem(title: item.title, label: item.label, value: !item.value, action: item.action) } @@ -521,6 +521,8 @@ public func storageUsageController(context: AccountContext, cacheUsagePromise: P var totalSize: Int64 = 0 + items.append(ActionSheetTextItem(title: presentationData.strings.ClearCache_ClearDescription)) + for categoryId in validCategories { if let (_, size) = sizeIndex[categoryId] { let categorySize: Int64 = size diff --git a/submodules/SettingsUI/Sources/Language Selection/LocalizationListController.swift b/submodules/SettingsUI/Sources/Language Selection/LocalizationListController.swift index fc42531e3e..82d7fef26d 100644 --- a/submodules/SettingsUI/Sources/Language Selection/LocalizationListController.swift +++ b/submodules/SettingsUI/Sources/Language Selection/LocalizationListController.swift @@ -92,7 +92,6 @@ public class LocalizationListController: ViewController { self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) self.controllerNode.updatePresentationData(self.presentationData) - let editItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.editPressed)) let doneItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) if self.navigationItem.rightBarButtonItem === self.editItem { @@ -124,6 +123,8 @@ public class LocalizationListController: ViewController { } }, present: { [weak self] c, a in self?.present(c, in: .window(.root), with: a) + }, push: { [weak self] c in + self?.push(c) }) self.controllerNode.listNode.visibleContentOffsetChanged = { [weak self] offset in diff --git a/submodules/SettingsUI/Sources/Language Selection/LocalizationListControllerNode.swift b/submodules/SettingsUI/Sources/Language Selection/LocalizationListControllerNode.swift index 909cb6c91b..c3a778db4b 100644 --- a/submodules/SettingsUI/Sources/Language Selection/LocalizationListControllerNode.swift +++ b/submodules/SettingsUI/Sources/Language Selection/LocalizationListControllerNode.swift @@ -14,14 +14,19 @@ import ShareController import SearchBarNode import SearchUI import UndoUI +import TelegramUIPreferences +import Translate private enum LanguageListSection: ItemListSectionId { + case translate case official case unofficial } private enum LanguageListEntryId: Hashable { case search + case translate(Int) + case localizationTitle case localization(String) } @@ -31,10 +36,26 @@ private enum LanguageListEntryType { } private enum LanguageListEntry: Comparable, Identifiable { + case translateTitle(text: String) + case translate(text: String, value: Bool) + case doNotTranslate(text: String, value: String) + case translateInfo(text: String) + + case localizationTitle(text: String, section: ItemListSectionId) case localization(index: Int, info: LocalizationInfo?, type: LanguageListEntryType, selected: Bool, activity: Bool, revealed: Bool, editing: Bool) var stableId: LanguageListEntryId { switch self { + case .translateTitle: + return .translate(0) + case .translate: + return .translate(1) + case .doNotTranslate: + return .translate(2) + case .translateInfo: + return .translate(3) + case .localizationTitle: + return .localizationTitle case let .localization(index, info, _, _, _, _, _): return .localization(info?.languageCode ?? "\(index)") } @@ -42,8 +63,18 @@ private enum LanguageListEntry: Comparable, Identifiable { private func index() -> Int { switch self { + case .translateTitle: + return 0 + case .translate: + return 1 + case .doNotTranslate: + return 2 + case .translateInfo: + return 3 + case .localizationTitle: + return 1000 case let .localization(index, _, _, _, _, _, _): - return index + return 1001 + index } } @@ -51,8 +82,22 @@ private enum LanguageListEntry: Comparable, Identifiable { return lhs.index() < rhs.index() } - func item(presentationData: PresentationData, searchMode: Bool, openSearch: @escaping () -> Void, selectLocalization: @escaping (LocalizationInfo) -> Void, setItemWithRevealedOptions: @escaping (String?, String?) -> Void, removeItem: @escaping (String) -> Void) -> ListViewItem { + func item(presentationData: PresentationData, searchMode: Bool, openSearch: @escaping () -> Void, toggleShowTranslate: @escaping (Bool) -> Void, openDoNotTranslate: @escaping () -> Void, selectLocalization: @escaping (LocalizationInfo) -> Void, setItemWithRevealedOptions: @escaping (String?, String?) -> Void, removeItem: @escaping (String) -> Void) -> ListViewItem { switch self { + case let .translateTitle(text): + return ItemListSectionHeaderItem(presentationData: ItemListPresentationData(presentationData), text: text, sectionId: LanguageListSection.translate.rawValue) + case let .translate(text, value): + return ItemListSwitchItem(presentationData: ItemListPresentationData(presentationData), title: text, value: value, sectionId: LanguageListSection.translate.rawValue, style: .blocks, updated: { value in + toggleShowTranslate(value) + }) + case let .doNotTranslate(text, value): + return ItemListDisclosureItem(presentationData: ItemListPresentationData(presentationData), title: text, label: value, sectionId: LanguageListSection.translate.rawValue, style: .blocks, action: { + openDoNotTranslate() + }) + case let .translateInfo(text): + return ItemListTextItem(presentationData: ItemListPresentationData(presentationData), text: .plain(text), sectionId: LanguageListSection.translate.rawValue) + case let .localizationTitle(text, section): + return ItemListSectionHeaderItem(presentationData: ItemListPresentationData(presentationData), text: text, sectionId: section) case let .localization(_, info, type, selected, activity, revealed, editing): return LocalizationListItem(presentationData: ItemListPresentationData(presentationData), id: info?.languageCode ?? "", title: info?.title ?? " ", subtitle: info?.localizedTitle ?? " ", checked: selected, activity: activity, loading: info == nil, editing: LocalizationListItemEditing(editable: !selected && !searchMode && !(info?.isOfficial ?? true), editing: editing, revealed: !selected && revealed, reorderable: false), sectionId: type == .official ? LanguageListSection.official.rawValue : LanguageListSection.unofficial.rawValue, alwaysPlain: searchMode, action: { if let info = info { @@ -74,8 +119,8 @@ private func preparedLanguageListSearchContainerTransition(presentationData: Pre let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries, allUpdated: forceUpdate) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, searchMode: true, openSearch: {}, selectLocalization: selectLocalization, setItemWithRevealedOptions: { _, _ in }, removeItem: { _ in }), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, searchMode: true, openSearch: {}, selectLocalization: selectLocalization, setItemWithRevealedOptions: { _, _ in }, removeItem: { _ in }), directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, searchMode: true, openSearch: {}, toggleShowTranslate: { _ in }, openDoNotTranslate: {}, selectLocalization: selectLocalization, setItemWithRevealedOptions: { _, _ in }, removeItem: { _ in }), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, searchMode: true, openSearch: {}, toggleShowTranslate: { _ in }, openDoNotTranslate: {}, selectLocalization: selectLocalization, setItemWithRevealedOptions: { _, _ in }, removeItem: { _ in }), directionHint: nil) } return LocalizationListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, isSearching: isSearching) } @@ -262,12 +307,12 @@ private struct LanguageListNodeTransition { let crossfade: Bool } -private func preparedLanguageListNodeTransition(presentationData: PresentationData, from fromEntries: [LanguageListEntry], to toEntries: [LanguageListEntry], openSearch: @escaping () -> Void, selectLocalization: @escaping (LocalizationInfo) -> Void, setItemWithRevealedOptions: @escaping (String?, String?) -> Void, removeItem: @escaping (String) -> Void, firstTime: Bool, isLoading: Bool, forceUpdate: Bool, animated: Bool, crossfade: Bool) -> LanguageListNodeTransition { +private func preparedLanguageListNodeTransition(presentationData: PresentationData, from fromEntries: [LanguageListEntry], to toEntries: [LanguageListEntry], openSearch: @escaping () -> Void, toggleShowTranslate: @escaping (Bool) -> Void, openDoNotTranslate: @escaping () -> Void, selectLocalization: @escaping (LocalizationInfo) -> Void, setItemWithRevealedOptions: @escaping (String?, String?) -> Void, removeItem: @escaping (String) -> Void, firstTime: Bool, isLoading: Bool, forceUpdate: Bool, animated: Bool, crossfade: Bool) -> LanguageListNodeTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries, allUpdated: forceUpdate) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, searchMode: false, openSearch: openSearch, selectLocalization: selectLocalization, setItemWithRevealedOptions: setItemWithRevealedOptions, removeItem: removeItem), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, searchMode: false, openSearch: openSearch, selectLocalization: selectLocalization, setItemWithRevealedOptions: setItemWithRevealedOptions, removeItem: removeItem), directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, searchMode: false, openSearch: openSearch, toggleShowTranslate: toggleShowTranslate, openDoNotTranslate: openDoNotTranslate, selectLocalization: selectLocalization, setItemWithRevealedOptions: setItemWithRevealedOptions, removeItem: removeItem), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, searchMode: false, openSearch: openSearch, toggleShowTranslate: toggleShowTranslate, openDoNotTranslate: openDoNotTranslate, selectLocalization: selectLocalization, setItemWithRevealedOptions: setItemWithRevealedOptions, removeItem: removeItem), directionHint: nil) } return LanguageListNodeTransition(deletions: deletions, insertions: insertions, updates: updates, firstTime: firstTime, isLoading: isLoading, animated: animated, crossfade: crossfade) } @@ -279,6 +324,7 @@ final class LocalizationListControllerNode: ViewControllerTracingNode { private let requestActivateSearch: () -> Void private let requestDeactivateSearch: () -> Void private let present: (ViewController, Any?) -> Void + private let push: (ViewController) -> Void private var didSetReady = false let _ready = ValuePromise() @@ -304,7 +350,7 @@ final class LocalizationListControllerNode: ViewControllerTracingNode { } } - init(context: AccountContext, presentationData: PresentationData, navigationBar: NavigationBar, requestActivateSearch: @escaping () -> Void, requestDeactivateSearch: @escaping () -> Void, updateCanStartEditing: @escaping (Bool?) -> Void, present: @escaping (ViewController, Any?) -> Void) { + init(context: AccountContext, presentationData: PresentationData, navigationBar: NavigationBar, requestActivateSearch: @escaping () -> Void, requestDeactivateSearch: @escaping () -> Void, updateCanStartEditing: @escaping (Bool?) -> Void, present: @escaping (ViewController, Any?) -> Void, push: @escaping (ViewController) -> Void) { self.context = context self.presentationData = presentationData self.presentationDataValue.set(.single(presentationData)) @@ -312,6 +358,7 @@ final class LocalizationListControllerNode: ViewControllerTracingNode { self.requestActivateSearch = requestActivateSearch self.requestDeactivateSearch = requestDeactivateSearch self.present = present + self.push = push self.listNode = ListView() self.listNode.keepTopItemOverscrollBackground = ListViewKeepTopItemOverscrollBackground(color: presentationData.theme.list.blocksBackgroundColor, direction: true) @@ -373,7 +420,7 @@ final class LocalizationListControllerNode: ViewControllerTracingNode { let preferencesKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.localizationListState])) let previousState = Atomic(value: nil) let previousEntriesHolder = Atomic<([LanguageListEntry], PresentationTheme, PresentationStrings)?>(value: nil) - self.listDisposable = combineLatest(queue: .mainQueue(), context.account.postbox.combinedView(keys: [preferencesKey]), context.sharedContext.accountManager.sharedData(keys: [SharedDataKeys.localizationSettings]), self.presentationDataValue.get(), self.applyingCode.get(), revealedCode.get(), self.isEditing.get()).start(next: { [weak self] view, sharedData, presentationData, applyingCode, revealedCode, isEditing in + self.listDisposable = combineLatest(queue: .mainQueue(), context.account.postbox.combinedView(keys: [preferencesKey]), context.sharedContext.accountManager.sharedData(keys: [SharedDataKeys.localizationSettings, ApplicationSpecificSharedDataKeys.translationSettings]), self.presentationDataValue.get(), self.applyingCode.get(), revealedCode.get(), self.isEditing.get()).start(next: { [weak self] view, sharedData, presentationData, applyingCode, revealedCode, isEditing in guard let strongSelf = self else { return } @@ -385,10 +432,48 @@ final class LocalizationListControllerNode: ViewControllerTracingNode { } var existingIds = Set() + var showTranslate = true + var ignoredLanguages: [String] = [] + if let translationSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.translationSettings]?.get(TranslationSettings.self) { + showTranslate = translationSettings.showTranslate + if let languages = translationSettings.ignoredLanguages { + ignoredLanguages = languages + } else { + if let activeLanguageCode = activeLanguageCode, supportedTranslationLanguages.contains(activeLanguageCode) { + ignoredLanguages = [activeLanguageCode] + } + } + } else { + if let activeLanguageCode = activeLanguageCode, supportedTranslationLanguages.contains(activeLanguageCode) { + ignoredLanguages = [activeLanguageCode] + } + } + let localizationListState = (view.views[preferencesKey] as? PreferencesView)?.values[PreferencesKeys.localizationListState]?.get(LocalizationListState.self) if let localizationListState = localizationListState, !localizationListState.availableOfficialLocalizations.isEmpty { strongSelf.currentListState = localizationListState + if #available(iOS 15.0, *) { + entries.append(.translateTitle(text: presentationData.strings.Localization_TranslateMessages.uppercased())) + entries.append(.translate(text: presentationData.strings.Localization_ShowTranslate, value: showTranslate)) + if showTranslate { + var value = "" + if ignoredLanguages.count > 1 { + value = ignoredLanguages.joined(separator: ", ") + } else if let code = ignoredLanguages.first { + let enLocale = Locale(identifier: "en") + if let title = enLocale.localizedString(forLanguageCode: code) { + value = title + } + } + + entries.append(.doNotTranslate(text: presentationData.strings.Localization_DoNotTranslate, value: value)) + entries.append(.translateInfo(text: ignoredLanguages.count > 1 ? presentationData.strings.Localization_DoNotTranslateManyInfo : presentationData.strings.Localization_DoNotTranslateInfo)) + } else { + entries.append(.translateInfo(text: presentationData.strings.Localization_ShowTranslateInfo)) + } + } + let availableSavedLocalizations = localizationListState.availableSavedLocalizations.filter({ info in !localizationListState.availableOfficialLocalizations.contains(where: { $0.languageCode == info.languageCode }) }) if availableSavedLocalizations.isEmpty { updateCanStartEditing(nil) @@ -396,6 +481,7 @@ final class LocalizationListControllerNode: ViewControllerTracingNode { updateCanStartEditing(isEditing) } if !availableSavedLocalizations.isEmpty { + entries.append(.localizationTitle(text: presentationData.strings.Localization_InterfaceLanguage.uppercased(), section: LanguageListSection.unofficial.rawValue)) for info in availableSavedLocalizations { if existingIds.contains(info.languageCode) { continue @@ -403,6 +489,8 @@ final class LocalizationListControllerNode: ViewControllerTracingNode { existingIds.insert(info.languageCode) entries.append(.localization(index: entries.count, info: info, type: .unofficial, selected: info.languageCode == activeLanguageCode, activity: applyingCode == info.languageCode, revealed: revealedCode == info.languageCode, editing: isEditing)) } + } else { + entries.append(.localizationTitle(text: presentationData.strings.Localization_InterfaceLanguage.uppercased(), section: LanguageListSection.official.rawValue)) } for info in localizationListState.availableOfficialLocalizations { if existingIds.contains(info.languageCode) { @@ -420,7 +508,19 @@ final class LocalizationListControllerNode: ViewControllerTracingNode { let previousState = previousState.swap(localizationListState) let previousEntriesAndPresentationData = previousEntriesHolder.swap((entries, presentationData.theme, presentationData.strings)) - let transition = preparedLanguageListNodeTransition(presentationData: presentationData, from: previousEntriesAndPresentationData?.0 ?? [], to: entries, openSearch: openSearch, selectLocalization: { [weak self] info in self?.selectLocalization(info) }, setItemWithRevealedOptions: setItemWithRevealedOptions, removeItem: removeItem, firstTime: previousEntriesAndPresentationData == nil, isLoading: entries.isEmpty, forceUpdate: previousEntriesAndPresentationData?.1 !== presentationData.theme || previousEntriesAndPresentationData?.2 !== presentationData.strings, animated: (previousEntriesAndPresentationData?.0.count ?? 0) >= entries.count, crossfade: (previousState == nil) != (localizationListState == nil)) + let transition = preparedLanguageListNodeTransition(presentationData: presentationData, from: previousEntriesAndPresentationData?.0 ?? [], to: entries, openSearch: openSearch, toggleShowTranslate: { value in + let _ = updateTranslationSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in + var updated = current.withUpdatedShowTranslate(value) + if !value { + updated = updated.withUpdatedIgnoredLanguages(nil) + } + return updated + }).start() + }, openDoNotTranslate: { [weak self] in + if let strongSelf = self { + strongSelf.push(translationSettingsController(context: strongSelf.context)) + } + }, selectLocalization: { [weak self] info in self?.selectLocalization(info) }, setItemWithRevealedOptions: setItemWithRevealedOptions, removeItem: removeItem, firstTime: previousEntriesAndPresentationData == nil, isLoading: entries.isEmpty, forceUpdate: previousEntriesAndPresentationData?.1 !== presentationData.theme || previousEntriesAndPresentationData?.2 !== presentationData.strings, animated: (previousEntriesAndPresentationData?.0.count ?? 0) >= entries.count, crossfade: (previousState == nil) != (localizationListState == nil)) strongSelf.enqueueTransition(transition) }) self.updatedDisposable = context.engine.localization.synchronizedLocalizationListState().start() diff --git a/submodules/SettingsUI/Sources/Language Selection/TranslatonSettingsController.swift b/submodules/SettingsUI/Sources/Language Selection/TranslatonSettingsController.swift new file mode 100644 index 0000000000..d3df163669 --- /dev/null +++ b/submodules/SettingsUI/Sources/Language Selection/TranslatonSettingsController.swift @@ -0,0 +1,147 @@ +import Foundation +import UIKit +import Display +import SwiftSignalKit +import Postbox +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import ItemListUI +import PresentationDataUtils +import TelegramStringFormatting +import AccountContext +import Translate + +private final class TranslationSettingsControllerArguments { + let context: AccountContext + let updateLanguageSelected: (String, Bool) -> Void + + init(context: AccountContext, updateLanguageSelected: @escaping (String, Bool) -> Void) { + self.context = context + self.updateLanguageSelected = updateLanguageSelected + } +} + +private enum TranslationSettingsControllerSection: Int32 { + case languages +} + +private enum TranslationSettingsControllerEntry: ItemListNodeEntry { + case language(Int32, PresentationTheme, String, String, Bool, String) + + var section: ItemListSectionId { + switch self { + case .language: + return TranslationSettingsControllerSection.languages.rawValue + } + } + + var stableId: Int32 { + switch self { + case let .language(index, _, _, _, _, _): + return index + } + } + + static func ==(lhs: TranslationSettingsControllerEntry, rhs: TranslationSettingsControllerEntry) -> Bool { + switch lhs { + case let .language(lhsIndex, lhsTheme, lhsTitle, lhsSubtitle, lhsValue, lhsCode): + if case let .language(rhsIndex, rhsTheme, rhsTitle, rhsSubtitle, rhsValue, rhsCode) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, lhsValue == rhsValue, lhsCode == rhsCode { + return true + } else { + return false + } + } + } + + static func <(lhs: TranslationSettingsControllerEntry, rhs: TranslationSettingsControllerEntry) -> Bool { + return lhs.stableId < rhs.stableId + } + + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { + let arguments = arguments as! TranslationSettingsControllerArguments + switch self { + case let .language(_, _, title, subtitle, value, code): + return LocalizationListItem(presentationData: presentationData, id: code, title: title, subtitle: subtitle, checked: value, activity: false, loading: false, editing: LocalizationListItemEditing(editable: false, editing: false, revealed: false, reorderable: false), sectionId: self.section, alwaysPlain: false, action: { + arguments.updateLanguageSelected(code, !value) + }, setItemWithRevealedOptions: { _, _ in }, removeItem: { _ in }) + } + } +} + +private func translationSettingsControllerEntries(theme: PresentationTheme, strings: PresentationStrings, settings: TranslationSettings, languages: [(String, String, String)]) -> [TranslationSettingsControllerEntry] { + var entries: [TranslationSettingsControllerEntry] = [] + + var index: Int32 = 0 + var selectedLanguages: Set + if let ignoredLanguages = settings.ignoredLanguages { + selectedLanguages = Set(ignoredLanguages) + } else { + selectedLanguages = Set([strings.baseLanguageCode]) + } + for (code, title, subtitle) in languages { + entries.append(.language(index, theme, title, subtitle, selectedLanguages.contains(code), code)) + index += 1 + } + + return entries +} + +public func translationSettingsController(context: AccountContext) -> ViewController { + let actionsDisposable = DisposableSet() + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let interfaceLanguageCode = presentationData.strings.baseLanguageCode + + let arguments = TranslationSettingsControllerArguments(context: context, updateLanguageSelected: { code, value in + let _ = updateTranslationSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in + var updated = current + var updatedIgnoredLanguages = updated.ignoredLanguages ?? [] + if value { + if current.ignoredLanguages == nil { + updatedIgnoredLanguages.append(interfaceLanguageCode) + } + if !updatedIgnoredLanguages.contains(code) { + updatedIgnoredLanguages.append(code) + } + } else { + updatedIgnoredLanguages.removeAll(where: { $0 == code }) + } + updated = updated.withUpdatedIgnoredLanguages(updatedIgnoredLanguages) + return updated + }).start() + }) + + + let enLocale = Locale(identifier: "en") + var languages: [(String, String, String)] = [] + for code in supportedTranslationLanguages { + if let title = enLocale.localizedString(forLanguageCode: code) { + let languageLocale = Locale(identifier: code) + let subtitle = languageLocale.localizedString(forLanguageCode: code) ?? title + let value = (code, title.capitalized, subtitle.capitalized) + if code == interfaceLanguageCode { + languages.insert(value, at: 0) + } else { + languages.append(value) + } + } + } + + let sharedData = context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.translationSettings]) + let signal = combineLatest(queue: Queue.mainQueue(), context.sharedContext.presentationData, sharedData) + |> map { presentationData, sharedData -> (ItemListControllerState, (ItemListNodeState, Any)) in + let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.translationSettings]?.get(TranslationSettings.self) ?? TranslationSettings.defaultSettings + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.DoNotTranslate_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: translationSettingsControllerEntries(theme: presentationData.theme, strings: presentationData.strings, settings: settings, languages: languages), style: .blocks, animateChanges: false) + + return (controllerState, (listState, arguments)) + } + |> afterDisposed { + actionsDisposable.dispose() + } + + let controller = ItemListController(context: context, state: signal) + controller.alwaysSynchronous = true + return controller +} diff --git a/submodules/SettingsUI/Sources/Privacy and Security/ForwardPrivacyChatPreviewItem.swift b/submodules/SettingsUI/Sources/Privacy and Security/ForwardPrivacyChatPreviewItem.swift index 2bab48b1c3..6746aa4469 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/ForwardPrivacyChatPreviewItem.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/ForwardPrivacyChatPreviewItem.swift @@ -149,7 +149,7 @@ class ForwardPrivacyChatPreviewItemNode: ListViewItemNode { let forwardInfo = MessageForwardInfo(author: item.linkEnabled ? peers[peerId] : nil, source: nil, sourceMessageId: nil, date: 0, authorSignature: item.linkEnabled ? nil : item.peerName, psaType: nil, flags: []) - let messageItem = item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: forwardInfo, author: nil, text: item.strings.Privacy_Forwards_PreviewMessageText, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [])], theme: item.theme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode) + let messageItem = item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: forwardInfo, author: nil, text: item.strings.Privacy_Forwards_PreviewMessageText, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [])], theme: item.theme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: nil) var node: ListViewItemNode? if let current = currentNode { diff --git a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsHeaderItem.swift b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsHeaderItem.swift index bfc12f83f1..ef9d32fa65 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsHeaderItem.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsHeaderItem.swift @@ -150,7 +150,7 @@ class RecentSessionsHeaderItemNode: ListViewItemNode { strongSelf.buttonNode.title = item.context.sharedContext.currentPresentationData.with { $0 }.strings.AuthSessions_LinkDesktopDevice if let _ = updatedTheme { - strongSelf.buttonNode.icon = generateTintedImage(image: UIImage(bundleImageName: "Settings/QrButtonIcon"), color: .white) + strongSelf.buttonNode.icon = UIImage(bundleImageName: "Settings/QrButtonIcon") strongSelf.buttonNode.updateTheme(SolidRoundedButtonTheme(theme: item.theme)) } diff --git a/submodules/SettingsUI/Sources/Reactions/QuickReactionSetupController.swift b/submodules/SettingsUI/Sources/Reactions/QuickReactionSetupController.swift new file mode 100644 index 0000000000..808f1d3101 --- /dev/null +++ b/submodules/SettingsUI/Sources/Reactions/QuickReactionSetupController.swift @@ -0,0 +1,350 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import Postbox +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import ItemListUI +import PresentationDataUtils +import AccountContext +import WebPBinding + +private final class QuickReactionSetupControllerArguments { + let context: AccountContext + let selectItem: (String) -> Void + + init( + context: AccountContext, + selectItem: @escaping (String) -> Void + ) { + self.context = context + self.selectItem = selectItem + } +} + +private enum QuickReactionSetupControllerSection: Int32 { + case demo + case items +} + +private enum QuickReactionSetupControllerEntry: ItemListNodeEntry { + enum StableId: Hashable { + case demoHeader + case demoMessage + case demoDescription + case itemsHeader + case item(String) + } + + case demoHeader(String) + case demoMessage(wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, bubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, availableReactions: AvailableReactions?, reaction: String?) + case demoDescription(String) + case itemsHeader(String) + case item(index: Int, value: String, image: UIImage?, text: String, isSelected: Bool) + + var section: ItemListSectionId { + switch self { + case .demoHeader, .demoMessage, .demoDescription: + return QuickReactionSetupControllerSection.demo.rawValue + case .itemsHeader, .item: + return QuickReactionSetupControllerSection.items.rawValue + } + } + + var stableId: StableId { + switch self { + case .demoHeader: + return .demoHeader + case .demoMessage: + return .demoMessage + case .demoDescription: + return .demoDescription + case .itemsHeader: + return .itemsHeader + case let .item(_, value, _, _, _): + return .item(value) + } + } + + var sortId: Int { + switch self { + case .demoHeader: + return 0 + case .demoMessage: + return 1 + case .demoDescription: + return 2 + case .itemsHeader: + return 3 + case let .item(index, _, _, _, _): + return 100 + index + } + } + + static func ==(lhs: QuickReactionSetupControllerEntry, rhs: QuickReactionSetupControllerEntry) -> Bool { + switch lhs { + case let .demoHeader(text): + if case .demoHeader(text) = rhs { + return true + } else { + return false + } + case let .demoMessage(lhsWallpaper, lhsFontSize, lhsBubbleCorners, lhsDateTimeFormat, lhsNameDisplayOrder, lhsAvailableReactions, lhsReaction): + if case let .demoMessage(rhsWallpaper, rhsFontSize, rhsBubbleCorners, rhsDateTimeFormat, rhsNameDisplayOrder, rhsAvailableReactions, rhsReaction) = rhs, lhsWallpaper == rhsWallpaper, lhsFontSize == rhsFontSize, lhsBubbleCorners == rhsBubbleCorners, lhsDateTimeFormat == rhsDateTimeFormat, lhsNameDisplayOrder == rhsNameDisplayOrder, lhsAvailableReactions == rhsAvailableReactions, lhsReaction == rhsReaction { + return true + } else { + return false + } + case let .demoDescription(text): + if case .demoDescription(text) = rhs { + return true + } else { + return false + } + case let .itemsHeader(text): + if case .itemsHeader(text) = rhs { + return true + } else { + return false + } + case let .item(index, value, file, text, isEnabled): + if case .item(index, value, file, text, isEnabled) = rhs { + return true + } else { + return false + } + } + } + + static func <(lhs: QuickReactionSetupControllerEntry, rhs: QuickReactionSetupControllerEntry) -> Bool { + return lhs.sortId < rhs.sortId + } + + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { + let arguments = arguments as! QuickReactionSetupControllerArguments + switch self { + case let .demoHeader(text): + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) + case let .demoMessage(wallpaper, fontSize, chatBubbleCorners, dateTimeFormat, nameDisplayOrder, availableReactions, reaction): + return ReactionChatPreviewItem( + context: arguments.context, + theme: presentationData.theme, + strings: presentationData.strings, + sectionId: self.section, + fontSize: fontSize, + chatBubbleCorners: chatBubbleCorners, + wallpaper: wallpaper, + dateTimeFormat: dateTimeFormat, + nameDisplayOrder: nameDisplayOrder, + availableReactions: availableReactions, + reaction: reaction + ) + case let .demoDescription(text): + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) + case let .itemsHeader(text): + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) + case let .item(_, value, image, text, isSelected): + return ItemListCheckboxItem( + presentationData: presentationData, + icon: image, + iconSize: image?.size.aspectFitted(CGSize(width: 30.0, height: 30.0)), + title: text, + style: .right, + color: .accent, + checked: isSelected, + zeroSeparatorInsets: false, + sectionId: self.section, + action: { + arguments.selectItem(value) + } + ) + } + } +} + +private struct QuickReactionSetupControllerState: Equatable { +} + +private func quickReactionSetupControllerEntries( + presentationData: PresentationData, + availableReactions: AvailableReactions?, + images: [String: UIImage], + reactionSettings: ReactionSettings +) -> [QuickReactionSetupControllerEntry] { + var entries: [QuickReactionSetupControllerEntry] = [] + + if let availableReactions = availableReactions { + //TODO:localize + entries.append(.demoHeader("DOUBLE TAP ON MESSAGE TO REACT")) + entries.append(.demoMessage( + wallpaper: presentationData.chatWallpaper, + fontSize: presentationData.chatFontSize, + bubbleCorners: presentationData.chatBubbleCorners, + dateTimeFormat: presentationData.dateTimeFormat, + nameDisplayOrder: presentationData.nameDisplayOrder, + availableReactions: availableReactions, + reaction: reactionSettings.quickReaction + )) + entries.append(.demoDescription("You can double tap on message for a quick reaction.")) + + entries.append(.itemsHeader("QUICK REACTION")) + var index = 0 + for availableReaction in availableReactions.reactions { + entries.append(.item( + index: index, + value: availableReaction.value, + image: images[availableReaction.value], + text: availableReaction.title, + isSelected: reactionSettings.quickReaction == availableReaction.value + )) + index += 1 + } + } + + return entries +} + +public func quickReactionSetupController( + context: AccountContext, + updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil +) -> ViewController { + let statePromise = ValuePromise(QuickReactionSetupControllerState(), ignoreRepeated: true) + let stateValue = Atomic(value: QuickReactionSetupControllerState()) + let updateState: ((QuickReactionSetupControllerState) -> QuickReactionSetupControllerState) -> Void = { f in + statePromise.set(stateValue.modify { f($0) }) + } + + var dismissImpl: (() -> Void)? + let _ = dismissImpl + + let _ = updateState + + let actionsDisposable = DisposableSet() + + let arguments = QuickReactionSetupControllerArguments( + context: context, + selectItem: { reaction in + let _ = updateReactionSettingsInteractively(postbox: context.account.postbox, { settings in + var settings = settings + settings.quickReaction = reaction + return settings + }).start() + } + ) + + let settings = context.account.postbox.preferencesView(keys: [PreferencesKeys.reactionSettings]) + |> map { preferencesView -> ReactionSettings in + let reactionSettings: ReactionSettings + if let entry = preferencesView.values[PreferencesKeys.reactionSettings], let value = entry.get(ReactionSettings.self) { + reactionSettings = value + } else { + reactionSettings = .default + } + return reactionSettings + } + + let images: Signal<[String: UIImage], NoError> = context.engine.stickers.availableReactions() + |> mapToSignal { availableReactions -> Signal<[String: UIImage], NoError> in + var signals: [Signal<(String, UIImage?), NoError>] = [] + + if let availableReactions = availableReactions { + for availableReaction in availableReactions.reactions { + let signal: Signal<(String, UIImage?), NoError> = context.account.postbox.mediaBox.resourceData(availableReaction.staticIcon.resource) + |> distinctUntilChanged(isEqual: { lhs, rhs in + return lhs.complete == rhs.complete + }) + |> map { data -> (String, UIImage?) in + guard data.complete else { + return (availableReaction.value, nil) + } + guard let dataValue = try? Data(contentsOf: URL(fileURLWithPath: data.path)) else { + return (availableReaction.value, nil) + } + guard let image = WebP.convert(fromWebP: dataValue) else { + return (availableReaction.value, nil) + } + return (availableReaction.value, image) + } + signals.append(signal) + } + } + + return combineLatest(queue: .mainQueue(), signals) + |> map { values -> [String: UIImage] in + var dict: [String: UIImage] = [:] + for (key, image) in values { + if let image = image { + dict[key] = image + } + } + return dict + } + } + + let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData + let signal = combineLatest(queue: .mainQueue(), + presentationData, + statePromise.get(), + context.engine.stickers.availableReactions(), + settings, + images + ) + |> deliverOnMainQueue + |> map { presentationData, _, availableReactions, settings, images -> (ItemListControllerState, (ItemListNodeState, Any)) in + //TODO:localize + let title: String = "Quick Reaction" + + let entries = quickReactionSetupControllerEntries( + presentationData: presentationData, + availableReactions: availableReactions, + images: images, + reactionSettings: settings + ) + + let controllerState = ItemListControllerState( + presentationData: ItemListPresentationData(presentationData), + title: .text(title), + leftNavigationButton: nil, + rightNavigationButton: nil, + backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), + animateChanges: false + ) + let listState = ItemListNodeState( + presentationData: ItemListPresentationData(presentationData), + entries: entries, + style: .blocks, + animateChanges: true + ) + + return (controllerState, (listState, arguments)) + } + |> afterDisposed { + actionsDisposable.dispose() + } + + let controller = ItemListController(context: context, state: signal) + + controller.didScrollWithOffset = { [weak controller] offset, transition, _ in + guard let controller = controller else { + return + } + controller.forEachItemNode { itemNode in + if let itemNode = itemNode as? ReactionChatPreviewItemNode { + itemNode.standaloneReactionAnimation?.addRelativeContentOffset(CGPoint(x: 0.0, y: offset), transition: transition) + } + } + } + + dismissImpl = { [weak controller] in + guard let controller = controller else { + return + } + controller.dismiss() + } + + return controller +} + diff --git a/submodules/SettingsUI/Sources/Reactions/ReactionChatPreviewItem.swift b/submodules/SettingsUI/Sources/Reactions/ReactionChatPreviewItem.swift new file mode 100644 index 0000000000..906d2f538b --- /dev/null +++ b/submodules/SettingsUI/Sources/Reactions/ReactionChatPreviewItem.swift @@ -0,0 +1,349 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramCore +import Postbox +import TelegramPresentationData +import TelegramUIPreferences +import ItemListUI +import PresentationDataUtils +import AccountContext +import WallpaperBackgroundNode +import AvatarNode +import ReactionSelectionNode + +class ReactionChatPreviewItem: ListViewItem, ItemListItem { + let context: AccountContext + let theme: PresentationTheme + let strings: PresentationStrings + let sectionId: ItemListSectionId + let fontSize: PresentationFontSize + let chatBubbleCorners: PresentationChatBubbleCorners + let wallpaper: TelegramWallpaper + let dateTimeFormat: PresentationDateTimeFormat + let nameDisplayOrder: PresentationPersonNameOrder + let availableReactions: AvailableReactions? + let reaction: String? + + init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, sectionId: ItemListSectionId, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, wallpaper: TelegramWallpaper, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, availableReactions: AvailableReactions?, reaction: String?) { + self.context = context + self.theme = theme + self.strings = strings + self.sectionId = sectionId + self.fontSize = fontSize + self.chatBubbleCorners = chatBubbleCorners + self.wallpaper = wallpaper + self.dateTimeFormat = dateTimeFormat + self.nameDisplayOrder = nameDisplayOrder + self.availableReactions = availableReactions + self.reaction = reaction + } + + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + async { + let node = ReactionChatPreviewItemNode() + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + Queue.mainQueue().async { + completion(node, { + return (nil, { _ in apply() }) + }) + } + } + } + + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + Queue.mainQueue().async { + if let nodeValue = node() as? ReactionChatPreviewItemNode { + let makeLayout = nodeValue.asyncLayout() + + async { + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + Queue.mainQueue().async { + completion(layout, { _ in + apply() + }) + } + } + } + } + } +} + +class ReactionChatPreviewItemNode: ListViewItemNode { + private var backgroundNode: WallpaperBackgroundNode? + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + private let maskNode: ASImageNode + private let avatarNode: ASImageNode + + private let containerNode: ASDisplayNode + + private var messageNode: ListViewItemNode? + + private var item: ReactionChatPreviewItem? + private(set) weak var standaloneReactionAnimation: StandaloneReactionAnimation? + + init() { + self.topStripeNode = ASDisplayNode() + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.isLayerBacked = true + + self.maskNode = ASImageNode() + + self.avatarNode = ASImageNode() + + self.containerNode = ASDisplayNode() + self.containerNode.subnodeTransform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) + + super.init(layerBacked: false, dynamicBounce: false) + + self.clipsToBounds = true + + self.addSubnode(self.avatarNode) + self.addSubnode(self.containerNode) + } + + override func didLoad() { + super.didLoad() + + let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:))) + recognizer.tapActionAtPoint = { _ in + return .waitForDoubleTap + } + self.view.addGestureRecognizer(recognizer) + } + + @objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { + switch recognizer.state { + case .ended: + if let (gesture, _) = recognizer.lastRecognizedGestureAndLocation { + switch gesture { + case .doubleTap: + if let item = self.item, let updatedReaction = item.reaction, let availableReactions = item.availableReactions, let messageNode = self.messageNode as? ChatMessageItemNodeProtocol { + if let targetView = messageNode.targetReactionView(value: updatedReaction) { + for reaction in availableReactions.reactions { + if reaction.value == updatedReaction { + if let standaloneReactionAnimation = self.standaloneReactionAnimation { + standaloneReactionAnimation.cancel() + standaloneReactionAnimation.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak standaloneReactionAnimation] _ in + standaloneReactionAnimation?.removeFromSupernode() + }) + self.standaloneReactionAnimation = nil + } + + if let supernode = self.supernode { + let standaloneReactionAnimation = StandaloneReactionAnimation() + self.standaloneReactionAnimation = standaloneReactionAnimation + + supernode.addSubnode(standaloneReactionAnimation) + standaloneReactionAnimation.frame = supernode.bounds + standaloneReactionAnimation.animateReactionSelection( + context: item.context, theme: item.theme, reaction: ReactionContextItem( + reaction: ReactionContextItem.Reaction(rawValue: reaction.value), + stillAnimation: reaction.selectAnimation, + listAnimation: reaction.activateAnimation, + applicationAnimation: reaction.effectAnimation + ), + targetView: targetView, + hideNode: true, + completion: { [weak standaloneReactionAnimation] in + standaloneReactionAnimation?.removeFromSupernode() + } + ) + } + + break + } + } + } + } + default: + break + } + } + default: + break + } + } + + func asyncLayout() -> (_ item: ReactionChatPreviewItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + let currentNode = self.messageNode + + var currentBackgroundNode = self.backgroundNode + + return { item, params, neighbors in + if currentBackgroundNode == nil { + currentBackgroundNode = createWallpaperBackgroundNode(context: item.context, forChatDisplay: false) + } + currentBackgroundNode?.update(wallpaper: item.wallpaper) + currentBackgroundNode?.updateBubbleTheme(bubbleTheme: item.theme, bubbleCorners: item.chatBubbleCorners) + + let insets: UIEdgeInsets + let separatorHeight = UIScreenPixel + + let chatPeerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(1)) + let userPeerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(2)) + + var peers = SimpleDictionary() + let messages = SimpleDictionary() + + peers[chatPeerId] = TelegramGroup(id: chatPeerId, title: "Chat", photo: [], participantCount: 1, role: .member, membership: .Member, flags: [], defaultBannedRights: nil, migrationReference: nil, creationDate: 1, version: 1) + //TODO:localize + peers[userPeerId] = TelegramUser(id: userPeerId, accessHash: nil, firstName: "Dino", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) + + //TODO:localize + let messageText = "I hope you're enjoying your day as much as I am." + + var attributes: [MessageAttribute] = [] + if let reaction = item.reaction { + attributes.append(ReactionsMessageAttribute(reactions: [MessageReaction(value: reaction, count: 1, isSelected: true)], recentPeers: [])) + } + + let messageItem = item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: chatPeerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[userPeerId], text: messageText, attributes: attributes, media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [])], theme: item.theme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: item.availableReactions) + + var node: ListViewItemNode? + if let current = currentNode { + node = current + messageItem.updateNode(async: { $0() }, node: { return current }, params: params, previousItem: nil, nextItem: nil, animation: .None, completion: { (layout, apply) in + let nodeFrame = CGRect(origin: current.frame.origin, size: CGSize(width: layout.size.width, height: layout.size.height)) + + current.contentSize = layout.contentSize + current.insets = layout.insets + current.frame = nodeFrame + + apply(ListViewItemApply(isOnScreen: true)) + }) + } else { + messageItem.nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: false, previousItem: nil, nextItem: nil, completion: { messageNode, apply in + node = messageNode + apply().1(ListViewItemApply(isOnScreen: true)) + }) + node?.isUserInteractionEnabled = false + } + + var contentSize = CGSize(width: params.width, height: 8.0 + 8.0) + if let node = node { + contentSize.height += node.frame.size.height + } + insets = itemListNeighborsGroupedInsets(neighbors, params) + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + let layoutSize = layout.size + + return (layout, { [weak self] in + if let strongSelf = self { + if let previousItem = strongSelf.item, previousItem.reaction != item.reaction { + if let standaloneReactionAnimation = strongSelf.standaloneReactionAnimation { + standaloneReactionAnimation.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak standaloneReactionAnimation] _ in + standaloneReactionAnimation?.removeFromSupernode() + }) + strongSelf.standaloneReactionAnimation = nil + } + } + + strongSelf.item = item + + strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: contentSize) + + var topOffset: CGFloat = 8.0 + if let node = node { + strongSelf.messageNode = node + if node.supernode == nil { + strongSelf.containerNode.addSubnode(node) + } + node.updateFrame(CGRect(origin: CGPoint(x: 0.0, y: topOffset), size: node.frame.size), within: layout.contentSize) + + let avatarSize: CGFloat = 34.0 + if strongSelf.avatarNode.image == nil { + strongSelf.avatarNode.image = generateImage(CGSize(width: avatarSize, height: avatarSize), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.addEllipse(in: CGRect(origin: CGPoint(), size: size)) + context.clip() + UIGraphicsPushContext(context) + if let image = UIImage(bundleImageName: "Avatar/SampleAvatar1") { + image.draw(in: CGRect(origin: CGPoint(), size: size)) + } + UIGraphicsPopContext() + }) + } + + topOffset += node.frame.size.height + + strongSelf.avatarNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 7.0, y: topOffset - avatarSize), size: CGSize(width: avatarSize, height: avatarSize)) + } + + strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + + if let currentBackgroundNode = currentBackgroundNode, strongSelf.backgroundNode !== currentBackgroundNode { + strongSelf.backgroundNode = currentBackgroundNode + strongSelf.insertSubnode(currentBackgroundNode, at: 0) + } + + if strongSelf.topStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) + } + if strongSelf.maskNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.maskNode, at: 3) + } + let hasCorners = itemListHasRoundedBlockLayout(params) + var hasTopCorners = false + var hasBottomCorners = false + switch neighbors.top { + case .sameSection(false): + strongSelf.topStripeNode.isHidden = true + default: + hasTopCorners = true + strongSelf.topStripeNode.isHidden = hasCorners + } + let bottomStripeInset: CGFloat + let bottomStripeOffset: CGFloat + switch neighbors.bottom { + case .sameSection(false): + bottomStripeInset = 0.0 + bottomStripeOffset = -separatorHeight + default: + bottomStripeInset = 0.0 + bottomStripeOffset = 0.0 + hasBottomCorners = true + strongSelf.bottomStripeNode.isHidden = hasCorners + } + + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + + let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + + if let backgroundNode = strongSelf.backgroundNode { + backgroundNode.frame = backgroundFrame.insetBy(dx: 0.0, dy: -100.0) + backgroundNode.update(wallpaper: item.wallpaper) + backgroundNode.updateBubbleTheme(bubbleTheme: item.theme, bubbleCorners: item.chatBubbleCorners) + backgroundNode.updateLayout(size: backgroundNode.bounds.size, transition: .immediate) + } + + strongSelf.maskNode.frame = backgroundFrame.insetBy(dx: params.leftInset, dy: 0.0) + strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) + } + }) + } + } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + } +} diff --git a/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift b/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift index cfaf3f9c61..ccfaa4c005 100644 --- a/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift +++ b/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift @@ -15,6 +15,7 @@ import ItemListStickerPackItem import ItemListPeerActionItem import UndoUI import ShareController +import WebPBinding private final class InstalledStickerPacksControllerArguments { let account: Account @@ -24,6 +25,7 @@ private final class InstalledStickerPacksControllerArguments { let removePack: (ArchivedStickerPackItem) -> Void let openStickersBot: () -> Void let openMasks: () -> Void + let openQuickReaction: () -> Void let openFeatured: () -> Void let openArchived: ([ArchivedStickerPackItem]?) -> Void let openSuggestOptions: () -> Void @@ -32,13 +34,14 @@ private final class InstalledStickerPacksControllerArguments { let expandTrendingPacks: () -> Void let addPack: (StickerPackCollectionInfo) -> Void - init(account: Account, openStickerPack: @escaping (StickerPackCollectionInfo) -> Void, setPackIdWithRevealedOptions: @escaping (ItemCollectionId?, ItemCollectionId?) -> Void, removePack: @escaping (ArchivedStickerPackItem) -> Void, openStickersBot: @escaping () -> Void, openMasks: @escaping () -> Void, openFeatured: @escaping () -> Void, openArchived: @escaping ([ArchivedStickerPackItem]?) -> Void, openSuggestOptions: @escaping () -> Void, toggleAnimatedStickers: @escaping (Bool) -> Void, togglePackSelected: @escaping (ItemCollectionId) -> Void, expandTrendingPacks: @escaping () -> Void, addPack: @escaping (StickerPackCollectionInfo) -> Void) { + init(account: Account, openStickerPack: @escaping (StickerPackCollectionInfo) -> Void, setPackIdWithRevealedOptions: @escaping (ItemCollectionId?, ItemCollectionId?) -> Void, removePack: @escaping (ArchivedStickerPackItem) -> Void, openStickersBot: @escaping () -> Void, openMasks: @escaping () -> Void, openQuickReaction: @escaping () -> Void, openFeatured: @escaping () -> Void, openArchived: @escaping ([ArchivedStickerPackItem]?) -> Void, openSuggestOptions: @escaping () -> Void, toggleAnimatedStickers: @escaping (Bool) -> Void, togglePackSelected: @escaping (ItemCollectionId) -> Void, expandTrendingPacks: @escaping () -> Void, addPack: @escaping (StickerPackCollectionInfo) -> Void) { self.account = account self.openStickerPack = openStickerPack self.setPackIdWithRevealedOptions = setPackIdWithRevealedOptions self.removePack = removePack self.openStickersBot = openStickersBot self.openMasks = openMasks + self.openQuickReaction = openQuickReaction self.openFeatured = openFeatured self.openArchived = openArchived self.openSuggestOptions = openSuggestOptions @@ -79,6 +82,7 @@ private indirect enum InstalledStickerPacksEntry: ItemListNodeEntry { case trending(PresentationTheme, String, Int32) case archived(PresentationTheme, String, Int32, [ArchivedStickerPackItem]?) case masks(PresentationTheme, String) + case quickReaction(String, UIImage?) case animatedStickers(PresentationTheme, String, Bool) case animatedStickersInfo(PresentationTheme, String) case trendingPacksTitle(PresentationTheme, String) @@ -90,7 +94,7 @@ private indirect enum InstalledStickerPacksEntry: ItemListNodeEntry { var section: ItemListSectionId { switch self { - case .suggestOptions, .trending, .masks, .archived, .animatedStickers, .animatedStickersInfo: + case .suggestOptions, .trending, .masks, .quickReaction, .archived, .animatedStickers, .animatedStickersInfo: return InstalledStickerPacksSection.service.rawValue case .trendingPacksTitle, .trendingPack, .trendingExpand: return InstalledStickerPacksSection.trending.rawValue @@ -109,22 +113,24 @@ private indirect enum InstalledStickerPacksEntry: ItemListNodeEntry { return .index(2) case .masks: return .index(3) - case .animatedStickers: + case .quickReaction: return .index(4) - case .animatedStickersInfo: + case .animatedStickers: return .index(5) - case .trendingPacksTitle: + case .animatedStickersInfo: return .index(6) + case .trendingPacksTitle: + return .index(7) case let .trendingPack(_, _, _, info, _, _, _, _, _): return .trendingPack(info.id) case .trendingExpand: - return .index(7) - case .packsTitle: return .index(8) + case .packsTitle: + return .index(9) case let .pack(_, _, _, info, _, _, _, _, _, _): return .pack(info.id) case .packsInfo: - return .index(9) + return .index(10) } } @@ -148,6 +154,12 @@ private indirect enum InstalledStickerPacksEntry: ItemListNodeEntry { } else { return false } + case let .quickReaction(lhsText, lhsImage): + if case let .quickReaction(rhsText, rhsImage) = rhs, lhsText == rhsText, lhsImage === rhsImage { + return true + } else { + return false + } case let .archived(lhsTheme, lhsText, lhsCount, _): if case let .archived(rhsTheme, rhsText, rhsCount, _) = rhs, lhsTheme === rhsTheme, lhsCount == rhsCount, lhsText == rhsText { return true @@ -292,23 +304,30 @@ private indirect enum InstalledStickerPacksEntry: ItemListNodeEntry { default: return true } + case .quickReaction: + switch rhs { + case .suggestOptions, .trending, .archived, .masks, .quickReaction: + return false + default: + return true + } case .animatedStickers: switch rhs { - case .suggestOptions, .trending, .archived, .masks, .animatedStickers: + case .suggestOptions, .trending, .archived, .masks, .quickReaction, .animatedStickers: return false default: return true } case .animatedStickersInfo: switch rhs { - case .suggestOptions, .trending, .archived, .masks, .animatedStickers, .animatedStickersInfo: + case .suggestOptions, .trending, .archived, .masks, .quickReaction, .animatedStickers, .animatedStickersInfo: return false default: return true } case .trendingPacksTitle: switch rhs { - case .suggestOptions, .trending, .masks, .archived, .animatedStickers, .animatedStickersInfo, .trendingPacksTitle: + case .suggestOptions, .trending, .masks, .quickReaction, .archived, .animatedStickers, .animatedStickersInfo, .trendingPacksTitle: return false default: return true @@ -324,14 +343,14 @@ private indirect enum InstalledStickerPacksEntry: ItemListNodeEntry { } case .trendingExpand: switch rhs { - case .suggestOptions, .trending, .masks, .archived, .animatedStickers, .animatedStickersInfo, .trendingPacksTitle, .trendingPack, .trendingExpand: + case .suggestOptions, .trending, .masks, .quickReaction, .archived, .animatedStickers, .animatedStickersInfo, .trendingPacksTitle, .trendingPack, .trendingExpand: return false default: return true } case .packsTitle: switch rhs { - case .suggestOptions, .trending, .masks, .archived, .animatedStickers, .animatedStickersInfo, .trendingPacksTitle, .trendingPack, .trendingExpand, .packsTitle: + case .suggestOptions, .trending, .masks, .quickReaction, .archived, .animatedStickers, .animatedStickersInfo, .trendingPacksTitle, .trendingPack, .trendingExpand, .packsTitle: return false default: return true @@ -370,6 +389,16 @@ private indirect enum InstalledStickerPacksEntry: ItemListNodeEntry { return ItemListDisclosureItem(presentationData: presentationData, title: text, label: "", sectionId: self.section, style: .blocks, action: { arguments.openMasks() }) + case let .quickReaction(title, image): + let labelStyle: ItemListDisclosureLabelStyle + if let image = image { + labelStyle = .image(image: image, size: image.size.aspectFitted(CGSize(width: 30.0, height: 30.0))) + } else { + labelStyle = .text + } + return ItemListDisclosureItem(presentationData: presentationData, title: title, label: "", labelStyle: labelStyle, sectionId: self.section, style: .blocks, action: { + arguments.openQuickReaction() + }) case let .archived(_, text, count, archived): return ItemListDisclosureItem(presentationData: presentationData, title: text, label: count == 0 ? "" : "\(count)", sectionId: self.section, style: .blocks, action: { arguments.openArchived(archived) @@ -480,7 +509,7 @@ private func namespaceForMode(_ mode: InstalledStickerPacksControllerMode) -> It private let maxTrendingPacksDisplayedLimit: Int32 = 3 -private func installedStickerPacksControllerEntries(presentationData: PresentationData, state: InstalledStickerPacksControllerState, mode: InstalledStickerPacksControllerMode, view: CombinedView, temporaryPackOrder: [ItemCollectionId]?, featured: [FeaturedStickerPackItem], archived: [ArchivedStickerPackItem]?, stickerSettings: StickerSettings) -> [InstalledStickerPacksEntry] { +private func installedStickerPacksControllerEntries(presentationData: PresentationData, state: InstalledStickerPacksControllerState, mode: InstalledStickerPacksControllerMode, view: CombinedView, temporaryPackOrder: [ItemCollectionId]?, featured: [FeaturedStickerPackItem], archived: [ArchivedStickerPackItem]?, stickerSettings: StickerSettings, quickReactionImage: UIImage?) -> [InstalledStickerPacksEntry] { var entries: [InstalledStickerPacksEntry] = [] var installedPacks = Set() @@ -514,6 +543,9 @@ private func installedStickerPacksControllerEntries(presentationData: Presentati } entries.append(.masks(presentationData.theme, presentationData.strings.MaskStickerSettings_Title)) + //TODO:localize + entries.append(.quickReaction("Quick Reaction", quickReactionImage)) + entries.append(.animatedStickers(presentationData.theme, presentationData.strings.StickerPacksSettings_AnimatedStickers, stickerSettings.loopAnimatedStickers)) entries.append(.animatedStickersInfo(presentationData.theme, presentationData.strings.StickerPacksSettings_AnimatedStickersInfo)) @@ -699,6 +731,10 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta })) }, openMasks: { pushControllerImpl?(installedStickerPacksController(context: context, mode: .masks, archivedPacks: archivedPacks, updatedPacks: { _ in})) + }, openQuickReaction: { + pushControllerImpl?(quickReactionSetupController( + context: context + )) }, openFeatured: { pushControllerImpl?(featuredStickerPacksController(context: context)) }, openArchived: { archived in @@ -783,14 +819,63 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta let temporaryPackOrder = Promise<[ItemCollectionId]?>(nil) let featured = Promise<[FeaturedStickerPackItem]>() + let quickReactionImage: Signal switch mode { case .general, .modal: featured.set(context.account.viewTracker.featuredStickerPacks()) archivedPromise.set(.single(archivedPacks) |> then(context.engine.stickers.archivedStickerPacks() |> map(Optional.init))) + quickReactionImage = combineLatest( + context.engine.stickers.availableReactions(), + context.account.postbox.preferencesView(keys: [PreferencesKeys.reactionSettings]) + ) + |> map { availableReactions, preferencesView -> TelegramMediaFile? in + guard let availableReactions = availableReactions else { + return nil + } + + let reactionSettings: ReactionSettings + if let entry = preferencesView.values[PreferencesKeys.reactionSettings], let value = entry.get(ReactionSettings.self) { + reactionSettings = value + } else { + reactionSettings = .default + } + + for reaction in availableReactions.reactions { + if reaction.value == reactionSettings.quickReaction { + return reaction.staticIcon + } + } + + return nil + } + |> distinctUntilChanged + |> mapToSignal { file -> Signal in + guard let file = file else { + return .single(nil) + } + + return context.account.postbox.mediaBox.resourceData(file.resource) + |> distinctUntilChanged(isEqual: { lhs, rhs in + return lhs.complete == rhs.complete + }) + |> map { data -> UIImage? in + guard data.complete else { + return nil + } + guard let dataValue = try? Data(contentsOf: URL(fileURLWithPath: data.path)) else { + return nil + } + guard let image = WebP.convert(fromWebP: dataValue) else { + return nil + } + return image + } + } case .masks: featured.set(.single([])) archivedPromise.set(.single(nil) |> then(context.engine.stickers.archivedStickerPacks(namespace: .masks) |> map(Optional.init))) + quickReactionImage = .single(nil) } var previousPackCount: Int? @@ -799,9 +884,11 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta stickerPacks.get(), temporaryPackOrder.get(), combineLatest(queue: .mainQueue(), featured.get(), archivedPromise.get()), - context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.stickerSettings])) + context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.stickerSettings]), + quickReactionImage + ) |> deliverOnMainQueue - |> map { presentationData, state, view, temporaryPackOrder, featuredAndArchived, sharedData -> (ItemListControllerState, (ItemListNodeState, Any)) in + |> map { presentationData, state, view, temporaryPackOrder, featuredAndArchived, sharedData, quickReactionImage -> (ItemListControllerState, (ItemListNodeState, Any)) in var stickerSettings = StickerSettings.defaultSettings if let value = sharedData.entries[ApplicationSpecificSharedDataKeys.stickerSettings]?.get(StickerSettings.self) { stickerSettings = value @@ -944,7 +1031,7 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: installedStickerPacksControllerEntries(presentationData: presentationData, state: state, mode: mode, view: view, temporaryPackOrder: temporaryPackOrder, featured: featuredAndArchived.0, archived: featuredAndArchived.1, stickerSettings: stickerSettings), style: .blocks, ensureVisibleItemTag: focusOnItemTag, toolbarItem: toolbarItem, animateChanges: previous != nil && packCount != nil && (previous! != 0 && previous! >= packCount! - 10)) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: installedStickerPacksControllerEntries(presentationData: presentationData, state: state, mode: mode, view: view, temporaryPackOrder: temporaryPackOrder, featured: featuredAndArchived.0, archived: featuredAndArchived.1, stickerSettings: stickerSettings, quickReactionImage: quickReactionImage), style: .blocks, ensureVisibleItemTag: focusOnItemTag, toolbarItem: toolbarItem, animateChanges: previous != nil && packCount != nil && (previous! != 0 && previous! >= packCount! - 10)) return (controllerState, (listState, arguments)) } |> afterDisposed { diff --git a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift index f3db9bc29d..f87e239b95 100644 --- a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift +++ b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift @@ -413,20 +413,20 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, UIScrollView messages[replyMessageId] = Message(stableId: 3, stableVersion: 0, id: replyMessageId, globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_1_Text, attributes: [], media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) let message1 = Message(stableId: 4, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 4), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66003, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_3_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []) - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message1], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode)) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message1], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil)) let message2 = Message(stableId: 3, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 3), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66002, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_2_Text, attributes: [ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []) - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message2], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode)) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message2], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil)) let waveformBase64 = "DAAOAAkACQAGAAwADwAMABAADQAPABsAGAALAA0AGAAfABoAHgATABgAGQAYABQADAAVABEAHwANAA0ACQAWABkACQAOAAwACQAfAAAAGQAVAAAAEwATAAAACAAfAAAAHAAAABwAHwAAABcAGQAAABQADgAAABQAHwAAAB8AHwAAAAwADwAAAB8AEwAAABoAFwAAAB8AFAAAAAAAHwAAAAAAHgAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAAAA=" let voiceAttributes: [TelegramMediaFileAttribute] = [.Audio(isVoice: true, duration: 23, title: nil, performer: nil, waveform: Data(base64Encoded: waveformBase64)!)] let voiceMedia = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: 0, attributes: voiceAttributes) let message3 = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66001, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: "", attributes: [], media: [voiceMedia], peers: peers, associatedMessages: messages, associatedMessageIds: []) - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message3], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: FileMediaResourceStatus(mediaStatus: .playbackStatus(.paused), fetchStatus: .Local), tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode)) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message3], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: FileMediaResourceStatus(mediaStatus: .playbackStatus(.paused), fetchStatus: .Local), tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil)) let message4 = Message(stableId: 2, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 2), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66001, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_1_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []) - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message4], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode)) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message4], theme: self.presentationData.theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.chatBackgroundNode, availableReactions: nil)) let width: CGFloat if case .regular = layout.metrics.widthClass { diff --git a/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift index ddac2cdc46..c651a07d05 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeAccentColorControllerNode.swift @@ -1043,7 +1043,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate return state }, animated: true) }, clickThroughMessage: { - }, backgroundNode: self.backgroundNode) + }, backgroundNode: self.backgroundNode, availableReactions: nil) return item } diff --git a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift index 850fad8014..e1a3c2e2b6 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift @@ -593,7 +593,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { sampleMessages.append(message8) items = sampleMessages.reversed().map { message in - self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message], theme: self.previewTheme, strings: self.presentationData.strings, wallpaper: self.wallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: !message.media.isEmpty ? FileMediaResourceStatus(mediaStatus: .playbackStatus(.paused), fetchStatus: .Local) : nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.wallpaperNode) + self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message], theme: self.previewTheme, strings: self.presentationData.strings, wallpaper: self.wallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: !message.media.isEmpty ? FileMediaResourceStatus(mediaStatus: .playbackStatus(.paused), fetchStatus: .Local) : nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.wallpaperNode, availableReactions: nil) } let width: CGFloat diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsChatPreviewItem.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsChatPreviewItem.swift index bbbfedb957..478ae7ec0d 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsChatPreviewItem.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsChatPreviewItem.swift @@ -160,7 +160,7 @@ class ThemeSettingsChatPreviewItemNode: ListViewItemNode { } let message = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: messageItem.outgoing ? otherPeerId : peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: messageItem.outgoing ? [] : [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: messageItem.outgoing ? TelegramUser(id: otherPeerId, accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) : nil, text: messageItem.text, attributes: messageItem.reply != nil ? [ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil)] : [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []) - items.append(item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [message], theme: item.componentTheme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode)) + items.append(item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [message], theme: item.componentTheme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: nil)) } var nodes: [ListViewItemNode] = [] diff --git a/submodules/SettingsUI/Sources/Themes/WallpaperGalleryItem.swift b/submodules/SettingsUI/Sources/Themes/WallpaperGalleryItem.swift index 68f9a32b3b..38a1c2c6fd 100644 --- a/submodules/SettingsUI/Sources/Themes/WallpaperGalleryItem.swift +++ b/submodules/SettingsUI/Sources/Themes/WallpaperGalleryItem.swift @@ -1103,10 +1103,10 @@ final class WallpaperGalleryItemNode: GalleryItemNode { let theme = self.presentationData.theme.withUpdated(preview: true) let message1 = Message(stableId: 2, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 2), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66001, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[otherPeerId], text: bottomMessageText, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []) - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message1], theme: theme, strings: self.presentationData.strings, wallpaper: currentWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.nativeNode)) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message1], theme: theme, strings: self.presentationData.strings, wallpaper: currentWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.nativeNode, availableReactions: nil)) let message2 = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: topMessageText, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: []) - items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message2], theme: theme, strings: self.presentationData.strings, wallpaper: currentWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.nativeNode)) + items.append(self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message2], theme: theme, strings: self.presentationData.strings, wallpaper: currentWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.nativeNode, availableReactions: nil)) let params = ListViewItemLayoutParams(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, availableHeight: layout.size.height) if let messageNodes = self.messageNodes { diff --git a/submodules/ShareController/Sources/ShareController.swift b/submodules/ShareController/Sources/ShareController.swift index 5e12d63613..ac4db6b397 100644 --- a/submodules/ShareController/Sources/ShareController.swift +++ b/submodules/ShareController/Sources/ShareController.swift @@ -1016,7 +1016,7 @@ final class MessageStoryRenderer { let theme = self.presentationData.theme.withUpdated(preview: true) let headerItem = self.context.sharedContext.makeChatMessageDateHeaderItem(context: self.context, timestamp: self.messages.first?.timestamp ?? 0, theme: theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder) - let items: [ListViewItem] = [self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: self.messages, theme: theme, strings: self.presentationData.strings, wallpaper: self.presentationData.theme.chat.defaultWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: nil)] + let items: [ListViewItem] = [self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: self.messages, theme: theme, strings: self.presentationData.strings, wallpaper: self.presentationData.theme.chat.defaultWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: nil, availableReactions: nil)] let inset: CGFloat = 16.0 let width = layout.size.width - inset * 2.0 diff --git a/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift b/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift index 6f0b95f0f4..eaca96a86b 100644 --- a/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift +++ b/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift @@ -69,7 +69,7 @@ public final class SolidRoundedButtonNode: ASDisplayNode { public var icon: UIImage? { didSet { - self.iconNode.image = generateTintedImage(image: self.iconNode.image, color: self.theme.foregroundColor) + self.iconNode.image = generateTintedImage(image: self.icon, color: self.theme.foregroundColor) } } diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index e011f5df7c..77f5e6bf04 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -758,7 +758,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1625153079] = { return Api.InputWebFileLocation.parse_inputWebFileGeoPointLocation($0) } dict[-1275374751] = { return Api.EmojiLanguage.parse_emojiLanguage($0) } dict[1601666510] = { return Api.MessageFwdHeader.parse_messageFwdHeader($0) } - dict[-783162982] = { return Api.SponsoredMessage.parse_sponsoredMessage($0) } + dict[981691896] = { return Api.SponsoredMessage.parse_sponsoredMessage($0) } dict[-1012849566] = { return Api.BaseTheme.parse_baseThemeClassic($0) } dict[-69724536] = { return Api.BaseTheme.parse_baseThemeDay($0) } dict[-1212997976] = { return Api.BaseTheme.parse_baseThemeNight($0) } @@ -791,7 +791,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[594408994] = { return Api.EmojiKeyword.parse_emojiKeywordDeleted($0) } dict[-290921362] = { return Api.upload.CdnFile.parse_cdnFileReuploadNeeded($0) } dict[-1449145777] = { return Api.upload.CdnFile.parse_cdnFile($0) } - dict[1679961905] = { return Api.AvailableReaction.parse_availableReaction($0) } + dict[1424116867] = { return Api.AvailableReaction.parse_availableReaction($0) } dict[415997816] = { return Api.help.InviteText.parse_inviteText($0) } dict[-1826077446] = { return Api.MessageUserReaction.parse_messageUserReaction($0) } dict[1984755728] = { return Api.BotInlineMessage.parse_botInlineMessageMediaAuto($0) } diff --git a/submodules/TelegramApi/Sources/Api2.swift b/submodules/TelegramApi/Sources/Api2.swift index 6dafefb779..ca5bf77426 100644 --- a/submodules/TelegramApi/Sources/Api2.swift +++ b/submodules/TelegramApi/Sources/Api2.swift @@ -19708,17 +19708,19 @@ public extension Api { } public enum SponsoredMessage: TypeConstructorDescription { - case sponsoredMessage(flags: Int32, randomId: Buffer, fromId: Api.Peer, channelPost: Int32?, startParam: String?, message: String, entities: [Api.MessageEntity]?) + case sponsoredMessage(flags: Int32, randomId: Buffer, fromId: Api.Peer?, chatInvite: Api.ChatInvite?, chatInviteHash: String?, channelPost: Int32?, startParam: String?, message: String, entities: [Api.MessageEntity]?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .sponsoredMessage(let flags, let randomId, let fromId, let channelPost, let startParam, let message, let entities): + case .sponsoredMessage(let flags, let randomId, let fromId, let chatInvite, let chatInviteHash, let channelPost, let startParam, let message, let entities): if boxed { - buffer.appendInt32(-783162982) + buffer.appendInt32(981691896) } serializeInt32(flags, buffer: buffer, boxed: false) serializeBytes(randomId, buffer: buffer, boxed: false) - fromId.serialize(buffer, true) + if Int(flags) & Int(1 << 3) != 0 {fromId!.serialize(buffer, true)} + if Int(flags) & Int(1 << 4) != 0 {chatInvite!.serialize(buffer, true)} + if Int(flags) & Int(1 << 4) != 0 {serializeString(chatInviteHash!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 2) != 0 {serializeInt32(channelPost!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 0) != 0 {serializeString(startParam!, buffer: buffer, boxed: false)} serializeString(message, buffer: buffer, boxed: false) @@ -19733,8 +19735,8 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .sponsoredMessage(let flags, let randomId, let fromId, let channelPost, let startParam, let message, let entities): - return ("sponsoredMessage", [("flags", flags), ("randomId", randomId), ("fromId", fromId), ("channelPost", channelPost), ("startParam", startParam), ("message", message), ("entities", entities)]) + case .sponsoredMessage(let flags, let randomId, let fromId, let chatInvite, let chatInviteHash, let channelPost, let startParam, let message, let entities): + return ("sponsoredMessage", [("flags", flags), ("randomId", randomId), ("fromId", fromId), ("chatInvite", chatInvite), ("chatInviteHash", chatInviteHash), ("channelPost", channelPost), ("startParam", startParam), ("message", message), ("entities", entities)]) } } @@ -19744,28 +19746,36 @@ public extension Api { var _2: Buffer? _2 = parseBytes(reader) var _3: Api.Peer? - if let signature = reader.readInt32() { + if Int(_1!) & Int(1 << 3) != 0 {if let signature = reader.readInt32() { _3 = Api.parse(reader, signature: signature) as? Api.Peer - } - var _4: Int32? - if Int(_1!) & Int(1 << 2) != 0 {_4 = reader.readInt32() } + } } + var _4: Api.ChatInvite? + if Int(_1!) & Int(1 << 4) != 0 {if let signature = reader.readInt32() { + _4 = Api.parse(reader, signature: signature) as? Api.ChatInvite + } } var _5: String? - if Int(_1!) & Int(1 << 0) != 0 {_5 = parseString(reader) } - var _6: String? - _6 = parseString(reader) - var _7: [Api.MessageEntity]? + if Int(_1!) & Int(1 << 4) != 0 {_5 = parseString(reader) } + var _6: Int32? + if Int(_1!) & Int(1 << 2) != 0 {_6 = reader.readInt32() } + var _7: String? + if Int(_1!) & Int(1 << 0) != 0 {_7 = parseString(reader) } + var _8: String? + _8 = parseString(reader) + var _9: [Api.MessageEntity]? if Int(_1!) & Int(1 << 1) != 0 {if let _ = reader.readInt32() { - _7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) + _9 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) } } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil - let _c6 = _6 != nil - let _c7 = (Int(_1!) & Int(1 << 1) == 0) || _7 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { - return Api.SponsoredMessage.sponsoredMessage(flags: _1!, randomId: _2!, fromId: _3!, channelPost: _4, startParam: _5, message: _6!, entities: _7) + let _c3 = (Int(_1!) & Int(1 << 3) == 0) || _3 != nil + let _c4 = (Int(_1!) & Int(1 << 4) == 0) || _4 != nil + let _c5 = (Int(_1!) & Int(1 << 4) == 0) || _5 != nil + let _c6 = (Int(_1!) & Int(1 << 2) == 0) || _6 != nil + let _c7 = (Int(_1!) & Int(1 << 0) == 0) || _7 != nil + let _c8 = _8 != nil + let _c9 = (Int(_1!) & Int(1 << 1) == 0) || _9 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { + return Api.SponsoredMessage.sponsoredMessage(flags: _1!, randomId: _2!, fromId: _3, chatInvite: _4, chatInviteHash: _5, channelPost: _6, startParam: _7, message: _8!, entities: _9) } else { return nil @@ -20190,17 +20200,18 @@ public extension Api { } public enum AvailableReaction: TypeConstructorDescription { - case availableReaction(reaction: String, title: String, staticIcon: Api.Document, selectAnimation: Api.Document, activateAnimation: Api.Document, effectAnimation: Api.Document) + case availableReaction(reaction: String, title: String, staticIcon: Api.Document, appearAnimation: Api.Document, selectAnimation: Api.Document, activateAnimation: Api.Document, effectAnimation: Api.Document) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .availableReaction(let reaction, let title, let staticIcon, let selectAnimation, let activateAnimation, let effectAnimation): + case .availableReaction(let reaction, let title, let staticIcon, let appearAnimation, let selectAnimation, let activateAnimation, let effectAnimation): if boxed { - buffer.appendInt32(1679961905) + buffer.appendInt32(1424116867) } serializeString(reaction, buffer: buffer, boxed: false) serializeString(title, buffer: buffer, boxed: false) staticIcon.serialize(buffer, true) + appearAnimation.serialize(buffer, true) selectAnimation.serialize(buffer, true) activateAnimation.serialize(buffer, true) effectAnimation.serialize(buffer, true) @@ -20210,8 +20221,8 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .availableReaction(let reaction, let title, let staticIcon, let selectAnimation, let activateAnimation, let effectAnimation): - return ("availableReaction", [("reaction", reaction), ("title", title), ("staticIcon", staticIcon), ("selectAnimation", selectAnimation), ("activateAnimation", activateAnimation), ("effectAnimation", effectAnimation)]) + case .availableReaction(let reaction, let title, let staticIcon, let appearAnimation, let selectAnimation, let activateAnimation, let effectAnimation): + return ("availableReaction", [("reaction", reaction), ("title", title), ("staticIcon", staticIcon), ("appearAnimation", appearAnimation), ("selectAnimation", selectAnimation), ("activateAnimation", activateAnimation), ("effectAnimation", effectAnimation)]) } } @@ -20236,14 +20247,19 @@ public extension Api { if let signature = reader.readInt32() { _6 = Api.parse(reader, signature: signature) as? Api.Document } + var _7: Api.Document? + if let signature = reader.readInt32() { + _7 = Api.parse(reader, signature: signature) as? Api.Document + } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil let _c6 = _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.AvailableReaction.availableReaction(reaction: _1!, title: _2!, staticIcon: _3!, selectAnimation: _4!, activateAnimation: _5!, effectAnimation: _6!) + let _c7 = _7 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { + return Api.AvailableReaction.availableReaction(reaction: _1!, title: _2!, staticIcon: _3!, appearAnimation: _4!, selectAnimation: _5!, activateAnimation: _6!, effectAnimation: _7!) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api4.swift b/submodules/TelegramApi/Sources/Api4.swift index 15a5792e21..8bf9c9f2df 100644 --- a/submodules/TelegramApi/Sources/Api4.swift +++ b/submodules/TelegramApi/Sources/Api4.swift @@ -4591,6 +4591,20 @@ public extension Api { return result }) } + + public static func setDefaultReaction(emoji: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1474910882) + serializeString(emoji, buffer: buffer, boxed: false) + return (FunctionDescription(name: "messages.setDefaultReaction", parameters: [("emoji", emoji)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } } public struct channels { public static func readHistory(channel: Api.InputChannel, maxId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatInfoContextItem.swift b/submodules/TelegramCallsUI/Sources/VoiceChatInfoContextItem.swift index 0e7e115a85..1354709d5b 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatInfoContextItem.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatInfoContextItem.swift @@ -97,4 +97,14 @@ private final class VoiceChatInfoContextItemNode: ASDisplayNode, ContextMenuCust let textFont = Font.regular(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0) self.textNode.attributedText = NSAttributedString(string: self.textNode.attributedText?.string ?? "", font: textFont, textColor: presentationData.theme.contextMenu.primaryColor) } + + func canBeHighlighted() -> Bool { + return false + } + + func updateIsHighlighted(isHighlighted: Bool) { + } + + func performAction() { + } } diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatRecordingContextItem.swift b/submodules/TelegramCallsUI/Sources/VoiceChatRecordingContextItem.swift index 9843fbcda4..3d90146154 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatRecordingContextItem.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatRecordingContextItem.swift @@ -266,6 +266,14 @@ private final class VoiceChatRecordingContextItemNode: ASDisplayNode, ContextMen self.performAction() } + func canBeHighlighted() -> Bool { + return true + } + + func updateIsHighlighted(isHighlighted: Bool) { + self.setIsHighlighted(isHighlighted) + } + func performAction() { guard let controller = self.getController() else { return diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatShareScreenContextItem.swift b/submodules/TelegramCallsUI/Sources/VoiceChatShareScreenContextItem.swift index 67531e289e..d3227b10b1 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatShareScreenContextItem.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatShareScreenContextItem.swift @@ -168,6 +168,14 @@ private final class VoiceChatShareScreenContextItemNode: ASDisplayNode, ContextM self.performAction() } + func canBeHighlighted() -> Bool { + return true + } + + func updateIsHighlighted(isHighlighted: Bool) { + self.setIsHighlighted(isHighlighted) + } + func performAction() { guard let controller = self.getController() else { return diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatVolumeContextItem.swift b/submodules/TelegramCallsUI/Sources/VoiceChatVolumeContextItem.swift index 448fcbca10..69aa80c1b8 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatVolumeContextItem.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatVolumeContextItem.swift @@ -194,4 +194,14 @@ private final class VoiceChatVolumeContextItemNode: ASDisplayNode, ContextMenuCu self.value = max(self.minValue, min(2.0, location.x / self.bounds.width * 2.0)) self.valueChanged(self.value, true) } + + func canBeHighlighted() -> Bool { + return false + } + + func updateIsHighlighted(isHighlighted: Bool) { + } + + func performAction() { + } } diff --git a/submodules/TelegramCore/Sources/Account/Account.swift b/submodules/TelegramCore/Sources/Account/Account.swift index 8012a5d5ae..f0b6e79b90 100644 --- a/submodules/TelegramCore/Sources/Account/Account.swift +++ b/submodules/TelegramCore/Sources/Account/Account.swift @@ -173,7 +173,8 @@ public func accountWithId(accountManager: AccountManager { } } - fileprivate init?(queue: Queue, basePath: String, isTemporary: Bool, isReadOnly: Bool, useCaches: Bool, temporarySessionId: Int64) { + fileprivate init?(queue: Queue, basePath: String, isTemporary: Bool, isReadOnly: Bool, useCaches: Bool, removeDatabaseOnError: Bool, temporarySessionId: Int64) { let startTime = CFAbsoluteTimeGetCurrent() self.queue = queue @@ -82,11 +82,11 @@ final class AccountManagerImpl { self.loginTokensPath = "\(basePath)/login-tokens" self.temporarySessionId = temporarySessionId let _ = try? FileManager.default.createDirectory(atPath: basePath, withIntermediateDirectories: true, attributes: nil) - guard let guardValueBox = SqliteValueBox(basePath: basePath + "/guard_db", queue: queue, isTemporary: isTemporary, isReadOnly: false, useCaches: useCaches, encryptionParameters: nil, upgradeProgress: { _ in }) else { + guard let guardValueBox = SqliteValueBox(basePath: basePath + "/guard_db", queue: queue, isTemporary: isTemporary, isReadOnly: false, useCaches: useCaches, removeDatabaseOnError: removeDatabaseOnError, encryptionParameters: nil, upgradeProgress: { _ in }) else { return nil } self.guardValueBox = guardValueBox - guard let valueBox = SqliteValueBox(basePath: basePath + "/db", queue: queue, isTemporary: isTemporary, isReadOnly: isReadOnly, useCaches: useCaches, encryptionParameters: nil, upgradeProgress: { _ in }) else { + guard let valueBox = SqliteValueBox(basePath: basePath + "/db", queue: queue, isTemporary: isTemporary, isReadOnly: isReadOnly, useCaches: useCaches, removeDatabaseOnError: removeDatabaseOnError, encryptionParameters: nil, upgradeProgress: { _ in }) else { return nil } self.valueBox = valueBox @@ -510,7 +510,7 @@ public final class AccountManager { return AccountManagerImpl.getCurrentRecords(basePath: basePath) } - public init(basePath: String, isTemporary: Bool, isReadOnly: Bool, useCaches: Bool) { + public init(basePath: String, isTemporary: Bool, isReadOnly: Bool, useCaches: Bool, removeDatabaseOnError: Bool) { self.queue = sharedQueue self.basePath = basePath var temporarySessionId: Int64 = 0 @@ -518,7 +518,7 @@ public final class AccountManager { self.temporarySessionId = temporarySessionId let queue = self.queue self.impl = QueueLocalObject(queue: queue, generate: { - if let value = AccountManagerImpl(queue: queue, basePath: basePath, isTemporary: isTemporary, isReadOnly: isReadOnly, useCaches: useCaches, temporarySessionId: temporarySessionId) { + if let value = AccountManagerImpl(queue: queue, basePath: basePath, isTemporary: isTemporary, isReadOnly: isReadOnly, useCaches: useCaches, removeDatabaseOnError: removeDatabaseOnError, temporarySessionId: temporarySessionId) { return value } else { preconditionFailure() diff --git a/submodules/TelegramCore/Sources/Settings/ReactionSettings.swift b/submodules/TelegramCore/Sources/Settings/ReactionSettings.swift new file mode 100644 index 0000000000..974e2c8a14 --- /dev/null +++ b/submodules/TelegramCore/Sources/Settings/ReactionSettings.swift @@ -0,0 +1,23 @@ +import Postbox +import SwiftSignalKit + +public struct ReactionSettings: Equatable, Codable { + public static var `default` = ReactionSettings(quickReaction: "👍") + + public var quickReaction: String + + public init(quickReaction: String) { + self.quickReaction = quickReaction + } +} + +public func updateReactionSettingsInteractively(postbox: Postbox, _ f: @escaping (ReactionSettings) -> ReactionSettings) -> Signal { + return postbox.transaction { transaction -> Void in + transaction.updatePreferencesEntry(key: PreferencesKeys.reactionSettings, { current in + let previous = current?.get(ReactionSettings.self) ?? ReactionSettings.default + let updated = f(previous) + return PreferencesEntry(updated) + }) + } + |> ignoreValues +} diff --git a/submodules/TelegramCore/Sources/State/AccountViewTracker.swift b/submodules/TelegramCore/Sources/State/AccountViewTracker.swift index 3d44d900cd..494caf4978 100644 --- a/submodules/TelegramCore/Sources/State/AccountViewTracker.swift +++ b/submodules/TelegramCore/Sources/State/AccountViewTracker.swift @@ -812,13 +812,13 @@ public final class AccountViewTracker { } } - public func updateReactionsForMessageIds(messageIds: Set) { + public func updateReactionsForMessageIds(messageIds: Set, force: Bool = false) { self.queue.async { var addedMessageIds: [MessageId] = [] let timestamp = Int32(CFAbsoluteTimeGetCurrent()) for messageId in messageIds { let messageTimestamp = self.updatedReactionsMessageIdsAndTimestamps[messageId] - if messageTimestamp == nil || messageTimestamp! < timestamp - 5 * 60 { + if messageTimestamp == nil || messageTimestamp! < timestamp - 1 * 20 || force { self.updatedReactionsMessageIdsAndTimestamps[messageId] = timestamp addedMessageIds.append(messageId) } diff --git a/submodules/TelegramCore/Sources/State/AvailableReactions.swift b/submodules/TelegramCore/Sources/State/AvailableReactions.swift index c041c41fd2..86d24e38de 100644 --- a/submodules/TelegramCore/Sources/State/AvailableReactions.swift +++ b/submodules/TelegramCore/Sources/State/AvailableReactions.swift @@ -9,6 +9,7 @@ public final class AvailableReactions: Equatable, Codable { case value case title case staticIcon + case appearAnimation case selectAnimation case activateAnimation case effectAnimation @@ -17,6 +18,7 @@ public final class AvailableReactions: Equatable, Codable { public let value: String public let title: String public let staticIcon: TelegramMediaFile + public let appearAnimation: TelegramMediaFile public let selectAnimation: TelegramMediaFile public let activateAnimation: TelegramMediaFile public let effectAnimation: TelegramMediaFile @@ -25,6 +27,7 @@ public final class AvailableReactions: Equatable, Codable { value: String, title: String, staticIcon: TelegramMediaFile, + appearAnimation: TelegramMediaFile, selectAnimation: TelegramMediaFile, activateAnimation: TelegramMediaFile, effectAnimation: TelegramMediaFile @@ -32,6 +35,7 @@ public final class AvailableReactions: Equatable, Codable { self.value = value self.title = title self.staticIcon = staticIcon + self.appearAnimation = appearAnimation self.selectAnimation = selectAnimation self.activateAnimation = activateAnimation self.effectAnimation = effectAnimation @@ -47,6 +51,9 @@ public final class AvailableReactions: Equatable, Codable { if lhs.staticIcon != rhs.staticIcon { return false } + if lhs.appearAnimation != rhs.appearAnimation { + return false + } if lhs.selectAnimation != rhs.selectAnimation { return false } @@ -68,6 +75,9 @@ public final class AvailableReactions: Equatable, Codable { let staticIconData = try container.decode(AdaptedPostboxDecoder.RawObjectData.self, forKey: .staticIcon) self.staticIcon = TelegramMediaFile(decoder: PostboxDecoder(buffer: MemoryBuffer(data: staticIconData.data))) + let appearAnimationData = try container.decode(AdaptedPostboxDecoder.RawObjectData.self, forKey: .appearAnimation) + self.appearAnimation = TelegramMediaFile(decoder: PostboxDecoder(buffer: MemoryBuffer(data: appearAnimationData.data))) + let selectAnimationData = try container.decode(AdaptedPostboxDecoder.RawObjectData.self, forKey: .selectAnimation) self.selectAnimation = TelegramMediaFile(decoder: PostboxDecoder(buffer: MemoryBuffer(data: selectAnimationData.data))) @@ -85,6 +95,7 @@ public final class AvailableReactions: Equatable, Codable { try container.encode(self.title, forKey: .title) try container.encode(PostboxEncoder().encodeObjectToRawData(self.staticIcon), forKey: .staticIcon) + try container.encode(PostboxEncoder().encodeObjectToRawData(self.appearAnimation), forKey: .appearAnimation) try container.encode(PostboxEncoder().encodeObjectToRawData(self.selectAnimation), forKey: .selectAnimation) try container.encode(PostboxEncoder().encodeObjectToRawData(self.activateAnimation), forKey: .activateAnimation) try container.encode(PostboxEncoder().encodeObjectToRawData(self.effectAnimation), forKey: .effectAnimation) @@ -135,10 +146,13 @@ public final class AvailableReactions: Equatable, Codable { private extension AvailableReactions.Reaction { convenience init?(apiReaction: Api.AvailableReaction) { switch apiReaction { - case let .availableReaction(reaction, title, staticIcon, selectAnimation, activateAnimation, effectAnimation): + case let .availableReaction(reaction, title, staticIcon, appearAnimation, selectAnimation, activateAnimation, effectAnimation): guard let staticIconFile = telegramMediaFileFromApiDocument(staticIcon) else { return nil } + guard let appearAnimationFile = telegramMediaFileFromApiDocument(appearAnimation) else { + return nil + } guard let selectAnimationFile = telegramMediaFileFromApiDocument(selectAnimation) else { return nil } @@ -152,6 +166,7 @@ private extension AvailableReactions.Reaction { value: reaction, title: title, staticIcon: staticIconFile, + appearAnimation: appearAnimationFile, selectAnimation: selectAnimationFile, activateAnimation: activateAnimationFile, effectAnimation: effectAnimationFile diff --git a/submodules/TelegramCore/Sources/State/MessageReactions.swift b/submodules/TelegramCore/Sources/State/MessageReactions.swift index 7168e62eb0..85b5c2f41f 100644 --- a/submodules/TelegramCore/Sources/State/MessageReactions.swift +++ b/submodules/TelegramCore/Sources/State/MessageReactions.swift @@ -242,7 +242,7 @@ public extension EngineMessageReactionListContext.State { self.init( totalCount: totalCount, items: [], - canLoadMore: true + canLoadMore: totalCount != 0 ) } } @@ -250,11 +250,11 @@ public extension EngineMessageReactionListContext.State { public final class EngineMessageReactionListContext { public final class Item: Equatable { public let peer: EnginePeer - public let reaction: String + public let reaction: String? - init( + public init( peer: EnginePeer, - reaction: String + reaction: String? ) { self.peer = peer self.reaction = reaction @@ -317,7 +317,9 @@ public final class EngineMessageReactionListContext { let initialState = EngineMessageReactionListContext.State(message: message, reaction: reaction) self.state = InternalState(totalCount: initialState.totalCount, items: initialState.items, canLoadMore: true, nextOffset: nil) - self.loadMore() + if initialState.canLoadMore { + self.loadMore() + } } deinit { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index 0dca08190e..bc1b8723cb 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -222,6 +222,7 @@ private enum PreferencesKeyValues: Int32 { case peersNearby = 21 case chatListFiltersFeaturedState = 22 case secretChatSettings = 23 + case reactionSettings = 24 } public func applicationSpecificPreferencesKey(_ value: Int32) -> ValueBoxKey { @@ -344,6 +345,12 @@ public struct PreferencesKeys { key.setInt32(0, value: PreferencesKeyValues.chatListFiltersFeaturedState.rawValue) return key }() + + public static let reactionSettings: ValueBoxKey = { + let key = ValueBoxKey(length: 4) + key.setInt32(0, value: PreferencesKeyValues.reactionSettings.rawValue) + return key + }() } private enum SharedDataKeyValues: Int32 { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_StandaloneAccountTransaction.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_StandaloneAccountTransaction.swift index ea3f266d0e..18ace49fe7 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_StandaloneAccountTransaction.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_StandaloneAccountTransaction.swift @@ -97,9 +97,9 @@ public enum AccountTransactionError { case couldNotOpen } -public func accountTransaction(rootPath: String, id: AccountRecordId, encryptionParameters: ValueBoxEncryptionParameters, isReadOnly: Bool, useCopy: Bool = false, useCaches: Bool = true, transaction: @escaping (Postbox, Transaction) -> T) -> Signal { +public func accountTransaction(rootPath: String, id: AccountRecordId, encryptionParameters: ValueBoxEncryptionParameters, isReadOnly: Bool, useCopy: Bool = false, useCaches: Bool = true, removeDatabaseOnError: Bool = true, transaction: @escaping (Postbox, Transaction) -> T) -> Signal { let path = "\(rootPath)/\(accountRecordIdPathName(id))" - let postbox = openPostbox(basePath: path + "/postbox", seedConfiguration: telegramPostboxSeedConfiguration, encryptionParameters: encryptionParameters, timestampForAbsoluteTimeBasedOperations: Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970), isTemporary: true, isReadOnly: isReadOnly, useCopy: useCopy, useCaches: useCaches) + let postbox = openPostbox(basePath: path + "/postbox", seedConfiguration: telegramPostboxSeedConfiguration, encryptionParameters: encryptionParameters, timestampForAbsoluteTimeBasedOperations: Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970), isTemporary: true, isReadOnly: isReadOnly, useCopy: useCopy, useCaches: useCaches, removeDatabaseOnError: removeDatabaseOnError) return postbox |> castError(AccountTransactionError.self) |> mapToSignal { value -> Signal in diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift index 037d7abd0a..f2f8013690 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift @@ -10,16 +10,38 @@ private class AdMessagesHistoryContextImpl { case text case textEntities case media - case authorId + case target case messageId case startParam } + + enum Target: Equatable, Codable { + enum DecodingError: Error { + case generic + } + + enum CodingKeys: String, CodingKey { + case peer + } + + case peer(PeerId) + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if let peer = try container.decodeIfPresent(Int64.self, forKey: .peer) { + self = .peer(PeerId(peer)) + } else { + throw DecodingError.generic + } + } + } public let opaqueId: Data public let text: String public let textEntities: [MessageTextEntity] public let media: [Media] - public let authorId: PeerId + public let target: Target public let messageId: MessageId? public let startParam: String? @@ -28,7 +50,7 @@ private class AdMessagesHistoryContextImpl { text: String, textEntities: [MessageTextEntity], media: [Media], - authorId: PeerId, + target: Target, messageId: MessageId?, startParam: String? ) { @@ -36,7 +58,7 @@ private class AdMessagesHistoryContextImpl { self.text = text self.textEntities = textEntities self.media = media - self.authorId = authorId + self.target = target self.messageId = messageId self.startParam = startParam } @@ -54,7 +76,7 @@ private class AdMessagesHistoryContextImpl { return PostboxDecoder(buffer: MemoryBuffer(data: data)).decodeRootObject() as? Media } - self.authorId = try container.decode(PeerId.self, forKey: .authorId) + self.target = try container.decode(Target.self, forKey: .target) self.messageId = try container.decodeIfPresent(MessageId.self, forKey: .messageId) self.startParam = try container.decodeIfPresent(String.self, forKey: .startParam) } @@ -73,7 +95,7 @@ private class AdMessagesHistoryContextImpl { } try container.encode(mediaData, forKey: .media) - try container.encode(self.authorId, forKey: .authorId) + try container.encode(self.target, forKey: .target) try container.encodeIfPresent(self.messageId, forKey: .messageId) try container.encodeIfPresent(self.startParam, forKey: .startParam) } @@ -96,7 +118,7 @@ private class AdMessagesHistoryContextImpl { return false } } - if lhs.authorId != rhs.authorId { + if lhs.target != rhs.target { return false } if lhs.messageId != rhs.messageId { @@ -108,7 +130,7 @@ private class AdMessagesHistoryContextImpl { return true } - func toMessage(peerId: PeerId, transaction: Transaction) -> Message { + func toMessage(peerId: PeerId, transaction: Transaction) -> Message? { var attributes: [MessageAttribute] = [] attributes.append(AdMessageAttribute(opaqueId: self.opaqueId, startParam: self.startParam, messageId: self.messageId)) @@ -122,9 +144,18 @@ private class AdMessagesHistoryContextImpl { if let peer = transaction.getPeer(peerId) { messagePeers[peer.id] = peer } - if let peer = transaction.getPeer(self.authorId) { - messagePeers[peer.id] = peer + + let author: Peer + switch self.target { + case let .peer(peerId): + if let peer = transaction.getPeer(peerId) { + author = peer + } else { + return nil + } } + + messagePeers[author.id] = author return Message( stableId: 0, @@ -140,7 +171,7 @@ private class AdMessagesHistoryContextImpl { globalTags: [], localTags: [], forwardInfo: nil, - author: transaction.getPeer(self.authorId), + author: author, text: self.text, attributes: attributes, media: self.media, @@ -270,7 +301,7 @@ private class AdMessagesHistoryContextImpl { |> mapToSignal { cachedState -> Signal in if let cachedState = cachedState, cachedState.timestamp >= Int32(Date().timeIntervalSince1970) - 5 * 60 { return account.postbox.transaction { transaction -> State in - return State(messages: cachedState.messages.map { message in + return State(messages: cachedState.messages.compactMap { message -> Message? in return message.toMessage(peerId: peerId, transaction: transaction) }) } @@ -325,35 +356,42 @@ private class AdMessagesHistoryContextImpl { for message in messages { switch message { - case let .sponsoredMessage(_, randomId, fromId, channelPost, startParam, message, entities): + case let .sponsoredMessage(_, randomId, fromId, chatInvite, chatInviteHash, channelPost, startParam, message, entities): var parsedEntities: [MessageTextEntity] = [] if let entities = entities { parsedEntities = messageTextEntitiesFromApiEntities(entities) } + + let _ = chatInvite + let _ = chatInviteHash + + var target: CachedMessage.Target? + if let fromId = fromId { + target = .peer(fromId.peerId) + } + + var messageId: MessageId? + if let fromId = fromId, let channelPost = channelPost { + messageId = MessageId(peerId: fromId.peerId, namespace: Namespaces.Message.Cloud, id: channelPost) + } - let parsedMedia: [Media] = [] - /*if let media = media { - let (mediaValue, _) = textMediaAndExpirationTimerFromApiMedia(media, peerId) - if let mediaValue = mediaValue { - parsedMedia.append(mediaValue) - } - }*/ - - parsedMessages.append(CachedMessage( - opaqueId: randomId.makeData(), - text: message, - textEntities: parsedEntities, - media: parsedMedia, - authorId: fromId.peerId, - messageId: channelPost.flatMap { MessageId(peerId: fromId.peerId, namespace: Namespaces.Message.Cloud, id: $0) }, - startParam: startParam - )) + if let target = target { + parsedMessages.append(CachedMessage( + opaqueId: randomId.makeData(), + text: message, + textEntities: parsedEntities, + media: [], + target: target, + messageId: messageId, + startParam: startParam + )) + } } } CachedState.setCached(transaction: transaction, peerId: peerId, state: CachedState(timestamp: Int32(Date().timeIntervalSince1970), messages: parsedMessages)) - return parsedMessages.map { message in + return parsedMessages.compactMap { message -> Message? in return message.toMessage(peerId: peerId, transaction: transaction) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift index e90149c97e..508e71d359 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift @@ -366,7 +366,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee .withUpdatedCallJoinPeerId(groupCallDefaultJoinAs?.peerId) .withUpdatedThemeEmoticon(chatFullThemeEmoticon) .withUpdatedInviteRequestsPending(chatFullRequestsPending) - .withUpdatedAllowedReactions(allowedReactions) + .withUpdatedAllowedReactions(allowedReactions ?? []) }) case .channelFull: break @@ -597,7 +597,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee .withUpdatedThemeEmoticon(themeEmoticon) .withUpdatedInviteRequestsPending(requestsPending) .withUpdatedSendAsPeerId(sendAsPeerId) - .withUpdatedAllowedReactions(allowedReactions) + .withUpdatedAllowedReactions(allowedReactions ?? []) }) if let minAvailableMessageId = minAvailableMessageId, minAvailableMessageIdUpdated { diff --git a/submodules/TelegramCore/Sources/Utils/MessageUtils.swift b/submodules/TelegramCore/Sources/Utils/MessageUtils.swift index 4ddf40bc18..44bd3906ac 100644 --- a/submodules/TelegramCore/Sources/Utils/MessageUtils.swift +++ b/submodules/TelegramCore/Sources/Utils/MessageUtils.swift @@ -330,6 +330,15 @@ public extension Message { } return false } + + var textEntitiesAttribute: TextEntitiesMessageAttribute? { + for attribute in self.attributes { + if let attribute = attribute as? TextEntitiesMessageAttribute { + return attribute + } + } + return nil + } } public func _internal_parseMediaAttachment(data: Data) -> Media? { diff --git a/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift b/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift index 981e8f9f82..781c2a4f4d 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift @@ -173,6 +173,20 @@ public func customizeDefaultDarkPresentationTheme(theme: PresentationTheme, edit animateMessageColors: animateBubbleColors, message: chat.message.withUpdated( incoming: chat.message.incoming.withUpdated( + bubble: chat.message.outgoing.bubble.withUpdated( + withWallpaper: chat.message.incoming.bubble.withWallpaper.withUpdated( + reactionInactiveBackground: UIColor(rgb: 0xffffff, alpha: 0.07), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: accentColor, + reactionActiveForeground: UIColor(rgb: 0xffffff) + ), + withoutWallpaper: chat.message.incoming.bubble.withoutWallpaper.withUpdated( + reactionInactiveBackground: UIColor(rgb: 0xffffff, alpha: 0.07), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: accentColor, + reactionActiveForeground: UIColor(rgb: 0xffffff) + ) + ), linkTextColor: accentColor, linkHighlightColor: accentColor?.withAlphaComponent(0.5), accentTextColor: accentColor, @@ -200,12 +214,20 @@ public func customizeDefaultDarkPresentationTheme(theme: PresentationTheme, edit withWallpaper: chat.message.outgoing.bubble.withWallpaper.withUpdated( fill: outgoingBubbleFillColors, highlightedFill: outgoingBubbleFillColors?.first?.withMultipliedBrightnessBy(1.421), - stroke: .clear + stroke: .clear, + reactionInactiveBackground: UIColor(rgb: 0xffffff, alpha: 0.12), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: UIColor(rgb: 0xffffff), + reactionActiveForeground: UIColor(rgb: 0x000000, alpha: 0.0) ), withoutWallpaper: chat.message.outgoing.bubble.withoutWallpaper.withUpdated( fill: outgoingBubbleFillColors, highlightedFill: outgoingBubbleFillColors?.first?.withMultipliedBrightnessBy(1.421), - stroke: .clear + stroke: .clear, + reactionInactiveBackground: UIColor(rgb: 0xffffff, alpha: 0.12), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: UIColor(rgb: 0xffffff), + reactionActiveForeground: UIColor(rgb: 0x000000, alpha: 0.0) ) ), primaryTextColor: outgoingPrimaryTextColor, @@ -223,6 +245,20 @@ public func customizeDefaultDarkPresentationTheme(theme: PresentationTheme, edit fileDurationColor: outgoingSecondaryTextColor, polls: chat.message.outgoing.polls.withUpdated(radioButton: outgoingPrimaryTextColor, radioProgress: outgoingPrimaryTextColor, highlight: outgoingPrimaryTextColor?.withAlphaComponent(0.12), separator: outgoingSecondaryTextColor, bar: outgoingPrimaryTextColor) ), + freeform: chat.message.freeform.withUpdated( + withWallpaper: chat.message.freeform.withWallpaper.withUpdated( + reactionInactiveBackground: UIColor(rgb: 0xffffff, alpha: 0.12), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: accentColor, + reactionActiveForeground: UIColor(rgb: 0xffffff) + ), + withoutWallpaper: chat.message.freeform.withoutWallpaper.withUpdated( + reactionInactiveBackground: chat.message.incoming.bubble.withoutWallpaper.fill.last, + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: accentColor, + reactionActiveForeground: UIColor(rgb: 0xffffff) + ) + ), infoLinkTextColor: accentColor, outgoingCheckColor: outgoingCheckColor, selectionControlColors: chat.message.selectionControlColors.withUpdated(fillColor: accentColor, foregroundColor: badgeTextColor) @@ -441,9 +477,78 @@ public func makeDefaultDarkPresentationTheme(extendingThemeReference: Presentati let incomingBubbleAlpha: CGFloat = 0.9 let message = PresentationThemeChatMessage( - incoming: PresentationThemePartedColors(bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0x1D1D1D, alpha: incomingBubbleAlpha)], highlightedFill: UIColor(rgb: 0x353539), stroke: UIColor(rgb: 0x262628), shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0x1D1D1D, alpha: incomingBubbleAlpha)], highlightedFill: UIColor(rgb: 0x353539), stroke: UIColor(rgb: 0x262628), shadow: nil)), primaryTextColor: UIColor(rgb: 0xffffff), secondaryTextColor: UIColor(rgb: 0xffffff, alpha: 0.5), linkTextColor: UIColor(rgb: 0xffffff), linkHighlightColor: UIColor(rgb: 0xffffff, alpha: 0.5), scamColor: UIColor(rgb: 0xeb5545), textHighlightColor: UIColor(rgb: 0xf5c038), accentTextColor: UIColor(rgb: 0xffffff), accentControlColor: UIColor(rgb: 0xffffff), accentControlDisabledColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaActiveControlColor: UIColor(rgb: 0xffffff), mediaInactiveControlColor: UIColor(rgb: 0xffffff, alpha: 0.4), mediaControlInnerBackgroundColor: UIColor(rgb: 0x262628), pendingActivityColor: UIColor(rgb: 0xffffff, alpha: 0.5), fileTitleColor: UIColor(rgb: 0xffffff), fileDescriptionColor: UIColor(rgb: 0xffffff, alpha: 0.5), fileDurationColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaPlaceholderColor: UIColor(rgb: 0x1f1f1f).mixedWith(UIColor(rgb: 0xffffff), alpha: 0.05), polls: PresentationThemeChatBubblePolls(radioButton: UIColor(rgb: 0x737373), radioProgress: UIColor(rgb: 0xffffff), highlight: UIColor(rgb: 0xffffff, alpha: 0.12), separator: UIColor(rgb: 0x000000), bar: UIColor(rgb: 0xffffff), barIconForeground: .clear, barPositive: UIColor(rgb: 0x00A700), barNegative: UIColor(rgb: 0xFE3824)), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0x000000, alpha: 0.5), withoutWallpaper: UIColor(rgb: 0x000000, alpha: 0.5)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xb2b2b2, alpha: 0.18)), actionButtonsTextColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xffffff)), textSelectionColor: UIColor(rgb: 0xffffff, alpha: 0.2), textSelectionKnobColor: UIColor(rgb: 0xffffff)), - outgoing: PresentationThemePartedColors(bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0x61BCF9), UIColor(rgb: 0x007AFF)], highlightedFill: UIColor(rgb: 0x61BCF9), stroke: .clear, shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0x61BCF9), UIColor(rgb: 0x007AFF)], highlightedFill: UIColor(rgb: 0x61BCF9), stroke: .clear, shadow: nil)), primaryTextColor: UIColor(rgb: 0xffffff), secondaryTextColor: UIColor(rgb: 0xffffff, alpha: 0.5), linkTextColor: UIColor(rgb: 0xffffff), linkHighlightColor: UIColor(rgb: 0xffffff, alpha: 0.5), scamColor: UIColor(rgb: 0xeb5545), textHighlightColor: UIColor(rgb: 0xf5c038), accentTextColor: UIColor(rgb: 0xffffff), accentControlColor: UIColor(rgb: 0xffffff), accentControlDisabledColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaActiveControlColor: UIColor(rgb: 0xffffff), mediaInactiveControlColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaControlInnerBackgroundColor: UIColor(rgb: 0x313131), pendingActivityColor: UIColor(rgb: 0xffffff, alpha: 0.5), fileTitleColor: UIColor(rgb: 0xffffff), fileDescriptionColor: UIColor(rgb: 0xffffff, alpha: 0.5), fileDurationColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaPlaceholderColor: UIColor(rgb: 0x313131).mixedWith(UIColor(rgb: 0xffffff), alpha: 0.05), polls: PresentationThemeChatBubblePolls(radioButton: UIColor(rgb: 0xffffff), radioProgress: UIColor(rgb: 0xffffff), highlight: UIColor(rgb: 0xffffff).withAlphaComponent(0.12), separator: UIColor(rgb: 0xffffff, alpha: 0.5), bar: UIColor(rgb: 0xffffff), barIconForeground: .clear, barPositive: UIColor(rgb: 0xffffff), barNegative: UIColor(rgb: 0xffffff)), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0x000000, alpha: 0.5), withoutWallpaper: UIColor(rgb: 0x000000, alpha: 0.5)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xb2b2b2, alpha: 0.18)), actionButtonsTextColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xffffff)), textSelectionColor: UIColor(rgb: 0xffffff, alpha: 0.2), textSelectionKnobColor: UIColor(rgb: 0xffffff)), - freeform: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0x1f1f1f)], highlightedFill: UIColor(rgb: 0x2a2a2a), stroke: UIColor(rgb: 0x1f1f1f), shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0x1f1f1f)], highlightedFill: UIColor(rgb: 0x2a2a2a), stroke: UIColor(rgb: 0x1f1f1f), shadow: nil)), + incoming: PresentationThemePartedColors( + bubble: PresentationThemeBubbleColor( + withWallpaper: PresentationThemeBubbleColorComponents( + fill: [UIColor(rgb: 0x1D1D1D, alpha: incomingBubbleAlpha)], + highlightedFill: UIColor(rgb: 0x353539), + stroke: UIColor(rgb: 0x262628), + shadow: nil, + reactionInactiveBackground: UIColor(rgb: 0xffffff, alpha: 0.1), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 1.0), + reactionActiveForeground: .clear + ), + withoutWallpaper: PresentationThemeBubbleColorComponents( + fill: [UIColor(rgb: 0x1D1D1D, alpha: incomingBubbleAlpha)], + highlightedFill: UIColor(rgb: 0x353539), + stroke: UIColor(rgb: 0x262628), + shadow: nil, + reactionInactiveBackground: UIColor(rgb: 0xffffff, alpha: 0.1), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 1.0), + reactionActiveForeground: .clear + ) + ), + primaryTextColor: UIColor(rgb: 0xffffff), + secondaryTextColor: UIColor(rgb: 0xffffff, alpha: 0.5), linkTextColor: UIColor(rgb: 0xffffff), linkHighlightColor: UIColor(rgb: 0xffffff, alpha: 0.5), scamColor: UIColor(rgb: 0xeb5545), textHighlightColor: UIColor(rgb: 0xf5c038), accentTextColor: UIColor(rgb: 0xffffff), accentControlColor: UIColor(rgb: 0xffffff), accentControlDisabledColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaActiveControlColor: UIColor(rgb: 0xffffff), mediaInactiveControlColor: UIColor(rgb: 0xffffff, alpha: 0.4), mediaControlInnerBackgroundColor: UIColor(rgb: 0x262628), pendingActivityColor: UIColor(rgb: 0xffffff, alpha: 0.5), fileTitleColor: UIColor(rgb: 0xffffff), fileDescriptionColor: UIColor(rgb: 0xffffff, alpha: 0.5), fileDurationColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaPlaceholderColor: UIColor(rgb: 0x1f1f1f).mixedWith(UIColor(rgb: 0xffffff), alpha: 0.05), polls: PresentationThemeChatBubblePolls(radioButton: UIColor(rgb: 0x737373), radioProgress: UIColor(rgb: 0xffffff), highlight: UIColor(rgb: 0xffffff, alpha: 0.12), separator: UIColor(rgb: 0x000000), bar: UIColor(rgb: 0xffffff), barIconForeground: .clear, barPositive: UIColor(rgb: 0x00A700), barNegative: UIColor(rgb: 0xFE3824)), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0x000000, alpha: 0.5), withoutWallpaper: UIColor(rgb: 0x000000, alpha: 0.5)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xb2b2b2, alpha: 0.18)), actionButtonsTextColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xffffff)), textSelectionColor: UIColor(rgb: 0xffffff, alpha: 0.2), textSelectionKnobColor: UIColor(rgb: 0xffffff) + ), + outgoing: PresentationThemePartedColors( + bubble: PresentationThemeBubbleColor( + withWallpaper: PresentationThemeBubbleColorComponents( + fill: [UIColor(rgb: 0x61BCF9), UIColor(rgb: 0x007AFF)], + highlightedFill: UIColor(rgb: 0x61BCF9), + stroke: .clear, + shadow: nil, + reactionInactiveBackground: UIColor(rgb: 0xffffff, alpha: 0.1), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 1.0), + reactionActiveForeground: .clear + ), + withoutWallpaper: PresentationThemeBubbleColorComponents( + fill: [UIColor(rgb: 0x61BCF9), UIColor(rgb: 0x007AFF)], + highlightedFill: UIColor(rgb: 0x61BCF9), + stroke: .clear, + shadow: nil, + reactionInactiveBackground: UIColor(rgb: 0xffffff, alpha: 0.1), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 1.0), + reactionActiveForeground: .clear + ) + ), primaryTextColor: UIColor(rgb: 0xffffff), secondaryTextColor: UIColor(rgb: 0xffffff, alpha: 0.5), linkTextColor: UIColor(rgb: 0xffffff), linkHighlightColor: UIColor(rgb: 0xffffff, alpha: 0.5), scamColor: UIColor(rgb: 0xeb5545), textHighlightColor: UIColor(rgb: 0xf5c038), accentTextColor: UIColor(rgb: 0xffffff), accentControlColor: UIColor(rgb: 0xffffff), accentControlDisabledColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaActiveControlColor: UIColor(rgb: 0xffffff), mediaInactiveControlColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaControlInnerBackgroundColor: UIColor(rgb: 0x313131), pendingActivityColor: UIColor(rgb: 0xffffff, alpha: 0.5), fileTitleColor: UIColor(rgb: 0xffffff), fileDescriptionColor: UIColor(rgb: 0xffffff, alpha: 0.5), fileDurationColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaPlaceholderColor: UIColor(rgb: 0x313131).mixedWith(UIColor(rgb: 0xffffff), alpha: 0.05), polls: PresentationThemeChatBubblePolls(radioButton: UIColor(rgb: 0xffffff), radioProgress: UIColor(rgb: 0xffffff), highlight: UIColor(rgb: 0xffffff).withAlphaComponent(0.12), separator: UIColor(rgb: 0xffffff, alpha: 0.5), bar: UIColor(rgb: 0xffffff), barIconForeground: .clear, barPositive: UIColor(rgb: 0xffffff), barNegative: UIColor(rgb: 0xffffff)), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0x000000, alpha: 0.5), withoutWallpaper: UIColor(rgb: 0x000000, alpha: 0.5)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xb2b2b2, alpha: 0.18)), actionButtonsTextColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xffffff)), textSelectionColor: UIColor(rgb: 0xffffff, alpha: 0.2), textSelectionKnobColor: UIColor(rgb: 0xffffff) + ), + freeform: PresentationThemeBubbleColor( + withWallpaper: PresentationThemeBubbleColorComponents( + fill: [UIColor(rgb: 0x1f1f1f)], + highlightedFill: UIColor(rgb: 0x2a2a2a), + stroke: UIColor(rgb: 0x1f1f1f), + shadow: nil, + reactionInactiveBackground: UIColor(rgb: 0x1f1f1f), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 1.0), + reactionActiveForeground: .clear + ), + withoutWallpaper: PresentationThemeBubbleColorComponents( + fill: [UIColor(rgb: 0x1f1f1f)], + highlightedFill: UIColor(rgb: 0x2a2a2a), + stroke: UIColor(rgb: 0x1f1f1f), + shadow: nil, + reactionInactiveBackground: UIColor(rgb: 0x1f1f1f), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 1.0), + reactionActiveForeground: .clear + ) + ), infoPrimaryTextColor: UIColor(rgb: 0xffffff), infoLinkTextColor: UIColor(rgb: 0xffffff), outgoingCheckColor: UIColor(rgb: 0xffffff), diff --git a/submodules/TelegramPresentationData/Sources/DefaultDarkTintedPresentationTheme.swift b/submodules/TelegramPresentationData/Sources/DefaultDarkTintedPresentationTheme.swift index c8ea3de526..fe7bcaead4 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultDarkTintedPresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultDarkTintedPresentationTheme.swift @@ -300,7 +300,7 @@ public func customizeDefaultDarkTintedPresentationTheme(theme: PresentationTheme } let incomingFillColor = mainBackgroundColor?.withMultipliedAlpha(0.9) - + chat = chat.withUpdated( defaultWallpaper: defaultWallpaper, animateMessageColors: animateBubbleColors, @@ -310,12 +310,20 @@ public func customizeDefaultDarkTintedPresentationTheme(theme: PresentationTheme withWallpaper: chat.message.outgoing.bubble.withWallpaper.withUpdated( fill: incomingFillColor.flatMap({ [$0] }), highlightedFill: highlightedIncomingBubbleColor, - stroke: mainBackgroundColor + stroke: mainBackgroundColor, + reactionInactiveBackground: UIColor(rgb: 0xffffff, alpha: 0.07), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: accentColor, + reactionActiveForeground: UIColor(rgb: 0xffffff) ), withoutWallpaper: chat.message.outgoing.bubble.withoutWallpaper.withUpdated( fill: incomingFillColor.flatMap({ [$0] }), highlightedFill: highlightedIncomingBubbleColor, - stroke: mainBackgroundColor + stroke: mainBackgroundColor, + reactionInactiveBackground: UIColor(rgb: 0xffffff, alpha: 0.07), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: accentColor, + reactionActiveForeground: UIColor(rgb: 0xffffff) ) ), secondaryTextColor: mainSecondaryTextColor?.withAlphaComponent(0.5), @@ -699,9 +707,76 @@ public func makeDefaultDarkTintedPresentationTheme(extendingThemeReference: Pres let incomingBubbleAlpha: CGFloat = 0.9 let message = PresentationThemeChatMessage( - incoming: PresentationThemePartedColors(bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: [incomingFillColor.withAlphaComponent(incomingBubbleAlpha)], highlightedFill: highlightedIncomingBubbleColor, stroke: mainBackgroundColor, shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: [incomingFillColor.withAlphaComponent(incomingBubbleAlpha)], highlightedFill: highlightedIncomingBubbleColor, stroke: mainBackgroundColor, shadow: nil)), primaryTextColor: .white, secondaryTextColor: mainSecondaryTextColor.withAlphaComponent(0.5), linkTextColor: accentColor, linkHighlightColor: accentColor.withAlphaComponent(0.5), scamColor: UIColor(rgb: 0xff6767), textHighlightColor: UIColor(rgb: 0xf5c038), accentTextColor: accentColor, accentControlColor: accentColor, accentControlDisabledColor: mainSecondaryTextColor.withAlphaComponent(0.5), mediaActiveControlColor: accentColor, mediaInactiveControlColor: accentColor.withAlphaComponent(0.5), mediaControlInnerBackgroundColor: mainBackgroundColor, pendingActivityColor: mainSecondaryTextColor.withAlphaComponent(0.5), fileTitleColor: accentColor, fileDescriptionColor: mainSecondaryTextColor.withAlphaComponent(0.5), fileDurationColor: mainSecondaryTextColor.withAlphaComponent(0.5), mediaPlaceholderColor: accentColor.withMultiplied(hue: 1.019, saturation: 0.585, brightness: 0.23), polls: PresentationThemeChatBubblePolls(radioButton: accentColor.withMultiplied(hue: 0.995, saturation: 0.317, brightness: 0.51), radioProgress: accentColor, highlight: accentColor.withAlphaComponent(0.12), separator: mainSeparatorColor, bar: accentColor, barIconForeground: .white, barPositive: UIColor(rgb: 0x00A700), barNegative: UIColor(rgb: 0xFE3824)), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: additionalBackgroundColor.withAlphaComponent(0.5), withoutWallpaper: additionalBackgroundColor.withAlphaComponent(0.5)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: buttonStrokeColor), actionButtonsTextColor: PresentationThemeVariableColor(color: .white), textSelectionColor: accentColor.withAlphaComponent(0.2), textSelectionKnobColor: accentColor), - outgoing: PresentationThemePartedColors(bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: outgoingBubbleFillColors, highlightedFill: highlightedOutgoingBubbleColor, stroke: outgoingBubbleFillColors[0], shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: outgoingBubbleFillColors, highlightedFill: highlightedOutgoingBubbleColor, stroke: outgoingBubbleFillColors[0], shadow: nil)), primaryTextColor: outgoingPrimaryTextColor, secondaryTextColor: outgoingSecondaryTextColor, linkTextColor: outgoingLinkTextColor, linkHighlightColor: UIColor.white.withAlphaComponent(0.5), scamColor: outgoingScamColor, textHighlightColor: UIColor(rgb: 0xf5c038), accentTextColor: outgoingPrimaryTextColor, accentControlColor: outgoingPrimaryTextColor, accentControlDisabledColor: mainSecondaryTextColor.withAlphaComponent(0.5), mediaActiveControlColor: outgoingPrimaryTextColor, mediaInactiveControlColor: outgoingSecondaryTextColor, mediaControlInnerBackgroundColor: outgoingBubbleFillColors[0], pendingActivityColor: outgoingSecondaryTextColor, fileTitleColor: outgoingPrimaryTextColor, fileDescriptionColor: outgoingSecondaryTextColor, fileDurationColor: outgoingSecondaryTextColor, mediaPlaceholderColor: accentColor.withMultiplied(hue: 1.019, saturation: 0.804, brightness: 0.51), polls: PresentationThemeChatBubblePolls(radioButton: outgoingPrimaryTextColor, radioProgress: accentColor.withMultiplied(hue: 0.99, saturation: 0.56, brightness: 1.0), highlight: accentColor.withMultiplied(hue: 0.99, saturation: 0.56, brightness: 1.0).withAlphaComponent(0.12), separator: mainSeparatorColor, bar: outgoingPrimaryTextColor, barIconForeground: .clear, barPositive: outgoingPrimaryTextColor, barNegative: outgoingPrimaryTextColor), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: additionalBackgroundColor.withAlphaComponent(0.5), withoutWallpaper: additionalBackgroundColor.withAlphaComponent(0.5)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: buttonStrokeColor), actionButtonsTextColor: PresentationThemeVariableColor(color: .white), textSelectionColor: UIColor.white.withAlphaComponent(0.2), textSelectionKnobColor: UIColor.white), - freeform: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: [mainBackgroundColor], highlightedFill: highlightedIncomingBubbleColor, stroke: mainBackgroundColor, shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: [mainBackgroundColor], highlightedFill: highlightedIncomingBubbleColor, stroke: mainBackgroundColor, shadow: nil)), + incoming: PresentationThemePartedColors( + bubble: PresentationThemeBubbleColor( + withWallpaper: PresentationThemeBubbleColorComponents( + fill: [incomingFillColor.withAlphaComponent(incomingBubbleAlpha)], + highlightedFill: highlightedIncomingBubbleColor, + stroke: mainBackgroundColor, + shadow: nil, + reactionInactiveBackground: UIColor(rgb: 0xffffff, alpha: 0.07), + reactionInactiveForeground: UIColor(rgb: 0xffffff, alpha: 1.0), + reactionActiveBackground: accentColor, + reactionActiveForeground: UIColor(rgb: 0xffffff, alpha: 1.0) + ), + withoutWallpaper: PresentationThemeBubbleColorComponents( + fill: [incomingFillColor.withAlphaComponent(incomingBubbleAlpha)], + highlightedFill: highlightedIncomingBubbleColor, + stroke: mainBackgroundColor, + shadow: nil, + reactionInactiveBackground: UIColor(rgb: 0xffffff, alpha: 0.07), + reactionInactiveForeground: UIColor(rgb: 0xffffff, alpha: 1.0), + reactionActiveBackground: accentColor, + reactionActiveForeground: UIColor(rgb: 0xffffff, alpha: 1.0) + ) + ), primaryTextColor: .white, secondaryTextColor: mainSecondaryTextColor.withAlphaComponent(0.5), linkTextColor: accentColor, linkHighlightColor: accentColor.withAlphaComponent(0.5), scamColor: UIColor(rgb: 0xff6767), textHighlightColor: UIColor(rgb: 0xf5c038), accentTextColor: accentColor, accentControlColor: accentColor, accentControlDisabledColor: mainSecondaryTextColor.withAlphaComponent(0.5), mediaActiveControlColor: accentColor, mediaInactiveControlColor: accentColor.withAlphaComponent(0.5), mediaControlInnerBackgroundColor: mainBackgroundColor, pendingActivityColor: mainSecondaryTextColor.withAlphaComponent(0.5), fileTitleColor: accentColor, fileDescriptionColor: mainSecondaryTextColor.withAlphaComponent(0.5), fileDurationColor: mainSecondaryTextColor.withAlphaComponent(0.5), mediaPlaceholderColor: accentColor.withMultiplied(hue: 1.019, saturation: 0.585, brightness: 0.23), polls: PresentationThemeChatBubblePolls(radioButton: accentColor.withMultiplied(hue: 0.995, saturation: 0.317, brightness: 0.51), radioProgress: accentColor, highlight: accentColor.withAlphaComponent(0.12), separator: mainSeparatorColor, bar: accentColor, barIconForeground: .white, barPositive: UIColor(rgb: 0x00A700), barNegative: UIColor(rgb: 0xFE3824)), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: additionalBackgroundColor.withAlphaComponent(0.5), withoutWallpaper: additionalBackgroundColor.withAlphaComponent(0.5)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: buttonStrokeColor), actionButtonsTextColor: PresentationThemeVariableColor(color: .white), textSelectionColor: accentColor.withAlphaComponent(0.2), textSelectionKnobColor: accentColor + ), + outgoing: PresentationThemePartedColors( + bubble: PresentationThemeBubbleColor( + withWallpaper: PresentationThemeBubbleColorComponents( + fill: outgoingBubbleFillColors, + highlightedFill: highlightedOutgoingBubbleColor, + stroke: outgoingBubbleFillColors[0], + shadow: nil, + reactionInactiveBackground: UIColor(rgb: 0xffffff, alpha: 0.1), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 1.0), + reactionActiveForeground: .clear + ), + withoutWallpaper: PresentationThemeBubbleColorComponents( + fill: outgoingBubbleFillColors, + highlightedFill: highlightedOutgoingBubbleColor, + stroke: outgoingBubbleFillColors[0], + shadow: nil, + reactionInactiveBackground: UIColor(rgb: 0xffffff, alpha: 0.1), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 1.0), + reactionActiveForeground: .clear + ) + ), primaryTextColor: outgoingPrimaryTextColor, secondaryTextColor: outgoingSecondaryTextColor, linkTextColor: outgoingLinkTextColor, linkHighlightColor: UIColor.white.withAlphaComponent(0.5), scamColor: outgoingScamColor, textHighlightColor: UIColor(rgb: 0xf5c038), accentTextColor: outgoingPrimaryTextColor, accentControlColor: outgoingPrimaryTextColor, accentControlDisabledColor: mainSecondaryTextColor.withAlphaComponent(0.5), mediaActiveControlColor: outgoingPrimaryTextColor, mediaInactiveControlColor: outgoingSecondaryTextColor, mediaControlInnerBackgroundColor: outgoingBubbleFillColors[0], pendingActivityColor: outgoingSecondaryTextColor, fileTitleColor: outgoingPrimaryTextColor, fileDescriptionColor: outgoingSecondaryTextColor, fileDurationColor: outgoingSecondaryTextColor, mediaPlaceholderColor: accentColor.withMultiplied(hue: 1.019, saturation: 0.804, brightness: 0.51), polls: PresentationThemeChatBubblePolls(radioButton: outgoingPrimaryTextColor, radioProgress: accentColor.withMultiplied(hue: 0.99, saturation: 0.56, brightness: 1.0), highlight: accentColor.withMultiplied(hue: 0.99, saturation: 0.56, brightness: 1.0).withAlphaComponent(0.12), separator: mainSeparatorColor, bar: outgoingPrimaryTextColor, barIconForeground: .clear, barPositive: outgoingPrimaryTextColor, barNegative: outgoingPrimaryTextColor), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: additionalBackgroundColor.withAlphaComponent(0.5), withoutWallpaper: additionalBackgroundColor.withAlphaComponent(0.5)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: buttonStrokeColor), actionButtonsTextColor: PresentationThemeVariableColor(color: .white), textSelectionColor: UIColor.white.withAlphaComponent(0.2), textSelectionKnobColor: UIColor.white + ), + freeform: PresentationThemeBubbleColor( + withWallpaper: PresentationThemeBubbleColorComponents( + fill: [mainBackgroundColor], + highlightedFill: highlightedIncomingBubbleColor, + stroke: mainBackgroundColor, + shadow: nil, + reactionInactiveBackground: incomingFillColor.withAlphaComponent(incomingBubbleAlpha), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: accentColor, + reactionActiveForeground: UIColor(rgb: 0xffffff) + ), + withoutWallpaper: PresentationThemeBubbleColorComponents( + fill: [mainBackgroundColor], + highlightedFill: highlightedIncomingBubbleColor, + stroke: mainBackgroundColor, + shadow: nil, + reactionInactiveBackground: incomingFillColor.withAlphaComponent(incomingBubbleAlpha), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: accentColor, + reactionActiveForeground: UIColor(rgb: 0xffffff) + ) + ), infoPrimaryTextColor: UIColor(rgb: 0xffffff), infoLinkTextColor: accentColor, outgoingCheckColor: outgoingCheckColor, diff --git a/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift b/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift index 02a483e189..1e68394109 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift @@ -15,6 +15,18 @@ public func selectDateFillStaticColor(theme: PresentationTheme, wallpaper: Teleg } } +public func selectReactionFillStaticColor(theme: PresentationTheme, wallpaper: TelegramWallpaper) -> UIColor { + if case .color = wallpaper { + return theme.chat.message.freeform.withoutWallpaper.reactionInactiveBackground + } else if theme.overallDarkAppearance { + return theme.chat.message.freeform.withoutWallpaper.reactionInactiveBackground + } else if case .builtin = wallpaper { + return UIColor(rgb: 0x748391, alpha: 0.45) + } else { + return theme.chat.serviceMessage.components.withCustomWallpaper.dateFillStatic + } +} + public func dateFillNeedsBlur(theme: PresentationTheme, wallpaper: TelegramWallpaper) -> Bool { if case .builtin = wallpaper { return false @@ -238,10 +250,18 @@ public func customizeDefaultDayTheme(theme: PresentationTheme, editing: Bool, ti incoming: chat.message.incoming.withUpdated( bubble: chat.message.incoming.bubble.withUpdated( withWallpaper: chat.message.incoming.bubble.withWallpaper.withUpdated( - stroke: incomingBubbleStrokeColor + stroke: incomingBubbleStrokeColor, + reactionInactiveBackground: accentColor?.withMultipliedAlpha(0.1), + reactionInactiveForeground: accentColor, + reactionActiveBackground: accentColor, + reactionActiveForeground: .clear ), withoutWallpaper: chat.message.incoming.bubble.withoutWallpaper.withUpdated( - stroke: incomingBubbleStrokeColor + stroke: incomingBubbleStrokeColor, + reactionInactiveBackground: accentColor?.withMultipliedAlpha(0.1), + reactionInactiveForeground: accentColor, + reactionActiveBackground: accentColor, + reactionActiveForeground: .clear ) ), linkHighlightColor: accentColor?.withAlphaComponent(0.3), @@ -266,12 +286,20 @@ public func customizeDefaultDayTheme(theme: PresentationTheme, editing: Bool, ti withWallpaper: chat.message.outgoing.bubble.withWallpaper.withUpdated( fill: outgoingBubbleFillColors, highlightedFill: outgoingBubbleHighlightedFill, - stroke: outgoingBubbleStrokeColor + stroke: outgoingBubbleStrokeColor, + reactionInactiveBackground: outgoingControlColor?.withMultipliedAlpha(0.1), + reactionInactiveForeground: outgoingControlColor, + reactionActiveBackground: outgoingControlColor, + reactionActiveForeground: .clear ), withoutWallpaper: chat.message.outgoing.bubble.withoutWallpaper.withUpdated( fill: outgoingBubbleFillColors, highlightedFill: outgoingBubbleHighlightedFill, - stroke: outgoingBubbleStrokeColor + stroke: outgoingBubbleStrokeColor, + reactionInactiveBackground: outgoingControlColor?.withMultipliedAlpha(0.1), + reactionInactiveForeground: outgoingControlColor, + reactionActiveBackground: outgoingControlColor, + reactionActiveForeground: .clear ) ), primaryTextColor: outgoingPrimaryTextColor, @@ -526,7 +554,28 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio let message = PresentationThemeChatMessage( incoming: PresentationThemePartedColors( - bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0xffffff)], highlightedFill: UIColor(rgb: 0xd9f4ff), stroke: bubbleStrokeColor, shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0xffffff)], highlightedFill: UIColor(rgb: 0xd9f4ff), stroke: bubbleStrokeColor, shadow: nil)), + bubble: PresentationThemeBubbleColor( + withWallpaper: PresentationThemeBubbleColorComponents( + fill: [UIColor(rgb: 0xffffff)], + highlightedFill: UIColor(rgb: 0xd9f4ff), + stroke: bubbleStrokeColor, + shadow: nil, + reactionInactiveBackground: defaultDayAccentColor.withMultipliedAlpha(0.1), + reactionInactiveForeground: defaultDayAccentColor, + reactionActiveBackground: defaultDayAccentColor, + reactionActiveForeground: .clear + ), + withoutWallpaper: PresentationThemeBubbleColorComponents( + fill: [UIColor(rgb: 0xffffff)], + highlightedFill: UIColor(rgb: 0xd9f4ff), + stroke: bubbleStrokeColor, + shadow: nil, + reactionInactiveBackground: defaultDayAccentColor.withMultipliedAlpha(0.1), + reactionInactiveForeground: defaultDayAccentColor, + reactionActiveBackground: defaultDayAccentColor, + reactionActiveForeground: .clear + ) + ), primaryTextColor: UIColor(rgb: 0x000000), secondaryTextColor: UIColor(rgb: 0x525252, alpha: 0.6), linkTextColor: UIColor(rgb: 0x004bad), @@ -548,7 +597,28 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: serviceBackgroundColor, withoutWallpaper: UIColor(rgb: 0x596e89, alpha: 0.35)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: .clear), actionButtonsTextColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xffffff)), textSelectionColor: defaultDayAccentColor.withAlphaComponent(0.2), textSelectionKnobColor: defaultDayAccentColor), outgoing: PresentationThemePartedColors( - bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0xe1ffc7)], highlightedFill: UIColor(rgb: 0xc8ffa6), stroke: bubbleStrokeColor, shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0xe1ffc7)], highlightedFill: UIColor(rgb: 0xc8ffa6), stroke: bubbleStrokeColor, shadow: nil)), + bubble: PresentationThemeBubbleColor( + withWallpaper: PresentationThemeBubbleColorComponents( + fill: [UIColor(rgb: 0xe1ffc7)], + highlightedFill: UIColor(rgb: 0xc8ffa6), + stroke: bubbleStrokeColor, + shadow: nil, + reactionInactiveBackground: UIColor(rgb: 0x2DA32F).withMultipliedAlpha(0.12), + reactionInactiveForeground: UIColor(rgb: 0x2DA32F), + reactionActiveBackground: UIColor(rgb: 0x2DA32F), + reactionActiveForeground: .clear + ), + withoutWallpaper: PresentationThemeBubbleColorComponents( + fill: [UIColor(rgb: 0xe1ffc7)], + highlightedFill: UIColor(rgb: 0xc8ffa6), + stroke: bubbleStrokeColor, + shadow: nil, + reactionInactiveBackground: UIColor(rgb: 0x2DA32F).withMultipliedAlpha(0.12), + reactionInactiveForeground: UIColor(rgb: 0x2DA32F), + reactionActiveBackground: UIColor(rgb: 0x2DA32F), + reactionActiveForeground: .clear + ) + ), primaryTextColor: UIColor(rgb: 0x000000), secondaryTextColor: UIColor(rgb: 0x008c09, alpha: 0.8), linkTextColor: UIColor(rgb: 0x004bad), @@ -572,7 +642,28 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio actionButtonsTextColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xffffff)), textSelectionColor: UIColor(rgb: 0xbbde9f), textSelectionKnobColor: UIColor(rgb: 0x3fc33b)), - freeform: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0xffffff)], highlightedFill: UIColor(rgb: 0xd9f4ff), stroke: UIColor(rgb: 0x86a9c9, alpha: 0.5), shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0xffffff)], highlightedFill: UIColor(rgb: 0xd9f4ff), stroke: UIColor(rgb: 0x86a9c9, alpha: 0.5), shadow: nil)), + freeform: PresentationThemeBubbleColor( + withWallpaper: PresentationThemeBubbleColorComponents( + fill: [UIColor(rgb: 0xffffff)], + highlightedFill: UIColor(rgb: 0xd9f4ff), + stroke: UIColor(rgb: 0x86a9c9, alpha: 0.5), + shadow: nil, + reactionInactiveBackground: UIColor(rgb: 0xffffff), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 0.8), + reactionActiveForeground: UIColor(white: 0.0, alpha: 0.1) + ), + withoutWallpaper: PresentationThemeBubbleColorComponents( + fill: [UIColor(rgb: 0xffffff)], + highlightedFill: UIColor(rgb: 0xd9f4ff), + stroke: UIColor(rgb: 0x86a9c9, alpha: 0.5), + shadow: nil, + reactionInactiveBackground: UIColor(rgb: 0xffffff), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 0.8), + reactionActiveForeground: UIColor(white: 0.0, alpha: 0.1) + ) + ), infoPrimaryTextColor: UIColor(rgb: 0x000000), infoLinkTextColor: UIColor(rgb: 0x004bad), outgoingCheckColor: UIColor(rgb: 0x19c700), @@ -591,7 +682,28 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio let messageDay = PresentationThemeChatMessage( incoming: PresentationThemePartedColors( - bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0xffffff)], highlightedFill: UIColor(rgb: 0xdadade), stroke: UIColor(rgb: 0xffffff), shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0xf1f1f4)], highlightedFill: UIColor(rgb: 0xdadade), stroke: UIColor(rgb: 0xf1f1f4), shadow: nil)), + bubble: PresentationThemeBubbleColor( + withWallpaper: PresentationThemeBubbleColorComponents( + fill: [UIColor(rgb: 0xffffff)], + highlightedFill: UIColor(rgb: 0xdadade), + stroke: UIColor(rgb: 0xffffff), + shadow: nil, + reactionInactiveBackground: defaultDayAccentColor.withMultipliedAlpha(0.1), + reactionInactiveForeground: defaultDayAccentColor, + reactionActiveBackground: defaultDayAccentColor, + reactionActiveForeground: .clear + ), + withoutWallpaper: PresentationThemeBubbleColorComponents( + fill: [UIColor(rgb: 0xf1f1f4)], + highlightedFill: UIColor(rgb: 0xdadade), + stroke: UIColor(rgb: 0xf1f1f4), + shadow: nil, + reactionInactiveBackground: .clear, + reactionInactiveForeground: defaultDayAccentColor, + reactionActiveBackground: defaultDayAccentColor, + reactionActiveForeground: .clear + ) + ), primaryTextColor: UIColor(rgb: 0x000000), secondaryTextColor: UIColor(rgb: 0x525252, alpha: 0.6), linkTextColor: UIColor(rgb: 0x004bad), @@ -616,7 +728,28 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio textSelectionColor: defaultDayAccentColor.withAlphaComponent(0.3), textSelectionKnobColor: defaultDayAccentColor), outgoing: PresentationThemePartedColors( - bubble: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0x57b2e0), defaultDayAccentColor], highlightedFill: UIColor(rgb: 0x57b2e0).withMultipliedBrightnessBy(0.7), stroke: .clear, shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0x57b2e0), defaultDayAccentColor], highlightedFill: UIColor(rgb: 0x57b2e0).withMultipliedBrightnessBy(0.7), stroke: .clear, shadow: nil)), + bubble: PresentationThemeBubbleColor( + withWallpaper: PresentationThemeBubbleColorComponents( + fill: [UIColor(rgb: 0x57b2e0), defaultDayAccentColor], + highlightedFill: UIColor(rgb: 0x57b2e0).withMultipliedBrightnessBy(0.7), + stroke: .clear, + shadow: nil, + reactionInactiveBackground: UIColor(rgb: 0xffffff, alpha: 0.12), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: UIColor(rgb: 0xffffff), + reactionActiveForeground: .clear + ), + withoutWallpaper: PresentationThemeBubbleColorComponents( + fill: [UIColor(rgb: 0x57b2e0), defaultDayAccentColor], + highlightedFill: UIColor(rgb: 0x57b2e0).withMultipliedBrightnessBy(0.7), + stroke: .clear, + shadow: nil, + reactionInactiveBackground: UIColor(rgb: 0xffffff, alpha: 0.12), + reactionInactiveForeground: UIColor(rgb: 0xffffff), + reactionActiveBackground: UIColor(rgb: 0xffffff), + reactionActiveForeground: .clear + ) + ), primaryTextColor: UIColor(rgb: 0xffffff), secondaryTextColor: UIColor(rgb: 0xffffff, alpha: 0.65), linkTextColor: UIColor(rgb: 0xffffff), @@ -640,7 +773,28 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio actionButtonsTextColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0xffffff), withoutWallpaper: defaultDayAccentColor), textSelectionColor: UIColor(rgb: 0xffffff, alpha: 0.2), textSelectionKnobColor: UIColor(rgb: 0xffffff)), - freeform: PresentationThemeBubbleColor(withWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0xe5e5ea)], highlightedFill: UIColor(rgb: 0xdadade), stroke: UIColor(rgb: 0xe5e5ea), shadow: nil), withoutWallpaper: PresentationThemeBubbleColorComponents(fill: [UIColor(rgb: 0xe5e5ea)], highlightedFill: UIColor(rgb: 0xdadade), stroke: UIColor(rgb: 0xe5e5ea), shadow: nil)), + freeform: PresentationThemeBubbleColor( + withWallpaper: PresentationThemeBubbleColorComponents( + fill: [UIColor(rgb: 0xe5e5ea)], + highlightedFill: UIColor(rgb: 0xdadade), + stroke: UIColor(rgb: 0xe5e5ea), + shadow: nil, + reactionInactiveBackground: defaultDayAccentColor.withMultipliedAlpha(0.1), + reactionInactiveForeground: defaultDayAccentColor, + reactionActiveBackground: defaultDayAccentColor, + reactionActiveForeground: .clear + ), + withoutWallpaper: PresentationThemeBubbleColorComponents( + fill: [UIColor(rgb: 0xe5e5ea)], + highlightedFill: UIColor(rgb: 0xdadade), + stroke: UIColor(rgb: 0xe5e5ea), + shadow: nil, + reactionInactiveBackground: UIColor(rgb: 0xF1F0F5), + reactionInactiveForeground: defaultDayAccentColor, + reactionActiveBackground: defaultDayAccentColor, + reactionActiveForeground: .clear + ) + ), infoPrimaryTextColor: UIColor(rgb: 0x000000), infoLinkTextColor: UIColor(rgb: 0x004bad), outgoingCheckColor: UIColor(rgb: 0xffffff), diff --git a/submodules/TelegramPresentationData/Sources/DefaultPresentationStrings.swift b/submodules/TelegramPresentationData/Sources/DefaultPresentationStrings.swift index 22191fc8e1..53da3d2101 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultPresentationStrings.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultPresentationStrings.swift @@ -95,3 +95,31 @@ public func dataSizeString(_ size: Int64, forceDecimal: Bool = false, formatting return formatting.byte("\(size)").string } } + +public func countString(_ count: Int64, forceDecimal: Bool = false) -> String { + let decimalSeparator = "." + if count >= 1000 * 1000 * 1000 { + let remainder = Int64((Double(count % (1000 * 1000 * 1000)) / (1000 * 1000 * 100.0)).rounded(.down)) + if remainder != 0 || forceDecimal { + return "\(count / (1000 * 1000 * 1000))\(decimalSeparator)\(remainder)T" + } else { + return "\(count / (1000 * 1000 * 1000))T" + } + } else if count >= 1000 * 1000 { + let remainder = Int64((Double(count % (1000 * 1000)) / (1000.0 * 100.0)).rounded(.down)) + if remainder != 0 || forceDecimal { + return "\(count / (1000 * 1000))\(decimalSeparator)\(remainder)M" + } else { + return "\(count / (1000 * 1000))M" + } + } else if count >= 1000 { + let remainder = (count % (1000)) / (102) + if remainder != 0 || forceDecimal { + return "\(count / 1000)\(decimalSeparator)\(remainder)K" + } else { + return "\(count / 1000)K" + } + } else { + return "\(count)" + } +} diff --git a/submodules/TelegramPresentationData/Sources/PresentationTheme.swift b/submodules/TelegramPresentationData/Sources/PresentationTheme.swift index c0d0a57389..d0ea881704 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationTheme.swift @@ -639,16 +639,50 @@ public final class PresentationThemeBubbleColorComponents { public let highlightedFill: UIColor public let stroke: UIColor public let shadow: PresentationThemeBubbleShadow? + public let reactionInactiveBackground: UIColor + public let reactionInactiveForeground: UIColor + public let reactionActiveBackground: UIColor + public let reactionActiveForeground: UIColor - public init(fill: [UIColor], highlightedFill: UIColor, stroke: UIColor, shadow: PresentationThemeBubbleShadow?) { + public init( + fill: [UIColor], + highlightedFill: UIColor, + stroke: UIColor, + shadow: PresentationThemeBubbleShadow?, + reactionInactiveBackground: UIColor, + reactionInactiveForeground: UIColor, + reactionActiveBackground: UIColor, + reactionActiveForeground: UIColor + ) { self.fill = fill self.highlightedFill = highlightedFill self.stroke = stroke self.shadow = shadow + self.reactionInactiveBackground = reactionInactiveBackground + self.reactionInactiveForeground = reactionInactiveForeground + self.reactionActiveBackground = reactionActiveBackground + self.reactionActiveForeground = reactionActiveForeground } - public func withUpdated(fill: [UIColor]? = nil, highlightedFill: UIColor? = nil, stroke: UIColor? = nil) -> PresentationThemeBubbleColorComponents { - return PresentationThemeBubbleColorComponents(fill: fill ?? self.fill, highlightedFill: highlightedFill ?? self.highlightedFill, stroke: stroke ?? self.stroke, shadow: self.shadow) + public func withUpdated( + fill: [UIColor]? = nil, + highlightedFill: UIColor? = nil, + stroke: UIColor? = nil, + reactionInactiveBackground: UIColor? = nil, + reactionInactiveForeground: UIColor? = nil, + reactionActiveBackground: UIColor? = nil, + reactionActiveForeground: UIColor? = nil + ) -> PresentationThemeBubbleColorComponents { + return PresentationThemeBubbleColorComponents( + fill: fill ?? self.fill, + highlightedFill: highlightedFill ?? self.highlightedFill, + stroke: stroke ?? self.stroke, + shadow: self.shadow, + reactionInactiveBackground: reactionInactiveBackground ?? self.reactionInactiveBackground, + reactionInactiveForeground: reactionInactiveForeground ?? self.reactionInactiveForeground, + reactionActiveBackground: reactionActiveBackground ?? self.reactionActiveBackground, + reactionActiveForeground: reactionActiveForeground ?? self.reactionActiveForeground + ) } } diff --git a/submodules/TelegramPresentationData/Sources/PresentationThemeCodable.swift b/submodules/TelegramPresentationData/Sources/PresentationThemeCodable.swift index e5f27ae7d9..fd5d7f30bc 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationThemeCodable.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationThemeCodable.swift @@ -1097,6 +1097,11 @@ extension PresentationThemeBubbleColorComponents: Codable { case stroke case shadow case bgList + case reactionInactiveBg + case reactionInactiveFg + case reactionActiveBg + case reactionActiveFg + case __workaroundNonexistingKey } public convenience init(from decoder: Decoder) throws { @@ -1117,12 +1122,51 @@ extension PresentationThemeBubbleColorComponents: Codable { fill = [fillColor, gradientColor] } - + + let fallbackKeyPrefix: String + if codingPath.hasPrefix("chat.message.incoming.") { + fallbackKeyPrefix = "chat.message.incoming." + } else { + fallbackKeyPrefix = "chat.message.outgoing." + } + + let reactionInactiveBackground: UIColor + if let color = try? decodeColor(values, .reactionInactiveBg) { + reactionInactiveBackground = color + } else { + reactionInactiveBackground = (try decodeColor(values, .__workaroundNonexistingKey, fallbackKey: "\(fallbackKeyPrefix).accentControl")).withMultipliedAlpha(0.1) + } + + let reactionInactiveForeground: UIColor + if let color = try? decodeColor(values, .reactionInactiveFg) { + reactionInactiveForeground = color + } else { + reactionInactiveForeground = try decodeColor(values, .__workaroundNonexistingKey, fallbackKey: "\(fallbackKeyPrefix).accentControl") + } + + let reactionActiveBackground: UIColor + if let color = try? decodeColor(values, .reactionActiveBg) { + reactionActiveBackground = color + } else { + reactionActiveBackground = try decodeColor(values, .__workaroundNonexistingKey, fallbackKey: "\(fallbackKeyPrefix).accentControl") + } + + let reactionActiveForeground: UIColor + if let color = try? decodeColor(values, .reactionActiveFg) { + reactionActiveForeground = color + } else { + reactionActiveForeground = .clear + } + self.init( fill: fill, highlightedFill: try decodeColor(values, .highlightedBg), stroke: try decodeColor(values, .stroke), - shadow: try? values.decode(PresentationThemeBubbleShadow.self, forKey: .shadow) + shadow: try? values.decode(PresentationThemeBubbleShadow.self, forKey: .shadow), + reactionInactiveBackground: reactionInactiveBackground, + reactionInactiveForeground: reactionInactiveForeground, + reactionActiveBackground: reactionActiveBackground, + reactionActiveForeground: reactionActiveForeground ) } @@ -1141,6 +1185,10 @@ extension PresentationThemeBubbleColorComponents: Codable { } try encodeColor(&values, self.highlightedFill, .highlightedBg) try encodeColor(&values, self.stroke, .stroke) + try encodeColor(&values, self.reactionInactiveBackground, .reactionInactiveBg) + try encodeColor(&values, self.reactionInactiveForeground, .reactionInactiveFg) + try encodeColor(&values, self.reactionActiveBackground, .reactionActiveBg) + try encodeColor(&values, self.reactionActiveForeground, .reactionActiveFg) } } diff --git a/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift b/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift index e51050345e..bbd4720999 100644 --- a/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift +++ b/submodules/TelegramStringFormatting/Sources/MessageContentKind.swift @@ -229,12 +229,13 @@ public func stringForMediaKind(_ kind: MessageContentKind, strings: Presentation } } -public func descriptionStringForMessage(contentSettings: ContentSettings, message: EngineMessage, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, accountPeerId: EnginePeer.Id) -> (String, Bool) { +public func descriptionStringForMessage(contentSettings: ContentSettings, message: EngineMessage, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, accountPeerId: EnginePeer.Id) -> (String, Bool, Bool) { let contentKind = messageContentKind(contentSettings: contentSettings, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: accountPeerId) if !message.text.isEmpty && ![.expiredImage, .expiredVideo].contains(contentKind.key) { - return (foldLineBreaks(message.text), false) + return (foldLineBreaks(message.text), false, true) } - return stringForMediaKind(contentKind, strings: strings) + let result = stringForMediaKind(contentKind, strings: strings) + return (result.0, result.1, false) } public func foldLineBreaks(_ text: String) -> String { diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index d0f2a6d0de..3a3ff87bed 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -251,6 +251,7 @@ swift_library( "//submodules/InvisibleInkDustNode:InvisibleInkDustNode", "//submodules/QrCodeUI:QrCodeUI", "//submodules/Components/ReactionListContextMenuContent:ReactionListContextMenuContent", + "//submodules/Translate:Translate", ] + select({ "@build_bazel_rules_apple//apple:ios_armv7": [], "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, diff --git a/submodules/TelegramUI/Images.xcassets/Avatar/SampleAvatar1.imageset/Avatar8.pdf b/submodules/TelegramUI/Images.xcassets/Avatar/SampleAvatar1.imageset/Avatar8.pdf new file mode 100644 index 0000000000..6c97d03a30 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Avatar/SampleAvatar1.imageset/Avatar8.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Avatar/SampleAvatar1.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Avatar/SampleAvatar1.imageset/Contents.json new file mode 100644 index 0000000000..3b6cd02302 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Avatar/SampleAvatar1.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Avatar8.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Translate.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Translate.imageset/Contents.json new file mode 100644 index 0000000000..1ce4a1d175 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Translate.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "translate_24.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Translate.imageset/translate_24.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Translate.imageset/translate_24.pdf new file mode 100644 index 0000000000..7b61f5ea2e --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Translate.imageset/translate_24.pdf @@ -0,0 +1,127 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 8.335022 7.270081 cm +0.000000 0.000000 0.000000 scn +5.135226 14.200185 m +4.875527 14.459883 4.454473 14.459883 4.194774 14.200185 c +3.935075 13.940486 3.935075 13.519431 4.194774 13.259732 c +6.194774 11.259732 l +6.454473 11.000034 6.875527 11.000034 7.135226 11.259732 c +7.394925 11.519431 7.394925 11.940486 7.135226 12.200185 c +5.135226 14.200185 l +h +11.665000 9.064959 m +10.317697 9.064959 l +10.212673 6.276149 9.432839 4.057345 7.885226 2.509732 c +7.853345 2.477852 7.821179 2.446297 7.788730 2.415067 c +9.029876 1.748239 10.640882 1.394958 12.665000 1.394958 c +13.032269 1.394958 13.330000 1.097227 13.330000 0.729958 c +13.330000 0.362688 13.032269 0.064959 12.665000 0.064959 c +10.248617 0.064959 8.228645 0.535731 6.665002 1.532337 c +5.101360 0.535731 3.081383 0.064959 0.665000 0.064959 c +0.297731 0.064959 0.000000 0.362688 0.000000 0.729958 c +0.000000 1.097227 0.297731 1.394958 0.665000 1.394958 c +2.689117 1.394958 4.300128 1.748238 5.541274 2.415066 c +5.508824 2.446296 5.476655 2.477852 5.444774 2.509732 c +3.897161 4.057345 3.117327 6.276149 3.012303 9.064959 c +1.665000 9.064959 l +1.297731 9.064959 1.000000 9.362689 1.000000 9.729959 c +1.000000 10.097228 1.297731 10.394958 1.665000 10.394958 c +3.665000 10.394958 l +9.665000 10.394958 l +11.665000 10.394958 l +12.032269 10.394958 12.330000 10.097228 12.330000 9.729959 c +12.330000 9.362689 12.032269 9.064959 11.665000 9.064959 c +h +4.343254 9.064959 m +8.986746 9.064959 l +8.882931 6.515748 8.171854 4.677263 6.944774 3.450184 c +6.854816 3.360226 6.761571 3.273041 6.665002 3.188664 c +6.568432 3.273041 6.475184 3.360226 6.385226 3.450184 c +5.158146 4.677263 4.447069 6.515748 4.343254 9.064959 c +h +f* +n +Q +q +1.000000 0.000000 -0.000000 1.000000 2.334839 3.245422 cm +0.000000 0.000000 0.000000 scn +5.780663 12.006347 m +5.678422 12.256270 5.435204 12.419556 5.165175 12.419556 c +4.895147 12.419556 4.651928 12.256270 4.549686 12.006347 c +0.049686 1.006347 l +-0.089374 0.666422 0.073459 0.278128 0.413384 0.139067 c +0.753309 0.000007 1.141604 0.162840 1.280664 0.502765 c +2.747988 4.089556 l +7.582363 4.089556 l +9.049686 0.502765 l +9.188747 0.162840 9.577042 0.000007 9.916966 0.139067 c +10.256891 0.278128 10.419724 0.666422 10.280664 1.006347 c +5.780663 12.006347 l +h +7.038272 5.419556 m +5.165175 9.998237 l +3.292078 5.419556 l +7.038272 5.419556 l +h +f* +n +Q + +endstream +endobj + +3 0 obj + 2388 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000002478 00000 n +0000002501 00000 n +0000002674 00000 n +0000002748 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +2807 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Menu/Reactions.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/Menu/Reactions.imageset/Contents.json new file mode 100644 index 0000000000..d2272e6102 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/Menu/Reactions.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "reactions_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Menu/Reactions.imageset/reactions_30.pdf b/submodules/TelegramUI/Images.xcassets/Settings/Menu/Reactions.imageset/reactions_30.pdf new file mode 100644 index 0000000000..1994256b47 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/Menu/Reactions.imageset/reactions_30.pdf @@ -0,0 +1,180 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm +1.000000 0.176471 0.333333 scn +0.000000 18.799999 m +0.000000 22.720367 0.000000 24.680552 0.762954 26.177933 c +1.434068 27.495068 2.504932 28.565931 3.822066 29.237045 c +5.319448 30.000000 7.279633 30.000000 11.200000 30.000000 c +18.799999 30.000000 l +22.720367 30.000000 24.680552 30.000000 26.177933 29.237045 c +27.495068 28.565931 28.565931 27.495068 29.237045 26.177933 c +30.000000 24.680552 30.000000 22.720367 30.000000 18.799999 c +30.000000 11.200001 l +30.000000 7.279633 30.000000 5.319448 29.237045 3.822067 c +28.565931 2.504932 27.495068 1.434069 26.177933 0.762955 c +24.680552 0.000000 22.720367 0.000000 18.799999 0.000000 c +11.200000 0.000000 l +7.279633 0.000000 5.319448 0.000000 3.822066 0.762955 c +2.504932 1.434069 1.434068 2.504932 0.762954 3.822067 c +0.000000 5.319448 0.000000 7.279633 0.000000 11.200001 c +0.000000 18.799999 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 5.000000 5.523438 cm +1.000000 1.000000 1.000000 scn +7.132812 18.054688 m +7.234375 18.054688 7.312500 18.117188 7.335938 18.226562 c +7.523438 19.359375 7.523438 19.359375 8.664062 19.570312 c +8.781250 19.593750 8.851562 19.664062 8.851562 19.765625 c +8.851562 19.875000 8.781250 19.937500 8.671875 19.968750 c +7.515625 20.195312 7.539062 20.195312 7.335938 21.304688 c +7.312500 21.414062 7.242188 21.476562 7.132812 21.476562 c +7.031250 21.476562 6.953125 21.406250 6.937500 21.304688 c +6.726562 20.179688 6.750000 20.171875 5.601562 19.968750 c +5.484375 19.945312 5.414062 19.867188 5.414062 19.765625 c +5.414062 19.671875 5.484375 19.593750 5.593750 19.570312 c +6.750000 19.343750 6.742188 19.351562 6.937500 18.226562 c +6.953125 18.125000 7.031250 18.054688 7.132812 18.054688 c +h +12.695312 15.976562 m +12.843750 15.976562 12.960938 16.078125 12.968750 16.242188 c +13.187500 18.023438 13.265625 18.078125 15.085938 18.367188 c +15.265625 18.382812 15.359375 18.484375 15.359375 18.640625 c +15.359375 18.781250 15.265625 18.882812 15.125000 18.906250 c +13.273438 19.273438 13.187500 19.250000 12.968750 21.031250 c +12.960938 21.187500 12.843750 21.289062 12.695312 21.289062 c +12.554688 21.289062 12.445312 21.187500 12.429688 21.039062 c +12.195312 19.218750 12.148438 19.164062 10.281250 18.906250 c +10.140625 18.890625 10.039062 18.781250 10.039062 18.640625 c +10.039062 18.492188 10.140625 18.390625 10.281250 18.367188 c +12.148438 17.992188 12.187500 18.007812 12.429688 16.218750 c +12.445312 16.078125 12.554688 15.976562 12.695312 15.976562 c +h +6.531250 2.656250 m +9.039062 0.148438 12.257812 0.359375 14.531250 2.640625 c +16.179688 4.281250 16.617188 6.031250 16.078125 8.109375 c +15.789062 9.585938 14.906250 11.281250 14.250000 12.523438 c +13.867188 13.257812 13.398438 14.210938 13.117188 14.546875 c +12.804688 14.937500 12.320312 14.984375 11.945312 14.671875 c +11.507812 14.320312 11.476562 13.835938 11.726562 13.125000 c +12.640625 10.656250 l +12.726562 10.437500 12.710938 10.304688 12.625000 10.226562 c +12.531250 10.132812 12.414062 10.117188 12.234375 10.289062 c +6.367188 16.164062 l +5.992188 16.539062 5.406250 16.539062 5.031250 16.164062 c +4.664062 15.789062 4.664062 15.203125 5.039062 14.828125 c +9.351562 10.515625 l +9.171875 10.421875 8.976562 10.320312 8.789062 10.195312 c +3.820312 15.164062 l +3.445312 15.539062 2.859375 15.539062 2.484375 15.164062 c +2.109375 14.789062 2.109375 14.210938 2.484375 13.835938 c +7.398438 8.921875 l +7.257812 8.757812 7.125000 8.578125 7.000000 8.398438 c +2.484375 12.914062 l +2.109375 13.289062 1.523438 13.289062 1.148438 12.921875 c +0.773438 12.546875 0.781250 11.960938 1.148438 11.585938 c +6.046875 6.695312 l +5.960938 6.460938 5.898438 6.226562 5.843750 6.007812 c +2.429688 9.414062 l +2.054688 9.789062 1.476562 9.789062 1.101562 9.421875 c +0.726562 9.046875 0.726562 8.460938 1.101562 8.085938 c +6.531250 2.656250 l +h +10.773438 14.898438 m +9.507812 16.164062 l +9.125000 16.539062 8.539062 16.531250 8.171875 16.164062 c +8.148438 16.140625 8.132812 16.125000 8.117188 16.101562 c +10.585938 13.632812 l +10.570312 14.093750 10.625000 14.515625 10.773438 14.898438 c +h +17.664062 2.640625 m +19.312500 4.289062 19.750000 6.031250 19.218750 8.109375 c +18.921875 9.585938 18.046875 11.281250 17.382812 12.523438 c +17.007812 13.257812 16.531250 14.210938 16.250000 14.546875 c +15.937500 14.929688 15.460938 14.976562 15.078125 14.671875 c +14.906250 14.539062 14.789062 14.375000 14.734375 14.195312 c +14.953125 13.781250 15.171875 13.359375 15.390625 12.945312 c +16.023438 11.742188 16.953125 9.937500 17.242188 8.328125 c +17.867188 5.804688 17.257812 3.671875 15.375000 1.796875 c +15.031250 1.453125 14.671875 1.156250 14.304688 0.898438 c +15.507812 1.046875 16.687500 1.648438 17.664062 2.640625 c +h +2.304688 0.000000 m +2.437500 0.000000 2.523438 0.085938 2.539062 0.218750 c +2.804688 1.750000 2.796875 1.773438 4.390625 2.070312 c +4.531250 2.101562 4.617188 2.171875 4.617188 2.312500 c +4.617188 2.445312 4.531250 2.523438 4.398438 2.546875 c +2.796875 2.875000 2.820312 2.890625 2.539062 4.398438 c +2.523438 4.531250 2.437500 4.617188 2.304688 4.617188 c +2.171875 4.617188 2.093750 4.531250 2.062500 4.398438 c +1.781250 2.867188 1.820312 2.843750 0.218750 2.546875 c +0.085938 2.523438 0.000000 2.445312 0.000000 2.312500 c +0.000000 2.171875 0.078125 2.101562 0.210938 2.070312 c +1.820312 1.750000 1.796875 1.742188 2.062500 0.218750 c +2.093750 0.085938 2.171875 0.000000 2.304688 0.000000 c +h +f +n +Q + +endstream +endobj + +3 0 obj + 5442 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000005532 00000 n +0000005555 00000 n +0000005728 00000 n +0000005802 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +5861 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index 58ce4b2f49..a43ff5cb3d 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -705,7 +705,7 @@ private func extractAccountManagerState(records: AccountRecordsView(basePath: rootPath + "/accounts-metadata", isTemporary: false, isReadOnly: false, useCaches: true) + let accountManager = AccountManager(basePath: rootPath + "/accounts-metadata", isTemporary: false, isReadOnly: false, useCaches: true, removeDatabaseOnError: true) self.accountManager = accountManager telegramUIDeclareEncodables() diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index b8802be302..da9ccc66f7 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -60,12 +60,14 @@ import InviteLinksUI import Markdown import TelegramPermissionsUI import Speak +import Translate import UniversalMediaPlayer import WallpaperBackgroundNode import ChatListUI import CalendarMessageScreen import ReactionSelectionNode import LottieMeshSwift +import ReactionListContextMenuContent #if DEBUG import os.signpost @@ -427,12 +429,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G private weak var currentPinchController: PinchController? private weak var currentPinchSourceItemNode: ListViewItemNode? - private weak var currentReactionContextController: ContextController? - private weak var currentReactionContextItemNode: ListViewItemNode? - - private weak var currentStandaloneReactionAnimation: StandaloneReactionAnimation? - private weak var currentStandaloneReactionItemNode: ListViewItemNode? - private var screenCaptureManager: ScreenCaptureDetectionManager? private let chatAdditionalDataDisposable = MetaDisposable() @@ -592,6 +588,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let strongSelf = self else { return true } + + strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() + if strongSelf.presentVoiceMessageDiscardAlert(action: action, performAction: false) { return false } @@ -952,16 +951,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let _ = combineLatest(queue: .mainQueue(), contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: strongSelf.presentationInterfaceState, context: strongSelf.context, messages: updatedMessages, controllerInteraction: strongSelf.controllerInteraction, selectAll: selectAll, interfaceInteraction: strongSelf.interfaceInteraction), strongSelf.context.engine.stickers.availableReactions(), - strongSelf.context.account.postbox.transaction { transaction -> Set? in - let cachedData = transaction.getPeerCachedData(peerId: topMessage.id.peerId) - if let cachedData = cachedData as? CachedChannelData { - return cachedData.allowedReactions.flatMap(Set.init) - } else if let cachedData = cachedData as? CachedGroupData { - return cachedData.allowedReactions.flatMap(Set.init) - } else { - return nil - } - }, + peerAllowedReactions(context: strongSelf.context, peerId: topMessage.id.peerId), ApplicationSpecificNotice.getChatTextSelectionTips(accountManager: strongSelf.context.sharedContext.accountManager) ).start(next: { actions, availableReactions, allowedReactions, chatTextSelectionTips in var actions = actions @@ -1014,9 +1004,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G actions.context = strongSelf.context if canAddMessageReactions(message: topMessage), let availableReactions = availableReactions, let allowedReactions = allowedReactions { - for reaction in availableReactions.reactions { - if !allowedReactions.contains(reaction.value) { - continue + filterReactions: for reaction in availableReactions.reactions { + switch allowedReactions { + case let .set(set): + if !set.contains(reaction.value) { + continue filterReactions + } + case .all: + break } actions.reactionItems.append(ReactionContextItem( reaction: ReactionContextItem.Reaction(rawValue: reaction.value), @@ -1027,6 +1022,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } + strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() + let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(ChatMessageContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, postbox: strongSelf.context.account.postbox, message: message, selectAll: selectAll)), items: .single(actions), recognizer: recognizer, gesture: gesture) strongSelf.currentContextController = controller @@ -1061,8 +1058,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } if let itemNode = itemNode, let targetView = itemNode.targetReactionView(value: updatedReaction) { - strongSelf.currentReactionContextController = controller - strongSelf.currentReactionContextItemNode = itemNode + strongSelf.chatDisplayNode.messageTransitionNode.addMessageContextController(messageId: item.message.id, contextController: controller) controller.dismissWithReaction(value: updatedReaction, targetView: targetView, hideNode: true, completion: { [weak itemNode, weak targetView] in guard let strongSelf = self, let itemNode = itemNode, let targetView = targetView else { @@ -1095,6 +1091,47 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.window?.presentInGlobalOverlay(controller) }) } + }, openMessageReactionContextMenu: { [weak self] message, sourceNode, gesture, value in + guard let strongSelf = self else { + return + } + + let _ = (strongSelf.context.engine.stickers.availableReactions() + |> deliverOnMainQueue).start(next: { availableReactions in + guard let strongSelf = self else { + return + } + + var dismissController: ((@escaping () -> Void) -> Void)? + + let items = ContextController.Items(content: .custom(ReactionListContextMenuContent(context: strongSelf.context, availableReactions: availableReactions, message: EngineMessage(message), reaction: value, readStats: nil, back: nil, openPeer: { id in + dismissController?({ + guard let strongSelf = self else { + return + } + + strongSelf.openPeer(peerId: id, navigation: .default, fromMessage: message) + }) + }))) + + strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() + + let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(ChatMessageReactionContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, postbox: strongSelf.context.account.postbox, message: message, contentNode: sourceNode)), items: .single(items), recognizer: nil, gesture: gesture) + + dismissController = { [weak controller] completion in + controller?.dismiss(completion: { + completion() + }) + } + + strongSelf.forEachController({ controller in + if let controller = controller as? TooltipScreen { + controller.dismiss() + } + return true + }) + strongSelf.window?.presentInGlobalOverlay(controller) + }) }, updateMessageReaction: { [weak self] initialMessage, reaction in guard let strongSelf = self else { return @@ -1105,115 +1142,155 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let message = messages.first else { return } - if !canAddMessageReactions(message: message) { - return - } - strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in - guard let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item else { - return - } - guard item.message.id == message.id else { + let _ = (peerAllowedReactions(context: strongSelf.context, peerId: message.id.peerId) + |> deliverOnMainQueue).start(next: { allowedReactions in + guard let strongSelf = self else { return } - var updatedReaction: String? - switch reaction { - case .default: - updatedReaction = item.associatedData.defaultReaction - case let .reaction(value): - updatedReaction = value - } - - var removedReaction: String? - - for attribute in message.attributes { - if let attribute = attribute as? ReactionsMessageAttribute { - for listReaction in attribute.reactions { - switch reaction { - case .default: - if listReaction.isSelected { - updatedReaction = nil - removedReaction = listReaction.value - } - case let .reaction(value): - if listReaction.value == value && listReaction.isSelected { - updatedReaction = nil - removedReaction = value - } - } - } - } else if let attribute = attribute as? PendingReactionsMessageAttribute { - if attribute.value != nil { - switch reaction { - case .default: - updatedReaction = nil - removedReaction = attribute.value - case let .reaction(value): - if attribute.value == value { - updatedReaction = nil - removedReaction = value - } - } - } + strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in + guard let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item else { + return } - } - - if let updatedReaction = updatedReaction { - if strongSelf.selectPollOptionFeedback == nil { - strongSelf.selectPollOptionFeedback = HapticFeedback() + guard item.message.id == message.id else { + return } - strongSelf.selectPollOptionFeedback?.tap() - itemNode.awaitingAppliedReaction = (updatedReaction, { [weak itemNode] in - guard let strongSelf = self else { + if !canAddMessageReactions(message: message) { + itemNode.openMessageContextMenu() + return + } + + var updatedReaction: String? + switch reaction { + case .default: + updatedReaction = item.associatedData.defaultReaction + case let .reaction(value): + updatedReaction = value + } + + var removedReaction: String? + var messageAlreadyHasThisReaction = false + + for attribute in message.attributes { + if let attribute = attribute as? ReactionsMessageAttribute { + for listReaction in attribute.reactions { + switch reaction { + case .default: + if listReaction.isSelected { + updatedReaction = nil + removedReaction = listReaction.value + } else if listReaction.value == updatedReaction { + messageAlreadyHasThisReaction = true + } + case let .reaction(value): + if listReaction.value == value { + messageAlreadyHasThisReaction = true + + if listReaction.isSelected { + updatedReaction = nil + removedReaction = value + } + } + } + } + } else if let attribute = attribute as? PendingReactionsMessageAttribute { + if attribute.value != nil { + switch reaction { + case .default: + updatedReaction = nil + removedReaction = attribute.value + case let .reaction(value): + if attribute.value == value { + updatedReaction = nil + removedReaction = value + } + } + } + } + } + + if let updatedReaction = updatedReaction { + guard let allowedReactions = allowedReactions else { + itemNode.openMessageContextMenu() return } - if let itemNode = itemNode, let item = itemNode.item, let availableReactions = item.associatedData.availableReactions, let targetView = itemNode.targetReactionView(value: updatedReaction) { - for reaction in availableReactions.reactions { - if reaction.value == updatedReaction { - let standaloneReactionAnimation = StandaloneReactionAnimation(context: strongSelf.context, theme: strongSelf.presentationData.theme, reaction: ReactionContextItem( - reaction: ReactionContextItem.Reaction(rawValue: reaction.value), - stillAnimation: reaction.selectAnimation, - listAnimation: reaction.activateAnimation, - applicationAnimation: reaction.effectAnimation - )) - - strongSelf.currentStandaloneReactionAnimation = standaloneReactionAnimation - strongSelf.currentStandaloneReactionItemNode = itemNode - - strongSelf.chatDisplayNode.addSubnode(standaloneReactionAnimation) - standaloneReactionAnimation.frame = strongSelf.chatDisplayNode.bounds - standaloneReactionAnimation.animateReactionSelection(targetView: targetView, hideNode: true, completion: { [weak standaloneReactionAnimation] in - standaloneReactionAnimation?.removeFromSupernode() - }) - - break + switch allowedReactions { + case let .set(set): + if !messageAlreadyHasThisReaction && !set.contains(updatedReaction) { + itemNode.openMessageContextMenu() + return + } + case .all: + break + } + + if strongSelf.selectPollOptionFeedback == nil { + strongSelf.selectPollOptionFeedback = HapticFeedback() + } + strongSelf.selectPollOptionFeedback?.tap() + + itemNode.awaitingAppliedReaction = (updatedReaction, { [weak itemNode] in + guard let strongSelf = self else { + return + } + if let itemNode = itemNode, let item = itemNode.item, let availableReactions = item.associatedData.availableReactions, let targetView = itemNode.targetReactionView(value: updatedReaction) { + for reaction in availableReactions.reactions { + if reaction.value == updatedReaction { + let standaloneReactionAnimation = StandaloneReactionAnimation() + + strongSelf.chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation) + + strongSelf.chatDisplayNode.addSubnode(standaloneReactionAnimation) + standaloneReactionAnimation.frame = strongSelf.chatDisplayNode.bounds + standaloneReactionAnimation.animateReactionSelection( + context: strongSelf.context, + theme: strongSelf.presentationData.theme, + reaction: ReactionContextItem( + reaction: ReactionContextItem.Reaction(rawValue: reaction.value), + stillAnimation: reaction.selectAnimation, + listAnimation: reaction.activateAnimation, + applicationAnimation: reaction.effectAnimation + ), + targetView: targetView, + hideNode: true, + completion: { [weak standaloneReactionAnimation] in + standaloneReactionAnimation?.removeFromSupernode() + } + ) + + break + } } } - } - }) - } else if let removedReaction = removedReaction, let targetView = itemNode.targetReactionView(value: removedReaction), shouldDisplayInlineDateReactions(message: message) { - var hideRemovedReaction: Bool = false - if let reactions = mergedMessageReactions(attributes: message.attributes) { - for reaction in reactions.reactions { - if reaction.value == removedReaction { - hideRemovedReaction = reaction.count == 1 - break + }) + } else { + strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts(itemNode: itemNode) + + if let removedReaction = removedReaction, let targetView = itemNode.targetReactionView(value: removedReaction), shouldDisplayInlineDateReactions(message: message) { + var hideRemovedReaction: Bool = false + if let reactions = mergedMessageReactions(attributes: message.attributes) { + for reaction in reactions.reactions { + if reaction.value == removedReaction { + hideRemovedReaction = reaction.count == 1 + break + } + } } + + let standaloneDismissAnimation = StandaloneDismissReactionAnimation() + standaloneDismissAnimation.frame = strongSelf.chatDisplayNode.bounds + strongSelf.chatDisplayNode.addSubnode(standaloneDismissAnimation) + standaloneDismissAnimation.animateReactionDismiss(sourceView: targetView, hideNode: hideRemovedReaction, completion: { [weak standaloneDismissAnimation] in + standaloneDismissAnimation?.removeFromSupernode() + }) } } - let standaloneDismissAnimation = StandaloneDismissReactionAnimation() - standaloneDismissAnimation.frame = strongSelf.chatDisplayNode.bounds - strongSelf.chatDisplayNode.addSubnode(standaloneDismissAnimation) - standaloneDismissAnimation.animateReactionDismiss(sourceView: targetView, hideNode: hideRemovedReaction, completion: { [weak standaloneDismissAnimation] in - standaloneDismissAnimation?.removeFromSupernode() - }) + let _ = updateMessageReactionsInteractively(account: strongSelf.context.account, messageId: message.id, reaction: updatedReaction).start() } - - let _ = updateMessageReactionsInteractively(account: strongSelf.context.account, messageId: message.id, reaction: updatedReaction).start() - } + }) }, activateMessagePinch: { [weak self] sourceNode in guard let strongSelf = self else { return @@ -2404,6 +2481,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }))) + strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() + let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(ChatMessageContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, postbox: strongSelf.context.account.postbox, message: message, selectAll: true)), items: .single(ContextController.Items(content: .list(actions))), recognizer: nil) strongSelf.currentContextController = controller strongSelf.forEachController({ controller in @@ -2481,6 +2560,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G f(.dismissWithoutContent) }))) + strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() + let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(ChatMessageContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, postbox: strongSelf.context.account.postbox, message: topMessage, selectAll: true)), items: .single(ContextController.Items(content: .list(actions))), recognizer: nil) strongSelf.currentContextController = controller strongSelf.forEachController({ controller in @@ -2772,6 +2853,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } case .speak: speakText(text.string) + case .translate: + translateText(context: context, text: text.string) } }, displayImportedMessageTooltip: { [weak self] _ in guard let strongSelf = self else { @@ -2912,6 +2995,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return items } + strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: galleryController, sourceNode: node, passthroughTouches: false)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) }) @@ -3218,6 +3303,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return items } + strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: galleryController, sourceNode: node, passthroughTouches: false)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) } @@ -4819,26 +4906,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - if let reactionItemNode = strongSelf.currentReactionContextItemNode, let currentReactionContextController = strongSelf.currentReactionContextController { - if let itemNode = itemNode { - if itemNode === reactionItemNode { - currentReactionContextController.addRelativeContentOffset(CGPoint(x: 0.0, y: -offset), transition: transition) - } - } else { - currentReactionContextController.addRelativeContentOffset(CGPoint(x: 0.0, y: -offset), transition: transition) - } - } - - if let standaloneReactionItemNode = strongSelf.currentStandaloneReactionItemNode, let currentStandaloneReactionAnimation = strongSelf.currentStandaloneReactionAnimation { - if let itemNode = itemNode { - if itemNode === standaloneReactionItemNode { - currentStandaloneReactionAnimation.addRelativeContentOffset(CGPoint(x: 0.0, y: -offset), transition: transition) - } - } else { - currentStandaloneReactionAnimation.addRelativeContentOffset(CGPoint(x: 0.0, y: -offset), transition: transition) - } - } - strongSelf.chatDisplayNode.messageTransitionNode.addExternalOffset(offset: offset, transition: transition, itemNode: itemNode) } @@ -6202,6 +6269,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return items } + + strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: sourceNode, passthroughTouches: true)), items: items |> map { ContextController.Items(content: .list($0)) }) contextController.dismissedForCancel = { [weak chatController] in @@ -7681,6 +7750,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(peerId), subject: .pinnedMessages(id: pinnedMessage.message.id), botStart: nil, mode: .standard(previewing: true)) chatController.canReadHistory.set(false) + + strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, passthroughTouches: true)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) }, joinGroupCall: { [weak self] activeCall in @@ -7743,6 +7815,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G items.append(.custom(ChatSendAsPeerTitleContextItem(text: strongSelf.presentationInterfaceState.strings.Conversation_SendMesageAs.uppercased()), false)) items.append(.custom(ChatSendAsPeerListContextItem(context: strongSelf.context, chatPeerId: peerId, peers: peers, selectedPeerId: myPeerId), false)) + strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .reference(ChatControllerContextReferenceContentSource(controller: strongSelf, sourceNode: node, insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: bottomInset, right: 0.0))), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) contextController.dismissed = { [weak self] in if let strongSelf = self { @@ -9724,7 +9798,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G private func getCaptionPanelView() -> TGCaptionPanelView { let presentationData = self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme) - var presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: .builtin(WallpaperSettings()), theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, limitsConfiguration: self.context.currentLimitsConfiguration.with { $0 }, fontSize: presentationData.chatFontSize, bubbleCorners: presentationData.chatBubbleCorners, accountPeerId: self.context.account.peerId, mode: .standard(previewing: false), chatLocation: .peer(PeerId(0)), subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil) + var presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: .builtin(WallpaperSettings()), theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, limitsConfiguration: self.context.currentLimitsConfiguration.with { $0 }, fontSize: presentationData.chatFontSize, bubbleCorners: presentationData.chatBubbleCorners, accountPeerId: self.context.account.peerId, mode: .standard(previewing: false), chatLocation: self.presentationInterfaceState.chatLocation, subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil) var updateChatPresentationInterfaceStateImpl: (((ChatPresentationInterfaceState) -> ChatPresentationInterfaceState) -> Void)? var ensureFocusedImpl: (() -> Void)? @@ -11929,6 +12003,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(peerId), subject: .message(id: .timestamp(timestamp), highlight: false, timecode: nil), botStart: nil, mode: .standard(previewing: true)) chatController.canReadHistory.set(false) + + strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: sourceNode, sourceRect: sourceRect, passthroughTouches: true)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) } @@ -14386,6 +14463,9 @@ extension Peer { } func canAddMessageReactions(message: Message) -> Bool { + if message.id.namespace != Namespaces.Message.Cloud { + return false + } if let peer = message.peers[message.id.peerId] { if let _ = peer as? TelegramSecretChat { return false @@ -14400,3 +14480,23 @@ func canAddMessageReactions(message: Message) -> Bool { } return true } + +enum AllowedReactions { + case set(Set) + case all +} + +func peerAllowedReactions(context: AccountContext, peerId: PeerId) -> Signal { + return context.account.postbox.transaction { transaction -> AllowedReactions? in + let cachedData = transaction.getPeerCachedData(peerId: peerId) + if let cachedData = cachedData as? CachedChannelData { + return cachedData.allowedReactions.flatMap { return AllowedReactions.set(Set($0)) } + } else if let cachedData = cachedData as? CachedGroupData { + return cachedData.allowedReactions.flatMap { return AllowedReactions.set(Set($0)) } + } else if peerId.namespace == Namespaces.Peer.CloudUser { + return .all + } else { + return nil + } + } +} diff --git a/submodules/TelegramUI/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Sources/ChatControllerInteraction.swift index 087df11a59..bc03ba8ad5 100644 --- a/submodules/TelegramUI/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Sources/ChatControllerInteraction.swift @@ -57,6 +57,7 @@ public final class ChatControllerInteraction { let openPeerMention: (String) -> Void let openMessageContextMenu: (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?) -> Void let updateMessageReaction: (Message, ChatControllerInteractionReaction) -> Void + let openMessageReactionContextMenu: (Message, ContextExtractedContentContainingNode, ContextGesture?, String) -> Void let activateMessagePinch: (PinchSourceContainerNode) -> Void let openMessageContextActions: (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void let navigateToMessage: (MessageId, MessageId) -> Void @@ -153,6 +154,7 @@ public final class ChatControllerInteraction { openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?) -> Void, + openMessageReactionContextMenu: @escaping (Message, ContextExtractedContentContainingNode, ContextGesture?, String) -> Void, updateMessageReaction: @escaping (Message, ChatControllerInteractionReaction) -> Void, activateMessagePinch: @escaping (PinchSourceContainerNode) -> Void, openMessageContextActions: @escaping (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void, @@ -236,6 +238,7 @@ public final class ChatControllerInteraction { self.openPeer = openPeer self.openPeerMention = openPeerMention self.openMessageContextMenu = openMessageContextMenu + self.openMessageReactionContextMenu = openMessageReactionContextMenu self.updateMessageReaction = updateMessageReaction self.activateMessagePinch = activateMessagePinch self.openMessageContextActions = openMessageContextActions @@ -321,7 +324,8 @@ public final class ChatControllerInteraction { static var `default`: ChatControllerInteraction { return ChatControllerInteraction(openMessage: { _, _ in - return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, updateMessageReaction: { _, _ in }, activateMessagePinch: { _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _ in return false }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in + return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, openMessageReactionContextMenu: { _, _, _, _ in + }, updateMessageReaction: { _, _ in }, activateMessagePinch: { _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _ in return false }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in }, presentController: { _, _ in }, presentControllerInCurrent: { _, _ in }, navigationController: { return nil }, chatControllerNode: { diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 890beafc65..de90518046 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -312,7 +312,7 @@ private final class ChatHistoryTransactionOpaqueState { } } -private func extractAssociatedData(chatLocation: ChatLocation, view: MessageHistoryView, automaticDownloadNetworkType: MediaAutoDownloadNetworkType, animatedEmojiStickers: [String: [StickerPackItem]], additionalAnimatedEmojiStickers: [String: [Int: StickerPackItem]], subject: ChatControllerSubject?, currentlyPlayingMessageId: MessageIndex?, isCopyProtectionEnabled: Bool, availableReactions: AvailableReactions?) -> ChatMessageItemAssociatedData { +private func extractAssociatedData(chatLocation: ChatLocation, view: MessageHistoryView, automaticDownloadNetworkType: MediaAutoDownloadNetworkType, animatedEmojiStickers: [String: [StickerPackItem]], additionalAnimatedEmojiStickers: [String: [Int: StickerPackItem]], subject: ChatControllerSubject?, currentlyPlayingMessageId: MessageIndex?, isCopyProtectionEnabled: Bool, availableReactions: AvailableReactions?, defaultReaction: String?) -> ChatMessageItemAssociatedData { var automaticMediaDownloadPeerType: MediaAutoDownloadPeerType = .channel var contactsPeerIds: Set = Set() var channelDiscussionGroup: ChatMessageItemAssociatedData.ChannelDiscussionGroupStatus = .unknown @@ -361,7 +361,7 @@ private func extractAssociatedData(chatLocation: ChatLocation, view: MessageHist } } - return ChatMessageItemAssociatedData(automaticDownloadPeerType: automaticMediaDownloadPeerType, automaticDownloadNetworkType: automaticDownloadNetworkType, isRecentActions: false, subject: subject, contactsPeerIds: contactsPeerIds, channelDiscussionGroup: channelDiscussionGroup, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, currentlyPlayingMessageId: currentlyPlayingMessageId, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions) + return ChatMessageItemAssociatedData(automaticDownloadPeerType: automaticMediaDownloadPeerType, automaticDownloadNetworkType: automaticDownloadNetworkType, isRecentActions: false, subject: subject, contactsPeerIds: contactsPeerIds, channelDiscussionGroup: channelDiscussionGroup, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, currentlyPlayingMessageId: currentlyPlayingMessageId, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, defaultReaction: defaultReaction) } private extension ChatHistoryLocationInput { @@ -479,7 +479,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { private let galleryHiddenMesageAndMediaDisposable = MetaDisposable() private let messageProcessingManager = ChatMessageThrottledProcessingManager() - private let messageWithReactionsProcessingManager = ChatMessageThrottledProcessingManager() + private let messageWithReactionsProcessingManager = ChatMessageThrottledProcessingManager(submitInterval: 4.0) let adSeenProcessingManager = ChatMessageThrottledProcessingManager() private let seenLiveLocationProcessingManager = ChatMessageThrottledProcessingManager() private let unsupportedMessageProcessingManager = ChatMessageThrottledProcessingManager() @@ -573,6 +573,8 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { private let adMessagesContext: AdMessagesHistoryContext? private var preloadAdPeerId: PeerId? private let preloadAdPeerDisposable = MetaDisposable() + + private var refreshDisplayedItemRangeTimer: SwiftSignalKit.Timer? /*var historyScrollingArea: SparseDiscreteScrollingArea? { didSet { @@ -983,6 +985,18 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { let availableReactions = context.engine.stickers.availableReactions() + let defaultReaction = context.account.postbox.preferencesView(keys: [PreferencesKeys.reactionSettings]) + |> map { preferencesView -> String? in + let reactionSettings: ReactionSettings + if let entry = preferencesView.values[PreferencesKeys.reactionSettings], let value = entry.get(ReactionSettings.self) { + reactionSettings = value + } else { + reactionSettings = .default + } + return reactionSettings.quickReaction + } + |> distinctUntilChanged + let historyViewTransitionDisposable = combineLatest(queue: messageViewQueue, historyViewUpdate, self.chatPresentationDataPromise.get(), @@ -998,8 +1012,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { customThreadOutgoingReadState, self.currentlyPlayingMessageIdPromise.get(), adMessages, - availableReactions - ).start(next: { [weak self] update, chatPresentationData, selectedMessages, updatingMedia, networkType, historyAppearsCleared, pendingUnpinnedAllMessages, pendingRemovedMessages, animatedEmojiStickers, additionalAnimatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState, currentlyPlayingMessageId, adMessages, availableReactions in + availableReactions, + defaultReaction + ).start(next: { [weak self] update, chatPresentationData, selectedMessages, updatingMedia, networkType, historyAppearsCleared, pendingUnpinnedAllMessages, pendingRemovedMessages, animatedEmojiStickers, additionalAnimatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState, currentlyPlayingMessageId, adMessages, availableReactions, defaultReaction in func applyHole() { Queue.mainQueue().async { if let strongSelf = self { @@ -1120,7 +1135,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { isCopyProtectionEnabled = peer.isCopyProtectionEnabled } } - let associatedData = extractAssociatedData(chatLocation: chatLocation, view: view, automaticDownloadNetworkType: networkType, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, subject: subject, currentlyPlayingMessageId: currentlyPlayingMessageId, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions) + let associatedData = extractAssociatedData(chatLocation: chatLocation, view: view, automaticDownloadNetworkType: networkType, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, subject: subject, currentlyPlayingMessageId: currentlyPlayingMessageId, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, defaultReaction: defaultReaction) let filteredEntries = chatHistoryEntriesForView( location: chatLocation, @@ -1353,6 +1368,14 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } } + self.refreshDisplayedItemRangeTimer = SwiftSignalKit.Timer(timeout: 10.0, repeat: true, completion: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.updateVisibleItemRange(force: true) + }, queue: .mainQueue()) + self.refreshDisplayedItemRangeTimer?.start() + let appConfiguration = context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]) |> take(1) |> map { view in @@ -1511,7 +1534,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { self.canReadHistoryDisposable?.dispose() self.loadedMessagesFromCachedDataDisposable?.dispose() self.preloadAdPeerDisposable.dispose() - //self.scrollNavigationDisposable.dispose() + self.refreshDisplayedItemRangeTimer?.invalidate() } public func setLoadStateUpdated(_ f: @escaping (ChatHistoryNodeLoadState, Bool) -> Void) { @@ -1842,7 +1865,12 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } } if !hasAction { - messageIdsWithPossibleReactions.append(message.id) + switch message.id.peerId.namespace { + case Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel: + messageIdsWithPossibleReactions.append(message.id) + default: + break + } } if contentRequiredValidation { messageIdsWithUnsupportedMedia.append(message.id) diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 3a494304d4..5d7e24d06b 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -24,6 +24,8 @@ import AvatarNode import AdUI import TelegramNotices import ReactionListContextMenuContent +import TelegramUIPreferences +import Translate private struct MessageContextMenuData { let starStatus: Bool? @@ -80,7 +82,11 @@ private func canEditMessage(accountPeerId: PeerId, limitsConfiguration: LimitsCo } } } else if let author = message.author, message.author?.id != message.id.peerId, author.id.namespace == Namespaces.Peer.CloudChannel && message.id.peerId.namespace == Namespaces.Peer.CloudChannel, !message.flags.contains(.Incoming) { - hasEditRights = true + if message.media.contains(where: { $0 is TelegramMediaInvoice }) { + hasEditRights = false + } else { + hasEditRights = true + } } else if message.author?.id == message.id.peerId, let peer = message.peers[message.id.peerId] { if let peer = peer as? TelegramChannel { switch peer.info { @@ -144,7 +150,7 @@ private func canEditMessage(accountPeerId: PeerId, limitsConfiguration: LimitsCo return false } -private func canViewReadStats(message: Message, isMessageRead: Bool, appConfig: AppConfiguration) -> Bool { +private func canViewReadStats(message: Message, cachedData: CachedPeerData?, isMessageRead: Bool, appConfig: AppConfiguration) -> Bool { guard let peer = message.peers[message.id.peerId] else { return false } @@ -193,6 +199,16 @@ private func canViewReadStats(message: Message, isMessageRead: Bool, appConfig: case let channel as TelegramChannel: if case .broadcast = channel.info { return false + } else if let cachedData = cachedData as? CachedChannelData { + if let memberCount = cachedData.participantsSummary.memberCount { + if Int(memberCount) > maxParticipantCount { + return false + } + } else { + return false + } + } else { + return false } case let group as TelegramGroup: if group.participantCount > maxParticipantCount { @@ -543,7 +559,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState return transaction.getCombinedPeerReadState(messages[0].id.peerId) } - let dataSignal: Signal<(MessageContextMenuData, [MessageId: ChatUpdatingMessageMedia], CachedPeerData?, AppConfiguration, Bool, Int32, AvailableReactions?), NoError> = combineLatest( + let dataSignal: Signal<(MessageContextMenuData, [MessageId: ChatUpdatingMessageMedia], CachedPeerData?, AppConfiguration, Bool, Int32, AvailableReactions?, TranslationSettings), NoError> = combineLatest( loadLimits, loadStickerSaveStatusSignal, loadResourceStatusSignal, @@ -553,9 +569,10 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState cachedData, readState, ApplicationSpecificNotice.getMessageViewsPrivacyTips(accountManager: context.sharedContext.accountManager), - context.engine.stickers.availableReactions() + context.engine.stickers.availableReactions(), + context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.translationSettings]) ) - |> map { limitsAndAppConfig, stickerSaveStatus, resourceStatus, messageActions, updatingMessageMedia, cachedData, readState, messageViewsPrivacyTips, availableReactions -> (MessageContextMenuData, [MessageId: ChatUpdatingMessageMedia], CachedPeerData?, AppConfiguration, Bool, Int32, AvailableReactions?) in + |> map { limitsAndAppConfig, stickerSaveStatus, resourceStatus, messageActions, updatingMessageMedia, cachedData, readState, messageViewsPrivacyTips, availableReactions, sharedData -> (MessageContextMenuData, [MessageId: ChatUpdatingMessageMedia], CachedPeerData?, AppConfiguration, Bool, Int32, AvailableReactions?, TranslationSettings) in let (limitsConfiguration, appConfig) = limitsAndAppConfig var canEdit = false if !isAction { @@ -568,12 +585,19 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState isMessageRead = readState.isOutgoingMessageIndexRead(message.index) } - return (MessageContextMenuData(starStatus: stickerSaveStatus, canReply: canReply, canPin: canPin, canEdit: canEdit, canSelect: canSelect, resourceStatus: resourceStatus, messageActions: messageActions), updatingMessageMedia, cachedData, appConfig, isMessageRead, messageViewsPrivacyTips, availableReactions) + let translationSettings: TranslationSettings + if let current = sharedData.entries[ApplicationSpecificSharedDataKeys.translationSettings]?.get(TranslationSettings.self) { + translationSettings = current + } else { + translationSettings = TranslationSettings.defaultSettings + } + + return (MessageContextMenuData(starStatus: stickerSaveStatus, canReply: canReply, canPin: canPin, canEdit: canEdit, canSelect: canSelect, resourceStatus: resourceStatus, messageActions: messageActions), updatingMessageMedia, cachedData, appConfig, isMessageRead, messageViewsPrivacyTips, availableReactions, translationSettings) } return dataSignal |> deliverOnMainQueue - |> map { data, updatingMessageMedia, cachedData, appConfig, isMessageRead, messageViewsPrivacyTips, availableReactions -> ContextController.Items in + |> map { data, updatingMessageMedia, cachedData, appConfig, isMessageRead, messageViewsPrivacyTips, availableReactions, translationSettings -> ContextController.Items in var actions: [ContextMenuItem] = [] var isPinnedMessages = false @@ -688,7 +712,19 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState resourceAvailable = false } - if (!messages[0].text.isEmpty || resourceAvailable || diceEmoji != nil) && !chatPresentationInterfaceState.copyProtectionEnabled { + var messageText: String = "" + for message in messages { + if !message.text.isEmpty { + if messageText.isEmpty { + messageText = message.text + } else { + messageText = "" + break + } + } + } + + if (!messageText.isEmpty || resourceAvailable || diceEmoji != nil) && !chatPresentationInterfaceState.copyProtectionEnabled { let message = messages[0] var isExpired = false for media in message.media { @@ -718,7 +754,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState if let restrictedText = restrictedText { storeMessageTextInPasteboard(restrictedText, entities: nil) } else { - storeMessageTextInPasteboard(message.text, entities: messageEntities) + storeMessageTextInPasteboard(messageText, entities: messageEntities) } Queue.mainQueue().after(0.2, { @@ -734,7 +770,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState |> deliverOnMainQueue).start(next: { data in if data.complete, let imageData = try? Data(contentsOf: URL(fileURLWithPath: data.path)) { if let image = UIImage(data: imageData) { - if !message.text.isEmpty { + if !messageText.isEmpty { copyTextWithEntities() } else { UIPasteboard.general.image = image @@ -760,11 +796,20 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState f(.default) }))) - if isSpeakSelectionEnabled() && !message.text.isEmpty { + if canTranslateText(context: context, text: messageText, showTranslate: translationSettings.showTranslate, ignoredLanguages: translationSettings.ignoredLanguages) { + actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuTranslate, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Translate"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + controllerInteraction.performTextSelectionAction(0, NSAttributedString(string: messageText), .translate) + f(.default) + }))) + } + + if isSpeakSelectionEnabled() && !messageText.isEmpty { actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuSpeak, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Message"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in - controllerInteraction.performTextSelectionAction(0, NSAttributedString(string: message.text), .speak) + controllerInteraction.performTextSelectionAction(0, NSAttributedString(string: messageText), .speak) f(.default) }))) } @@ -1172,7 +1217,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState } } - let canViewStats = canViewReadStats(message: message, isMessageRead: isMessageRead, appConfig: appConfig) + let canViewStats = canViewReadStats(message: message, cachedData: cachedData, isMessageRead: isMessageRead, appConfig: appConfig) var reactionCount = 0 for reaction in mergedMessageReactionsAndPeers(message: message).reactions { reactionCount += Int(reaction.count) @@ -1192,6 +1237,8 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState if group.participantCount <= 50 { hasReadReports = true } + } else { + reactionCount = 0 } var readStats = readStats @@ -1205,51 +1252,18 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState } actions.insert(.custom(ChatReadReportContextItem(context: context, message: message, stats: readStats, action: { c, f, stats in - if reactionCount == 0 && stats.peers.count == 1 { + if reactionCount == 0, let stats = stats, stats.peers.count == 1 { c.dismiss(completion: { controllerInteraction.openPeer(stats.peers[0].id, .default, nil) }) - } else if !stats.peers.isEmpty || reactionCount != 0 { - if reactionCount != 0 { - c.pushItems(items: .single(ContextController.Items(content: .custom(ReactionListContextMenuContent(context: context, availableReactions: availableReactions, message: EngineMessage(message), back: { [weak c] in - c?.popItems() - }, openPeer: { [weak c] id in - c?.dismiss(completion: { - controllerInteraction.openPeer(id, .default, nil) - }) - })), tip: nil))) - } else { - var subActions: [ContextMenuItem] = [] - - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - - subActions.append(.action(ContextMenuActionItem(text: presentationData.strings.Common_Back, textColor: .primary, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor) - }, action: { controller, _ in - controller.setItems(contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: chatPresentationInterfaceState, context: context, messages: messages, controllerInteraction: controllerInteraction, selectAll: selectAll, interfaceInteraction: interfaceInteraction, readStats: stats), minHeight: nil, previousActionsTransition: .slide(forward: false)) - }))) - - subActions.append(.separator) - - for peer in stats.peers { - let avatarSignal = peerAvatarCompleteImage(account: context.account, peer: peer, size: CGSize(width: 30.0, height: 30.0)) - - subActions.append(.action(ContextMenuActionItem(text: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), textLayout: .singleLine, icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: CGSize(width: 30.0, height: 30.0), signal: avatarSignal), action: { _, f in - c.dismiss(completion: { - controllerInteraction.openPeer(peer.id, .default, nil) - }) - }))) - } - - var tip: ContextController.Tip? - if messageViewsPrivacyTips < 3 { - tip = .messageViewsPrivacy - let _ = ApplicationSpecificNotice.incrementMessageViewsPrivacyTips(accountManager: context.sharedContext.accountManager).start() - } - - let minHeight = c.getActionsMinHeight() - c.setItems(.single(ContextController.Items(content: .list(subActions), tip: tip)), minHeight: minHeight, previousActionsTransition: .slide(forward: true)) - } + } else if (stats != nil && !stats!.peers.isEmpty) || reactionCount != 0 { + c.pushItems(items: .single(ContextController.Items(content: .custom(ReactionListContextMenuContent(context: context, availableReactions: availableReactions, message: EngineMessage(message), reaction: nil, readStats: stats, back: { [weak c] in + c?.popItems() + }, openPeer: { [weak c] id in + c?.dismiss(completion: { + controllerInteraction.openPeer(id, .default, nil) + }) + })), tip: nil))) } else { f(.default) } @@ -1757,6 +1771,14 @@ private final class ChatDeleteMessageContextItemNode: ASDisplayNode, ContextMenu } } + func canBeHighlighted() -> Bool { + return self.isActionEnabled + } + + func updateIsHighlighted(isHighlighted: Bool) { + self.setIsHighlighted(isHighlighted) + } + func actionNode(at point: CGPoint) -> ContextActionNodeProtocol { return self } @@ -1766,9 +1788,9 @@ final class ChatReadReportContextItem: ContextMenuCustomItem { fileprivate let context: AccountContext fileprivate let message: Message fileprivate let stats: MessageReadStats? - fileprivate let action: (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void, MessageReadStats) -> Void + fileprivate let action: (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void, MessageReadStats?) -> Void - init(context: AccountContext, message: Message, stats: MessageReadStats?, action: @escaping (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void, MessageReadStats) -> Void) { + init(context: AccountContext, message: Message, stats: MessageReadStats?, action: @escaping (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void, MessageReadStats?) -> Void) { self.context = context self.message = message self.stats = stats @@ -1899,6 +1921,8 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus } }) } + + item.context.account.viewTracker.updateReactionsForMessageIds(messageIds: [item.message.id], force: true) } deinit { @@ -2110,6 +2134,14 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus } private var actionTemporarilyDisabled: Bool = false + + func canBeHighlighted() -> Bool { + return self.isActionEnabled + } + + func updateIsHighlighted(isHighlighted: Bool) { + self.setIsHighlighted(isHighlighted) + } func performAction() { if self.actionTemporarilyDisabled { @@ -2120,15 +2152,22 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus self?.actionTemporarilyDisabled = false } - guard let controller = self.getController(), let currentStats = self.currentStats else { + guard let controller = self.getController() else { return } self.item.action(controller, { [weak self] result in self?.actionSelected(result) - }, currentStats) + }, self.currentStats) } var isActionEnabled: Bool { + var reactionCount = 0 + for reaction in mergedMessageReactionsAndPeers(message: self.item.message).reactions { + reactionCount += Int(reaction.count) + } + if reactionCount >= 0 { + return true + } guard let currentStats = self.currentStats else { return false } diff --git a/submodules/TelegramUI/Sources/ChatMessageActionButtonsNode.swift b/submodules/TelegramUI/Sources/ChatMessageActionButtonsNode.swift index d41796e3cd..133d59024f 100644 --- a/submodules/TelegramUI/Sources/ChatMessageActionButtonsNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageActionButtonsNode.swift @@ -195,7 +195,7 @@ private final class ChatMessageActionButtonNode: ASDisplayNode { animation.animator.updateFrame(layer: buttonView.layer, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: 42.0)), completion: nil) } if let iconNode = node.iconNode { - animation.animator.updateFrame(layer: iconNode.layer, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: 42.0)), completion: nil) + animation.animator.updateFrame(layer: iconNode.layer, frame: CGRect(x: width - 16.0, y: 4.0, width: 12.0, height: 12.0), completion: nil) } node.accessibilityArea.accessibilityLabel = title diff --git a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift index 48a2db54c8..d2bdf2f2dd 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -335,6 +335,11 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { if let shareButtonNode = strongSelf.shareButtonNode, shareButtonNode.frame.contains(point) { return .fail } + if let reactionButtonsNode = strongSelf.reactionButtonsNode { + if let _ = reactionButtonsNode.hitTest(strongSelf.view.convert(point, to: reactionButtonsNode.view), with: nil) { + return .fail + } + } if false, strongSelf.telegramFile == nil { if let animationNode = strongSelf.animationNode, animationNode.frame.contains(point) { @@ -838,7 +843,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } } - if item.associatedData.isCopyProtectionEnabled { + if item.associatedData.isCopyProtectionEnabled || item.message.isCopyProtected() { needsShareButton = false } } @@ -885,8 +890,8 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { var dateReplies = 0 let dateReactionsAndPeers = mergedMessageReactionsAndPeers(message: item.message) for attribute in item.message.attributes { - if let _ = attribute as? EditedMessageAttribute, isEmoji { - edited = true + if let attribute = attribute as? EditedMessageAttribute, isEmoji { + edited = !attribute.isHidden } else if let attribute = attribute as? ViewCountMessageAttribute { viewCount = attribute.count } else if let attribute = attribute as? ReplyThreadMessageAttribute, case .peer = item.chatLocation { @@ -917,7 +922,8 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { reactionPeers: dateReactionsAndPeers.peers, replyCount: dateReplies, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, - hasAutoremove: item.message.isSelfExpiring + hasAutoremove: item.message.isSelfExpiring, + canViewReactionList: canViewMessageReactionList(message: item.message) )) let (dateAndStatusSize, dateAndStatusApply) = statusSuggestedWidthAndContinue.1(statusSuggestedWidthAndContinue.0) @@ -1083,7 +1089,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { layoutSize.height += actionButtonsSizeAndApply.0.height } if let reactionButtonsSizeAndApply = reactionButtonsSizeAndApply { - layoutSize.height += reactionButtonsSizeAndApply.0.height + layoutSize.height += 4.0 + reactionButtonsSizeAndApply.0.height } return (ListViewItemNodeLayout(contentSize: layoutSize, insets: layoutInsets), { [weak self] animation, _, _ in @@ -1350,7 +1356,10 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { if let reactionButtonsSizeAndApply = reactionButtonsSizeAndApply { let reactionButtonsNode = reactionButtonsSizeAndApply.1(animation) - let reactionButtonsFrame = CGRect(origin: CGPoint(x: imageFrame.minX, y: imageFrame.maxY), size: reactionButtonsSizeAndApply.0) + var reactionButtonsFrame = CGRect(origin: CGPoint(x: imageFrame.minX, y: imageFrame.maxY), size: reactionButtonsSizeAndApply.0) + if let actionButtonsSizeAndApply = actionButtonsSizeAndApply { + reactionButtonsFrame.origin.y += 4.0 + actionButtonsSizeAndApply.0.height + } if reactionButtonsNode !== strongSelf.reactionButtonsNode { strongSelf.reactionButtonsNode = reactionButtonsNode reactionButtonsNode.reactionSelected = { value in @@ -1359,6 +1368,14 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } item.controllerInteraction.updateMessageReaction(item.message, .reaction(value)) } + reactionButtonsNode.openReactionPreview = { gesture, sourceNode, value in + guard let strongSelf = self, let item = strongSelf.item else { + gesture?.cancel() + return + } + + item.controllerInteraction.openMessageReactionContextMenu(item.message, sourceNode, gesture, value) + } reactionButtonsNode.frame = reactionButtonsFrame if let (rect, containerSize) = strongSelf.absoluteRect { var rect = rect @@ -2327,6 +2344,13 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } } + override func openMessageContextMenu() { + guard let item = self.item else { + return + } + item.controllerInteraction.openMessageContextMenu(item.message, false, self, self.imageNode.frame, nil) + } + override func targetReactionView(value: String) -> UIView? { if let result = self.reactionButtonsNode?.reactionTargetView(value: value) { return result diff --git a/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift index 339ac262f2..41fc48ffca 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift @@ -565,7 +565,26 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { } } - let (_, refineLayout) = contentFileLayout(context, presentationData, message, message, associatedData, chatLocation, attributes, message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread, false, file, automaticDownload, message.effectivelyIncoming(context.account.peerId), false, associatedData.forcedResourceStatus, statusType, nil, CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)) + let (_, refineLayout) = contentFileLayout(ChatMessageInteractiveFileNode.Arguments( + context: context, + presentationData: presentationData, + message: message, + topMessage: message, + associatedData: associatedData, + chatLocation: chatLocation, + attributes: attributes, + isPinned: message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread, + forcedIsEdited: false, + file: file, + automaticDownload: automaticDownload, + incoming: message.effectivelyIncoming(context.account.peerId), + isRecentActions: false, + forcedResourceStatus: associatedData.forcedResourceStatus, + dateAndStatusType: statusType, + displayReactions: false, + messageSelection: nil, + constrainedSize: CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height) + )) refineContentFileLayout = refineLayout } } else if let image = media as? TelegramMediaImage { @@ -642,7 +661,8 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { reactionPeers: dateReactionsAndPeers.peers, replyCount: dateReplies, isPinned: message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread, - hasAutoremove: message.isSelfExpiring + hasAutoremove: message.isSelfExpiring, + canViewReactionList: canViewMessageReactionList(message: message) )) } let _ = statusSuggestedWidthAndContinue @@ -938,11 +958,18 @@ final class ChatMessageAttachedContentNode: ASDisplayNode { strongSelf.textNode.frame = textFrame.offsetBy(dx: 0.0, dy: textVerticalOffset) if let statusSizeAndApply = statusSizeAndApply { + var statusFrame = CGRect(origin: CGPoint(x: strongSelf.textNode.frame.minX, y: strongSelf.textNode.frame.maxY), size: statusSizeAndApply.0) + if let imageFrame = imageFrame { + statusFrame.origin.y = max(statusFrame.minY, imageFrame.maxY + 2.0) + } if strongSelf.statusNode.supernode == nil { strongSelf.addSubnode(strongSelf.statusNode) + strongSelf.statusNode.frame = statusFrame + statusSizeAndApply.1(.None) + } else { + animation.animator.updateFrame(layer: strongSelf.statusNode.layer, frame: statusFrame, completion: nil) + statusSizeAndApply.1(animation) } - strongSelf.statusNode.frame = CGRect(origin: CGPoint(x: strongSelf.textNode.frame.minX, y: strongSelf.textNode.frame.maxY), size: statusSizeAndApply.0) - statusSizeAndApply.1(animation) } else if strongSelf.statusNode.supernode != nil { strongSelf.statusNode.removeFromSupernode() } diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift index 26bbb5a99a..435e43d03a 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift @@ -249,11 +249,16 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ result.append((firstMessage, ChatMessageReactionsFooterContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: true, neighborType: .freeform, neighborSpacing: .default))) needReactions = false } else if result.last?.1 == ChatMessageCommentFooterContentNode.self { - if result[result.count - 2].1 == ChatMessageTextBubbleContentNode.self { + if result[result.count - 2].1 == ChatMessageWebpageBubbleContentNode.self || + result[result.count - 2].1 == ChatMessagePollBubbleContentNode.self || + result[result.count - 2].1 == ChatMessageContactBubbleContentNode.self { + result.insert((firstMessage, ChatMessageReactionsFooterContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: true, neighborType: .freeform, neighborSpacing: .default)), at: result.count - 1) + } + /*if result[result.count - 2].1 == ChatMessageTextBubbleContentNode.self { } else { result.insert((firstMessage, ChatMessageReactionsFooterContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: true, neighborType: .freeform, neighborSpacing: .default)), at: result.count - 1) needReactions = false - } + }*/ } } } @@ -805,6 +810,12 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode return .fail } + if let reactionButtonsNode = strongSelf.reactionButtonsNode { + if let _ = reactionButtonsNode.hitTest(strongSelf.view.convert(point, to: reactionButtonsNode.view), with: nil) { + return .fail + } + } + if let avatarNode = strongSelf.accessoryItemNode as? ChatMessageAvatarAccessoryItemNode, avatarNode.frame.contains(point) { return .waitForSingleTap } @@ -829,7 +840,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } } for contentNode in strongSelf.contentNodes { - let tapAction = contentNode.tapActionAtPoint(CGPoint(x: point.x - contentNode.frame.minX, y: point.y - contentNode.frame.minY), gesture: .tap, isEstimating: true) + let contentNodePoint = strongSelf.view.convert(point, to: contentNode.view) + let tapAction = contentNode.tapActionAtPoint(contentNodePoint, gesture: .tap, isEstimating: true) switch tapAction { case .none: if let _ = strongSelf.item?.controllerInteraction.tapMessage { @@ -1161,7 +1173,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } } - if item.associatedData.isCopyProtectionEnabled { + if item.associatedData.isCopyProtectionEnabled || item.message.isCopyProtected() { needsShareButton = false } } @@ -1596,7 +1608,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode reactionPeers: dateReactionsAndPeers.peers, replyCount: dateReplies, isPinned: message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, - hasAutoremove: message.isSelfExpiring + hasAutoremove: message.isSelfExpiring, + canViewReactionList: canViewMessageReactionList(message: message) )) mosaicStatusSizeAndApply = statusSuggestedWidthAndContinue.1(statusSuggestedWidthAndContinue.0) @@ -2689,14 +2702,18 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } if let mosaicStatusOrigin = mosaicStatusOrigin, let (size, apply) = mosaicStatusSizeAndApply { - let mosaicStatusNode = apply(animation) + var statusNodeAnimation = animation + if strongSelf.mosaicStatusNode == nil { + statusNodeAnimation = .None + } + let mosaicStatusNode = apply(statusNodeAnimation) if mosaicStatusNode !== strongSelf.mosaicStatusNode { strongSelf.mosaicStatusNode?.removeFromSupernode() strongSelf.mosaicStatusNode = mosaicStatusNode strongSelf.clippingNode.addSubnode(mosaicStatusNode) } let absoluteOrigin = mosaicStatusOrigin.offsetBy(dx: contentOrigin.x, dy: contentOrigin.y) - mosaicStatusNode.frame = CGRect(origin: CGPoint(x: absoluteOrigin.x - layoutConstants.image.statusInsets.right - size.width, y: absoluteOrigin.y - layoutConstants.image.statusInsets.bottom - size.height), size: size) + statusNodeAnimation.animator.updateFrame(layer: mosaicStatusNode.layer, frame: CGRect(origin: CGPoint(x: absoluteOrigin.x - layoutConstants.image.statusInsets.right - size.width, y: absoluteOrigin.y - layoutConstants.image.statusInsets.bottom - size.height), size: size), completion: nil) } else if let mosaicStatusNode = strongSelf.mosaicStatusNode { strongSelf.mosaicStatusNode = nil mosaicStatusNode.removeFromSupernode() @@ -2829,6 +2846,14 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } item.controllerInteraction.updateMessageReaction(item.message, .reaction(value)) } + reactionButtonsNode.openReactionPreview = { [weak strongSelf] gesture, sourceNode, value in + guard let strongSelf = strongSelf, let item = strongSelf.item else { + gesture?.cancel() + return + } + + item.controllerInteraction.openMessageReactionContextMenu(item.message, sourceNode, gesture, value) + } reactionButtonsNode.frame = reactionButtonsFrame strongSelf.addSubnode(reactionButtonsNode) if animation.isAnimated { @@ -3848,6 +3873,14 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode return self.mainContextSourceNode.isExtractedToContextPreview || hasWallpaper || isPreview } + override func openMessageContextMenu() { + guard let item = self.item else { + return + } + let subFrame = self.backgroundNode.frame + item.controllerInteraction.openMessageContextMenu(item.message, true, self, subFrame, nil) + } + override func targetReactionView(value: String) -> UIView? { if let result = self.reactionButtonsNode?.reactionTargetView(value: value) { return result @@ -3857,6 +3890,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode return result } } + if let mosaicStatusNode = self.mosaicStatusNode, let result = mosaicStatusNode.reactionView(value: value) { + return result + } return nil } diff --git a/submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift index 8c6be2f6fe..e89c1e0dca 100644 --- a/submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift @@ -48,6 +48,15 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { } item.controllerInteraction.updateMessageReaction(item.message, .reaction(value)) } + + self.dateAndStatusNode.openReactionPreview = { [weak self] gesture, sourceNode, value in + guard let strongSelf = self, let item = strongSelf.item else { + gesture?.cancel() + return + } + + item.controllerInteraction.openMessageReactionContextMenu(item.topMessage, sourceNode, gesture, value) + } } override func accessibilityActivate() -> Bool { @@ -217,7 +226,8 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode { reactionPeers: dateReactionsAndPeers.peers, replyCount: dateReplies, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread, - hasAutoremove: item.message.isSelfExpiring + hasAutoremove: item.message.isSelfExpiring, + canViewReactionList: canViewMessageReactionList(message: item.message) )) } diff --git a/submodules/TelegramUI/Sources/ChatMessageContextControllerContentSource.swift b/submodules/TelegramUI/Sources/ChatMessageContextControllerContentSource.swift index 79f72e066f..8a37064a6c 100644 --- a/submodules/TelegramUI/Sources/ChatMessageContextControllerContentSource.swift +++ b/submodules/TelegramUI/Sources/ChatMessageContextControllerContentSource.swift @@ -81,3 +81,82 @@ final class ChatMessageContextExtractedContentSource: ContextExtractedContentSou return result } } + +final class ChatMessageReactionContextExtractedContentSource: ContextExtractedContentSource { + let keepInPlace: Bool = false + let ignoreContentTouches: Bool = true + let blurBackground: Bool = true + let centerActionsHorizontally: Bool = true + + private weak var chatNode: ChatControllerNode? + private let postbox: Postbox + private let message: Message + private let contentNode: ContextExtractedContentContainingNode + + var shouldBeDismissed: Signal { + if self.message.adAttribute != nil { + return .single(false) + } + let viewKey = PostboxViewKey.messages(Set([self.message.id])) + return self.postbox.combinedView(keys: [viewKey]) + |> map { views -> Bool in + guard let view = views.views[viewKey] as? MessagesView else { + return false + } + if view.messages.isEmpty { + return true + } else { + return false + } + } + |> distinctUntilChanged + } + + init(chatNode: ChatControllerNode, postbox: Postbox, message: Message, contentNode: ContextExtractedContentContainingNode) { + self.chatNode = chatNode + self.postbox = postbox + self.message = message + self.contentNode = contentNode + } + + func takeView() -> ContextControllerTakeViewInfo? { + guard let chatNode = self.chatNode else { + return nil + } + + var result: ContextControllerTakeViewInfo? + chatNode.historyNode.forEachItemNode { itemNode in + guard let itemNode = itemNode as? ChatMessageItemView else { + return + } + guard let item = itemNode.item else { + return + } + if item.content.contains(where: { $0.0.stableId == self.message.stableId }) { + result = ContextControllerTakeViewInfo(contentContainingNode: self.contentNode, contentAreaInScreenSpace: chatNode.convert(chatNode.frameForVisibleArea(), to: nil)) + } + } + return result + } + + func putBack() -> ContextControllerPutBackViewInfo? { + guard let chatNode = self.chatNode else { + return nil + } + + var result: ContextControllerPutBackViewInfo? + chatNode.historyNode.forEachItemNode { itemNode in + guard let itemNode = itemNode as? ChatMessageItemView else { + return + } + guard let item = itemNode.item else { + return + } + if item.content.contains(where: { $0.0.stableId == self.message.stableId }) { + result = ContextControllerPutBackViewInfo(contentAreaInScreenSpace: chatNode.convert(chatNode.frameForVisibleArea(), to: nil)) + } + } + return result + } +} + diff --git a/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift b/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift index 68d66a57b3..302fb56e16 100644 --- a/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift @@ -70,7 +70,7 @@ private final class StatusReactionNode: ASDisplayNode { if self.value != value { self.value = value - let defaultImageSize = CGSize(width: 19.0, height: 19.0) + let defaultImageSize = CGSize(width: 17.0, height: 17.0) let imageSize: CGSize if let file = file { self.iconImageDisposable.set((context.account.postbox.mediaBox.resourceData(file.resource) @@ -148,6 +148,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { var replyCount: Int var isPinned: Bool var hasAutoremove: Bool + var canViewReactionList: Bool init( context: AccountContext, @@ -163,7 +164,8 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { reactionPeers: [(String, EnginePeer)], replyCount: Int, isPinned: Bool, - hasAutoremove: Bool + hasAutoremove: Bool, + canViewReactionList: Bool ) { self.context = context self.presentationData = presentationData @@ -179,6 +181,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { self.replyCount = replyCount self.isPinned = isPinned self.hasAutoremove = hasAutoremove + self.canViewReactionList = canViewReactionList } } @@ -220,6 +223,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { } } var reactionSelected: ((String) -> Void)? + var openReactionPreview: ((ContextGesture?, ContextExtractedContentContainingNode, String) -> Void)? override init() { self.dateNode = TextNode() @@ -284,18 +288,26 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { let reactionColors: ReactionButtonComponent.Colors switch arguments.type { case .BubbleIncoming, .ImageIncoming, .FreeIncoming: + let themeColors = bubbleColorComponents(theme: arguments.presentationData.theme.theme, incoming: true, wallpaper: !arguments.presentationData.theme.wallpaper.isEmpty) + reactionColors = ReactionButtonComponent.Colors( - deselectedBackground: arguments.presentationData.theme.theme.chat.message.incoming.accentControlColor.withMultipliedAlpha(0.1).argb, - selectedBackground: arguments.presentationData.theme.theme.chat.message.incoming.accentControlColor.withMultipliedAlpha(1.0).argb, - deselectedForeground: arguments.presentationData.theme.theme.chat.message.incoming.accentTextColor.argb, - selectedForeground: arguments.presentationData.theme.theme.chat.message.incoming.bubble.withWallpaper.fill.last!.argb + deselectedBackground: themeColors.reactionInactiveBackground.argb, + selectedBackground: themeColors.reactionActiveBackground.argb, + deselectedForeground: themeColors.reactionInactiveForeground.argb, + selectedForeground: themeColors.reactionActiveForeground.argb, + extractedBackground: arguments.presentationData.theme.theme.contextMenu.backgroundColor.argb, + extractedForeground: arguments.presentationData.theme.theme.contextMenu.primaryColor.argb ) case .BubbleOutgoing, .ImageOutgoing, .FreeOutgoing: + let themeColors = bubbleColorComponents(theme: arguments.presentationData.theme.theme, incoming: false, wallpaper: !arguments.presentationData.theme.wallpaper.isEmpty) + reactionColors = ReactionButtonComponent.Colors( - deselectedBackground: arguments.presentationData.theme.theme.chat.message.outgoing.accentControlColor.withMultipliedAlpha(0.1).argb, - selectedBackground: arguments.presentationData.theme.theme.chat.message.outgoing.accentControlColor.withMultipliedAlpha(1.0).argb, - deselectedForeground: arguments.presentationData.theme.theme.chat.message.outgoing.accentTextColor.argb, - selectedForeground: arguments.presentationData.theme.theme.chat.message.outgoing.bubble.withWallpaper.fill.last!.argb + deselectedBackground: themeColors.reactionInactiveBackground.argb, + selectedBackground: themeColors.reactionActiveBackground.argb, + deselectedForeground: themeColors.reactionInactiveForeground.argb, + selectedForeground: themeColors.reactionActiveForeground.argb, + extractedBackground: arguments.presentationData.theme.theme.contextMenu.backgroundColor.argb, + extractedForeground: arguments.presentationData.theme.theme.contextMenu.primaryColor.argb ) } @@ -564,7 +576,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { var replyCountLayoutAndApply: (TextNodeLayout, () -> TextNode)? - let reactionSize: CGFloat = 19.0 + let reactionSize: CGFloat = 17.0 var reactionCountLayoutAndApply: (TextNodeLayout, () -> TextNode)? let reactionSpacing: CGFloat = 2.0 let reactionTrailingSpacing: CGFloat = 6.0 @@ -641,6 +653,11 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { ) case let .trailingContent(contentWidth, reactionSettings): if let reactionSettings = reactionSettings, !reactionSettings.displayInline { + var totalReactionCount: Int = 0 + for reaction in arguments.reactions { + totalReactionCount += Int(reaction.count) + } + reactionButtonsResult = reactionButtonsContainer.update( context: arguments.context, action: { value in @@ -667,11 +684,11 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { peers.append(peer) } } - if peers.count != Int(reaction.count) { + if peers.count != Int(reaction.count) || arguments.reactionPeers.count != totalReactionCount { peers.removeAll() } - return ReactionButtonsLayoutContainer.Reaction( + return ReactionButtonsAsyncLayoutContainer.Reaction( reaction: ReactionButtonComponent.Reaction( value: reaction.value, iconFile: iconFile @@ -774,32 +791,51 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { var reactionButtonPosition = CGPoint(x: -1.0, y: verticalReactionsInset) for item in reactionButtons.items { if reactionButtonPosition.x + item.size.width > boundingWidth { - reactionButtonPosition.x = 0.0 + reactionButtonPosition.x = -1.0 reactionButtonPosition.y += item.size.height + 6.0 } - if item.view.superview == nil { - strongSelf.view.addSubview(item.view) - item.view.frame = CGRect(origin: reactionButtonPosition, size: item.size) + if item.node.supernode == nil { + strongSelf.addSubnode(item.node) + item.node.frame = CGRect(origin: reactionButtonPosition, size: item.size) if animation.isAnimated { - item.view.layer.animateScale(from: 0.01, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) - item.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + item.node.layer.animateScale(from: 0.01, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) + item.node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + item.node.isGestureEnabled = true + let itemValue = item.value + let itemNode = item.node + item.node.isGestureEnabled = arguments.canViewReactionList + item.node.activated = { [weak itemNode] gesture, _ in + guard let strongSelf = self else { + return + } + guard let itemNode = itemNode else { + return + } + + if let openReactionPreview = strongSelf.openReactionPreview { + openReactionPreview(gesture, itemNode.containerNode, itemValue) + } else { + gesture.cancel() + } } } else { - animation.animator.updateFrame(layer: item.view.layer, frame: CGRect(origin: reactionButtonPosition, size: item.size), completion: nil) + animation.animator.updateFrame(layer: item.node.layer, frame: CGRect(origin: reactionButtonPosition, size: item.size), completion: nil) } reactionButtonPosition.x += item.size.width + 6.0 } - for view in reactionButtons.removedViews { + for node in reactionButtons.removedNodes { if animation.isAnimated { - view.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) - view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak view] _ in - view?.removeFromSuperview() + node.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + node.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak node] _ in + node?.removeFromSupernode() }) } else { - view.removeFromSuperview() + node.removeFromSupernode() } } @@ -1160,7 +1196,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { for (_, button) in self.reactionButtonsContainer.buttons { if button.frame.contains(point) { - if let result = button.hitTest(self.view.convert(point, to: button), with: event) { + if let result = button.hitTest(self.view.convert(point, to: button.view), with: event) { return result } } diff --git a/submodules/TelegramUI/Sources/ChatMessageFileBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageFileBubbleContentNode.swift index 935f6aa1b1..ffe13fabdd 100644 --- a/submodules/TelegramUI/Sources/ChatMessageFileBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageFileBubbleContentNode.swift @@ -63,6 +63,15 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { } item.controllerInteraction.updateMessageReaction(item.message, .reaction(value)) } + + self.interactiveFileNode.dateAndStatusNode.openReactionPreview = { [weak self] gesture, sourceNode, value in + guard let strongSelf = self, let item = strongSelf.item else { + gesture?.cancel() + return + } + + item.controllerInteraction.openMessageReactionContextMenu(item.topMessage, sourceNode, gesture, value) + } } override func accessibilityActivate() -> Bool { @@ -108,7 +117,26 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { let automaticDownload = shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: selectedFile!) - let (initialWidth, refineLayout) = interactiveFileLayout(item.context, item.presentationData, item.message, item.topMessage, item.associatedData, item.chatLocation, item.attributes, item.isItemPinned, item.isItemEdited, selectedFile!, automaticDownload, item.message.effectivelyIncoming(item.context.account.peerId), item.associatedData.isRecentActions, item.associatedData.forcedResourceStatus, statusType, item.message.groupingKey != nil ? selection : nil, CGSize(width: constrainedSize.width - layoutConstants.file.bubbleInsets.left - layoutConstants.file.bubbleInsets.right, height: constrainedSize.height)) + let (initialWidth, refineLayout) = interactiveFileLayout(ChatMessageInteractiveFileNode.Arguments( + context: item.context, + presentationData: item.presentationData, + message: item.message, + topMessage: item.topMessage, + associatedData: item.associatedData, + chatLocation: item.chatLocation, + attributes: item.attributes, + isPinned: item.isItemPinned, + forcedIsEdited: item.isItemEdited, + file: selectedFile!, + automaticDownload: automaticDownload, + incoming: item.message.effectivelyIncoming(item.context.account.peerId), + isRecentActions: item.associatedData.isRecentActions, + forcedResourceStatus: item.associatedData.forcedResourceStatus, + dateAndStatusType: statusType, + displayReactions: true, + messageSelection: item.message.groupingKey != nil ? selection : nil, + constrainedSize: CGSize(width: constrainedSize.width - layoutConstants.file.bubbleInsets.left - layoutConstants.file.bubbleInsets.right, height: constrainedSize.height) + )) let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none) @@ -172,6 +200,9 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { if self.interactiveFileNode.dateAndStatusNode.supernode != nil, let _ = self.interactiveFileNode.dateAndStatusNode.hitTest(self.view.convert(point, to: self.interactiveFileNode.dateAndStatusNode.view), with: nil) { return .ignore } + if self.interactiveFileNode.hasTapAction(at: self.view.convert(point, to: self.interactiveFileNode.view)) { + return .ignore + } return super.tapActionAtPoint(point, gesture: gesture, isEstimating: isEstimating) } diff --git a/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift index 4027a3fc32..0857dcdb14 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift @@ -184,6 +184,12 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD return .fail } } + + if let reactionButtonsNode = strongSelf.reactionButtonsNode { + if let _ = reactionButtonsNode.hitTest(strongSelf.view.convert(point, to: reactionButtonsNode.view), with: nil) { + return .fail + } + } } return .waitForSingleTap } @@ -351,7 +357,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD } } - if item.associatedData.isCopyProtectionEnabled { + if item.associatedData.isCopyProtectionEnabled || item.message.isCopyProtected() { needsShareButton = false } } @@ -552,7 +558,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD if !reactions.reactions.isEmpty { let totalInset = params.leftInset + layoutConstants.bubble.edgeInset * 2.0 + avatarInset + layoutConstants.bubble.contentInsets.left + params.rightInset + layoutConstants.bubble.contentInsets.right - let maxReactionsWidth = params.width - totalInset + let maxReactionsWidth = params.width - totalInset - 8.0 let (minWidth, buttonsLayout) = reactionButtonsLayout(ChatMessageReactionButtonsNode.Arguments( context: item.context, presentationData: item.presentationData, @@ -582,13 +588,6 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD return (ListViewItemNodeLayout(contentSize: layoutSize, insets: layoutInsets), { [weak self] animation, _, _ in if let strongSelf = self { - let transition: ContainedViewLayoutTransition - if animation.isAnimated { - transition = .animated(duration: 0.2, curve: .spring) - } else { - transition = .immediate - } - strongSelf.contextSourceNode.frame = CGRect(origin: CGPoint(), size: layoutSize) strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: layoutSize) strongSelf.contextSourceNode.contentNode.frame = CGRect(origin: CGPoint(), size: layoutSize) @@ -718,14 +717,14 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD let deliveryFailedFrame = CGRect(origin: CGPoint(x: videoFrame.maxX + deliveryFailedInset - deliveryFailedSize.width, y: videoFrame.maxY - deliveryFailedSize.height), size: deliveryFailedSize) if isAppearing { deliveryFailedNode.frame = deliveryFailedFrame - transition.animatePositionAdditive(node: deliveryFailedNode, offset: CGPoint(x: deliveryFailedInset, y: 0.0)) + animation.transition.animatePositionAdditive(node: deliveryFailedNode, offset: CGPoint(x: deliveryFailedInset, y: 0.0)) } else { - transition.updateFrame(node: deliveryFailedNode, frame: deliveryFailedFrame) + animation.animator.updateFrame(layer: deliveryFailedNode.layer, frame: deliveryFailedFrame, completion: nil) } } else if let deliveryFailedNode = strongSelf.deliveryFailedNode { strongSelf.deliveryFailedNode = nil - transition.updateAlpha(node: deliveryFailedNode, alpha: 0.0) - transition.updateFrame(node: deliveryFailedNode, frame: deliveryFailedNode.frame.offsetBy(dx: 24.0, dy: 0.0), completion: { [weak deliveryFailedNode] _ in + animation.animator.updateAlpha(layer: deliveryFailedNode.layer, alpha: 0.0, completion: nil) + animation.animator.updateFrame(layer: deliveryFailedNode.layer, frame: deliveryFailedNode.frame.offsetBy(dx: 24.0, dy: 0.0), completion: { [weak deliveryFailedNode] _ in deliveryFailedNode?.removeFromSupernode() }) } @@ -788,7 +787,10 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD if let reactionButtonsSizeAndApply = reactionButtonsSizeAndApply { let reactionButtonsNode = reactionButtonsSizeAndApply.1(animation) - let reactionButtonsFrame = CGRect(origin: CGPoint(x: videoFrame.minX, y: videoFrame.maxY + 6.0), size: reactionButtonsSizeAndApply.0) + var reactionButtonsFrame = CGRect(origin: CGPoint(x: videoFrame.minX, y: videoFrame.maxY + 6.0), size: reactionButtonsSizeAndApply.0) + if let actionButtonsSizeAndApply = actionButtonsSizeAndApply { + reactionButtonsFrame.origin.y += 4.0 + actionButtonsSizeAndApply.0.height + } if reactionButtonsNode !== strongSelf.reactionButtonsNode { strongSelf.reactionButtonsNode = reactionButtonsNode reactionButtonsNode.reactionSelected = { value in @@ -797,7 +799,25 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD } item.controllerInteraction.updateMessageReaction(item.message, .reaction(value)) } + reactionButtonsNode.openReactionPreview = { gesture, sourceNode, value in + guard let strongSelf = self, let item = strongSelf.item else { + gesture?.cancel() + return + } + + item.controllerInteraction.openMessageReactionContextMenu(item.message, sourceNode, gesture, value) + } reactionButtonsNode.frame = reactionButtonsFrame + if let (rect, containerSize) = strongSelf.absoluteRect { + var rect = rect + rect.origin.y = containerSize.height - rect.maxY + strongSelf.insets.top + + var reactionButtonsNodeFrame = reactionButtonsFrame + reactionButtonsNodeFrame.origin.x += rect.minX + reactionButtonsNodeFrame.origin.y += rect.minY + + reactionButtonsNode.update(rect: rect, within: containerSize, transition: .immediate) + } strongSelf.addSubnode(reactionButtonsNode) if animation.isAnimated { reactionButtonsNode.animateIn(animation: animation) @@ -1234,14 +1254,20 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD let animationProgress: CGFloat = (currentValue - initialHeight) / (targetHeight - initialHeight) let scaleProgress: CGFloat var effectiveAvatarInset = avatarInset - if currentValue < targetHeight { - initialSize = displaySize - targetSize = maximumDisplaySize - scaleProgress = animationProgress - } else if currentValue > targetHeight { - initialSize = maximumDisplaySize - targetSize = displaySize - scaleProgress = 1.0 - animationProgress + if abs(targetHeight - initialHeight) > 100.0 { + if currentValue < targetHeight { + initialSize = displaySize + targetSize = maximumDisplaySize + scaleProgress = animationProgress + } else if currentValue > targetHeight { + initialSize = maximumDisplaySize + targetSize = displaySize + scaleProgress = 1.0 - animationProgress + } else { + initialSize = isPlaying ? maximumDisplaySize : displaySize + targetSize = initialSize + scaleProgress = isPlaying ? 1.0 : 0.0 + } } else { initialSize = isPlaying ? maximumDisplaySize : displaySize targetSize = initialSize @@ -1310,6 +1336,41 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD let actionButtonsFrame = CGRect(origin: CGPoint(x: videoFrame.minX, y: videoFrame.maxY), size: actionButtonsSize) actionButtonsNode.frame = actionButtonsFrame } + + if let reactionButtonsNode = self.reactionButtonsNode { + let reactionButtonsSize = reactionButtonsNode.frame.size + let reactionButtonsFrame = CGRect(origin: CGPoint(x: videoFrame.minX, y: videoFrame.maxY + 6.0), size: reactionButtonsSize) + reactionButtonsNode.frame = reactionButtonsFrame + } + } + + override func openMessageContextMenu() { + guard let item = self.item else { + return + } + item.controllerInteraction.openMessageContextMenu(item.message, false, self, self.interactiveVideoNode.frame, nil) + } + + private var absoluteRect: (CGRect, CGSize)? + override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + self.absoluteRect = (rect, containerSize) + + var rect = rect + rect.origin.y = containerSize.height - rect.maxY + self.insets.top + + if let reactionButtonsNode = self.reactionButtonsNode { + var reactionButtonsNodeFrame = reactionButtonsNode.frame + reactionButtonsNodeFrame.origin.x += rect.minX + reactionButtonsNodeFrame.origin.y += rect.minY + + reactionButtonsNode.update(rect: rect, within: containerSize, transition: .immediate) + } + } + + override func applyAbsoluteOffset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) { + if let reactionButtonsNode = self.reactionButtonsNode { + reactionButtonsNode.offset(value: value, animationCurve: animationCurve, duration: duration) + } } override func targetReactionView(value: String) -> UIView? { diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift index 87cbc47714..8d4d30b2c4 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift @@ -24,6 +24,67 @@ private struct FetchControls { } final class ChatMessageInteractiveFileNode: ASDisplayNode { + final class Arguments { + let context: AccountContext + let presentationData: ChatPresentationData + let message: Message + let topMessage: Message + let associatedData: ChatMessageItemAssociatedData + let chatLocation: ChatLocation + let attributes: ChatMessageEntryAttributes + let isPinned: Bool + let forcedIsEdited: Bool + let file: TelegramMediaFile + let automaticDownload: Bool + let incoming: Bool + let isRecentActions: Bool + let forcedResourceStatus: FileMediaResourceStatus? + let dateAndStatusType: ChatMessageDateAndStatusType? + let displayReactions: Bool + let messageSelection: Bool? + let constrainedSize: CGSize + + init( + context: AccountContext, + presentationData: ChatPresentationData, + message: Message, + topMessage: Message, + associatedData: ChatMessageItemAssociatedData, + chatLocation: ChatLocation, + attributes: ChatMessageEntryAttributes, + isPinned: Bool, + forcedIsEdited: Bool, + file: TelegramMediaFile, + automaticDownload: Bool, + incoming: Bool, + isRecentActions: Bool, + forcedResourceStatus: FileMediaResourceStatus?, + dateAndStatusType: ChatMessageDateAndStatusType?, + displayReactions: Bool, + messageSelection: Bool?, + constrainedSize: CGSize + ) { + self.context = context + self.presentationData = presentationData + self.message = message + self.topMessage = topMessage + self.associatedData = associatedData + self.chatLocation = chatLocation + self.attributes = attributes + self.isPinned = isPinned + self.forcedIsEdited = forcedIsEdited + self.file = file + self.automaticDownload = automaticDownload + self.incoming = incoming + self.isRecentActions = isRecentActions + self.forcedResourceStatus = forcedResourceStatus + self.dateAndStatusType = dateAndStatusType + self.displayReactions = displayReactions + self.messageSelection = messageSelection + self.constrainedSize = constrainedSize + } + } + private var selectionNode: FileMessageSelectionNode? private let titleNode: TextNode @@ -213,7 +274,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { } } - func asyncLayout() -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ topMessage: Message, _ associatedData: ChatMessageItemAssociatedData, _ chatLocation: ChatLocation, _ attributes: ChatMessageEntryAttributes, _ isPinned: Bool, _ forcedIsEdited: Bool, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ isRecentActions: Bool, _ forcedResourceStatus: FileMediaResourceStatus?, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool, ListViewItemUpdateAnimation) -> Void))) { + func asyncLayout() -> (Arguments) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool, ListViewItemUpdateAnimation) -> Void))) { let currentFile = self.file let titleAsyncLayout = TextNode.asyncLayout(self.titleNode) @@ -223,11 +284,11 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { let currentMessage = self.message - return { context, presentationData, message, topMessage, associatedData, chatLocation, attributes, isPinned, forcedIsEdited, file, automaticDownload, incoming, isRecentActions, forcedResourceStatus, dateAndStatusType, messageSelection, constrainedSize in + return { arguments in return (CGFloat.greatestFiniteMagnitude, { constrainedSize in - let titleFont = Font.regular(floor(presentationData.fontSize.baseDisplaySize * 16.0 / 17.0)) - let descriptionFont = Font.with(size: floor(presentationData.fontSize.baseDisplaySize * 13.0 / 17.0), design: .regular, weight: .regular, traits: [.monospacedNumbers]) - let durationFont = Font.regular(floor(presentationData.fontSize.baseDisplaySize * 11.0 / 17.0)) + let titleFont = Font.regular(floor(arguments.presentationData.fontSize.baseDisplaySize * 16.0 / 17.0)) + let descriptionFont = Font.with(size: floor(arguments.presentationData.fontSize.baseDisplaySize * 13.0 / 17.0), design: .regular, weight: .regular, traits: [.monospacedNumbers]) + let durationFont = Font.regular(floor(arguments.presentationData.fontSize.baseDisplaySize * 11.0 / 17.0)) var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? var updatedStatusSignal: Signal<(FileMediaResourceStatus, MediaResourceStatus?), NoError>? @@ -237,58 +298,58 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { var mediaUpdated = false if let currentFile = currentFile { - mediaUpdated = file != currentFile + mediaUpdated = arguments.file != currentFile } else { mediaUpdated = true } var statusUpdated = mediaUpdated - if currentMessage?.id != message.id || currentMessage?.flags != message.flags { + if currentMessage?.id != arguments.message.id || currentMessage?.flags != arguments.message.flags { statusUpdated = true } - let hasThumbnail = (!file.previewRepresentations.isEmpty || file.immediateThumbnailData != nil) && !file.isMusic && !file.isVoice + let hasThumbnail = (!arguments.file.previewRepresentations.isEmpty || arguments.file.immediateThumbnailData != nil) && !arguments.file.isMusic && !arguments.file.isVoice if mediaUpdated { - if largestImageRepresentation(file.previewRepresentations) != nil || file.immediateThumbnailData != nil { - updateImageSignal = chatMessageImageFile(account: context.account, fileReference: .message(message: MessageReference(message), media: file), thumbnail: true) + if largestImageRepresentation(arguments.file.previewRepresentations) != nil || arguments.file.immediateThumbnailData != nil { + updateImageSignal = chatMessageImageFile(account: arguments.context.account, fileReference: .message(message: MessageReference(arguments.message), media: arguments.file), thumbnail: true) } updatedFetchControls = FetchControls(fetch: { [weak self] in if let strongSelf = self { - strongSelf.fetchDisposable.set(messageMediaFileInteractiveFetched(context: context, message: message, file: file, userInitiated: true).start()) + strongSelf.fetchDisposable.set(messageMediaFileInteractiveFetched(context: arguments.context, message: arguments.message, file: arguments.file, userInitiated: true).start()) } }, cancel: { - messageMediaFileCancelInteractiveFetch(context: context, messageId: message.id, file: file) + messageMediaFileCancelInteractiveFetch(context: arguments.context, messageId: arguments.message.id, file: arguments.file) }) } if statusUpdated { - if message.flags.isSending { - updatedStatusSignal = combineLatest(messageFileMediaResourceStatus(context: context, file: file, message: message, isRecentActions: isRecentActions), messageMediaFileStatus(context: context, messageId: message.id, file: file)) + if arguments.message.flags.isSending { + updatedStatusSignal = combineLatest(messageFileMediaResourceStatus(context: arguments.context, file: arguments.file, message: arguments.message, isRecentActions: arguments.isRecentActions), messageMediaFileStatus(context: arguments.context, messageId: arguments.message.id, file: arguments.file)) |> map { resourceStatus, actualFetchStatus -> (FileMediaResourceStatus, MediaResourceStatus?) in return (resourceStatus, actualFetchStatus) } - updatedAudioLevelEventsSignal = messageFileMediaPlaybackAudioLevelEvents(context: context, file: file, message: message, isRecentActions: isRecentActions, isGlobalSearch: false) + updatedAudioLevelEventsSignal = messageFileMediaPlaybackAudioLevelEvents(context: arguments.context, file: arguments.file, message: arguments.message, isRecentActions: arguments.isRecentActions, isGlobalSearch: false) } else { - updatedStatusSignal = messageFileMediaResourceStatus(context: context, file: file, message: message, isRecentActions: isRecentActions) + updatedStatusSignal = messageFileMediaResourceStatus(context: arguments.context, file: arguments.file, message: arguments.message, isRecentActions: arguments.isRecentActions) |> map { resourceStatus -> (FileMediaResourceStatus, MediaResourceStatus?) in return (resourceStatus, nil) } - updatedAudioLevelEventsSignal = messageFileMediaPlaybackAudioLevelEvents(context: context, file: file, message: message, isRecentActions: isRecentActions, isGlobalSearch: false) + updatedAudioLevelEventsSignal = messageFileMediaPlaybackAudioLevelEvents(context: arguments.context, file: arguments.file, message: arguments.message, isRecentActions: arguments.isRecentActions, isGlobalSearch: false) } - updatedPlaybackStatusSignal = messageFileMediaPlaybackStatus(context: context, file: file, message: message, isRecentActions: isRecentActions, isGlobalSearch: false) + updatedPlaybackStatusSignal = messageFileMediaPlaybackStatus(context: arguments.context, file: arguments.file, message: arguments.message, isRecentActions: arguments.isRecentActions, isGlobalSearch: false) } var consumableContentIcon: UIImage? - for attribute in message.attributes { + for attribute in arguments.message.attributes { if let attribute = attribute as? ConsumableContentMessageAttribute { let isConsumed = attribute.consumed if !isConsumed { - if incoming { - consumableContentIcon = PresentationResourcesChat.chatBubbleConsumableContentIncomingIcon(presentationData.theme.theme) + if arguments.incoming { + consumableContentIcon = PresentationResourcesChat.chatBubbleConsumableContentIncomingIcon(arguments.presentationData.theme.theme) } else { - consumableContentIcon = PresentationResourcesChat.chatBubbleConsumableContentOutgoingIcon(presentationData.theme.theme) + consumableContentIcon = PresentationResourcesChat.chatBubbleConsumableContentOutgoingIcon(arguments.presentationData.theme.theme) } } break @@ -303,20 +364,20 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { var isVoice = false var audioDuration: Int32 = 0 - let messageTheme = incoming ? presentationData.theme.theme.chat.message.incoming : presentationData.theme.theme.chat.message.outgoing + let messageTheme = arguments.incoming ? arguments.presentationData.theme.theme.chat.message.incoming : arguments.presentationData.theme.theme.chat.message.outgoing - for attribute in file.attributes { + for attribute in arguments.file.attributes { if case let .Audio(voice, duration, title, performer, waveform) = attribute { isAudio = true - if let forcedResourceStatus = forcedResourceStatus, statusUpdated { + if let forcedResourceStatus = arguments.forcedResourceStatus, statusUpdated { updatedStatusSignal = .single((forcedResourceStatus, nil)) } else if let currentUpdatedStatusSignal = updatedStatusSignal { updatedStatusSignal = currentUpdatedStatusSignal |> map { status, _ in switch status.mediaStatus { case let .fetchStatus(fetchStatus): - if !voice && !message.flags.isSending { + if !voice && !arguments.message.flags.isSending { return (FileMediaResourceStatus(mediaStatus: .fetchStatus(.Local), fetchStatus: status.fetchStatus), nil) } else { return (FileMediaResourceStatus(mediaStatus: .fetchStatus(fetchStatus), fetchStatus: status.fetchStatus), nil) @@ -336,12 +397,12 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { audioWaveform = AudioWaveform(bitstream: waveform, bitsPerSample: 5) } } else { - candidateTitleString = NSAttributedString(string: title ?? (file.fileName ?? "Unknown Track"), font: titleFont, textColor: messageTheme.fileTitleColor) + candidateTitleString = NSAttributedString(string: title ?? (arguments.file.fileName ?? "Unknown Track"), font: titleFont, textColor: messageTheme.fileTitleColor) let descriptionText: String if let performer = performer { descriptionText = performer - } else if let size = file.size { - descriptionText = dataSizeString(size, formatting: DataSizeStringFormatting(chatPresentationData: presentationData)) + } else if let size = arguments.file.size { + descriptionText = dataSizeString(size, formatting: DataSizeStringFormatting(chatPresentationData: arguments.presentationData)) } else { descriptionText = "" } @@ -357,15 +418,15 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { if let candidateTitleString = candidateTitleString { titleString = candidateTitleString } else if !isVoice { - titleString = NSAttributedString(string: file.fileName ?? "File", font: titleFont, textColor: messageTheme.fileTitleColor) + titleString = NSAttributedString(string: arguments.file.fileName ?? "File", font: titleFont, textColor: messageTheme.fileTitleColor) } if let candidateDescriptionString = candidateDescriptionString { descriptionString = candidateDescriptionString } else if !isVoice { let descriptionText: String - if let size = file.size { - descriptionText = dataSizeString(size, formatting: DataSizeStringFormatting(chatPresentationData: presentationData)) + if let size = arguments.file.size { + descriptionText = dataSizeString(size, formatting: DataSizeStringFormatting(chatPresentationData: arguments.presentationData)) } else { descriptionText = "" } @@ -383,7 +444,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { let (descriptionLayout, descriptionApply) = descriptionAsyncLayout(TextNodeLayoutArguments(attributedString: descriptionString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let fileSizeString: String - if let _ = file.size { + if let _ = arguments.file.size { fileSizeString = "000.0 MB" } else { fileSizeString = "" @@ -423,46 +484,54 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { } var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))? - if let statusType = dateAndStatusType { + if let statusType = arguments.dateAndStatusType { var edited = false - if attributes.updatingMedia != nil { + if arguments.attributes.updatingMedia != nil { edited = true } var viewCount: Int? var dateReplies = 0 - let dateReactionsAndPeers = mergedMessageReactionsAndPeers(message: topMessage) - for attribute in message.attributes { + let dateReactionsAndPeers = mergedMessageReactionsAndPeers(message: arguments.topMessage) + for attribute in arguments.message.attributes { if let attribute = attribute as? EditedMessageAttribute { edited = !attribute.isHidden } else if let attribute = attribute as? ViewCountMessageAttribute { viewCount = attribute.count - } else if let attribute = attribute as? ReplyThreadMessageAttribute, case .peer = chatLocation { - if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .group = channel.info { + } else if let attribute = attribute as? ReplyThreadMessageAttribute, case .peer = arguments.chatLocation { + if let channel = arguments.message.peers[arguments.message.id.peerId] as? TelegramChannel, case .group = channel.info { dateReplies = Int(attribute.count) } } } - if forcedIsEdited { + if arguments.forcedIsEdited { edited = true } - let dateText = stringForMessageTimestampStatus(accountPeerId: context.account.peerId, message: message, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, strings: presentationData.strings) + let dateText = stringForMessageTimestampStatus(accountPeerId: arguments.context.account.peerId, message: arguments.message, dateTimeFormat: arguments.presentationData.dateTimeFormat, nameDisplayOrder: arguments.presentationData.nameDisplayOrder, strings: arguments.presentationData.strings) + + let displayReactionsInline = shouldDisplayInlineDateReactions(message: arguments.message) + var reactionSettings: ChatMessageDateAndStatusNode.TrailingReactionSettings? + + if displayReactionsInline || arguments.displayReactions { + reactionSettings = ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: displayReactionsInline, preferAdditionalInset: !displayReactionsInline) + } statusSuggestedWidthAndContinue = statusLayout(ChatMessageDateAndStatusNode.Arguments( - context: context, - presentationData: presentationData, + context: arguments.context, + presentationData: arguments.presentationData, edited: edited, impressionCount: viewCount, dateText: dateText, type: statusType, - layoutInput: .trailingContent(contentWidth: iconFrame == nil ? 1000.0 : controlAreaWidth, reactionSettings: ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: shouldDisplayInlineDateReactions(message: message), preferAdditionalInset: !shouldDisplayInlineDateReactions(message: message))), + layoutInput: .trailingContent(contentWidth: iconFrame == nil ? 1000.0 : controlAreaWidth, reactionSettings: reactionSettings), constrainedSize: constrainedSize, - availableReactions: associatedData.availableReactions, + availableReactions: arguments.associatedData.availableReactions, reactions: dateReactionsAndPeers.reactions, reactionPeers: dateReactionsAndPeers.peers, replyCount: dateReplies, - isPinned: isPinned && !associatedData.isInPinnedListMode, - hasAutoremove: message.isSelfExpiring + isPinned: arguments.isPinned && !arguments.associatedData.isInPinnedListMode, + hasAutoremove: arguments.message.isSelfExpiring, + canViewReactionList: canViewMessageReactionList(message: arguments.message) )) } @@ -487,9 +556,9 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { if hasThumbnail { fileIconImage = nil } else { - let principalGraphics = PresentationResourcesChat.principalGraphics(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper, bubbleCorners: presentationData.chatBubbleCorners) + let principalGraphics = PresentationResourcesChat.principalGraphics(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper, bubbleCorners: arguments.presentationData.chatBubbleCorners) - fileIconImage = incoming ? principalGraphics.radialIndicatorFileIconIncoming : principalGraphics.radialIndicatorFileIconOutgoing + fileIconImage = arguments.incoming ? principalGraphics.radialIndicatorFileIconIncoming : principalGraphics.radialIndicatorFileIconOutgoing } return (minLayoutWidth, { boundingWidth in @@ -536,7 +605,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { } let streamingCacheStatusFrame: CGRect - if (isAudio && !isVoice) || file.previewRepresentations.isEmpty { + if (isAudio && !isVoice) || arguments.file.previewRepresentations.isEmpty { streamingCacheStatusFrame = CGRect(origin: CGPoint(x: progressFrame.maxX - streamingProgressDiameter + 2.0, y: progressFrame.maxY - streamingProgressDiameter + 2.0), size: CGSize(width: streamingProgressDiameter, height: streamingProgressDiameter)) } else { streamingCacheStatusFrame = CGRect() @@ -544,10 +613,10 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { return (fittedLayoutSize, { [weak self] synchronousLoads, animation in if let strongSelf = self { - strongSelf.context = context - strongSelf.presentationData = presentationData - strongSelf.message = message - strongSelf.file = file + strongSelf.context = arguments.context + strongSelf.presentationData = arguments.presentationData + strongSelf.message = arguments.message + strongSelf.file = arguments.file let _ = titleApply() let _ = descriptionApply() @@ -603,7 +672,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { } strongSelf.waveformScrubbingNode?.frame = CGRect(origin: CGPoint(x: 57.0, y: 1.0), size: CGSize(width: boundingWidth - 60.0, height: 15.0)) let waveformColor: UIColor - if incoming { + if arguments.incoming { if consumableContentIcon != nil { waveformColor = messageTheme.mediaActiveControlColor } else { @@ -679,8 +748,8 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { })) } - strongSelf.waveformNode.displaysAsynchronously = !presentationData.isPreview - strongSelf.statusNode?.displaysAsynchronously = !presentationData.isPreview + strongSelf.waveformNode.displaysAsynchronously = !arguments.presentationData.isPreview + strongSelf.statusNode?.displaysAsynchronously = !arguments.presentationData.isPreview strongSelf.statusNode?.frame = CGRect(origin: CGPoint(), size: progressFrame.size) strongSelf.statusContainerNode.frame = progressFrame @@ -694,14 +763,14 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { if let updatedFetchControls = updatedFetchControls { let _ = strongSelf.fetchControls.swap(updatedFetchControls) - if automaticDownload { + if arguments.automaticDownload { updatedFetchControls.fetch() } } let isAnimated = !synchronousLoads let transition: ContainedViewLayoutTransition = isAnimated ? .animated(duration: 0.2, curve: .spring) : .immediate - if let selection = messageSelection { + if let selection = arguments.messageSelection { if let streamingStatusNode = strongSelf.streamingStatusNode { transition.updateAlpha(node: streamingStatusNode, alpha: 0.0) transition.updateTransformScale(node: streamingStatusNode, scale: 0.2) @@ -712,14 +781,14 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { selectionNode.updateSelected(selection, animated: isAnimated) } else { let type: FileMessageSelectionNode.NodeType - if file.isVoice { + if arguments.file.isVoice { type = .voice - } else if file.isMusic || file.previewRepresentations.isEmpty { + } else if arguments.file.isMusic || arguments.file.previewRepresentations.isEmpty { type = .file } else { type = .media } - let selectionNode = FileMessageSelectionNode(theme: presentationData.theme.theme, incoming: incoming, type: type, toggle: { [weak self] value in + let selectionNode = FileMessageSelectionNode(theme: arguments.presentationData.theme.theme, incoming: arguments.incoming, type: type, toggle: { [weak self] value in self?.toggleSelection(value) }) strongSelf.selectionNode = selectionNode @@ -749,7 +818,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { strongSelf.updateStatus(animated: isAnimated) - if let forwardInfo = message.forwardInfo, forwardInfo.flags.contains(.isImported) { + if let forwardInfo = arguments.message.forwardInfo, forwardInfo.flags.contains(.isImported) { strongSelf.dateAndStatusNode.pressed = { guard let strongSelf = self else { return @@ -1061,12 +1130,12 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { self.fetchingCompactTextNode.frame = CGRect(origin: self.descriptionNode.frame.origin, size: fetchingCompactSize) } - static func asyncLayout(_ node: ChatMessageInteractiveFileNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ topMessage: Message, _ associatedData: ChatMessageItemAssociatedData, _ chatLocation: ChatLocation, _ attributes: ChatMessageEntryAttributes, _ isPinned: Bool, _ forcedIsEdited: Bool, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ isRecentActions: Bool, _ forcedResourceStatus: FileMediaResourceStatus?, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool, ListViewItemUpdateAnimation) -> ChatMessageInteractiveFileNode))) { + static func asyncLayout(_ node: ChatMessageInteractiveFileNode?) -> (Arguments) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool, ListViewItemUpdateAnimation) -> ChatMessageInteractiveFileNode))) { let currentAsyncLayout = node?.asyncLayout() - return { context, presentationData, message, topMessage, associatedData, chatLocation, attributes, isPinned, forcedIsEdited, file, automaticDownload, incoming, isRecentActions, forcedResourceStatus, dateAndStatusType, messageSelection, constrainedSize in + return { arguments in var fileNode: ChatMessageInteractiveFileNode - var fileLayout: (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ topMessage: Message, _ associatedData: ChatMessageItemAssociatedData, _ chatLocation: ChatLocation, _ attributes: ChatMessageEntryAttributes, _ isPinned: Bool, _ forcedIsEdited: Bool, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ isRecentActions: Bool, _ forcedResourceStatus: FileMediaResourceStatus?, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool, ListViewItemUpdateAnimation) -> Void))) + var fileLayout: (Arguments) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool, ListViewItemUpdateAnimation) -> Void))) if let node = node, let currentAsyncLayout = currentAsyncLayout { fileNode = node @@ -1076,7 +1145,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { fileLayout = fileNode.asyncLayout() } - let (initialWidth, continueLayout) = fileLayout(context, presentationData, message, topMessage, associatedData, chatLocation, attributes, isPinned, forcedIsEdited, file, automaticDownload, incoming, isRecentActions, forcedResourceStatus, dateAndStatusType, messageSelection, constrainedSize) + let (initialWidth, continueLayout) = fileLayout(arguments) return (initialWidth, { constrainedSize in let (finalWidth, finalLayout) = continueLayout(constrainedSize) @@ -1145,6 +1214,13 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { } return super.hitTest(point, with: event) } + + func hasTapAction(at point: CGPoint) -> Bool { + if let _ = self.dateAndStatusNode.hitTest(self.view.convert(point, to: self.dateAndStatusNode.view), with: nil) { + return true + } + return false + } } diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift index c033782e6a..e0ca23424b 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift @@ -303,7 +303,8 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { reactionPeers: dateReactionsAndPeers.peers, replyCount: dateReplies, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, - hasAutoremove: item.message.isSelfExpiring + hasAutoremove: item.message.isSelfExpiring, + canViewReactionList: canViewMessageReactionList(message: item.message) )) let (dateAndStatusSize, dateAndStatusApply) = statusSuggestedWidthAndContinue.1(statusSuggestedWidthAndContinue.0) diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift index ecac91cdca..08514b4d60 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift @@ -522,7 +522,8 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio reactionPeers: dateAndStatus.dateReactionPeers, replyCount: dateAndStatus.dateReplies, isPinned: dateAndStatus.isPinned, - hasAutoremove: message.isSelfExpiring + hasAutoremove: message.isSelfExpiring, + canViewReactionList: canViewMessageReactionList(message: message) )) let (size, apply) = statusSuggestedWidthAndContinue.1(statusSuggestedWidthAndContinue.0) diff --git a/submodules/TelegramUI/Sources/ChatMessageItemView.swift b/submodules/TelegramUI/Sources/ChatMessageItemView.swift index 43d64f554f..f08b5e9318 100644 --- a/submodules/TelegramUI/Sources/ChatMessageItemView.swift +++ b/submodules/TelegramUI/Sources/ChatMessageItemView.swift @@ -675,7 +675,7 @@ final class ChatMessageAccessibilityData { } } -public class ChatMessageItemView: ListViewItemNode { +public class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol { let layoutConstants = (ChatMessageItemLayoutConstants.compact, ChatMessageItemLayoutConstants.regular) var item: ChatMessageItem? @@ -869,7 +869,10 @@ public class ChatMessageItemView: ListViewItemNode { } } - func targetReactionView(value: String) -> UIView? { + func openMessageContextMenu() { + } + + public func targetReactionView(value: String) -> UIView? { return nil } diff --git a/submodules/TelegramUI/Sources/ChatMessageMapBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageMapBubbleContentNode.swift index 1e0f25f693..eb244603e7 100644 --- a/submodules/TelegramUI/Sources/ChatMessageMapBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageMapBubbleContentNode.swift @@ -259,7 +259,8 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { reactionPeers: dateReactionsAndPeers.peers, replyCount: dateReplies, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, - hasAutoremove: item.message.isSelfExpiring + hasAutoremove: item.message.isSelfExpiring, + canViewReactionList: canViewMessageReactionList(message: item.message) )) let (dateAndStatusSize, dateAndStatusApply) = statusSuggestedWidthAndContinue.1(statusSuggestedWidthAndContinue.0) diff --git a/submodules/TelegramUI/Sources/ChatMessageNotificationItem.swift b/submodules/TelegramUI/Sources/ChatMessageNotificationItem.swift index 3333e967ff..589d06b240 100644 --- a/submodules/TelegramUI/Sources/ChatMessageNotificationItem.swift +++ b/submodules/TelegramUI/Sources/ChatMessageNotificationItem.swift @@ -13,6 +13,8 @@ import LocalizedPeerData import StickerResources import PhotoResources import TelegramStringFormatting +import TextFormat +import InvisibleInkDustNode public final class ChatMessageNotificationItem: NotificationItem { let context: AccountContext @@ -68,6 +70,7 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { private let titleIconNode: ASImageNode private let titleNode: TextNode private let textNode: TextNode + private var dustNode: InvisibleInkDustNode? private let imageNode: TransformImageNode private var titleAttributedText: NSAttributedString? @@ -157,6 +160,7 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { var imageDimensions: CGSize? var isRound = false var messageText: String + var messageEntities: [MessageTextEntity]? if item.messages.first?.id.peerId.namespace == Namespaces.Peer.SecretChat { messageText = item.strings.PUSH_ENCRYPTED_MESSAGE("").string } else if item.messages.count == 1 { @@ -180,7 +184,19 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { if message.containsSecretMedia { imageDimensions = nil } - messageText = descriptionStringForMessage(contentSettings: item.context.currentContentSettings.with { $0 }, message: EngineMessage(message), strings: item.strings, nameDisplayOrder: item.nameDisplayOrder, dateTimeFormat: item.dateTimeFormat, accountPeerId: item.context.account.peerId).0 + let (textString, _, isText) = descriptionStringForMessage(contentSettings: item.context.currentContentSettings.with { $0 }, message: EngineMessage(message), strings: item.strings, nameDisplayOrder: item.nameDisplayOrder, dateTimeFormat: item.dateTimeFormat, accountPeerId: item.context.account.peerId) + if isText { + messageText = message.text + messageEntities = message.textEntitiesAttribute?.entities.filter { entity in + if case .Spoiler = entity.type { + return true + } else { + return false + } + } + } else { + messageText = textString + } } else if item.messages.count > 1, let peer = item.messages[0].peers[item.messages[0].id.peerId] { var displayAuthor = true if let channel = peer as? TelegramChannel { @@ -286,8 +302,15 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { title = "📅 \(currentTitle)" } - messageText = messageText.replacingOccurrences(of: "\n\n", with: " ") - + let textFont = compact ? Font.regular(15.0) : Font.regular(16.0) + let textColor = presentationData.theme.inAppNotification.primaryTextColor + var attributedMessageText: NSAttributedString + if let messageEntities = messageEntities { + attributedMessageText = stringWithAppliedEntities(messageText, entities: messageEntities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false) + } else { + attributedMessageText = NSAttributedString(string: messageText.replacingOccurrences(of: "\n\n", with: " "), font: textFont, textColor: textColor) + } + self.titleAttributedText = NSAttributedString(string: title ?? "", font: compact ? Font.semibold(15.0) : Font.semibold(16.0), textColor: presentationData.theme.inAppNotification.primaryTextColor) let imageNodeLayout = self.imageNode.asyncLayout() @@ -325,7 +348,7 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { self.imageNode.setSignal(updateImageSignal) } - self.textAttributedText = NSAttributedString(string: messageText, font: compact ? Font.regular(15.0) : Font.regular(16.0), textColor: presentationData.theme.inAppNotification.primaryTextColor) + self.textAttributedText = attributedMessageText if let width = self.validLayout { let _ = self.updateLayout(width: width, transition: .immediate) @@ -361,7 +384,7 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: self.textAttributedText, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .left, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) let _ = titleApply() let _ = textApply() - + let textSpacing: CGFloat = 1.0 let titleFrame = CGRect(origin: CGPoint(x: leftInset + titleInset, y: 1.0 + floor((panelHeight - textLayout.size.height - titleLayout.size.height - textSpacing) / 2.0)), size: titleLayout.size) @@ -371,10 +394,28 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { transition.updateFrame(node: self.titleIconNode, frame: CGRect(origin: CGPoint(x: leftInset + 1.0, y: titleFrame.minY + 3.0), size: image.size)) } - transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + textSpacing), size: textLayout.size)) + let textFrame = CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + textSpacing), size: textLayout.size) + transition.updateFrame(node: self.textNode, frame: textFrame) transition.updateFrame(node: self.imageNode, frame: CGRect(origin: CGPoint(x: width - 10.0 - imageSize.width, y: (panelHeight - imageSize.height) / 2.0), size: imageSize)) + if !textLayout.spoilers.isEmpty, let presentationData = self.item?.context.sharedContext.currentPresentationData.with({ $0 }) { + let dustNode: InvisibleInkDustNode + if let current = self.dustNode { + dustNode = current + } else { + dustNode = InvisibleInkDustNode(textNode: nil) + dustNode.isUserInteractionEnabled = false + self.dustNode = dustNode + self.insertSubnode(dustNode, aboveSubnode: self.textNode) + } + dustNode.frame = textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0) + dustNode.update(size: dustNode.frame.size, color: presentationData.theme.inAppNotification.primaryTextColor, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }, wordRects: textLayout.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }) + } else if let dustNode = self.dustNode { + dustNode.removeFromSupernode() + self.dustNode = nil + } + return panelHeight } } diff --git a/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift index 8363389824..bcff6c1fc7 100644 --- a/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift @@ -1079,7 +1079,8 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { reactionPeers: dateReactionsAndPeers.peers, replyCount: dateReplies, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, - hasAutoremove: item.message.isSelfExpiring + hasAutoremove: item.message.isSelfExpiring, + canViewReactionList: canViewMessageReactionList(message: item.message) )) } diff --git a/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift index d3f853a2b8..db8d90b2d0 100644 --- a/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift @@ -13,6 +13,24 @@ import ReactionButtonListComponent import AccountContext import WallpaperBackgroundNode +func canViewMessageReactionList(message: Message) -> Bool { + if let peer = message.peers[message.id.peerId] { + if let channel = peer as? TelegramChannel { + if case .broadcast = channel.info { + return false + } else { + return true + } + } else if let _ = peer as? TelegramGroup { + return true + } else { + return false + } + } else { + return false + } +} + final class MessageReactionButtonsNode: ASDisplayNode { enum DisplayType { case incoming @@ -29,7 +47,9 @@ final class MessageReactionButtonsNode: ASDisplayNode { private let container: ReactionButtonsAsyncLayoutContainer private let backgroundMaskView: UIView private var backgroundMaskButtons: [String: UIView] = [:] + var reactionSelected: ((String) -> Void)? + var openReactionPreview: ((ContextGesture?, ContextExtractedContentContainingNode, String) -> Void)? override init() { self.container = ReactionButtonsAsyncLayoutContainer() @@ -53,7 +73,46 @@ final class MessageReactionButtonsNode: ASDisplayNode { type: DisplayType ) -> (proposedWidth: CGFloat, continueLayout: (CGFloat) -> (size: CGSize, apply: (ListViewItemUpdateAnimation) -> Void)) { let reactionColors: ReactionButtonComponent.Colors + let themeColors: PresentationThemeBubbleColorComponents switch type { + case .incoming: + themeColors = bubbleColorComponents(theme: presentationData.theme.theme, incoming: true, wallpaper: !presentationData.theme.wallpaper.isEmpty) + reactionColors = ReactionButtonComponent.Colors( + deselectedBackground: themeColors.reactionInactiveBackground.argb, + selectedBackground: themeColors.reactionActiveBackground.argb, + deselectedForeground: themeColors.reactionInactiveForeground.argb, + selectedForeground: themeColors.reactionActiveForeground.argb, + extractedBackground: presentationData.theme.theme.contextMenu.backgroundColor.argb, + extractedForeground: presentationData.theme.theme.contextMenu.primaryColor.argb + ) + case .outgoing: + themeColors = bubbleColorComponents(theme: presentationData.theme.theme, incoming: false, wallpaper: !presentationData.theme.wallpaper.isEmpty) + reactionColors = ReactionButtonComponent.Colors( + deselectedBackground: themeColors.reactionInactiveBackground.argb, + selectedBackground: themeColors.reactionActiveBackground.argb, + deselectedForeground: themeColors.reactionInactiveForeground.argb, + selectedForeground: themeColors.reactionActiveForeground.argb, + extractedBackground: presentationData.theme.theme.contextMenu.backgroundColor.argb, + extractedForeground: presentationData.theme.theme.contextMenu.primaryColor.argb + ) + case .freeform: + if presentationData.theme.wallpaper.isEmpty { + themeColors = presentationData.theme.theme.chat.message.freeform.withoutWallpaper + } else { + themeColors = presentationData.theme.theme.chat.message.freeform.withWallpaper + } + + reactionColors = ReactionButtonComponent.Colors( + deselectedBackground: selectReactionFillStaticColor(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper).argb, + selectedBackground: themeColors.reactionActiveBackground.argb, + deselectedForeground: themeColors.reactionInactiveForeground.argb, + selectedForeground: themeColors.reactionActiveForeground.argb, + extractedBackground: presentationData.theme.theme.contextMenu.backgroundColor.argb, + extractedForeground: presentationData.theme.theme.contextMenu.primaryColor.argb + ) + } + + /*switch type { case .incoming: reactionColors = ReactionButtonComponent.Colors( deselectedBackground: presentationData.theme.theme.chat.message.incoming.accentControlColor.withMultipliedAlpha(0.1).argb, @@ -75,6 +134,11 @@ final class MessageReactionButtonsNode: ASDisplayNode { deselectedForeground: UIColor(white: 1.0, alpha: 1.0).argb, selectedForeground: UIColor(white: 0.0, alpha: 0.1).argb ) + }*/ + + var totalReactionCount: Int = 0 + for reaction in reactions.reactions { + totalReactionCount += Int(reaction.count) } let reactionButtonsResult = self.container.update( @@ -109,11 +173,11 @@ final class MessageReactionButtonsNode: ASDisplayNode { } } - if peers.count != Int(reaction.count) { + if peers.count != Int(reaction.count) || totalReactionCount != reactions.recentPeers.count { peers.removeAll() } - return ReactionButtonsLayoutContainer.Reaction( + return ReactionButtonsAsyncLayoutContainer.Reaction( reaction: ReactionButtonComponent.Reaction( value: reaction.value, iconFile: iconFile @@ -211,7 +275,7 @@ final class MessageReactionButtonsNode: ASDisplayNode { switch alignment { case .left: if reactionButtonPosition.x + item.size.width > boundingWidth { - reactionButtonPosition.x = 0.0 + reactionButtonPosition.x = -1.0 reactionButtonPosition.y += item.size.height + 6.0 } case .right: @@ -244,15 +308,27 @@ final class MessageReactionButtonsNode: ASDisplayNode { strongSelf.backgroundMaskButtons[item.value] = itemMaskView } - if item.view.superview == nil { - strongSelf.view.addSubview(item.view) + if item.node.supernode == nil { + strongSelf.addSubnode(item.node) if animation.isAnimated { - item.view.layer.animateScale(from: 0.01, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) - item.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + item.node.layer.animateScale(from: 0.01, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) + item.node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } - item.view.frame = itemFrame + item.node.frame = itemFrame + + let itemValue = item.value + let itemNode = item.node + item.node.isGestureEnabled = canViewMessageReactionList(message: message) + item.node.activated = { [weak itemNode] gesture, _ in + guard let strongSelf = self, let itemNode = itemNode else { + gesture.cancel() + return + } + strongSelf.openReactionPreview?(gesture, itemNode.containerNode, itemValue) + } + item.node.additionalActivationProgressLayer = itemMaskView.layer } else { - animation.animator.updateFrame(layer: item.view.layer, frame: itemFrame, completion: nil) + animation.animator.updateFrame(layer: item.node.layer, frame: itemFrame, completion: nil) } if itemMaskView.superview == nil { @@ -285,14 +361,14 @@ final class MessageReactionButtonsNode: ASDisplayNode { strongSelf.backgroundMaskButtons.removeValue(forKey: id) } - for view in reactionButtons.removedViews { + for node in reactionButtons.removedNodes { if animation.isAnimated { - view.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) - view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak view] _ in - view?.removeFromSuperview() + node.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + node.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak node] _ in + node?.removeFromSupernode() }) } else { - view.removeFromSuperview() + node.removeFromSupernode() } } }) @@ -349,6 +425,18 @@ final class MessageReactionButtonsNode: ASDisplayNode { animation.animator.updateScale(layer: button.layer, scale: 0.01, completion: nil) } } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + for (_, button) in self.container.buttons { + if button.frame.contains(point) { + if let result = button.hitTest(self.view.convert(point, to: button.view), with: event) { + return result + } + } + } + + return nil + } } final class ChatMessageReactionsFooterContentNode: ChatMessageBubbleContentNode { @@ -367,6 +455,15 @@ final class ChatMessageReactionsFooterContentNode: ChatMessageBubbleContentNode } item.controllerInteraction.updateMessageReaction(item.message, .reaction(value)) } + + self.buttonsNode.openReactionPreview = { [weak self] gesture, sourceNode, value in + guard let strongSelf = self, let item = strongSelf.item else { + gesture?.cancel() + return + } + + item.controllerInteraction.openMessageReactionContextMenu(item.topMessage, sourceNode, gesture, value) + } } required init?(coder aDecoder: NSCoder) { @@ -502,6 +599,7 @@ final class ChatMessageReactionButtonsNode: ASDisplayNode { private let buttonsNode: MessageReactionButtonsNode var reactionSelected: ((String) -> Void)? + var openReactionPreview: ((ContextGesture?, ContextExtractedContentContainingNode, String) -> Void)? override init() { self.buttonsNode = MessageReactionButtonsNode() @@ -509,9 +607,14 @@ final class ChatMessageReactionButtonsNode: ASDisplayNode { super.init() self.addSubnode(self.buttonsNode) + self.buttonsNode.reactionSelected = { [weak self] value in self?.reactionSelected?(value) } + + self.buttonsNode.openReactionPreview = { [weak self] gesture, sourceNode, value in + self?.openReactionPreview?(gesture, sourceNode, value) + } } class func asyncLayout(_ maybeNode: ChatMessageReactionButtonsNode?) -> (_ arguments: ChatMessageReactionButtonsNode.Arguments) -> (minWidth: CGFloat, layout: (CGFloat) -> (size: CGSize, apply: (_ animation: ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode)) { diff --git a/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift b/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift index 10c2a1ac0d..504c9a139f 100644 --- a/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift @@ -10,6 +10,8 @@ import AccountContext import LocalizedPeerData import PhotoResources import TelegramStringFormatting +import TextFormat +import InvisibleInkDustNode enum ChatMessageReplyInfoType { case bubble(incoming: Bool) @@ -21,6 +23,7 @@ class ChatMessageReplyInfoNode: ASDisplayNode { private let lineNode: ASImageNode private var titleNode: TextNode? private var textNode: TextNode? + private var dustNode: InvisibleInkDustNode? private var imageNode: TransformImageNode? private var previousMediaReference: AnyMediaReference? @@ -64,12 +67,13 @@ class ChatMessageReplyInfoNode: ASDisplayNode { } } - let (textString, isMedia) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: EngineMessage(message), strings: strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat, accountPeerId: context.account.peerId) + let (textString, isMedia, isText) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: EngineMessage(message), strings: strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat, accountPeerId: context.account.peerId) let placeholderColor: UIColor = message.effectivelyIncoming(context.account.peerId) ? presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor : presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor let titleColor: UIColor let lineImage: UIImage? let textColor: UIColor + let dustColor: UIColor switch type { case let .bubble(incoming): @@ -80,6 +84,7 @@ class ChatMessageReplyInfoNode: ASDisplayNode { } else { textColor = incoming ? presentationData.theme.theme.chat.message.incoming.primaryTextColor : presentationData.theme.theme.chat.message.outgoing.primaryTextColor } + dustColor = incoming ? presentationData.theme.theme.chat.message.incoming.secondaryTextColor : presentationData.theme.theme.chat.message.outgoing.secondaryTextColor case .standalone: let serviceColor = serviceMessageColorComponents(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper) titleColor = serviceColor.primaryText @@ -87,6 +92,22 @@ class ChatMessageReplyInfoNode: ASDisplayNode { let graphics = PresentationResourcesChat.additionalGraphics(presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper, bubbleCorners: presentationData.chatBubbleCorners) lineImage = graphics.chatServiceVerticalLineImage textColor = titleColor + dustColor = titleColor + } + + + let messageText: NSAttributedString + if isText { + let entities = (message.textEntitiesAttribute?.entities ?? []).filter { entity in + if case .Spoiler = entity.type { + return true + } else { + return false + } + } + messageText = stringWithAppliedEntities(message.text, entities: entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false) + } else { + messageText = NSAttributedString(string: textString, font: textFont, textColor: textColor) } var leftInset: CGFloat = 11.0 @@ -131,7 +152,7 @@ class ChatMessageReplyInfoNode: ASDisplayNode { let textInsets = UIEdgeInsets(top: 3.0, left: 0.0, bottom: 3.0, right: 0.0) let (titleLayout, titleApply) = titleNodeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: titleString, font: titleFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: contrainedTextSize, alignment: .natural, cutout: nil, insets: textInsets)) - let (textLayout, textApply) = textNodeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: textString, font: textFont, textColor: textColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: contrainedTextSize, alignment: .natural, cutout: nil, insets: textInsets)) + let (textLayout, textApply) = textNodeLayout(TextNodeLayoutArguments(attributedString: messageText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: contrainedTextSize, alignment: .natural, cutout: nil, insets: textInsets)) let imageSide = titleLayout.size.height + textLayout.size.height - 16.0 @@ -218,8 +239,27 @@ class ChatMessageReplyInfoNode: ASDisplayNode { node.imageNode?.captureProtected = message.isCopyProtected() titleNode.frame = CGRect(origin: CGPoint(x: leftInset - textInsets.left, y: spacing - textInsets.top), size: titleLayout.size) - textNode.frame = CGRect(origin: CGPoint(x: leftInset - textInsets.left, y: titleNode.frame.maxY - textInsets.bottom + spacing - textInsets.top), size: textLayout.size) + let textFrame = CGRect(origin: CGPoint(x: leftInset - textInsets.left, y: titleNode.frame.maxY - textInsets.bottom + spacing - textInsets.top), size: textLayout.size) + textNode.frame = textFrame + + if !textLayout.spoilers.isEmpty { + let dustNode: InvisibleInkDustNode + if let current = node.dustNode { + dustNode = current + } else { + dustNode = InvisibleInkDustNode(textNode: nil) + dustNode.isUserInteractionEnabled = false + node.dustNode = dustNode + node.contentNode.insertSubnode(dustNode, aboveSubnode: textNode) + } + dustNode.frame = textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0) + dustNode.update(size: dustNode.frame.size, color: dustColor, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }, wordRects: textLayout.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }) + } else if let dustNode = node.dustNode { + dustNode.removeFromSupernode() + node.dustNode = nil + } + node.lineNode.image = lineImage node.lineNode.frame = CGRect(origin: CGPoint(x: 1.0, y: 3.0), size: CGSize(width: 2.0, height: max(0.0, size.height - 5.0))) diff --git a/submodules/TelegramUI/Sources/ChatMessageRestrictedBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageRestrictedBubbleContentNode.swift index 13df9e7b0c..c7e3cb838a 100644 --- a/submodules/TelegramUI/Sources/ChatMessageRestrictedBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageRestrictedBubbleContentNode.swift @@ -127,7 +127,8 @@ class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNode { reactionPeers: dateReactionsAndPeers.peers, replyCount: dateReplies, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread, - hasAutoremove: item.message.isSelfExpiring + hasAutoremove: item.message.isSelfExpiring, + canViewReactionList: canViewMessageReactionList(message: item.message) )) } diff --git a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift index 856bf74daa..b34d37ca52 100644 --- a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift @@ -173,6 +173,12 @@ class ChatMessageStickerItemNode: ChatMessageItemView { return .fail } + if let reactionButtonsNode = strongSelf.reactionButtonsNode { + if let _ = reactionButtonsNode.hitTest(strongSelf.view.convert(point, to: reactionButtonsNode.view), with: nil) { + return .fail + } + } + if let item = strongSelf.item, item.presentationData.largeEmoji && messageIsElligibleForLargeEmoji(item.message) { if strongSelf.imageNode.frame.contains(point) { return .waitForDoubleTap @@ -436,7 +442,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } } - if item.associatedData.isCopyProtectionEnabled { + if item.associatedData.isCopyProtectionEnabled || item.message.isCopyProtected() { needsShareButton = false } } @@ -479,8 +485,8 @@ class ChatMessageStickerItemNode: ChatMessageItemView { var dateReplies = 0 let dateReactionsAndPeers = mergedMessageReactionsAndPeers(message: item.message) for attribute in item.message.attributes { - if let _ = attribute as? EditedMessageAttribute, isEmoji { - edited = true + if let attribute = attribute as? EditedMessageAttribute, isEmoji { + edited = !attribute.isHidden } else if let attribute = attribute as? ViewCountMessageAttribute { viewCount = attribute.count } else if let attribute = attribute as? ReplyThreadMessageAttribute, case .peer = item.chatLocation { @@ -511,7 +517,8 @@ class ChatMessageStickerItemNode: ChatMessageItemView { reactionPeers: dateReactionsAndPeers.peers, replyCount: dateReplies, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, - hasAutoremove: item.message.isSelfExpiring + hasAutoremove: item.message.isSelfExpiring, + canViewReactionList: canViewMessageReactionList(message: item.message) )) let (dateAndStatusSize, dateAndStatusApply) = statusSuggestedWidthAndContinue.1(statusSuggestedWidthAndContinue.0) @@ -682,7 +689,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { layoutSize.height += dateAndStatusSize.height } if let reactionButtonsSizeAndApply = reactionButtonsSizeAndApply { - layoutSize.height += reactionButtonsSizeAndApply.0.height + layoutSize.height += 4.0 + reactionButtonsSizeAndApply.0.height } if let actionButtonsSizeAndApply = actionButtonsSizeAndApply { layoutSize.height += actionButtonsSizeAndApply.0.height @@ -979,7 +986,10 @@ class ChatMessageStickerItemNode: ChatMessageItemView { if let reactionButtonsSizeAndApply = reactionButtonsSizeAndApply { let reactionButtonsNode = reactionButtonsSizeAndApply.1(animation) - let reactionButtonsFrame = CGRect(origin: CGPoint(x: imageFrame.minX, y: imageFrame.maxY - 10.0), size: reactionButtonsSizeAndApply.0) + var reactionButtonsFrame = CGRect(origin: CGPoint(x: imageFrame.minX, y: imageFrame.maxY - 4.0), size: reactionButtonsSizeAndApply.0) + if let actionButtonsSizeAndApply = actionButtonsSizeAndApply { + reactionButtonsFrame.origin.y += 4.0 + actionButtonsSizeAndApply.0.height + } if reactionButtonsNode !== strongSelf.reactionButtonsNode { strongSelf.reactionButtonsNode = reactionButtonsNode reactionButtonsNode.reactionSelected = { value in @@ -988,6 +998,14 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } item.controllerInteraction.updateMessageReaction(item.message, .reaction(value)) } + reactionButtonsNode.openReactionPreview = { gesture, sourceNode, value in + guard let strongSelf = self, let item = strongSelf.item else { + gesture?.cancel() + return + } + + item.controllerInteraction.openMessageReactionContextMenu(item.message, sourceNode, gesture, value) + } reactionButtonsNode.frame = reactionButtonsFrame strongSelf.addSubnode(reactionButtonsNode) if animation.isAnimated { @@ -1599,6 +1617,13 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } } + override func openMessageContextMenu() { + guard let item = self.item else { + return + } + item.controllerInteraction.openMessageContextMenu(item.message, false, self, self.imageNode.frame, nil) + } + override func targetReactionView(value: String) -> UIView? { if let result = self.reactionButtonsNode?.reactionTargetView(value: value) { return result diff --git a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift index 167cdfa360..0360664c40 100644 --- a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift @@ -76,6 +76,15 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } item.controllerInteraction.updateMessageReaction(item.message, .reaction(value)) } + + self.statusNode.openReactionPreview = { [weak self] gesture, sourceNode, value in + guard let strongSelf = self, let item = strongSelf.item else { + gesture?.cancel() + return + } + + item.controllerInteraction.openMessageReactionContextMenu(item.topMessage, sourceNode, gesture, value) + } } required init?(coder aDecoder: NSCoder) { @@ -119,7 +128,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } var viewCount: Int? var dateReplies = 0 - let dateReactionsAndPeers = mergedMessageReactionsAndPeers(message: item.message) + let dateReactionsAndPeers = mergedMessageReactionsAndPeers(message: item.topMessage) for attribute in item.message.attributes { if let attribute = attribute as? EditedMessageAttribute { @@ -309,7 +318,8 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { reactionPeers: dateReactionsAndPeers.peers, replyCount: dateReplies, isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread, - hasAutoremove: item.message.isSelfExpiring + hasAutoremove: item.message.isSelfExpiring, + canViewReactionList: canViewMessageReactionList(message: item.message) )) } @@ -374,6 +384,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { if let (_, spoilerTextApply) = spoilerTextLayoutAndApply { let spoilerTextNode = spoilerTextApply() if strongSelf.spoilerTextNode == nil { + spoilerTextNode.alpha = 0.0 spoilerTextNode.isUserInteractionEnabled = false spoilerTextNode.contentMode = .topLeft spoilerTextNode.contentsScale = UIScreenScale @@ -384,8 +395,6 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } strongSelf.spoilerTextNode?.frame = textFrame - strongSelf.spoilerTextNode?.isHidden = false - strongSelf.spoilerTextNode?.alpha = 0.0 let dustNode: InvisibleInkDustNode if let current = strongSelf.dustNode { @@ -395,11 +404,16 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { strongSelf.dustNode = dustNode strongSelf.insertSubnode(dustNode, aboveSubnode: spoilerTextNode) } - dustNode.update(size: textFrame.size, color: messageTheme.primaryTextColor, rects: textLayout.spoilers.map { $0.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 0.0, dy: 1.0) }) dustNode.frame = textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0) + dustNode.update(size: dustNode.frame.size, color: messageTheme.secondaryTextColor, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }, wordRects: textLayout.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }) } else if let spoilerTextNode = strongSelf.spoilerTextNode { strongSelf.spoilerTextNode = nil spoilerTextNode.removeFromSupernode() + + if let dustNode = strongSelf.dustNode { + strongSelf.dustNode = nil + dustNode.removeFromSupernode() + } } if let textSelectionNode = strongSelf.textSelectionNode { @@ -632,7 +646,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { override func updateIsExtractedToContextPreview(_ value: Bool) { if value { - if self.textSelectionNode == nil, let item = self.item, !item.associatedData.isCopyProtectionEnabled, let rootNode = item.controllerInteraction.chatControllerNode() { + if self.textSelectionNode == nil, let item = self.item, !item.associatedData.isCopyProtectionEnabled && !item.message.isCopyProtected(), let rootNode = item.controllerInteraction.chatControllerNode() { let selectionColor: UIColor let knobColor: UIColor if item.message.effectivelyIncoming(item.context.account.peerId) { @@ -653,20 +667,36 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } item.controllerInteraction.performTextSelectionAction(item.message.stableId, text, action) }) + textSelectionNode.updateRange = { [weak self] selectionRange in + if let strongSelf = self, let dustNode = strongSelf.dustNode, !dustNode.isRevealed, let textLayout = strongSelf.textNode.cachedLayout, !textLayout.spoilers.isEmpty, let selectionRange = selectionRange { + for (spoilerRange, _) in textLayout.spoilers { + if let intersection = selectionRange.intersection(spoilerRange), intersection.length > 0 { + dustNode.update(revealed: true) + return + } + } + } + } self.textSelectionNode = textSelectionNode self.addSubnode(textSelectionNode) self.insertSubnode(textSelectionNode.highlightAreaNode, belowSubnode: self.textNode) textSelectionNode.frame = self.textNode.frame textSelectionNode.highlightAreaNode.frame = self.textNode.frame } - } else if let textSelectionNode = self.textSelectionNode { - self.textSelectionNode = nil - self.updateIsTextSelectionActive?(false) - textSelectionNode.highlightAreaNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) - textSelectionNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak textSelectionNode] _ in - textSelectionNode?.highlightAreaNode.removeFromSupernode() - textSelectionNode?.removeFromSupernode() - }) + } else { + if let textSelectionNode = self.textSelectionNode { + self.textSelectionNode = nil + self.updateIsTextSelectionActive?(false) + textSelectionNode.highlightAreaNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + textSelectionNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak textSelectionNode] _ in + textSelectionNode?.highlightAreaNode.removeFromSupernode() + textSelectionNode?.removeFromSupernode() + }) + } + + if let dustNode = self.dustNode, dustNode.isRevealed { + dustNode.update(revealed: false) + } } } diff --git a/submodules/TelegramUI/Sources/ChatMessageThrottledProcessingManager.swift b/submodules/TelegramUI/Sources/ChatMessageThrottledProcessingManager.swift index 712db909a9..5816f49a00 100644 --- a/submodules/TelegramUI/Sources/ChatMessageThrottledProcessingManager.swift +++ b/submodules/TelegramUI/Sources/ChatMessageThrottledProcessingManager.swift @@ -7,16 +7,18 @@ final class ChatMessageThrottledProcessingManager { private let queue = Queue() private let delay: Double + private let submitInterval: Double? var process: ((Set) -> Void)? private var timer: SwiftSignalKit.Timer? private var processedList: [MessageId] = [] - private var processed = Set() + private var processed: [MessageId: Double] = [:] private var buffer = Set() - init(delay: Double = 1.0) { + init(delay: Double = 1.0, submitInterval: Double? = nil) { self.delay = delay + self.submitInterval = submitInterval } func setProcess(process: @escaping (Set) -> Void) { @@ -27,9 +29,17 @@ final class ChatMessageThrottledProcessingManager { func add(_ messageIds: [MessageId]) { self.queue.async { + let timestamp = CFAbsoluteTimeGetCurrent() + for id in messageIds { - if !self.processed.contains(id) { - self.processed.insert(id) + if let processedTimestamp = self.processed[id] { + if let submitInterval = self.submitInterval, (timestamp - processedTimestamp) >= submitInterval { + self.processed[id] = timestamp + self.processedList.append(id) + self.buffer.insert(id) + } + } else { + self.processed[id] = timestamp self.processedList.append(id) self.buffer.insert(id) } @@ -37,7 +47,7 @@ final class ChatMessageThrottledProcessingManager { if self.processedList.count > 1000 { for i in 0 ..< 200 { - self.processed.remove(self.processedList[i]) + self.processed.removeValue(forKey: self.processedList[i]) } self.processedList.removeSubrange(0 ..< 200) } diff --git a/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift b/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift index cf4d579b2c..62e47ed088 100644 --- a/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift @@ -5,6 +5,10 @@ import Display import ContextUI import AnimatedStickerNode import SwiftSignalKit +import ContextUI +import Postbox +import TelegramCore +import ReactionSelectionNode private final class OverlayTransitionContainerNode: ViewControllerTracingNode { override init() { @@ -595,6 +599,53 @@ public final class ChatMessageTransitionNode: ASDisplayNode { } } } + + private final class MessageReactionContext { + private(set) weak var itemNode: ListViewItemNode? + private(set) weak var contextController: ContextController? + private(set) weak var standaloneReactionAnimation: StandaloneReactionAnimation? + + var isEmpty: Bool { + return self.contextController == nil && self.standaloneReactionAnimation == nil + } + + init(itemNode: ListViewItemNode, contextController: ContextController?, standaloneReactionAnimation: StandaloneReactionAnimation?) { + self.itemNode = itemNode + self.contextController = contextController + self.standaloneReactionAnimation = standaloneReactionAnimation + } + + func addExternalOffset(offset: CGFloat, transition: ContainedViewLayoutTransition, itemNode: ListViewItemNode?) { + guard let currentItemNode = self.itemNode else { + return + } + if itemNode == nil || itemNode === currentItemNode { + if let contextController = self.contextController { + contextController.addRelativeContentOffset(CGPoint(x: 0.0, y: -offset), transition: transition) + } + if let standaloneReactionAnimation = self.standaloneReactionAnimation { + standaloneReactionAnimation.addRelativeContentOffset(CGPoint(x: 0.0, y: -offset), transition: transition) + } + } + } + + func addContentOffset(offset: CGFloat, itemNode: ListViewItemNode?) { + } + + func dismiss() { + if let contextController = self.contextController { + contextController.cancelReactionAnimation() + contextController.view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + }) + } + if let standaloneReactionAnimation = self.standaloneReactionAnimation { + standaloneReactionAnimation.cancel() + standaloneReactionAnimation.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak standaloneReactionAnimation] _ in + standaloneReactionAnimation?.removeFromSupernode() + }) + } + } + } private let listNode: ChatHistoryListNode private let getContentAreaInScreenSpace: () -> CGRect @@ -604,6 +655,7 @@ public final class ChatMessageTransitionNode: ASDisplayNode { private var animatingItemNodes: [AnimatingItemNode] = [] private var decorationItemNodes: [DecorationItemNode] = [] + private var messageReactionContexts: [MessageReactionContext] = [] var hasScheduledTransitions: Bool { return self.currentPendingItem != nil @@ -714,6 +766,61 @@ public final class ChatMessageTransitionNode: ASDisplayNode { override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { return nil } + + private func removeEmptyMessageReactionContexts() { + for i in (0 ..< self.messageReactionContexts.count).reversed() { + if self.messageReactionContexts[i].isEmpty { + self.messageReactionContexts.remove(at: i) + } + } + } + + func dismissMessageReactionContexts(itemNode: ListViewItemNode? = nil) { + for i in (0 ..< self.messageReactionContexts.count).reversed() { + let messageReactionContext = self.messageReactionContexts[i] + if itemNode == nil || messageReactionContext.itemNode === itemNode { + self.messageReactionContexts.remove(at: i) + messageReactionContext.dismiss() + } + } + } + + func addMessageContextController(messageId: MessageId, contextController: ContextController) { + self.addMessageReactionContextContext(messageId: messageId, contextController: contextController, standaloneReactionAnimation: nil) + } + + func addMessageStandaloneReactionAnimation(messageId: MessageId, standaloneReactionAnimation: StandaloneReactionAnimation) { + self.addMessageReactionContextContext(messageId: messageId, contextController: nil, standaloneReactionAnimation: standaloneReactionAnimation) + } + + private func addMessageReactionContextContext(messageId: MessageId, contextController: ContextController?, standaloneReactionAnimation: StandaloneReactionAnimation?) { + self.removeEmptyMessageReactionContexts() + + var messageItemNode: ListViewItemNode? + self.listNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatMessageItemView { + if let item = itemNode.item { + for (message, _) in item.content { + if message.id == messageId { + messageItemNode = itemNode + break + } + } + } + } + } + + if let messageItemNode = messageItemNode { + for i in 0 ..< self.messageReactionContexts.count { + if self.messageReactionContexts[i].itemNode === messageItemNode { + self.messageReactionContexts[i].dismiss() + self.messageReactionContexts.remove(at: i) + break + } + } + self.messageReactionContexts.append(MessageReactionContext(itemNode: messageItemNode, contextController: contextController, standaloneReactionAnimation: standaloneReactionAnimation)) + } + } func addExternalOffset(offset: CGFloat, transition: ContainedViewLayoutTransition, itemNode: ListViewItemNode?) { for animatingItemNode in self.animatingItemNodes { @@ -724,6 +831,9 @@ public final class ChatMessageTransitionNode: ASDisplayNode { decorationItemNode.addExternalOffset(offset: offset, transition: transition) } } + for messageReactionContext in self.messageReactionContexts { + messageReactionContext.addExternalOffset(offset: offset, transition: transition, itemNode: itemNode) + } } func addContentOffset(offset: CGFloat, itemNode: ListViewItemNode?) { @@ -735,6 +845,9 @@ public final class ChatMessageTransitionNode: ASDisplayNode { decorationItemNode.addContentOffset(offset: offset) } } + for messageReactionContext in self.messageReactionContexts { + messageReactionContext.addContentOffset(offset: offset, itemNode: itemNode) + } } func isAnimatingMessage(stableId: UInt32) -> Bool { diff --git a/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift index d4158f8c69..e84f1a03aa 100644 --- a/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift @@ -15,6 +15,8 @@ import AnimatedCountLabelNode import AnimatedNavigationStripeNode import ContextUI import RadialStatusNode +import InvisibleInkDustNode +import TextFormat private enum PinnedMessageAnimation { case slideToTop @@ -50,6 +52,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { private let lineNode: AnimatedNavigationStripeNode private let titleNode: AnimatedCountLabelNode private let textNode: TextNode + private var dustNode: InvisibleInkDustNode? private let imageNode: TransformImageNode private let imageNodeContainer: ASDisplayNode @@ -451,7 +454,25 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { } let (titleLayout, titleApply) = makeTitleLayout(CGSize(width: width - textLineInset - contentLeftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), titleStrings) - let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: foldLineBreaks(descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: EngineMessage(message), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: accountPeerId).0), font: Font.regular(15.0), textColor: message.media.isEmpty || message.media.first is TelegramMediaWebpage ? theme.chat.inputPanel.primaryTextColor : theme.chat.inputPanel.secondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - textLineInset - contentLeftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 0.0, bottom: 2.0, right: 0.0))) + let (textString, _, isText) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: EngineMessage(message), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: accountPeerId) + + let messageText: NSAttributedString + let textFont = Font.regular(15.0) + if isText { + let entities = (message.textEntitiesAttribute?.entities ?? []).filter { entity in + if case .Spoiler = entity.type { + return true + } else { + return false + } + } + let textColor = theme.chat.inputPanel.primaryTextColor + messageText = stringWithAppliedEntities(message.text, entities: entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false) + } else { + messageText = NSAttributedString(string: foldLineBreaks(textString), font: textFont, textColor: message.media.isEmpty || message.media.first is TelegramMediaWebpage ? theme.chat.inputPanel.primaryTextColor : theme.chat.inputPanel.secondaryTextColor) + } + + let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: messageText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - textLineInset - contentLeftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 0.0, bottom: 2.0, right: 0.0))) Queue.mainQueue().async { if let strongSelf = self { @@ -463,7 +484,26 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { animationTransition.updateFrameAdditive(node: strongSelf.contentTextContainer, frame: CGRect(origin: CGPoint(x: contentLeftInset + textLineInset, y: 0.0), size: CGSize())) strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 5.0), size: titleLayout.size) - strongSelf.textNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 23.0), size: textLayout.size) + + let textFrame = CGRect(origin: CGPoint(x: 0.0, y: 23.0), size: textLayout.size) + strongSelf.textNode.frame = textFrame + + if !textLayout.spoilers.isEmpty { + let dustNode: InvisibleInkDustNode + if let current = strongSelf.dustNode { + dustNode = current + } else { + dustNode = InvisibleInkDustNode(textNode: nil) + dustNode.isUserInteractionEnabled = false + strongSelf.dustNode = dustNode + strongSelf.contentTextContainer.insertSubnode(dustNode, aboveSubnode: strongSelf.textNode) + } + dustNode.frame = textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0) + dustNode.update(size: dustNode.frame.size, color: theme.chat.inputPanel.primaryTextColor, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }, wordRects: textLayout.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }) + } else if let dustNode = strongSelf.dustNode { + dustNode.removeFromSupernode() + strongSelf.dustNode = nil + } let lineFrame = CGRect(origin: CGPoint(x: contentLeftInset, y: 0.0), size: CGSize(width: 2.0, height: panelHeight)) animationTransition.updateFrame(node: strongSelf.lineNode, frame: lineFrame) diff --git a/submodules/TelegramUI/Sources/ChatQrCodeScreen.swift b/submodules/TelegramUI/Sources/ChatQrCodeScreen.swift new file mode 100644 index 0000000000..2f142ff0f0 --- /dev/null +++ b/submodules/TelegramUI/Sources/ChatQrCodeScreen.swift @@ -0,0 +1,1322 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import Postbox +import TelegramCore +import SwiftSignalKit +import AccountContext +import SolidRoundedButtonNode +import TelegramPresentationData +import TelegramUIPreferences +import TelegramNotices +import PresentationDataUtils +import AnimationUI +import MergeLists +import MediaResources +import StickerResources +import WallpaperResources +import TooltipUI +import AnimatedStickerNode +import TelegramAnimatedStickerNode +import ShimmerEffect +import WallpaperBackgroundNode +import QrCode +import AvatarNode + +private func closeButtonImage(theme: PresentationTheme) -> UIImage? { + return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.setFillColor(UIColor(rgb: 0x808084, alpha: 0.1).cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + + context.setLineWidth(2.0) + context.setLineCap(.round) + context.setStrokeColor(theme.actionSheet.inputClearButtonColor.cgColor) + + context.move(to: CGPoint(x: 10.0, y: 10.0)) + context.addLine(to: CGPoint(x: 20.0, y: 20.0)) + context.strokePath() + + context.move(to: CGPoint(x: 20.0, y: 10.0)) + context.addLine(to: CGPoint(x: 10.0, y: 20.0)) + context.strokePath() + }) +} + +private struct ThemeSettingsThemeEntry: Comparable, Identifiable { + let index: Int + let emoticon: String? + let emojiFile: TelegramMediaFile? + let themeReference: PresentationThemeReference? + let nightMode: Bool + var selected: Bool + let theme: PresentationTheme + let strings: PresentationStrings + let wallpaper: TelegramWallpaper? + + var stableId: Int { + return index + } + + static func ==(lhs: ThemeSettingsThemeEntry, rhs: ThemeSettingsThemeEntry) -> Bool { + if lhs.index != rhs.index { + return false + } + if lhs.emoticon != rhs.emoticon { + return false + } + + if lhs.themeReference?.index != rhs.themeReference?.index { + return false + } + if lhs.nightMode != rhs.nightMode { + return false + } + if lhs.selected != rhs.selected { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.wallpaper != rhs.wallpaper { + return false + } + return true + } + + static func <(lhs: ThemeSettingsThemeEntry, rhs: ThemeSettingsThemeEntry) -> Bool { + return lhs.index < rhs.index + } + + func item(context: AccountContext, action: @escaping (String?) -> Void) -> ListViewItem { + return ThemeSettingsThemeIconItem(context: context, emoticon: self.emoticon, emojiFile: self.emojiFile, themeReference: self.themeReference, nightMode: self.nightMode, selected: self.selected, theme: self.theme, strings: self.strings, wallpaper: self.wallpaper, action: action) + } +} + + +private class ThemeSettingsThemeIconItem: ListViewItem { + let context: AccountContext + let emoticon: String? + let emojiFile: TelegramMediaFile? + let themeReference: PresentationThemeReference? + let nightMode: Bool + let selected: Bool + let theme: PresentationTheme + let strings: PresentationStrings + let wallpaper: TelegramWallpaper? + let action: (String?) -> Void + + public init(context: AccountContext, emoticon: String?, emojiFile: TelegramMediaFile?, themeReference: PresentationThemeReference?, nightMode: Bool, selected: Bool, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper?, action: @escaping (String?) -> Void) { + self.context = context + self.emoticon = emoticon + self.emojiFile = emojiFile + self.themeReference = themeReference + self.nightMode = nightMode + self.selected = selected + self.theme = theme + self.strings = strings + self.wallpaper = wallpaper + self.action = action + } + + public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + async { + let node = ThemeSettingsThemeItemIconNode() + let (nodeLayout, apply) = node.asyncLayout()(self, params) + node.insets = nodeLayout.insets + node.contentSize = nodeLayout.contentSize + + Queue.mainQueue().async { + completion(node, { + return (nil, { _ in + apply(false) + }) + }) + } + } + } + + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + Queue.mainQueue().async { + assert(node() is ThemeSettingsThemeItemIconNode) + if let nodeValue = node() as? ThemeSettingsThemeItemIconNode { + let layout = nodeValue.asyncLayout() + async { + let (nodeLayout, apply) = layout(self, params) + Queue.mainQueue().async { + completion(nodeLayout, { _ in + apply(animation.isAnimated) + }) + } + } + } + } + } + + public var selectable = true + public func selected(listView: ListView) { + self.action(self.emoticon) + } +} + +private struct ThemeSettingsThemeItemNodeTransition { + let deletions: [ListViewDeleteItem] + let insertions: [ListViewInsertItem] + let updates: [ListViewUpdateItem] + let crossfade: Bool + let entries: [ThemeSettingsThemeEntry] +} + +private func ensureThemeVisible(listNode: ListView, emoticon: String?, animated: Bool) -> Bool { + var resultNode: ThemeSettingsThemeItemIconNode? + var previousNode: ThemeSettingsThemeItemIconNode? + var nextNode: ThemeSettingsThemeItemIconNode? + listNode.forEachItemNode { node in + guard let node = node as? ThemeSettingsThemeItemIconNode else { + return + } + if resultNode == nil { + if node.item?.emoticon == emoticon { + resultNode = node + } else { + previousNode = node + } + } else if nextNode == nil { + nextNode = node + } + } + if let resultNode = resultNode { + var nodeToEnsure = resultNode + if case let .visible(resultVisibility) = resultNode.visibility, resultVisibility == 1.0 { + if let previousNode = previousNode, case let .visible(previousVisibility) = previousNode.visibility, previousVisibility < 0.5 { + nodeToEnsure = previousNode + } else if let nextNode = nextNode, case let .visible(nextVisibility) = nextNode.visibility, nextVisibility < 0.5 { + nodeToEnsure = nextNode + } + } + listNode.ensureItemNodeVisible(nodeToEnsure, animated: animated, overflow: 57.0) + return true + } else { + return false + } +} + +private func preparedTransition(context: AccountContext, action: @escaping (String?) -> Void, from fromEntries: [ThemeSettingsThemeEntry], to toEntries: [ThemeSettingsThemeEntry], crossfade: Bool) -> ThemeSettingsThemeItemNodeTransition { + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) + + let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, action: action), directionHint: .Down) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, action: action), directionHint: nil) } + + return ThemeSettingsThemeItemNodeTransition(deletions: deletions, insertions: insertions, updates: updates, crossfade: crossfade, entries: toEntries) +} + +private var cachedBorderImages: [String: UIImage] = [:] +private func generateBorderImage(theme: PresentationTheme, bordered: Bool, selected: Bool) -> UIImage? { + let key = "\(theme.list.itemBlocksBackgroundColor.hexString)_\(selected ? "s" + theme.list.itemAccentColor.hexString : theme.list.disclosureArrowColor.hexString)" + if let image = cachedBorderImages[key] { + return image + } else { + let image = generateImage(CGSize(width: 18.0, height: 18.0), rotatedContext: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + let lineWidth: CGFloat + if selected { + lineWidth = 2.0 + context.setLineWidth(lineWidth) + context.setStrokeColor(theme.list.itemBlocksBackgroundColor.cgColor) + + context.strokeEllipse(in: bounds.insetBy(dx: 3.0 + lineWidth / 2.0, dy: 3.0 + lineWidth / 2.0)) + + var accentColor = theme.list.itemAccentColor + if accentColor.rgb == 0xffffff { + accentColor = UIColor(rgb: 0x999999) + } + context.setStrokeColor(accentColor.cgColor) + } else { + context.setStrokeColor(theme.list.disclosureArrowColor.withAlphaComponent(0.4).cgColor) + lineWidth = 1.0 + } + + if bordered || selected { + context.setLineWidth(lineWidth) + context.strokeEllipse(in: bounds.insetBy(dx: 1.0 + lineWidth / 2.0, dy: 1.0 + lineWidth / 2.0)) + } + })?.stretchableImage(withLeftCapWidth: 9, topCapHeight: 9) + cachedBorderImages[key] = image + return image + } +} + +private final class ThemeSettingsThemeItemIconNode : ListViewItemNode { + private let containerNode: ASDisplayNode + private let emojiContainerNode: ASDisplayNode + private let imageNode: TransformImageNode + private let overlayNode: ASImageNode + private let textNode: TextNode + private let emojiNode: TextNode + private let emojiImageNode: TransformImageNode + private var animatedStickerNode: AnimatedStickerNode? + private var placeholderNode: StickerShimmerEffectNode + var snapshotView: UIView? + + var item: ThemeSettingsThemeIconItem? + + override var visibility: ListViewItemNodeVisibility { + didSet { + self.visibilityStatus = self.visibility != .none + } + } + + private var visibilityStatus: Bool = false { + didSet { + if self.visibilityStatus != oldValue { + self.animatedStickerNode?.visibility = self.visibilityStatus + } + } + } + + private let stickerFetchedDisposable = MetaDisposable() + + init() { + self.containerNode = ASDisplayNode() + self.emojiContainerNode = ASDisplayNode() + + self.imageNode = TransformImageNode() + self.imageNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 82.0, height: 108.0)) + self.imageNode.isLayerBacked = true + self.imageNode.cornerRadius = 8.0 + self.imageNode.clipsToBounds = true + + self.overlayNode = ASImageNode() + self.overlayNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 84.0, height: 110.0)) + self.overlayNode.isLayerBacked = true + + self.textNode = TextNode() + self.textNode.isUserInteractionEnabled = false + self.textNode.displaysAsynchronously = false + + self.emojiNode = TextNode() + self.emojiNode.isUserInteractionEnabled = false + self.emojiNode.displaysAsynchronously = false + + self.emojiImageNode = TransformImageNode() + + self.placeholderNode = StickerShimmerEffectNode() + + super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) + + self.addSubnode(self.containerNode) + self.containerNode.addSubnode(self.imageNode) + self.containerNode.addSubnode(self.overlayNode) + self.containerNode.addSubnode(self.textNode) + + self.addSubnode(self.emojiContainerNode) + self.emojiContainerNode.addSubnode(self.emojiNode) + self.emojiContainerNode.addSubnode(self.emojiImageNode) + self.emojiContainerNode.addSubnode(self.placeholderNode) + + var firstTime = true + self.emojiImageNode.imageUpdated = { [weak self] image in + guard let strongSelf = self else { + return + } + if image != nil { + strongSelf.removePlaceholder(animated: !firstTime) + if firstTime { + strongSelf.emojiImageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } + firstTime = false + } + } + + deinit { + self.stickerFetchedDisposable.dispose() + } + + private func removePlaceholder(animated: Bool) { + if !animated { + self.placeholderNode.removeFromSupernode() + } else { + self.placeholderNode.alpha = 0.0 + self.placeholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in + self?.placeholderNode.removeFromSupernode() + }) + } + } + + override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + let emojiFrame = CGRect(origin: CGPoint(x: 28.0, y: 71.0), size: CGSize(width: 34.0, height: 34.0)) + self.placeholderNode.updateAbsoluteRect(CGRect(origin: CGPoint(x: rect.minX + emojiFrame.minX, y: rect.minY + emojiFrame.minY), size: emojiFrame.size), within: containerSize) + } + + override func selected() { + let wasSelected = self.item?.selected ?? false + super.selected() + + if let animatedStickerNode = self.animatedStickerNode { + Queue.mainQueue().after(0.1) { + if !wasSelected { + animatedStickerNode.seekTo(.frameIndex(0)) + animatedStickerNode.play() + + let scale: CGFloat = 1.95 + animatedStickerNode.transform = CATransform3DMakeScale(scale, scale, 1.0) + animatedStickerNode.layer.animateSpring(from: 1.0 as NSNumber, to: scale as NSNumber, keyPath: "transform.scale", duration: 0.45) + + animatedStickerNode.completed = { [weak animatedStickerNode, weak self] _ in + guard let item = self?.item, item.selected else { + return + } + animatedStickerNode?.transform = CATransform3DIdentity + animatedStickerNode?.layer.animateSpring(from: scale as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.45) + } + } + } + } + + } + + func asyncLayout() -> (ThemeSettingsThemeIconItem, ListViewItemLayoutParams) -> (ListViewItemNodeLayout, (Bool) -> Void) { + let makeTextLayout = TextNode.asyncLayout(self.textNode) + let makeEmojiLayout = TextNode.asyncLayout(self.emojiNode) + let makeImageLayout = self.imageNode.asyncLayout() + + let currentItem = self.item + + return { [weak self] item, params in + var updatedEmoticon = false + var updatedThemeReference = false + var updatedTheme = false + var updatedWallpaper = false + var updatedSelected = false + var updatedNightMode = false + + if currentItem?.emoticon != item.emoticon { + updatedEmoticon = true + } + if currentItem?.themeReference != item.themeReference { + updatedThemeReference = true + } + if currentItem?.wallpaper != item.wallpaper { + updatedWallpaper = true + } + if currentItem?.theme !== item.theme { + updatedTheme = true + } + if currentItem?.selected != item.selected { + updatedSelected = true + } + if currentItem?.nightMode != item.nightMode { + updatedNightMode = true + } + + let text = NSAttributedString(string: item.strings.Conversation_Theme_NoTheme, font: Font.semibold(15.0), textColor: item.theme.actionSheet.controlAccentColor) + let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: text, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) + + let emoticon = item.emoticon + let title = NSAttributedString(string: emoticon != nil ? "" : "❌", font: Font.regular(22.0), textColor: .black) + let (_, emojiApply) = makeEmojiLayout(TextNodeLayoutArguments(attributedString: title, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) + + let itemLayout = ListViewItemNodeLayout(contentSize: CGSize(width: 120.0, height: 90.0), insets: UIEdgeInsets()) + return (itemLayout, { animated in + if let strongSelf = self { + strongSelf.item = item + + if updatedThemeReference || updatedWallpaper || updatedNightMode { + if let themeReference = item.themeReference { + strongSelf.imageNode.setSignal(themeIconImage(account: item.context.account, accountManager: item.context.sharedContext.accountManager, theme: themeReference, color: nil, wallpaper: item.wallpaper, nightMode: item.nightMode, emoticon: true, qr: true)) + strongSelf.imageNode.backgroundColor = nil + } + } + if item.themeReference == nil { + strongSelf.imageNode.backgroundColor = item.theme.actionSheet.opaqueItemBackgroundColor + } + + if updatedTheme || updatedSelected { + strongSelf.overlayNode.image = generateBorderImage(theme: item.theme, bordered: false, selected: item.selected) + } + + if !item.selected && currentItem?.selected == true, let animatedStickerNode = strongSelf.animatedStickerNode { + animatedStickerNode.transform = CATransform3DIdentity + + let initialScale: CGFloat = CGFloat((animatedStickerNode.value(forKeyPath: "layer.presentationLayer.transform.scale.x") as? NSNumber)?.floatValue ?? 1.0) + animatedStickerNode.layer.animateSpring(from: initialScale as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.45) + } + + strongSelf.textNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((90.0 - textLayout.size.width) / 2.0), y: 24.0), size: textLayout.size) + strongSelf.textNode.isHidden = item.emoticon != nil + + strongSelf.containerNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) + strongSelf.containerNode.frame = CGRect(origin: CGPoint(x: 15.0, y: -15.0), size: CGSize(width: 90.0, height: 120.0)) + + strongSelf.emojiContainerNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) + strongSelf.emojiContainerNode.frame = CGRect(origin: CGPoint(x: 15.0, y: -15.0), size: CGSize(width: 90.0, height: 120.0)) + + let _ = textApply() + let _ = emojiApply() + + let imageSize = CGSize(width: 82.0, height: 108.0) + strongSelf.imageNode.frame = CGRect(origin: CGPoint(x: 4.0, y: 6.0), size: imageSize) + let applyLayout = makeImageLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: .clear)) + applyLayout() + + strongSelf.overlayNode.frame = strongSelf.imageNode.frame.insetBy(dx: -1.0, dy: -1.0) + strongSelf.emojiNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 79.0), size: CGSize(width: 90.0, height: 30.0)) + + let emojiFrame = CGRect(origin: CGPoint(x: 28.0, y: 71.0), size: CGSize(width: 34.0, height: 34.0)) + if let file = item.emojiFile, updatedEmoticon { + let imageApply = strongSelf.emojiImageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: emojiFrame.size, boundingSize: emojiFrame.size, intrinsicInsets: UIEdgeInsets())) + imageApply() + strongSelf.emojiImageNode.setSignal(chatMessageStickerPackThumbnail(postbox: item.context.account.postbox, resource: file.resource, animated: true, nilIfEmpty: true)) + strongSelf.emojiImageNode.frame = emojiFrame + + let animatedStickerNode: AnimatedStickerNode + if let current = strongSelf.animatedStickerNode { + animatedStickerNode = current + } else { + animatedStickerNode = AnimatedStickerNode() + animatedStickerNode.started = { [weak self] in + self?.emojiImageNode.isHidden = true + } + strongSelf.animatedStickerNode = animatedStickerNode + strongSelf.emojiContainerNode.insertSubnode(animatedStickerNode, belowSubnode: strongSelf.placeholderNode) + let pathPrefix = item.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id) + animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: item.context.account, resource: file.resource), width: 128, height: 128, playbackMode: .still(.start), mode: .direct(cachePathPrefix: pathPrefix)) + + animatedStickerNode.anchorPoint = CGPoint(x: 0.5, y: 1.0) + } + animatedStickerNode.autoplay = true + animatedStickerNode.visibility = strongSelf.visibilityStatus + + strongSelf.stickerFetchedDisposable.set(fetchedMediaResource(mediaBox: item.context.account.postbox.mediaBox, reference: MediaResourceReference.media(media: .standalone(media: file), resource: file.resource)).start()) + + let thumbnailDimensions = PixelDimensions(width: 512, height: 512) + strongSelf.placeholderNode.update(backgroundColor: nil, foregroundColor: UIColor(rgb: 0xffffff, alpha: 0.2), shimmeringColor: UIColor(rgb: 0xffffff, alpha: 0.3), data: file.immediateThumbnailData, size: emojiFrame.size, imageSize: thumbnailDimensions.cgSize) + strongSelf.placeholderNode.frame = emojiFrame + } + + if let animatedStickerNode = strongSelf.animatedStickerNode { + animatedStickerNode.frame = emojiFrame + animatedStickerNode.updateLayout(size: emojiFrame.size) + } + } + }) + } + } + + func crossfade() { + if let snapshotView = self.containerNode.view.snapshotView(afterScreenUpdates: false) { + snapshotView.transform = self.containerNode.view.transform + snapshotView.frame = self.containerNode.view.frame + self.view.insertSubview(snapshotView, aboveSubview: self.containerNode.view) + + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: ChatQrCodeScreen.themeCrossfadeDuration, delay: ChatQrCodeScreen.themeCrossfadeDelay, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + } + } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + super.animateInsertion(currentTimestamp, duration: duration, short: short) + + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + super.animateRemoved(currentTimestamp, duration: duration) + + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + } + + override func animateAdded(_ currentTimestamp: Double, duration: Double) { + super.animateAdded(currentTimestamp, duration: duration) + + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } +} + +final class ChatQrCodeScreen: ViewController { + static let themeCrossfadeDuration: Double = 0.3 + static let themeCrossfadeDelay: Double = 0.05 + + private var controllerNode: ChatQrCodeScreenNode { + return self.displayNode as! ChatQrCodeScreenNode + } + + private var animatedIn = false + + private let context: AccountContext + private let animatedEmojiStickers: [String: [StickerPackItem]] + private let peer: Peer + + private var presentationData: PresentationData + private var presentationThemePromise = Promise() + private var presentationDataDisposable: Disposable? + + var dismissed: (() -> Void)? + + init(context: AccountContext, animatedEmojiStickers: [String: [StickerPackItem]], peer: Peer) { + self.context = context + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.animatedEmojiStickers = animatedEmojiStickers + self.peer = peer + + super.init(navigationBarPresentationData: nil) + + self.statusBar.statusBarStyle = .Ignore + self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) + + self.blocksBackgroundWhenInOverlay = true + + self.presentationThemePromise.set(.single(nil)) + + self.presentationDataDisposable = (combineLatest(context.sharedContext.presentationData, self.presentationThemePromise.get()) + |> deliverOnMainQueue).start(next: { [weak self] presentationData, theme in + if let strongSelf = self { + var presentationData = presentationData + if let theme = theme { + presentationData = presentationData.withUpdated(theme: theme) + } + strongSelf.presentationData = presentationData + strongSelf.controllerNode.updatePresentationData(presentationData) + } + }) + + self.statusBar.statusBarStyle = .Ignore + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.presentationDataDisposable?.dispose() + } + + override public func loadDisplayNode() { + self.displayNode = ChatQrCodeScreenNode(context: self.context, presentationData: self.presentationData, controller: self, animatedEmojiStickers: self.animatedEmojiStickers, peer: self.peer) + self.controllerNode.previewTheme = { [weak self] _, _, theme in + self?.presentationThemePromise.set(.single(theme)) + } + self.controllerNode.present = { [weak self] c in + self?.present(c, in: .current) + } + self.controllerNode.completion = { [weak self] emoticon in + guard let strongSelf = self else { + return + } + strongSelf.dismiss() + } + self.controllerNode.dismiss = { [weak self] in + self?.presentingViewController?.dismiss(animated: false, completion: nil) + } + self.controllerNode.cancel = { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.dismiss() + } + } + + override public func loadView() { + super.loadView() + + self.view.disablesInteractiveTransitionGestureRecognizer = true + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if !self.animatedIn { + self.animatedIn = true + self.controllerNode.animateIn() + } + } + + override public func dismiss(completion: (() -> Void)? = nil) { + self.forEachController({ controller in + if let controller = controller as? TooltipScreen { + controller.dismiss() + } + return true + }) + + self.controllerNode.animateOut(completion: completion) + + self.dismissed?() + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) + } + + func dimTapped() { + self.controllerNode.dimTapped() + } +} + +private func iconColors(theme: PresentationTheme) -> [String: UIColor] { + let accentColor = theme.actionSheet.controlAccentColor + var colors: [String: UIColor] = [:] + colors["Sunny.Path 14.Path.Stroke 1"] = accentColor + colors["Sunny.Path 15.Path.Stroke 1"] = accentColor + colors["Path.Path.Stroke 1"] = accentColor + colors["Sunny.Path 39.Path.Stroke 1"] = accentColor + colors["Sunny.Path 24.Path.Stroke 1"] = accentColor + colors["Sunny.Path 25.Path.Stroke 1"] = accentColor + colors["Sunny.Path 18.Path.Stroke 1"] = accentColor + colors["Sunny.Path 41.Path.Stroke 1"] = accentColor + colors["Sunny.Path 43.Path.Stroke 1"] = accentColor + colors["Path 10.Path.Fill 1"] = accentColor + colors["Path 11.Path.Fill 1"] = accentColor + return colors +} + +private let defaultEmoticon = "🏠" + +private class ChatQrCodeScreenNode: ViewControllerTracingNode, UIScrollViewDelegate { + private let context: AccountContext + private var presentationData: PresentationData + private weak var controller: ChatQrCodeScreen? + + private let dimNode: ASDisplayNode + private let containerNode: ASDisplayNode + private let wallpaperBackgroundNode: WallpaperBackgroundNode + private let codeBackgroundNode: ASDisplayNode + private let codeForegroundNode: ASDisplayNode + private var codeForegroundContentNode: ASDisplayNode? + private var codeForegroundDimNode: ASDisplayNode + private let codeMaskNode: ASDisplayNode + private let codeTextNode: ImmediateTextNode + private let codeImageNode: TransformImageNode + private let codeIconBackgroundNode: ASImageNode + private let codeIconNode: AnimatedStickerNode + private let avatarNode: ImageNode + private var qrCodeSize: Int? + + private let wrappingScrollNode: ASScrollNode + private let contentContainerNode: ASDisplayNode + private let topContentContainerNode: SparseNode + private let effectNode: ASDisplayNode + private let backgroundNode: ASDisplayNode + private let contentBackgroundNode: ASDisplayNode + private let titleNode: ASTextNode + private let cancelButton: HighlightableButtonNode + private let switchThemeButton: HighlightTrackingButtonNode + private let animationContainerNode: ASDisplayNode + private var animationNode: AnimationNode + private let doneButton: SolidRoundedButtonNode + + private let listNode: ListView + private var entries: [ThemeSettingsThemeEntry]? + private var enqueuedTransitions: [ThemeSettingsThemeItemNodeTransition] = [] + private var initialized = false + private var themes: [TelegramTheme] = [] + + private let peer: Peer + + private var selectedEmoticon: String? { + didSet { + self.selectedEmoticonPromise.set(self.selectedEmoticon) + } + } + private var selectedEmoticonPromise: ValuePromise + + private var isDarkAppearancePromise: ValuePromise + private var isDarkAppearance: Bool = false { + didSet { + self.isDarkAppearancePromise.set(self.isDarkAppearance) + } + } + + private var containerLayout: (ContainerViewLayout, CGFloat)? + + private let disposable = MetaDisposable() + + var present: ((ViewController) -> Void)? + var previewTheme: ((String?, Bool?, PresentationTheme) -> Void)? + var completion: ((String?) -> Void)? + var dismiss: (() -> Void)? + var cancel: (() -> Void)? + + init(context: AccountContext, presentationData: PresentationData, controller: ChatQrCodeScreen, animatedEmojiStickers: [String: [StickerPackItem]], peer: Peer) { + self.context = context + self.controller = controller + self.peer = peer + self.selectedEmoticon = defaultEmoticon + self.selectedEmoticonPromise = ValuePromise(self.selectedEmoticon) + self.presentationData = presentationData + + self.wrappingScrollNode = ASScrollNode() + self.wrappingScrollNode.view.alwaysBounceVertical = true + self.wrappingScrollNode.view.delaysContentTouches = false + self.wrappingScrollNode.view.canCancelContentTouches = true + + self.dimNode = ASDisplayNode() + self.dimNode.backgroundColor = .clear + + self.containerNode = ASDisplayNode() + + self.wallpaperBackgroundNode = createWallpaperBackgroundNode(context: context, forChatDisplay: true, useSharedAnimationPhase: false, useExperimentalImplementation: self.context.sharedContext.immediateExperimentalUISettings.experimentalBackground) + + self.contentContainerNode = ASDisplayNode() + self.contentContainerNode.isOpaque = false + + self.topContentContainerNode = SparseNode() + self.topContentContainerNode.isOpaque = false + + self.backgroundNode = ASDisplayNode() + self.backgroundNode.clipsToBounds = true + self.backgroundNode.cornerRadius = 16.0 + + self.isDarkAppearance = self.presentationData.theme.overallDarkAppearance + self.isDarkAppearancePromise = ValuePromise(self.presentationData.theme.overallDarkAppearance) + + let backgroundColor = self.presentationData.theme.actionSheet.itemBackgroundColor + let textColor = self.presentationData.theme.actionSheet.primaryTextColor + let blurStyle: UIBlurEffect.Style = self.presentationData.theme.actionSheet.backgroundType == .light ? .light : .dark + + self.effectNode = ASDisplayNode(viewBlock: { + return UIVisualEffectView(effect: UIBlurEffect(style: blurStyle)) + }) + + self.contentBackgroundNode = ASDisplayNode() + self.contentBackgroundNode.backgroundColor = backgroundColor + + self.titleNode = ASTextNode() + self.titleNode.attributedText = NSAttributedString(string: self.presentationData.strings.PeerInfo_QRCode_Title, font: Font.semibold(16.0), textColor: textColor) + + self.cancelButton = HighlightableButtonNode() + self.cancelButton.setImage(closeButtonImage(theme: self.presentationData.theme), for: .normal) + + self.switchThemeButton = HighlightTrackingButtonNode() + self.animationContainerNode = ASDisplayNode() + self.animationContainerNode.isUserInteractionEnabled = false + + self.animationNode = AnimationNode(animation: self.isDarkAppearance ? "anim_sun_reverse" : "anim_sun", colors: iconColors(theme: self.presentationData.theme), scale: 1.0) + self.animationNode.isUserInteractionEnabled = false + + self.doneButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: self.presentationData.theme), height: 52.0, cornerRadius: 11.0, gloss: false) + self.doneButton.title = self.presentationData.strings.InviteLink_QRCode_Share + + self.listNode = ListView() + self.listNode.transform = CATransform3DMakeRotation(-CGFloat.pi / 2.0, 0.0, 0.0, 1.0) + + self.codeBackgroundNode = ASDisplayNode() + self.codeBackgroundNode.backgroundColor = .white + self.codeBackgroundNode.cornerRadius = 42.0 + if #available(iOS 13.0, *) { + self.codeBackgroundNode.layer.cornerCurve = .continuous + } + self.codeForegroundNode = ASDisplayNode() + self.codeForegroundNode.backgroundColor = .black + + self.codeForegroundDimNode = ASDisplayNode() + self.codeForegroundDimNode.alpha = 0.3 + self.codeForegroundDimNode.backgroundColor = .black + + self.codeMaskNode = ASDisplayNode() + + self.codeImageNode = TransformImageNode() + + self.codeIconBackgroundNode = ASImageNode() + + self.codeIconNode = AnimatedStickerNode() + self.codeIconNode.setup(source: AnimatedStickerNodeLocalFileSource(name: "PlaneLogoPlain"), width: 240, height: 240, mode: .direct(cachePathPrefix: nil)) + self.codeIconNode.visibility = true + + self.codeTextNode = ImmediateTextNode() + self.codeTextNode.attributedText = NSAttributedString(string: "@\(peer.addressName ?? "")".uppercased(), font: Font.with(size: 24.0, design: .round, weight: .bold, traits: []), textColor: .black) + + self.avatarNode = ImageNode() + self.avatarNode.displaysAsynchronously = false + self.avatarNode.setSignal(peerAvatarCompleteImage(account: self.context.account, peer: EnginePeer(peer), size: CGSize(width: 180.0, height: 180.0), font: avatarPlaceholderFont(size: 78.0), fullSize: true)) + + super.init() + + self.backgroundColor = nil + self.isOpaque = false + + self.addSubnode(self.dimNode) + + self.wrappingScrollNode.view.delegate = self + self.addSubnode(self.wrappingScrollNode) + + self.wrappingScrollNode.addSubnode(self.containerNode) + + self.containerNode.addSubnode(self.wallpaperBackgroundNode) + + self.containerNode.addSubnode(self.codeBackgroundNode) + self.containerNode.addSubnode(self.codeForegroundNode) + + self.codeForegroundNode.addSubnode(self.codeForegroundDimNode) + + self.codeMaskNode.addSubnode(self.codeImageNode) + self.codeMaskNode.addSubnode(self.codeIconBackgroundNode) + self.codeMaskNode.addSubnode(self.codeTextNode) + + self.containerNode.addSubnode(self.avatarNode) + + self.wrappingScrollNode.addSubnode(self.codeIconNode) + + self.wrappingScrollNode.addSubnode(self.backgroundNode) + self.wrappingScrollNode.addSubnode(self.contentContainerNode) + self.wrappingScrollNode.addSubnode(self.topContentContainerNode) + + self.backgroundNode.addSubnode(self.effectNode) + self.backgroundNode.addSubnode(self.contentBackgroundNode) + self.contentContainerNode.addSubnode(self.titleNode) + self.contentContainerNode.addSubnode(self.doneButton) + + self.topContentContainerNode.addSubnode(self.animationContainerNode) + self.animationContainerNode.addSubnode(self.animationNode) + self.topContentContainerNode.addSubnode(self.switchThemeButton) + self.topContentContainerNode.addSubnode(self.listNode) + self.topContentContainerNode.addSubnode(self.cancelButton) + + self.switchThemeButton.addTarget(self, action: #selector(self.switchThemePressed), forControlEvents: .touchUpInside) + self.cancelButton.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside) + self.doneButton.pressed = { [weak self] in + if let strongSelf = self { + strongSelf.doneButton.isUserInteractionEnabled = false + strongSelf.completion?(strongSelf.selectedEmoticon) + } + } + + self.disposable.set(combineLatest(queue: Queue.mainQueue(), self.context.engine.themes.getChatThemes(accountManager: self.context.sharedContext.accountManager), self.selectedEmoticonPromise.get(), self.isDarkAppearancePromise.get()).start(next: { [weak self] themes, selectedEmoticon, isDarkAppearance in + guard let strongSelf = self else { + return + } + + let isFirstTime = strongSelf.entries == nil + let presentationData = strongSelf.presentationData + + var entries: [ThemeSettingsThemeEntry] = [] + entries.append(ThemeSettingsThemeEntry(index: 0, emoticon: defaultEmoticon, emojiFile: animatedEmojiStickers[defaultEmoticon]?.first?.file, themeReference: .builtin(.dayClassic), nightMode: isDarkAppearance, selected: selectedEmoticon == defaultEmoticon, theme: presentationData.theme, strings: presentationData.strings, wallpaper: nil)) + for theme in themes { + guard let emoticon = theme.emoticon else { + continue + } + entries.append(ThemeSettingsThemeEntry(index: entries.count, emoticon: emoticon, emojiFile: animatedEmojiStickers[emoticon]?.first?.file, themeReference: .cloud(PresentationCloudTheme(theme: theme, resolvedWallpaper: nil, creatorAccountId: nil)), nightMode: isDarkAppearance, selected: selectedEmoticon == theme.emoticon, theme: presentationData.theme, strings: presentationData.strings, wallpaper: nil)) + } + + if selectedEmoticon == defaultEmoticon { + let presentationTheme = makeDefaultPresentationTheme(reference: isDarkAppearance ? .night : .dayClassic, serviceBackgroundColor: nil) + strongSelf.wallpaperBackgroundNode.update(wallpaper: presentationTheme.chat.defaultWallpaper) + } else if let theme = themes.first(where: { $0.emoticon == selectedEmoticon }) { + if let presentationTheme = makePresentationTheme(cloudTheme: theme, dark: isDarkAppearance) { + strongSelf.wallpaperBackgroundNode.update(wallpaper: presentationTheme.chat.defaultWallpaper) + } + } + + let action: (String?) -> Void = { [weak self] emoticon in + if let strongSelf = self, strongSelf.selectedEmoticon != emoticon { + strongSelf.animateCrossfade(animateIcon: true) + + var presentationTheme: PresentationTheme? + if emoticon == defaultEmoticon { + presentationTheme = makeDefaultPresentationTheme(reference: isDarkAppearance ? .night : .dayClassic, serviceBackgroundColor: nil) + } else if let theme = themes.first(where: { $0.emoticon == emoticon }) { + if let theme = makePresentationTheme(cloudTheme: theme, dark: isDarkAppearance) { + presentationTheme = theme + } + } + if let presentationTheme = presentationTheme { + strongSelf.previewTheme?(emoticon, strongSelf.isDarkAppearance, presentationTheme) + } + strongSelf.selectedEmoticon = emoticon + let _ = ensureThemeVisible(listNode: strongSelf.listNode, emoticon: emoticon, animated: true) + } + } + let previousEntries = strongSelf.entries ?? [] + let crossfade = previousEntries.count != entries.count + let transition = preparedTransition(context: strongSelf.context, action: action, from: previousEntries, to: entries, crossfade: crossfade) + strongSelf.enqueueTransition(transition) + + strongSelf.entries = entries + strongSelf.themes = themes + + if isDarkAppearance && selectedEmoticon == defaultEmoticon { + strongSelf.codeForegroundDimNode.alpha = 1.0 + } else { + strongSelf.codeForegroundDimNode.alpha = isDarkAppearance ? 0.4 : 0.3 + } + if strongSelf.codeForegroundContentNode == nil, let contentNode = strongSelf.wallpaperBackgroundNode.makeDimmedNode() { + contentNode.frame = CGRect(origin: CGPoint(x: -strongSelf.codeForegroundNode.frame.minX, y: -strongSelf.codeForegroundNode.frame.minY), size: strongSelf.wallpaperBackgroundNode.frame.size) + strongSelf.codeForegroundContentNode = contentNode + strongSelf.codeForegroundNode.insertSubnode(contentNode, at: 0) + } + + if isFirstTime { + for theme in themes { + if let wallpaper = theme.settings?.first?.wallpaper, case let .file(file) = wallpaper { + let account = strongSelf.context.account + let accountManager = strongSelf.context.sharedContext.accountManager + let path = accountManager.mediaBox.cachedRepresentationCompletePath(file.file.resource.id, representation: CachedPreparedPatternWallpaperRepresentation()) + if !FileManager.default.fileExists(atPath: path) { + let accountFullSizeData = Signal<(Data?, Bool), NoError> { subscriber in + let accountResource = account.postbox.mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedPreparedPatternWallpaperRepresentation(), complete: false, fetch: true) + + let fetchedFullSize = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: .media(media: .standalone(media: file.file), resource: file.file.resource)) + let fetchedFullSizeDisposable = fetchedFullSize.start() + let fullSizeDisposable = accountResource.start(next: { next in + subscriber.putNext((next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete)) + + if next.complete, let data = try? Data(contentsOf: URL(fileURLWithPath: next.path), options: .mappedRead) { + accountManager.mediaBox.storeCachedResourceRepresentation(file.file.resource, representation: CachedPreparedPatternWallpaperRepresentation(), data: data) + } + }, error: subscriber.putError, completed: subscriber.putCompletion) + + return ActionDisposable { + fetchedFullSizeDisposable.dispose() + fullSizeDisposable.dispose() + } + } + let _ = accountFullSizeData.start() + } + } + } + } + })) + + self.switchThemeButton.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.animationNode.layer.removeAnimation(forKey: "opacity") + strongSelf.animationNode.alpha = 0.4 + } else { + strongSelf.animationNode.alpha = 1.0 + strongSelf.animationNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + + self.codeImageNode.setSignal(qrCode(string: "https://t.me/\(peer.addressName ?? "")", color: .black, backgroundColor: nil, icon: .cutout, ecl: "Q") |> beforeNext { [weak self] size, _ in + guard let strongSelf = self else { + return + } + strongSelf.qrCodeSize = size + if let (layout, navigationHeight) = strongSelf.containerLayout { + strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .immediate) + } + } |> map { $0.1 }, attemptSynchronously: true) + } + + private func enqueueTransition(_ transition: ThemeSettingsThemeItemNodeTransition) { + self.enqueuedTransitions.append(transition) + + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransition() + } + } + + private func dequeueTransition() { + guard let transition = self.enqueuedTransitions.first else { + return + } + self.enqueuedTransitions.remove(at: 0) + + var options = ListViewDeleteAndInsertOptions() + if self.initialized && transition.crossfade { + options.insert(.AnimateCrossfade) + } + options.insert(.Synchronous) + + var scrollToItem: ListViewScrollToItem? + if !self.initialized { + scrollToItem = ListViewScrollToItem(index: 0, position: .bottom(-57.0), animated: false, curve: .Default(duration: 0.0), directionHint: .Down) + self.initialized = true + } + + self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, scrollToItem: scrollToItem, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { _ in + }) + } + + func updatePresentationData(_ presentationData: PresentationData) { + guard !self.animatedOut else { + return + } + let previousTheme = self.presentationData.theme + self.presentationData = presentationData + + self.titleNode.attributedText = NSAttributedString(string: self.titleNode.attributedText?.string ?? "", font: Font.semibold(16.0), textColor: self.presentationData.theme.actionSheet.primaryTextColor) + + if previousTheme !== presentationData.theme, let (layout, navigationBarHeight) = self.containerLayout { + self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) + } + + self.cancelButton.setImage(closeButtonImage(theme: self.presentationData.theme), for: .normal) + self.doneButton.updateTheme(SolidRoundedButtonTheme(theme: self.presentationData.theme)) + + if self.animationNode.isPlaying { + if let animationNode = self.animationNode.makeCopy(colors: iconColors(theme: self.presentationData.theme), progress: 0.2) { + let previousAnimationNode = self.animationNode + self.animationNode = animationNode + + animationNode.completion = { [weak previousAnimationNode] in + previousAnimationNode?.removeFromSupernode() + } + animationNode.isUserInteractionEnabled = false + animationNode.frame = previousAnimationNode.frame + previousAnimationNode.supernode?.insertSubnode(animationNode, belowSubnode: previousAnimationNode) + previousAnimationNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: ChatQrCodeScreen.themeCrossfadeDuration, removeOnCompletion: false) + animationNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } else { + self.animationNode.setAnimation(name: self.isDarkAppearance ? "anim_sun_reverse" : "anim_sun", colors: iconColors(theme: self.presentationData.theme)) + } + } + + override func didLoad() { + super.didLoad() + + if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { + self.wrappingScrollNode.view.contentInsetAdjustmentBehavior = .never + } + + self.codeForegroundNode.view.mask = self.codeMaskNode.view + + self.listNode.view.disablesInteractiveTransitionGestureRecognizer = true + } + + @objc func cancelButtonPressed() { + self.cancel?() + } + + func dimTapped() { + self.cancelButtonPressed() + } + + @objc func switchThemePressed() { + self.switchThemeButton.isUserInteractionEnabled = false + Queue.mainQueue().after(0.5) { + self.switchThemeButton.isUserInteractionEnabled = true + } + + self.animateCrossfade(animateIcon: false) + self.animationNode.setAnimation(name: self.isDarkAppearance ? "anim_sun_reverse" : "anim_sun", colors: iconColors(theme: self.presentationData.theme)) + self.animationNode.playOnce() + + let isDarkAppearance = !self.isDarkAppearance + + var presentationTheme: PresentationTheme? + if self.selectedEmoticon == defaultEmoticon { + presentationTheme = makeDefaultPresentationTheme(reference: isDarkAppearance ? .night : .dayClassic, serviceBackgroundColor: nil) + } else if let theme = self.themes.first(where: { $0.emoticon == self.selectedEmoticon }) { + if let theme = makePresentationTheme(cloudTheme: theme, dark: isDarkAppearance) { + presentationTheme = theme + } + } + if let presentationTheme = presentationTheme { + self.previewTheme?(self.selectedEmoticon, isDarkAppearance, presentationTheme) + } + + self.isDarkAppearance = isDarkAppearance + + if isDarkAppearance { + let _ = ApplicationSpecificNotice.incrementChatSpecificThemeDarkPreviewTip(accountManager: self.context.sharedContext.accountManager, count: 3, timestamp: Int32(Date().timeIntervalSince1970)).start() + } else { + let _ = ApplicationSpecificNotice.incrementChatSpecificThemeLightPreviewTip(accountManager: self.context.sharedContext.accountManager, count: 3, timestamp: Int32(Date().timeIntervalSince1970)).start() + } + } + + private func animateCrossfade(animateIcon: Bool) { + if let snapshotView = self.containerNode.view.snapshotView(afterScreenUpdates: false) { + self.wrappingScrollNode.view.insertSubview(snapshotView, aboveSubview: self.containerNode.view) + + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: ChatQrCodeScreen.themeCrossfadeDuration, delay: ChatQrCodeScreen.themeCrossfadeDelay, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + } + + if animateIcon, let snapshotView = self.animationNode.view.snapshotView(afterScreenUpdates: false) { + snapshotView.frame = self.animationNode.frame + self.animationNode.view.superview?.insertSubview(snapshotView, aboveSubview: self.animationNode.view) + + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: ChatQrCodeScreen.themeCrossfadeDuration, delay: ChatQrCodeScreen.themeCrossfadeDelay, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + } + + Queue.mainQueue().after(ChatQrCodeScreen.themeCrossfadeDelay) { + if let effectView = self.effectNode.view as? UIVisualEffectView { + UIView.animate(withDuration: ChatQrCodeScreen.themeCrossfadeDuration, delay: 0.0, options: .curveLinear) { + effectView.effect = UIBlurEffect(style: self.presentationData.theme.actionSheet.backgroundType == .light ? .light : .dark) + } completion: { _ in + } + } + + let previousColor = self.contentBackgroundNode.backgroundColor ?? .clear + self.contentBackgroundNode.backgroundColor = self.presentationData.theme.actionSheet.itemBackgroundColor + self.contentBackgroundNode.layer.animate(from: previousColor.cgColor, to: (self.contentBackgroundNode.backgroundColor ?? .clear).cgColor, keyPath: "backgroundColor", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: ChatQrCodeScreen.themeCrossfadeDuration) + } + + if let snapshotView = self.contentContainerNode.view.snapshotView(afterScreenUpdates: false) { + snapshotView.frame = self.contentContainerNode.frame + self.contentContainerNode.view.superview?.insertSubview(snapshotView, aboveSubview: self.contentContainerNode.view) + + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: ChatQrCodeScreen.themeCrossfadeDuration, delay: ChatQrCodeScreen.themeCrossfadeDelay, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + } + + self.listNode.forEachVisibleItemNode { node in + if let node = node as? ThemeSettingsThemeItemIconNode { + node.crossfade() + } + } + } + + private var animatedOut = false + func animateIn() { + let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY + let dimPosition = self.dimNode.layer.position + + let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) + let targetBounds = self.bounds + self.bounds = self.bounds.offsetBy(dx: 0.0, dy: -offset) + self.dimNode.position = CGPoint(x: dimPosition.x, y: dimPosition.y - offset) + transition.animateView({ + self.bounds = targetBounds + self.dimNode.position = dimPosition + }) + } + + func animateOut(completion: (() -> Void)? = nil) { + self.animatedOut = true + + let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY + self.wrappingScrollNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: -offset, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in + if let strongSelf = self { + strongSelf.dismiss?() + completion?() + } + }) + } + + func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + let contentOffset = scrollView.contentOffset + let additionalTopHeight = max(0.0, -contentOffset.y) + + if additionalTopHeight >= 30.0 { + self.cancelButtonPressed() + } + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + self.containerLayout = (layout, navigationBarHeight) + + var insets = layout.insets(options: [.statusBar, .input]) + let cleanInsets = layout.insets(options: [.statusBar]) + insets.top = max(10.0, insets.top) + + let bottomInset: CGFloat = 10.0 + cleanInsets.bottom + let titleHeight: CGFloat = 54.0 + let contentHeight = titleHeight + bottomInset + 188.0 + + let width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: 0.0) + + let sideInset = floor((layout.size.width - width) / 2.0) + let contentContainerFrame = CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - contentHeight), size: CGSize(width: width, height: contentHeight)) + let contentFrame = contentContainerFrame + + var backgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minX, y: contentFrame.minY), size: CGSize(width: contentFrame.width, height: contentFrame.height + 2000.0)) + if backgroundFrame.minY < contentFrame.minY { + backgroundFrame.origin.y = contentFrame.minY + } + transition.updateFrame(node: self.wallpaperBackgroundNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + self.wallpaperBackgroundNode.updateLayout(size: layout.size, transition: transition) + + transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + + transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) + transition.updateFrame(node: self.effectNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) + transition.updateFrame(node: self.contentBackgroundNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) + transition.updateFrame(node: self.wrappingScrollNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + + let titleSize = self.titleNode.measure(CGSize(width: width - 90.0, height: titleHeight)) + let titleFrame = CGRect(origin: CGPoint(x: floor((contentFrame.width - titleSize.width) / 2.0), y: 19.0 + UIScreenPixel), size: titleSize) + transition.updateFrame(node: self.titleNode, frame: titleFrame) + + let switchThemeSize = CGSize(width: 44.0, height: 44.0) + let switchThemeFrame = CGRect(origin: CGPoint(x: 3.0, y: 6.0), size: switchThemeSize) + transition.updateFrame(node: self.switchThemeButton, frame: switchThemeFrame) + transition.updateFrame(node: self.animationContainerNode, frame: switchThemeFrame.insetBy(dx: 9.0, dy: 9.0)) + transition.updateFrame(node: self.animationNode, frame: CGRect(origin: CGPoint(), size: self.animationContainerNode.frame.size)) + + let cancelSize = CGSize(width: 44.0, height: 44.0) + let cancelFrame = CGRect(origin: CGPoint(x: contentFrame.width - cancelSize.width - 3.0, y: 6.0), size: cancelSize) + transition.updateFrame(node: self.cancelButton, frame: cancelFrame) + + let buttonInset: CGFloat = 16.0 + let doneButtonHeight = self.doneButton.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition) + transition.updateFrame(node: self.doneButton, frame: CGRect(x: buttonInset, y: contentHeight - doneButtonHeight - insets.bottom - 6.0, width: contentFrame.width, height: doneButtonHeight)) + + transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame) + transition.updateFrame(node: self.topContentContainerNode, frame: contentContainerFrame) + + var listInsets = UIEdgeInsets() + listInsets.top += layout.safeInsets.left + 12.0 + listInsets.bottom += layout.safeInsets.right + 12.0 + + let contentSize = CGSize(width: contentFrame.width, height: 120.0) + + self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: contentSize.height, height: contentSize.width) + self.listNode.position = CGPoint(x: contentSize.width / 2.0, y: contentSize.height / 2.0 + titleHeight + 6.0) + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: CGSize(width: contentSize.height, height: contentSize.width), insets: listInsets, duration: 0.0, curve: .Default(duration: nil)), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + + let codeInset: CGFloat = 45.0 + let codeBackgroundWidth = layout.size.width - codeInset * 2.0 + let codeBackgroundHeight = floor(codeBackgroundWidth * 1.1) + let codeBackgroundFrame = CGRect(x: codeInset, y: floor((layout.size.height - contentHeight - codeBackgroundHeight) / 2.0) + 44.0, width: codeBackgroundWidth, height: codeBackgroundHeight) + transition.updateFrame(node: self.codeBackgroundNode, frame: codeBackgroundFrame) + transition.updateFrame(node: self.codeForegroundNode, frame: codeBackgroundFrame) + transition.updateFrame(node: self.codeMaskNode, frame: CGRect(origin: CGPoint(), size: codeBackgroundFrame.size)) + transition.updateFrame(node: self.codeForegroundDimNode, frame: CGRect(origin: CGPoint(), size: codeBackgroundFrame.size)) + + if let codeForegroundContentNode = self.codeForegroundContentNode { + codeForegroundContentNode.frame = CGRect(origin: CGPoint(x: -self.codeForegroundNode.frame.minX, y: -self.codeForegroundNode.frame.minY), size: self.wallpaperBackgroundNode.frame.size) + } + + let makeImageLayout = self.codeImageNode.asyncLayout() + let imageSide: CGFloat = 220.0 + let imageSize = CGSize(width: imageSide, height: imageSide) + let imageApply = makeImageLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: nil)) + let _ = imageApply() + + let imageFrame = CGRect(origin: CGPoint(x: floor((codeBackgroundFrame.width - imageSize.width) / 2.0), y: floor((codeBackgroundFrame.width - imageSize.height) / 2.0)), size: imageSize) + transition.updateFrame(node: self.codeImageNode, frame: imageFrame) + + let codeTextSize = self.codeTextNode.updateLayout(codeBackgroundFrame.size) + transition.updateFrame(node: self.codeTextNode, frame: CGRect(origin: CGPoint(x: floor((codeBackgroundFrame.width - codeTextSize.width) / 2.0), y: imageFrame.maxY + floor((codeBackgroundHeight - imageFrame.maxY - codeTextSize.height) / 2.0) - 7.0), size: codeTextSize)) + + let avatarSize = CGSize(width: 100.0, height: 100.0) + transition.updateFrame(node: self.avatarNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - avatarSize.width) / 2.0), y: codeBackgroundFrame.minY - 70.0), size: avatarSize)) + + if let qrCodeSize = self.qrCodeSize { + let (_, cutoutFrame, _) = qrCodeCutout(size: qrCodeSize, dimensions: imageSize, scale: nil) + self.codeIconNode.updateLayout(size: cutoutFrame.size) + + let backgroundSize = CGSize(width: floorToScreenPixels(cutoutFrame.width - 8.0), height: floorToScreenPixels(cutoutFrame.height - 8.0)) + transition.updateFrame(node: self.codeIconBackgroundNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels(imageFrame.center.x - backgroundSize.width / 2.0), y: floorToScreenPixels(imageFrame.center.y - backgroundSize.height / 2.0)), size: backgroundSize)) + if self.codeIconBackgroundNode.image == nil { + self.codeIconBackgroundNode.image = generateFilledCircleImage(diameter: backgroundSize.width, color: .black) + } + + let imageCenter = imageFrame.center.offsetBy(dx: codeBackgroundFrame.minX, dy: codeBackgroundFrame.minY) + transition.updateBounds(node: self.codeIconNode, bounds: CGRect(origin: CGPoint(), size: cutoutFrame.size)) + transition.updatePosition(node: self.codeIconNode, position: imageCenter.offsetBy(dx: 0.0, dy: -1.0)) + } + } +} diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift index fc705ac0ad..c0f14cb7a1 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift @@ -255,6 +255,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { self?.openPeerMention(name) }, openMessageContextMenu: { [weak self] message, selectAll, node, frame, _ in self?.openMessageContextMenu(message: message, selectAll: selectAll, node: node, frame: frame) + }, openMessageReactionContextMenu: { _, _, _, _ in }, updateMessageReaction: { _, _ in }, activateMessagePinch: { _ in }, openMessageContextActions: { _, _, _, _ in diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsHistoryTransition.swift b/submodules/TelegramUI/Sources/ChatRecentActionsHistoryTransition.swift index 0118afeb5e..8867084c92 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsHistoryTransition.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsHistoryTransition.swift @@ -114,7 +114,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.titleUpdated(title: new) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .changeAbout(prev, new): var peers = SimpleDictionary() var author: Peer? @@ -145,14 +145,14 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case .content: let peers = SimpleDictionary() let attributes: [MessageAttribute] = [] let prevMessage = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: prev, attributes: [], media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: new, attributes: attributes, media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil), additionalContent: !prev.isEmpty ? .eventLogPreviousDescription(prevMessage) : nil) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil), additionalContent: !prev.isEmpty ? .eventLogPreviousDescription(prevMessage) : nil) } case let .changeUsername(prev, new): var peers = SimpleDictionary() @@ -183,7 +183,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } let action: TelegramMediaActionType = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case .content: var previousAttributes: [MessageAttribute] = [] var attributes: [MessageAttribute] = [] @@ -202,7 +202,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let prevMessage = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: prevText, attributes: previousAttributes, media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: text, attributes: attributes, media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil), additionalContent: !prev.isEmpty ? .eventLogPreviousLink(prevMessage) : nil) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil), additionalContent: !prev.isEmpty ? .eventLogPreviousLink(prevMessage) : nil) } case let .changePhoto(_, new): var peers = SimpleDictionary() @@ -221,7 +221,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.photoUpdated(image: photo) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .toggleInvites(value): var peers = SimpleDictionary() var author: Peer? @@ -248,7 +248,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .toggleSignatures(value): var peers = SimpleDictionary() var author: Peer? @@ -275,7 +275,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .updatePinned(message): switch self.id.contentIndex { case .header: @@ -306,7 +306,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case .content: if let message = message { var peers = SimpleDictionary() @@ -324,7 +324,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } } let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: message.effectiveAuthor, text: message.text, attributes: attributes, media: message.media, peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) } else { var peers = SimpleDictionary() var author: Peer? @@ -346,7 +346,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 0), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) } } case let .editMessage(prev, message): @@ -391,7 +391,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case .content: var peers = SimpleDictionary() var attributes: [MessageAttribute] = [] @@ -408,7 +408,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } } let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: message.effectiveAuthor, text: message.text, attributes: attributes, media: message.media, peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: filterOriginalMessageFlags(message), read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil), additionalContent: !prev.text.isEmpty || !message.text.isEmpty ? .eventLogPreviousMessage(filterOriginalMessageFlags(prev)) : nil) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: filterOriginalMessageFlags(message), read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil), additionalContent: !prev.text.isEmpty || !message.text.isEmpty ? .eventLogPreviousMessage(filterOriginalMessageFlags(prev)) : nil) } case let .deleteMessage(message): switch self.id.contentIndex { @@ -434,7 +434,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case .content: var peers = SimpleDictionary() var attributes: [MessageAttribute] = [] @@ -458,7 +458,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } } let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: message.effectiveAuthor, text: message.text, attributes: attributes, media: message.media, peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) } case .participantJoin, .participantLeave: var peers = SimpleDictionary() @@ -476,7 +476,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { action = TelegramMediaActionType.removedMembers(peerIds: [self.entry.event.peerId]) } let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .participantInvite(participant): var peers = SimpleDictionary() var author: Peer? @@ -493,7 +493,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action: TelegramMediaActionType action = TelegramMediaActionType.addedMembers(peerIds: [participant.peer.id]) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .participantToggleBan(prev, new): var peers = SimpleDictionary() var attributes: [MessageAttribute] = [] @@ -623,7 +623,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } } let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: text, attributes: attributes, media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .participantToggleAdmin(prev, new): var peers = SimpleDictionary() var attributes: [MessageAttribute] = [] @@ -856,7 +856,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } } let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: text, attributes: attributes, media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .changeStickerPack(_, new): var peers = SimpleDictionary() var author: Peer? @@ -885,7 +885,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .togglePreHistoryHidden(value): var peers = SimpleDictionary() var author: Peer? @@ -915,7 +915,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .updateDefaultBannedRights(prev, new): var peers = SimpleDictionary() var attributes: [MessageAttribute] = [] @@ -973,7 +973,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } } let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: text, attributes: attributes, media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .pollStopped(message): switch self.id.contentIndex { case .header: @@ -1001,7 +1001,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case .content: var peers = SimpleDictionary() var attributes: [MessageAttribute] = [] @@ -1018,7 +1018,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } } let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: message.author, text: message.text, attributes: attributes, media: message.media, peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: filterOriginalMessageFlags(message), read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil), additionalContent: nil) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: filterOriginalMessageFlags(message), read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil), additionalContent: nil) } case let .linkedPeerUpdated(previous, updated): var peers = SimpleDictionary() @@ -1074,7 +1074,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .changeGeoLocation(_, updated): var peers = SimpleDictionary() var author: Peer? @@ -1096,12 +1096,12 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let mediaMap = TelegramMediaMap(latitude: updated.latitude, longitude: updated.longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: text, attributes: [], media: [mediaMap], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) } else { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) } case let .updateSlowmode(_, newValue): var peers = SimpleDictionary() @@ -1132,7 +1132,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case .startGroupCall, .endGroupCall: var peers = SimpleDictionary() var author: Peer? @@ -1169,7 +1169,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .groupCallUpdateParticipantMuteStatus(participantId, isMuted): var peers = SimpleDictionary() var author: Peer? @@ -1203,7 +1203,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .updateGroupCallSettings(joinMuted): var peers = SimpleDictionary() var author: Peer? @@ -1232,7 +1232,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .groupCallUpdateParticipantVolume(participantId, volume): var peers = SimpleDictionary() var author: Peer? @@ -1263,7 +1263,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .deleteExportedInvitation(invite): var peers = SimpleDictionary() var author: Peer? @@ -1289,7 +1289,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .revokeExportedInvitation(invite): var peers = SimpleDictionary() var author: Peer? @@ -1315,7 +1315,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .editExportedInvitation(_, updatedInvite): var peers = SimpleDictionary() var author: Peer? @@ -1341,7 +1341,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .participantJoinedViaInvite(invite): var peers = SimpleDictionary() var author: Peer? @@ -1367,7 +1367,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .changeHistoryTTL(_, updatedValue): var peers = SimpleDictionary() var author: Peer? @@ -1398,7 +1398,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .changeTheme(_, updatedValue): var peers = SimpleDictionary() var author: Peer? @@ -1429,7 +1429,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .participantJoinByRequest(invite, approvedBy): var peers = SimpleDictionary() var author: Peer? @@ -1462,7 +1462,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .toggleCopyProtection(value): var peers = SimpleDictionary() var author: Peer? @@ -1489,7 +1489,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case let .sendMessage(message): switch self.id.contentIndex { case .header: @@ -1514,7 +1514,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities) let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 1), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) case .content: var peers = SimpleDictionary() var attributes: [MessageAttribute] = [] @@ -1531,7 +1531,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } } let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: message.effectiveAuthor, text: message.text, attributes: attributes, media: message.media, peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) - return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) } } } diff --git a/submodules/TelegramUI/Sources/ChatSendAsPeerListContextItem.swift b/submodules/TelegramUI/Sources/ChatSendAsPeerListContextItem.swift index cd4293db3b..92fb7d3a96 100644 --- a/submodules/TelegramUI/Sources/ChatSendAsPeerListContextItem.swift +++ b/submodules/TelegramUI/Sources/ChatSendAsPeerListContextItem.swift @@ -215,6 +215,14 @@ private final class ChatSendAsPeerListContextItemNode: ASDisplayNode, ContextMen func setIsHighlighted(_ value: Bool) { } + func canBeHighlighted() -> Bool { + return self.isActionEnabled + } + + func updateIsHighlighted(isHighlighted: Bool) { + self.setIsHighlighted(isHighlighted) + } + func actionNode(at point: CGPoint) -> ContextActionNodeProtocol { for actionNode in self.actionNodes { let frame = actionNode.convert(actionNode.bounds, to: self) diff --git a/submodules/TelegramUI/Sources/ChatSendAsPeerTitleContextItem.swift b/submodules/TelegramUI/Sources/ChatSendAsPeerTitleContextItem.swift index 67375e0284..b8f843f98a 100644 --- a/submodules/TelegramUI/Sources/ChatSendAsPeerTitleContextItem.swift +++ b/submodules/TelegramUI/Sources/ChatSendAsPeerTitleContextItem.swift @@ -75,4 +75,14 @@ private final class ChatSendAsPeerTitleContextItemNode: ASDisplayNode, ContextMe let textFont = Font.regular(presentationData.listsFontSize.baseDisplaySize * 12.0 / 17.0) self.textNode.attributedText = NSAttributedString(string: self.textNode.attributedText?.string ?? "", font: textFont, textColor: presentationData.theme.contextMenu.secondaryColor) } + + func canBeHighlighted() -> Bool { + return false + } + + func updateIsHighlighted(isHighlighted: Bool) { + } + + func performAction() { + } } diff --git a/submodules/TelegramUI/Sources/ChatTextInputMenu.swift b/submodules/TelegramUI/Sources/ChatTextInputMenu.swift index 630f06b617..402145b38e 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputMenu.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputMenu.swift @@ -17,6 +17,8 @@ final class ChatTextInputMenu { private var stringUnderline: String = "Underline" private var stringSpoiler: String = "Spoiler" + private let hasSpoilers: Bool + private(set) var state: ChatTextInputMenuState = .inactive { didSet { if self.state != oldValue { @@ -26,15 +28,18 @@ final class ChatTextInputMenu { case .general: UIMenuController.shared.menuItems = [] case .format: - UIMenuController.shared.menuItems = [ + var menuItems: [UIMenuItem] = [ UIMenuItem(title: self.stringBold, action: Selector(("formatAttributesBold:"))), UIMenuItem(title: self.stringItalic, action: Selector(("formatAttributesItalic:"))), UIMenuItem(title: self.stringMonospace, action: Selector(("formatAttributesMonospace:"))), UIMenuItem(title: self.stringLink, action: Selector(("formatAttributesLink:"))), - UIMenuItem(title: self.stringSpoiler, action: Selector(("formatAttributesSpoiler:"))), UIMenuItem(title: self.stringStrikethrough, action: Selector(("formatAttributesStrikethrough:"))), UIMenuItem(title: self.stringUnderline, action: Selector(("formatAttributesUnderline:"))) ] + if self.hasSpoilers { + menuItems.insert(UIMenuItem(title: self.stringSpoiler, action: Selector(("formatAttributesSpoiler:"))), at: 0) + } + UIMenuController.shared.menuItems = menuItems } } @@ -43,7 +48,8 @@ final class ChatTextInputMenu { private var observer: NSObjectProtocol? - init() { + init(hasSpoilers: Bool = false) { + self.hasSpoilers = hasSpoilers self.observer = NotificationCenter.default.addObserver(forName: UIMenuController.didHideMenuNotification, object: nil, queue: nil, using: { [weak self] _ in self?.back() }) diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index ba2f818023..d6900ef29b 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -17,6 +17,7 @@ import Speak import ObjCRuntimeUtils import AvatarNode import ContextUI +import InvisibleInkDustNode private let accessoryButtonFont = Font.medium(14.0) private let counterFont = Font.with(size: 14.0, design: .regular, traits: [.monospacedNumbers]) @@ -240,6 +241,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { let textInputContainerBackgroundNode: ASImageNode let textInputContainer: ASDisplayNode var textInputNode: EditableTextNode? + var dustNode: InvisibleInkDustNode? let textInputBackgroundNode: ASImageNode private var transparentTextInputBackgroundImage: UIImage? @@ -294,7 +296,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { var isMediaDeleted: Bool = false - private let inputMenu = ChatTextInputMenu() + private let inputMenu: ChatTextInputMenu private var theme: PresentationTheme? private var strings: PresentationStrings? @@ -402,12 +404,13 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { accentTextColor = presentationInterfaceState.theme.chat.inputPanel.panelControlAccentColor baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) } - textInputNode.attributedText = textAttributedStringForStateText(state.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil) + textInputNode.attributedText = textAttributedStringForStateText(state.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed) textInputNode.selectedRange = NSMakeRange(state.selectionRange.lowerBound, state.selectionRange.count) self.updatingInputState = false self.keepSendButtonEnabled = keepSendButtonEnabled self.extendedSearchLayout = extendedSearchLayout self.updateTextNodeText(animated: animated) + self.updateSpoiler() } } @@ -440,9 +443,17 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { private let accessoryButtonSpacing: CGFloat = 0.0 private let accessoryButtonInset: CGFloat = 2.0 + private var spoilersRevealed = false + init(presentationInterfaceState: ChatPresentationInterfaceState, presentationContext: ChatPresentationContext?, presentController: @escaping (ViewController) -> Void) { self.presentationInterfaceState = presentationInterfaceState + var hasSpoilers = true + if presentationInterfaceState.chatLocation.peerId.namespace == Namespaces.Peer.SecretChat { + hasSpoilers = false + } + self.inputMenu = ChatTextInputMenu(hasSpoilers: hasSpoilers) + self.clippingNode = ASDisplayNode() self.clippingNode.clipsToBounds = true @@ -708,6 +719,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } textInputNode.frame = CGRect(origin: CGPoint(x: self.textInputViewInternalInsets.left, y: self.textInputViewInternalInsets.top), size: CGSize(width: textInputFrame.size.width - (self.textInputViewInternalInsets.left + self.textInputViewInternalInsets.right), height: textInputFrame.size.height - self.textInputViewInternalInsets.top - self.textInputViewInternalInsets.bottom)) + textInputNode.view.layoutIfNeeded() + self.updateSpoiler() } self.textInputBackgroundNode.isUserInteractionEnabled = false @@ -896,6 +909,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } textInputNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(baseFontSize), NSAttributedString.Key.foregroundColor.rawValue: textColor] textInputNode.tintColor = tintColor + + self.updateSpoiler() } } @@ -1770,9 +1785,11 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { @objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { if let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState { let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) - refreshChatTextInputAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) + refreshChatTextInputAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed) refreshChatTextInputTypingAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) + self.updateSpoiler() + let inputTextState = self.inputTextState self.interfaceInteraction?.updateTextInputStateAndMode({ _, inputMode in return (inputTextState, inputMode) }) @@ -1783,6 +1800,148 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } + private func updateSpoiler() { + guard let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState else { + return + } + + let textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor + + var rects: [CGRect] = [] + + if let attributedText = textInputNode.attributedText { + let beginning = textInputNode.textView.beginningOfDocument + attributedText.enumerateAttributes(in: NSMakeRange(0, attributedText.length), options: [], using: { attributes, range, _ in + if let _ = attributes[ChatTextInputAttributes.spoiler] { + func addSpoiler(startIndex: Int, endIndex: Int) { + if let start = textInputNode.textView.position(from: beginning, offset: startIndex), let end = textInputNode.textView.position(from: start, offset: endIndex - startIndex), let textRange = textInputNode.textView.textRange(from: start, to: end) { + let textRects = textInputNode.textView.selectionRects(for: textRange) + for textRect in textRects { + rects.append(textRect.rect.insetBy(dx: 1.0, dy: 1.0).offsetBy(dx: 0.0, dy: 1.0)) + } + } + } + + var startIndex: Int? + var currentIndex: Int? + + let nsString = (attributedText.string as NSString) + nsString.enumerateSubstrings(in: range, options: .byComposedCharacterSequences) { substring, range, _, _ in + if let substring = substring, substring.rangeOfCharacter(from: .whitespacesAndNewlines) != nil { + if let currentStartIndex = startIndex { + startIndex = nil + let endIndex = range.location + addSpoiler(startIndex: currentStartIndex, endIndex: endIndex) + } + } else if startIndex == nil { + startIndex = range.location + } + currentIndex = range.location + range.length + } + + if let currentStartIndex = startIndex, let currentIndex = currentIndex { + startIndex = nil + let endIndex = currentIndex + addSpoiler(startIndex: currentStartIndex, endIndex: endIndex) + } + } + }) + } + + if !rects.isEmpty { + let dustNode: InvisibleInkDustNode + if let current = self.dustNode { + dustNode = current + } else { + dustNode = InvisibleInkDustNode(textNode: nil) + dustNode.alpha = self.spoilersRevealed ? 0.0 : 1.0 + dustNode.isUserInteractionEnabled = false + textInputNode.textView.addSubview(dustNode.view) + self.dustNode = dustNode + } + dustNode.frame = CGRect(origin: CGPoint(), size: textInputNode.textView.contentSize) + dustNode.update(size: textInputNode.textView.contentSize, color: textColor, rects: rects, wordRects: rects) + } else if let dustNode = self.dustNode { + dustNode.removeFromSupernode() + self.dustNode = nil + } + } + + private func updateSpoilersRevealed(animated: Bool = true) { + guard let textInputNode = self.textInputNode else { + return + } + + let selectionRange = textInputNode.textView.selectedRange + + var revealed = false + if let attributedText = textInputNode.attributedText { + attributedText.enumerateAttributes(in: NSMakeRange(0, attributedText.length), options: [], using: { attributes, range, _ in + if let _ = attributes[ChatTextInputAttributes.spoiler] { + if let _ = selectionRange.intersection(range) { + revealed = true + } + } + }) + } + + guard self.spoilersRevealed != revealed else { + return + } + self.spoilersRevealed = revealed + + if revealed { + self.updateInternalSpoilersRevealed(true, animated: animated) + } else { + Queue.mainQueue().after(1.5, { + self.updateInternalSpoilersRevealed(false, animated: true) + }) + } + } + + private func updateInternalSpoilersRevealed(_ revealed: Bool, animated: Bool) { + guard self.spoilersRevealed == revealed, let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState else { + return + } + + let textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor + let accentTextColor = presentationInterfaceState.theme.chat.inputPanel.panelControlAccentColor + let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) + + refreshChatTextInputAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed) + + textInputNode.attributedText = textAttributedStringForStateText(self.inputTextState.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed) + + if textInputNode.textView.subviews.count > 1, animated { + let containerView = textInputNode.textView.subviews[1] + if let canvasView = containerView.subviews.first { + if let snapshotView = canvasView.snapshotView(afterScreenUpdates: false) { + textInputNode.view.insertSubview(snapshotView, at: 0) + canvasView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + } + } + } + + if animated { + if revealed { + let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear) + if let dustNode = self.dustNode { + transition.updateAlpha(node: dustNode, alpha: 0.0) + } + } else { + let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear) + if let dustNode = self.dustNode { + transition.updateAlpha(node: dustNode, alpha: 1.0) + } + } + } else if let dustNode = self.dustNode { + dustNode.alpha = revealed ? 0.0 : 1.0 + } + } + private func updateCounterTextNode(transition: ContainedViewLayoutTransition) { if let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState, let editMessage = presentationInterfaceState.interfaceState.editMessage, let inputTextMaxLength = editMessage.inputTextMaxLength { let textCount = Int32(textInputNode.textView.text.count) @@ -2053,6 +2212,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) refreshChatTextInputTypingAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) + + self.updateSpoilersRevealed() } } @@ -2174,9 +2335,21 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { @objc func formatAttributesSpoiler(_ sender: Any) { self.inputMenu.back() + + var animated = false + if let attributedText = self.textInputNode?.attributedText { + attributedText.enumerateAttributes(in: NSMakeRange(0, attributedText.length), options: [], using: { attributes, _, _ in + if let _ = attributes[ChatTextInputAttributes.spoiler] { + animated = true + } + }) + } + self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.spoiler), inputMode) } + + self.updateSpoilersRevealed(animated: animated) } @objc func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { @@ -2203,7 +2376,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { accentTextColor = presentationInterfaceState.theme.chat.inputPanel.panelControlAccentColor baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) } - let cleanReplacementString = textAttributedStringForStateText(NSAttributedString(string: cleanText), fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil) + let cleanReplacementString = textAttributedStringForStateText(NSAttributedString(string: cleanText), fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed) string.replaceCharacters(in: range, with: cleanReplacementString) self.textInputNode?.attributedText = string self.textInputNode?.selectedRange = NSMakeRange(range.lowerBound + cleanReplacementString.length, 0) diff --git a/submodules/TelegramUI/Sources/ChatThemeScreen.swift b/submodules/TelegramUI/Sources/ChatThemeScreen.swift index 1e4084d40e..e24a60d6f0 100644 --- a/submodules/TelegramUI/Sources/ChatThemeScreen.swift +++ b/submodules/TelegramUI/Sources/ChatThemeScreen.swift @@ -394,6 +394,7 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode { var updatedTheme = false var updatedWallpaper = false var updatedSelected = false + var updatedNightMode = false if currentItem?.emoticon != item.emoticon { updatedEmoticon = true @@ -410,6 +411,9 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode { if currentItem?.selected != item.selected { updatedSelected = true } + if currentItem?.nightMode != item.nightMode { + updatedNightMode = true + } let text = NSAttributedString(string: item.strings.Conversation_Theme_NoTheme, font: Font.semibold(15.0), textColor: item.theme.actionSheet.controlAccentColor) let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: text, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) @@ -423,7 +427,7 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode { if let strongSelf = self { strongSelf.item = item - if updatedThemeReference || updatedWallpaper { + if updatedThemeReference || updatedWallpaper || updatedNightMode { if let themeReference = item.themeReference { strongSelf.imageNode.setSignal(themeIconImage(account: item.context.account, accountManager: item.context.sharedContext.accountManager, theme: themeReference, color: nil, wallpaper: item.wallpaper, nightMode: item.nightMode, emoticon: true)) strongSelf.imageNode.backgroundColor = nil diff --git a/submodules/TelegramUI/Sources/DrawingStickersScreen.swift b/submodules/TelegramUI/Sources/DrawingStickersScreen.swift index 93cac14f9d..e498f2a6d4 100644 --- a/submodules/TelegramUI/Sources/DrawingStickersScreen.swift +++ b/submodules/TelegramUI/Sources/DrawingStickersScreen.swift @@ -108,7 +108,8 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode { var selectStickerImpl: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? self.controllerInteraction = ChatControllerInteraction(openMessage: { _, _ in - return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, updateMessageReaction: { _, _ in }, activateMessagePinch: { _ in + return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, openMessageReactionContextMenu: { _, _, _, _ in + }, updateMessageReaction: { _, _ in }, activateMessagePinch: { _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { fileReference, _, _, _, _, node, rect in return selectStickerImpl?(fileReference, node, rect) ?? false }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in }, presentController: { _, _ in diff --git a/submodules/TelegramUI/Sources/EditAccessoryPanelNode.swift b/submodules/TelegramUI/Sources/EditAccessoryPanelNode.swift index 15a1315d92..6100fa6f54 100644 --- a/submodules/TelegramUI/Sources/EditAccessoryPanelNode.swift +++ b/submodules/TelegramUI/Sources/EditAccessoryPanelNode.swift @@ -169,7 +169,7 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { if let currentEditMediaReference = self.currentEditMediaReference { effectiveMessage = effectiveMessage.withUpdatedMedia([currentEditMediaReference.media]) } - (text, _) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: EngineMessage(effectiveMessage), strings: self.strings, nameDisplayOrder: self.nameDisplayOrder, dateTimeFormat: self.dateTimeFormat, accountPeerId: self.context.account.peerId) + (text, _, _) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: EngineMessage(effectiveMessage), strings: self.strings, nameDisplayOrder: self.nameDisplayOrder, dateTimeFormat: self.dateTimeFormat, accountPeerId: self.context.account.peerId) } var updatedMediaReference: AnyMediaReference? diff --git a/submodules/TelegramUI/Sources/NotificationContentContext.swift b/submodules/TelegramUI/Sources/NotificationContentContext.swift index 0925c65132..7d6151ecf1 100644 --- a/submodules/TelegramUI/Sources/NotificationContentContext.swift +++ b/submodules/TelegramUI/Sources/NotificationContentContext.swift @@ -94,7 +94,7 @@ public final class NotificationViewControllerImpl { if sharedAccountContext == nil { initializeAccountManagement() - let accountManager = AccountManager(basePath: rootPath + "/accounts-metadata", isTemporary: true, isReadOnly: false, useCaches: false) + let accountManager = AccountManager(basePath: rootPath + "/accounts-metadata", isTemporary: true, isReadOnly: false, useCaches: false, removeDatabaseOnError: false) var initialPresentationDataAndSettings: InitialPresentationDataAndSettings? let semaphore = DispatchSemaphore(value: 0) diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index 80644ecb5a..ad27f89381 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -69,6 +69,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in + }, openMessageReactionContextMenu: { _, _, _, _ in }, updateMessageReaction: { _, _ in }, activateMessagePinch: { _ in }, openMessageContextActions: { _, _, _, _ in diff --git a/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenLabeledValueItem.swift b/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenLabeledValueItem.swift index 5ab17d132e..67b0302480 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenLabeledValueItem.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenLabeledValueItem.swift @@ -279,7 +279,7 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode { textColorValue = presentationData.theme.list.itemAccentColor } - self.expandNode.attributedText = NSAttributedString(string: presentationData.strings.PeerInfo_BioExpand.uppercased(), font: Font.medium(15.0), textColor: presentationData.theme.list.itemAccentColor) + self.expandNode.attributedText = NSAttributedString(string: presentationData.strings.PeerInfo_BioExpand, font: Font.medium(15.0), textColor: presentationData.theme.list.itemAccentColor) let expandSize = self.expandNode.updateLayout(CGSize(width: width, height: 100.0)) self.labelNode.attributedText = NSAttributedString(string: item.label, font: Font.regular(14.0), textColor: presentationData.theme.list.itemPrimaryTextColor) diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 8296a53bab..fc84b8c5f4 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -716,7 +716,7 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p } else { stickersLabel = "" } - items[.advanced]!.append(PeerInfoScreenDisclosureItem(id: 5, label: .badge(stickersLabel, presentationData.theme.list.itemAccentColor), text: presentationData.strings.ChatSettings_Stickers, icon: PresentationResourcesSettings.stickers, action: { + items[.advanced]!.append(PeerInfoScreenDisclosureItem(id: 5, label: .badge(stickersLabel, presentationData.theme.list.itemAccentColor), text: presentationData.strings.ChatSettings_StickersAndReactions, icon: PresentationResourcesSettings.stickers, action: { interaction.openSettings(.stickers) })) @@ -1182,7 +1182,17 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr if isCreator || (channel.adminRights?.rights.contains(.canChangeInfo) == true) { //TODO:localize - items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemReactions, label: .none, text: "Reactions", icon: UIImage(bundleImageName: "Chat/Info/GroupDiscussionIcon"), action: { + let label: String + if let cachedData = data.cachedData as? CachedChannelData, let allowedReactions = cachedData.allowedReactions { + if allowedReactions.isEmpty { + label = "Disabled" + } else { + label = "\(allowedReactions.count)" + } + } else { + label = "" + } + items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemReactions, label: .text(label), text: "Reactions", icon: UIImage(bundleImageName: "Settings/Menu/Reactions"), action: { interaction.editingOpenReactionsSetup() })) } @@ -1297,7 +1307,17 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr if isCreator || (channel.adminRights?.rights.contains(.canChangeInfo) == true) { //TODO:localize - items[.peerPublicSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemReactions, label: .none, text: "Reactions", icon: UIImage(bundleImageName: "Chat/Info/GroupDiscussionIcon"), action: { + let label: String + if let cachedData = data.cachedData as? CachedChannelData, let allowedReactions = cachedData.allowedReactions { + if allowedReactions.isEmpty { + label = "Disabled" + } else { + label = "\(allowedReactions.count)" + } + } else { + label = "" + } + items[.peerPublicSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemReactions, label: .text(label), text: "Reactions", icon: UIImage(bundleImageName: "Settings/Menu/Reactions"), action: { interaction.editingOpenReactionsSetup() })) } @@ -1310,7 +1330,17 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr } else { if isCreator || (channel.adminRights?.rights.contains(.canChangeInfo) == true) { //TODO:localize - items[.peerPublicSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemReactions, label: .none, text: "Reactions", icon: UIImage(bundleImageName: "Chat/Info/GroupDiscussionIcon"), action: { + let label: String + if let cachedData = data.cachedData as? CachedChannelData, let allowedReactions = cachedData.allowedReactions { + if allowedReactions.isEmpty { + label = "Disabled" + } else { + label = "\(allowedReactions.count)" + } + } else { + label = "" + } + items[.peerPublicSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemReactions, label: .text(label), text: "Reactions", icon: UIImage(bundleImageName: "Settings/Menu/Reactions"), action: { interaction.editingOpenReactionsSetup() })) } @@ -1409,10 +1439,22 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr interaction.editingOpenPreHistorySetup() })) - //TODO:localize - items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemReactions, label: .none, text: "Reactions", icon: UIImage(bundleImageName: "Chat/Info/GroupDiscussionIcon"), action: { - interaction.editingOpenReactionsSetup() - })) + do { + //TODO:localize + let label: String + if let cachedData = data.cachedData as? CachedGroupData, let allowedReactions = cachedData.allowedReactions { + if allowedReactions.isEmpty { + label = "Disabled" + } else { + label = "\(allowedReactions.count)" + } + } else { + label = "" + } + items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemReactions, label: .text(label), text: "Reactions", icon: UIImage(bundleImageName: "Settings/Menu/Reactions"), action: { + interaction.editingOpenReactionsSetup() + })) + } canViewAdminsAndBanned = true } else if case let .admin(rights, _) = group.role { @@ -1881,6 +1923,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(MessageContextExtractedContentSource(sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture) strongSelf.controller?.window?.presentInGlobalOverlay(controller) }) + }, openMessageReactionContextMenu: { _, _, _, _ in }, updateMessageReaction: { _, _ in }, activateMessagePinch: { _ in }, openMessageContextActions: { [weak self] message, node, rect, gesture in @@ -5436,7 +5479,35 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate guard let data = self.data, let peer = data.peer, let controller = self.controller else { return } - controller.present(QrCodeScreen(context: self.context, updatedPresentationData: controller.updatedPresentationData, subject: .peer(peer: EnginePeer(peer))), in: .window(.root)) + + let animatedEmojiStickers = context.engine.stickers.loadedStickerPack(reference: .animatedEmoji, forceActualized: false) + |> map { animatedEmoji -> [String: [StickerPackItem]] in + var animatedEmojiStickers: [String: [StickerPackItem]] = [:] + switch animatedEmoji { + case let .result(_, items, _): + for item in items { + if let emoji = item.getStringRepresentationsOfIndexKeys().first { + animatedEmojiStickers[emoji.basicEmoji.0] = [item] + let strippedEmoji = emoji.basicEmoji.0.strippedEmoji + if animatedEmojiStickers[strippedEmoji] == nil { + animatedEmojiStickers[strippedEmoji] = [item] + } + } + } + default: + break + } + return animatedEmojiStickers + } + + let _ = (animatedEmojiStickers + |> deliverOnMainQueue).start(next: { [weak self, weak controller] animatedEmojiStickers in + if let strongSelf = self, let controller = controller { + controller.present(ChatQrCodeScreen(context: strongSelf.context, animatedEmojiStickers: animatedEmojiStickers, peer: peer), in: .window(.root)) + } + }) + +// controller.present(QrCodeScreen(context: self.context, updatedPresentationData: controller.updatedPresentationData, subject: .peer(peer: EnginePeer(peer))), in: .window(.root)) } fileprivate func openSettings(section: PeerInfoSettingsSection) { @@ -6681,7 +6752,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate rightNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .done, isForExpandedView: false)) } else { if self.isSettings { - leftNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .qrCode, isForExpandedView: false)) + if let addressName = self.data?.peer?.addressName, !addressName.isEmpty { + leftNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .qrCode, isForExpandedView: false)) + } rightNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .edit, isForExpandedView: false)) rightNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .search, isForExpandedView: true)) diff --git a/submodules/TelegramUI/Sources/PeerSelectionTextInputPanelNode.swift b/submodules/TelegramUI/Sources/PeerSelectionTextInputPanelNode.swift index d1f2131210..775e22681c 100644 --- a/submodules/TelegramUI/Sources/PeerSelectionTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/PeerSelectionTextInputPanelNode.swift @@ -141,7 +141,7 @@ class PeerSelectionTextInputPanelNode: ChatInputPanelNode, TGCaptionPanelView, A private var presentationInterfaceState: ChatPresentationInterfaceState? private var initializedPlaceholder = false - private let inputMenu = ChatTextInputMenu() + private let inputMenu: ChatTextInputMenu private var theme: PresentationTheme? private var strings: PresentationStrings? @@ -207,7 +207,7 @@ class PeerSelectionTextInputPanelNode: ChatInputPanelNode, TGCaptionPanelView, A accentTextColor = presentationInterfaceState.theme.chat.inputPanel.panelControlAccentColor baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) } - textInputNode.attributedText = textAttributedStringForStateText(state.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil) + textInputNode.attributedText = textAttributedStringForStateText(state.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: false) textInputNode.selectedRange = NSMakeRange(state.selectionRange.lowerBound, state.selectionRange.count) self.updatingInputState = false self.updateTextNodeText(animated: animated) @@ -241,6 +241,12 @@ class PeerSelectionTextInputPanelNode: ChatInputPanelNode, TGCaptionPanelView, A self.presentationInterfaceState = presentationInterfaceState self.isCaption = isCaption + var hasSpoilers = true + if presentationInterfaceState.chatLocation.peerId.namespace == Namespaces.Peer.SecretChat { + hasSpoilers = false + } + self.inputMenu = ChatTextInputMenu(hasSpoilers: hasSpoilers) + self.textInputContainerBackgroundNode = ASImageNode() self.textInputContainerBackgroundNode.isUserInteractionEnabled = false self.textInputContainerBackgroundNode.displaysAsynchronously = false @@ -697,7 +703,7 @@ class PeerSelectionTextInputPanelNode: ChatInputPanelNode, TGCaptionPanelView, A @objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { if let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState { let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) - refreshChatTextInputAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) + refreshChatTextInputAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: false) refreshChatTextInputTypingAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) let inputTextState = self.inputTextState @@ -999,7 +1005,7 @@ class PeerSelectionTextInputPanelNode: ChatInputPanelNode, TGCaptionPanelView, A accentTextColor = presentationInterfaceState.theme.chat.inputPanel.panelControlAccentColor baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) } - let cleanReplacementString = textAttributedStringForStateText(NSAttributedString(string: cleanText), fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil) + let cleanReplacementString = textAttributedStringForStateText(NSAttributedString(string: cleanText), fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: false) string.replaceCharacters(in: range, with: cleanReplacementString) self.textInputNode?.attributedText = string self.textInputNode?.selectedRange = NSMakeRange(range.lowerBound + cleanReplacementString.length, 0) diff --git a/submodules/TelegramUI/Sources/ReplyAccessoryPanelNode.swift b/submodules/TelegramUI/Sources/ReplyAccessoryPanelNode.swift index 2cff35dd92..89553036a2 100644 --- a/submodules/TelegramUI/Sources/ReplyAccessoryPanelNode.swift +++ b/submodules/TelegramUI/Sources/ReplyAccessoryPanelNode.swift @@ -11,6 +11,8 @@ import AccountContext import LocalizedPeerData import PhotoResources import TelegramStringFormatting +import InvisibleInkDustNode +import TextFormat final class ReplyAccessoryPanelNode: AccessoryPanelNode { private let messageDisposable = MetaDisposable() @@ -23,6 +25,7 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { let iconNode: ASImageNode let titleNode: ImmediateTextNode let textNode: ImmediateTextNode + var dustNode: InvisibleInkDustNode? let imageNode: TransformImageNode private let actionArea: AccessibilityAreaNode @@ -96,6 +99,7 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { var authorName = "" var text = "" + var isText = true if let forwardInfo = message?.forwardInfo, forwardInfo.flags.contains(.isImported) { if let author = forwardInfo.author { authorName = EnginePeer(author).displayTitle(strings: strings, displayOrder: nameDisplayOrder) @@ -105,8 +109,34 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { } else if let author = message?.effectiveAuthor { authorName = EnginePeer(author).displayTitle(strings: strings, displayOrder: nameDisplayOrder) } + + let isMedia: Bool if let message = message { - (text, _) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: EngineMessage(message), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: context.account.peerId) + switch messageContentKind(contentSettings: context.currentContentSettings.with { $0 }, message: EngineMessage(message), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: context.account.peerId) { + case .text: + isMedia = false + default: + isMedia = true + } + (text, _, isText) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: EngineMessage(message), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: context.account.peerId) + } else { + isMedia = false + } + + let textFont = Font.regular(14.0) + let messageText: NSAttributedString + if isText, let message = message { + let entities = (message.textEntitiesAttribute?.entities ?? []).filter { entity in + if case .Spoiler = entity.type { + return true + } else { + return false + } + } + let textColor = strongSelf.theme.chat.inputPanel.primaryTextColor + messageText = stringWithAppliedEntities(message.text, entities: entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false) + } else { + messageText = NSAttributedString(string: text, font: textFont, textColor: isMedia ? strongSelf.theme.chat.inputPanel.secondaryTextColor : strongSelf.theme.chat.inputPanel.primaryTextColor) } var updatedMediaReference: AnyMediaReference? @@ -169,22 +199,10 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { updateImageSignal = .single({ _ in return nil }) } } - - let isMedia: Bool - if let message = message { - switch messageContentKind(contentSettings: context.currentContentSettings.with { $0 }, message: EngineMessage(message), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: context.account.peerId) { - case .text: - isMedia = false - default: - isMedia = true - } - } else { - isMedia = false - } - + strongSelf.titleNode.attributedText = NSAttributedString(string: strongSelf.strings.Conversation_ReplyMessagePanelTitle(authorName).string, font: Font.medium(14.0), textColor: strongSelf.theme.chat.inputPanel.panelControlAccentColor) - strongSelf.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(14.0), textColor: isMedia ? strongSelf.theme.chat.inputPanel.secondaryTextColor : strongSelf.theme.chat.inputPanel.primaryTextColor) - + strongSelf.textNode.attributedText = messageText + let headerString: String if let message = message, message.flags.contains(.Incoming), let author = message.author { headerString = "Reply to message. From: \(EnginePeer(author).displayTitle(strings: strings, displayOrder: nameDisplayOrder))" @@ -295,8 +313,25 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { } let textSize = self.textNode.updateLayout(CGSize(width: bounds.size.width - leftInset - textLineInset - rightInset - textRightInset - imageTextInset, height: bounds.size.height)) + let textFrame = CGRect(origin: CGPoint(x: leftInset + textLineInset + imageTextInset - self.textNode.insets.left, y: 25.0 - self.textNode.insets.top), size: textSize) if self.textNode.supernode == self { - self.textNode.frame = CGRect(origin: CGPoint(x: leftInset + textLineInset + imageTextInset - self.textNode.insets.left, y: 25.0 - self.textNode.insets.top), size: textSize) + self.textNode.frame = textFrame + } + + if let textLayout = self.textNode.cachedLayout, !textLayout.spoilers.isEmpty { + if self.dustNode == nil { + let dustNode = InvisibleInkDustNode(textNode: nil) + self.dustNode = dustNode + self.textNode.supernode?.insertSubnode(dustNode, aboveSubnode: self.textNode) + + } + if let dustNode = self.dustNode { + dustNode.update(size: textFrame.size, color: self.theme.chat.inputPanel.primaryTextColor, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }, wordRects: textLayout.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }) + dustNode.frame = textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0) + } + } else if let dustNode = self.dustNode { + self.dustNode = nil + dustNode.removeFromSupernode() } } diff --git a/submodules/TelegramUI/Sources/ShareExtensionContext.swift b/submodules/TelegramUI/Sources/ShareExtensionContext.swift index af225ff735..9d4fefba03 100644 --- a/submodules/TelegramUI/Sources/ShareExtensionContext.swift +++ b/submodules/TelegramUI/Sources/ShareExtensionContext.swift @@ -205,7 +205,7 @@ public class ShareRootControllerImpl { let internalContext: InternalContext - let accountManager = AccountManager(basePath: rootPath + "/accounts-metadata", isTemporary: true, isReadOnly: false, useCaches: false) + let accountManager = AccountManager(basePath: rootPath + "/accounts-metadata", isTemporary: true, isReadOnly: false, useCaches: false, removeDatabaseOnError: false) if let globalInternalContext = globalInternalContext { internalContext = globalInternalContext diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index d36d7c4937..27ee669a26 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1235,11 +1235,12 @@ public final class SharedAccountContextImpl: SharedAccountContext { return PeerSelectionControllerImpl(params) } - public func makeChatMessagePreviewItem(context: AccountContext, messages: [Message], theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder, forcedResourceStatus: FileMediaResourceStatus?, tapMessage: ((Message) -> Void)? = nil, clickThroughMessage: (() -> Void)? = nil, backgroundNode: ASDisplayNode?) -> ListViewItem { + public func makeChatMessagePreviewItem(context: AccountContext, messages: [Message], theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder, forcedResourceStatus: FileMediaResourceStatus?, tapMessage: ((Message) -> Void)? = nil, clickThroughMessage: (() -> Void)? = nil, backgroundNode: ASDisplayNode?, availableReactions: AvailableReactions?) -> ListViewItem { let controllerInteraction: ChatControllerInteraction controllerInteraction = ChatControllerInteraction(openMessage: { _, _ in - return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, updateMessageReaction: { _, _ in }, activateMessagePinch: { _ in + return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, openMessageReactionContextMenu: { _, _, _, _ in + }, updateMessageReaction: { _, _ in }, activateMessagePinch: { _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in }, tapMessage: { message in tapMessage?(message) @@ -1308,7 +1309,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { chatLocation = .peer(messages.first!.id.peerId) } - return ChatMessageItem(presentationData: ChatPresentationData(theme: ChatPresentationThemeData(theme: theme, wallpaper: wallpaper), fontSize: fontSize, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameOrder, disableAnimations: false, largeEmoji: false, chatBubbleCorners: chatBubbleCorners, animatedEmojiScale: 1.0, isPreview: true), context: context, chatLocation: chatLocation, associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .contact, automaticDownloadNetworkType: .cellular, isRecentActions: false, subject: nil, contactsPeerIds: Set(), animatedEmojiStickers: [:], forcedResourceStatus: forcedResourceStatus, availableReactions: nil), controllerInteraction: controllerInteraction, content: content, disableDate: true, additionalContent: nil) + return ChatMessageItem(presentationData: ChatPresentationData(theme: ChatPresentationThemeData(theme: theme, wallpaper: wallpaper), fontSize: fontSize, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameOrder, disableAnimations: false, largeEmoji: false, chatBubbleCorners: chatBubbleCorners, animatedEmojiScale: 1.0, isPreview: true), context: context, chatLocation: chatLocation, associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .contact, automaticDownloadNetworkType: .cellular, isRecentActions: false, subject: nil, contactsPeerIds: Set(), animatedEmojiStickers: [:], forcedResourceStatus: forcedResourceStatus, availableReactions: availableReactions, defaultReaction: nil), controllerInteraction: controllerInteraction, content: content, disableDate: true, additionalContent: nil) } public func makeChatMessageDateHeaderItem(context: AccountContext, timestamp: Int32, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder) -> ListViewItemHeader { diff --git a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift index 0fad7a6334..a97a6c14f6 100644 --- a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift +++ b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift @@ -19,6 +19,7 @@ public struct ExperimentalUISettings: Codable, Equatable { public var enableDebugDataDisplay: Bool public var acceleratedStickers: Bool public var experimentalBackground: Bool + public var snow: Bool public static var defaultSettings: ExperimentalUISettings { return ExperimentalUISettings( @@ -36,7 +37,8 @@ public struct ExperimentalUISettings: Codable, Equatable { experimentalCompatibility: false, enableDebugDataDisplay: false, acceleratedStickers: false, - experimentalBackground: false + experimentalBackground: false, + snow: false ) } @@ -55,7 +57,8 @@ public struct ExperimentalUISettings: Codable, Equatable { experimentalCompatibility: Bool, enableDebugDataDisplay: Bool, acceleratedStickers: Bool, - experimentalBackground: Bool + experimentalBackground: Bool, + snow: Bool ) { self.keepChatNavigationStack = keepChatNavigationStack self.skipReadHistory = skipReadHistory @@ -72,6 +75,7 @@ public struct ExperimentalUISettings: Codable, Equatable { self.enableDebugDataDisplay = enableDebugDataDisplay self.acceleratedStickers = acceleratedStickers self.experimentalBackground = experimentalBackground + self.snow = snow } public init(from decoder: Decoder) throws { @@ -92,6 +96,7 @@ public struct ExperimentalUISettings: Codable, Equatable { self.enableDebugDataDisplay = (try container.decodeIfPresent(Int32.self, forKey: "enableDebugDataDisplay") ?? 0) != 0 self.acceleratedStickers = (try container.decodeIfPresent(Int32.self, forKey: "acceleratedStickers") ?? 0) != 0 self.experimentalBackground = (try container.decodeIfPresent(Int32.self, forKey: "experimentalBackground") ?? 0) != 0 + self.snow = (try container.decodeIfPresent(Int32.self, forKey: "snow") ?? 0) != 0 } public func encode(to encoder: Encoder) throws { @@ -112,6 +117,7 @@ public struct ExperimentalUISettings: Codable, Equatable { try container.encode((self.enableDebugDataDisplay ? 1 : 0) as Int32, forKey: "enableDebugDataDisplay") try container.encode((self.acceleratedStickers ? 1 : 0) as Int32, forKey: "acceleratedStickers") try container.encode((self.experimentalBackground ? 1 : 0) as Int32, forKey: "experimentalBackground") + try container.encode((self.snow ? 1 : 0) as Int32, forKey: "snow") } } diff --git a/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift b/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift index 73e2effe84..5fde289241 100644 --- a/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift +++ b/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift @@ -35,6 +35,7 @@ private enum ApplicationSpecificSharedDataKeyValues: Int32 { case contactSynchronizationSettings = 15 case webBrowserSettings = 16 case intentsSettings = 17 + case translationSettings = 18 } public struct ApplicationSpecificSharedDataKeys { @@ -56,6 +57,7 @@ public struct ApplicationSpecificSharedDataKeys { public static let contactSynchronizationSettings = applicationSpecificPreferencesKey(ApplicationSpecificSharedDataKeyValues.contactSynchronizationSettings.rawValue) public static let webBrowserSettings = applicationSpecificPreferencesKey(ApplicationSpecificSharedDataKeyValues.webBrowserSettings.rawValue) public static let intentsSettings = applicationSpecificPreferencesKey(ApplicationSpecificSharedDataKeyValues.intentsSettings.rawValue) + public static let translationSettings = applicationSpecificPreferencesKey(ApplicationSpecificSharedDataKeyValues.translationSettings.rawValue) } private enum ApplicationSpecificItemCacheCollectionIdValues: Int8 { diff --git a/submodules/TelegramUIPreferences/Sources/TranslationSettings.swift b/submodules/TelegramUIPreferences/Sources/TranslationSettings.swift new file mode 100644 index 0000000000..51c6a77818 --- /dev/null +++ b/submodules/TelegramUIPreferences/Sources/TranslationSettings.swift @@ -0,0 +1,58 @@ +import Foundation +import Postbox +import TelegramCore +import SwiftSignalKit + +public struct TranslationSettings: Codable, Equatable { + public var showTranslate: Bool + public var ignoredLanguages: [String]? + + public static var defaultSettings: TranslationSettings { + return TranslationSettings(showTranslate: true, ignoredLanguages: nil) + } + + init(showTranslate: Bool, ignoredLanguages: [String]?) { + self.showTranslate = showTranslate + self.ignoredLanguages = ignoredLanguages + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: StringCodingKey.self) + + self.showTranslate = try container.decodeIfPresent(Bool.self, forKey: "showTranslate") ?? true + self.ignoredLanguages = try container.decodeIfPresent([String].self, forKey: "ignoredLanguages") + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: StringCodingKey.self) + + try container.encode(self.showTranslate, forKey: "showTranslate") + try container.encodeIfPresent(self.ignoredLanguages, forKey: "ignoredLanguages") + } + + public static func ==(lhs: TranslationSettings, rhs: TranslationSettings) -> Bool { + return lhs.showTranslate == rhs.showTranslate && lhs.ignoredLanguages == rhs.ignoredLanguages + } + + public func withUpdatedShowTranslate(_ showTranslate: Bool) -> TranslationSettings { + return TranslationSettings(showTranslate: showTranslate, ignoredLanguages: self.ignoredLanguages) + } + + public func withUpdatedIgnoredLanguages(_ ignoredLanguages: [String]?) -> TranslationSettings { + return TranslationSettings(showTranslate: self.showTranslate, ignoredLanguages: ignoredLanguages) + } +} + +public func updateTranslationSettingsInteractively(accountManager: AccountManager, _ f: @escaping (TranslationSettings) -> TranslationSettings) -> Signal { + return accountManager.transaction { transaction -> Void in + transaction.updateSharedData(ApplicationSpecificSharedDataKeys.translationSettings, { entry in + let currentSettings: TranslationSettings + if let entry = entry?.get(TranslationSettings.self) { + currentSettings = entry + } else { + currentSettings = TranslationSettings.defaultSettings + } + return PreferencesEntry(f(currentSettings)) + }) + } +} diff --git a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift index 1a3d5022b4..2c1bd90901 100644 --- a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift +++ b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift @@ -47,7 +47,7 @@ public struct ChatTextFontAttributes: OptionSet { public static let blockQuote = ChatTextFontAttributes(rawValue: 1 << 3) } -public func textAttributedStringForStateText(_ stateText: NSAttributedString, fontSize: CGFloat, textColor: UIColor, accentTextColor: UIColor, writingDirection: NSWritingDirection?) -> NSAttributedString { +public func textAttributedStringForStateText(_ stateText: NSAttributedString, fontSize: CGFloat, textColor: UIColor, accentTextColor: UIColor, writingDirection: NSWritingDirection?, spoilersRevealed: Bool) -> NSAttributedString { let result = NSMutableAttributedString(string: stateText.string) let fullRange = NSRange(location: 0, length: result.length) @@ -86,7 +86,11 @@ public func textAttributedStringForStateText(_ stateText: NSAttributedString, fo result.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range) } else if key == ChatTextInputAttributes.spoiler { result.addAttribute(key, value: value, range: range) - result.addAttribute(NSAttributedString.Key.backgroundColor, value: textColor.withAlphaComponent(0.15), range: range) + if spoilersRevealed { + result.addAttribute(NSAttributedString.Key.backgroundColor, value: textColor.withAlphaComponent(0.15), range: range) + } else { + result.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.clear, range: range) + } } } @@ -408,7 +412,7 @@ private func refreshTextUrls(text: NSString, initialAttributedText: NSAttributed } } -public func refreshChatTextInputAttributes(_ textNode: ASEditableTextNode, theme: PresentationTheme, baseFontSize: CGFloat) { +public func refreshChatTextInputAttributes(_ textNode: ASEditableTextNode, theme: PresentationTheme, baseFontSize: CGFloat, spoilersRevealed: Bool) { guard let initialAttributedText = textNode.attributedText, initialAttributedText.length != 0 else { return } @@ -423,14 +427,14 @@ public func refreshChatTextInputAttributes(_ textNode: ASEditableTextNode, theme var attributedText = NSMutableAttributedString(attributedString: stateAttributedStringForText(initialAttributedText)) refreshTextMentions(text: text, initialAttributedText: initialAttributedText, attributedText: attributedText, fullRange: fullRange) - var resultAttributedText = textAttributedStringForStateText(attributedText, fontSize: baseFontSize, textColor: theme.chat.inputPanel.primaryTextColor, accentTextColor: theme.chat.inputPanel.panelControlAccentColor, writingDirection: writingDirection) + var resultAttributedText = textAttributedStringForStateText(attributedText, fontSize: baseFontSize, textColor: theme.chat.inputPanel.primaryTextColor, accentTextColor: theme.chat.inputPanel.panelControlAccentColor, writingDirection: writingDirection, spoilersRevealed: spoilersRevealed) text = resultAttributedText.string as NSString fullRange = NSRange(location: 0, length: initialAttributedText.length) attributedText = NSMutableAttributedString(attributedString: stateAttributedStringForText(resultAttributedText)) refreshTextUrls(text: text, initialAttributedText: resultAttributedText, attributedText: attributedText, fullRange: fullRange) - resultAttributedText = textAttributedStringForStateText(attributedText, fontSize: baseFontSize, textColor: theme.chat.inputPanel.primaryTextColor, accentTextColor: theme.chat.inputPanel.panelControlAccentColor, writingDirection: writingDirection) + resultAttributedText = textAttributedStringForStateText(attributedText, fontSize: baseFontSize, textColor: theme.chat.inputPanel.primaryTextColor, accentTextColor: theme.chat.inputPanel.panelControlAccentColor, writingDirection: writingDirection, spoilersRevealed: spoilersRevealed) if !resultAttributedText.isEqual(to: initialAttributedText) { textNode.textView.textStorage.removeAttribute(NSAttributedString.Key.font, range: fullRange) @@ -472,7 +476,11 @@ public func refreshChatTextInputAttributes(_ textNode: ASEditableTextNode, theme textNode.textView.textStorage.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range) } else if key == ChatTextInputAttributes.spoiler { textNode.textView.textStorage.addAttribute(key, value: value, range: range) - textNode.textView.textStorage.addAttribute(NSAttributedString.Key.backgroundColor, value: theme.chat.inputPanel.primaryTextColor.withAlphaComponent(0.15), range: range) + if spoilersRevealed { + textNode.textView.textStorage.addAttribute(NSAttributedString.Key.backgroundColor, value: theme.chat.inputPanel.primaryTextColor.withAlphaComponent(0.15), range: range) + } else { + textNode.textView.textStorage.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.clear, range: range) + } } } @@ -502,7 +510,7 @@ public func refreshChatTextInputAttributes(_ textNode: ASEditableTextNode, theme } } -public func refreshGenericTextInputAttributes(_ textNode: ASEditableTextNode, theme: PresentationTheme, baseFontSize: CGFloat) { +public func refreshGenericTextInputAttributes(_ textNode: ASEditableTextNode, theme: PresentationTheme, baseFontSize: CGFloat, spoilersRevealed: Bool = false) { guard let initialAttributedText = textNode.attributedText, initialAttributedText.length != 0 else { return } @@ -515,14 +523,14 @@ public func refreshGenericTextInputAttributes(_ textNode: ASEditableTextNode, th var text: NSString = initialAttributedText.string as NSString var fullRange = NSRange(location: 0, length: initialAttributedText.length) var attributedText = NSMutableAttributedString(attributedString: stateAttributedStringForText(initialAttributedText)) - var resultAttributedText = textAttributedStringForStateText(attributedText, fontSize: baseFontSize, textColor: theme.chat.inputPanel.primaryTextColor, accentTextColor: theme.chat.inputPanel.panelControlAccentColor, writingDirection: writingDirection) + var resultAttributedText = textAttributedStringForStateText(attributedText, fontSize: baseFontSize, textColor: theme.chat.inputPanel.primaryTextColor, accentTextColor: theme.chat.inputPanel.panelControlAccentColor, writingDirection: writingDirection, spoilersRevealed: spoilersRevealed) text = resultAttributedText.string as NSString fullRange = NSRange(location: 0, length: initialAttributedText.length) attributedText = NSMutableAttributedString(attributedString: stateAttributedStringForText(resultAttributedText)) refreshTextUrls(text: text, initialAttributedText: resultAttributedText, attributedText: attributedText, fullRange: fullRange) - resultAttributedText = textAttributedStringForStateText(attributedText, fontSize: baseFontSize, textColor: theme.chat.inputPanel.primaryTextColor, accentTextColor: theme.chat.inputPanel.panelControlAccentColor, writingDirection: writingDirection) + resultAttributedText = textAttributedStringForStateText(attributedText, fontSize: baseFontSize, textColor: theme.chat.inputPanel.primaryTextColor, accentTextColor: theme.chat.inputPanel.panelControlAccentColor, writingDirection: writingDirection, spoilersRevealed: spoilersRevealed) if !resultAttributedText.isEqual(to: initialAttributedText) { textNode.textView.textStorage.removeAttribute(NSAttributedString.Key.font, range: fullRange) @@ -564,7 +572,11 @@ public func refreshGenericTextInputAttributes(_ textNode: ASEditableTextNode, th textNode.textView.textStorage.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range) } else if key == ChatTextInputAttributes.spoiler { textNode.textView.textStorage.addAttribute(key, value: value, range: range) - textNode.textView.textStorage.addAttribute(NSAttributedString.Key.backgroundColor, value: UIColor.clear, range: range) + if spoilersRevealed { + textNode.textView.textStorage.addAttribute(NSAttributedString.Key.backgroundColor, value: theme.chat.inputPanel.primaryTextColor.withAlphaComponent(0.15), range: range) + } else { + textNode.textView.textStorage.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.clear, range: range) + } } } @@ -698,7 +710,7 @@ public func breakChatInputText(_ text: NSAttributedString) -> [NSAttributedStrin } } -private let markdownRegexFormat = "(^|\\s|\\n)(````?)([\\s\\S]+?)(````?)([\\s\\n\\.,:?!;]|$)|(^|\\s)(`|\\*\\*|__|~~)([^\\n]+?)\\7([\\s\\.,:?!;]|$)|@(\\d+)\\s*\\((.+?)\\)" +private let markdownRegexFormat = "(^|\\s|\\n)(````?)([\\s\\S]+?)(````?)([\\s\\n\\.,:?!;]|$)|(^|\\s)(`|\\*\\*|__|~~|\\|\\|)([^\\n]+?)\\7([\\s\\.,:?!;]|$)|@(\\d+)\\s*\\((.+?)\\)" private let markdownRegex = try? NSRegularExpression(pattern: markdownRegexFormat, options: [.caseInsensitive, .anchorsMatchLines]) public func convertMarkdownToAttributes(_ text: NSAttributedString) -> NSAttributedString { diff --git a/submodules/TextSelectionNode/Sources/TextSelectionNode.swift b/submodules/TextSelectionNode/Sources/TextSelectionNode.swift index 93f3111b3d..930eaaf914 100644 --- a/submodules/TextSelectionNode/Sources/TextSelectionNode.swift +++ b/submodules/TextSelectionNode/Sources/TextSelectionNode.swift @@ -188,6 +188,7 @@ public enum TextSelectionAction { case share case lookup case speak + case translate } public final class TextSelectionNode: ASDisplayNode { @@ -195,6 +196,7 @@ public final class TextSelectionNode: ASDisplayNode { private let strings: PresentationStrings private let textNode: TextNode private let updateIsActive: (Bool) -> Void + public var updateRange: ((NSRange?) -> Void)? private let present: (ViewController, Any?) -> Void private weak var rootNode: ASDisplayNode? private let performAction: (NSAttributedString, TextSelectionAction) -> Void @@ -395,6 +397,8 @@ public final class TextSelectionNode: ASDisplayNode { } private func updateSelection(range: NSRange?, animateIn: Bool) { + self.updateRange?(range) + var rects: (rects: [CGRect], start: TextRangeRectEdge, end: TextRangeRectEdge)? if let range = range { @@ -501,12 +505,18 @@ public final class TextSelectionNode: ASDisplayNode { self?.performAction(attributedText, .lookup) self?.dismissSelection() })) - if isSpeakSelectionEnabled() { - actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuSpeak, accessibilityLabel: self.strings.Conversation_ContextMenuSpeak), action: { [weak self] in - self?.performAction(attributedText, .speak) + if #available(iOS 15.0, *) { + actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuTranslate, accessibilityLabel: self.strings.Conversation_ContextMenuTranslate), action: { [weak self] in + self?.performAction(attributedText, .translate) self?.dismissSelection() })) } +// if isSpeakSelectionEnabled() { +// actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuSpeak, accessibilityLabel: self.strings.Conversation_ContextMenuSpeak), action: { [weak self] in +// self?.performAction(attributedText, .speak) +// self?.dismissSelection() +// })) +// } actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuShare, accessibilityLabel: self.strings.Conversation_ContextMenuShare), action: { [weak self] in self?.performAction(attributedText, .share) self?.dismissSelection() diff --git a/submodules/Translate/BUILD b/submodules/Translate/BUILD new file mode 100644 index 0000000000..a6dcb04479 --- /dev/null +++ b/submodules/Translate/BUILD @@ -0,0 +1,19 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "Translate", + module_name = "Translate", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display:Display", + "//submodules/AccountContext:AccountContext", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/Translate/Sources/Translate.swift b/submodules/Translate/Sources/Translate.swift new file mode 100644 index 0000000000..984f805382 --- /dev/null +++ b/submodules/Translate/Sources/Translate.swift @@ -0,0 +1,74 @@ +import Foundation +import UIKit +import Display +import AccountContext +import NaturalLanguage + +// Incuding at least one Objective-C class in a swift file ensures that it doesn't get stripped by the linker +private final class LinkHelperClass: NSObject { +} + +public var supportedTranslationLanguages = [ + "en", + "ar", + "zh", + "fr", + "de", + "it", + "jp", + "ko", + "pt", + "ru", + "es" +] + +@available(iOS 12.0, *) +private let languageRecognizer = NLLanguageRecognizer() + +public func canTranslateText(context: AccountContext, text: String, showTranslate: Bool, ignoredLanguages: [String]?) -> Bool { + guard showTranslate, text.count > 0 else { + return false + } + + if #available(iOS 15.0, *) { + var dontTranslateLanguages: [String] = [] + if let ignoredLanguages = ignoredLanguages { + dontTranslateLanguages = ignoredLanguages + } else { + dontTranslateLanguages = [context.sharedContext.currentPresentationData.with { $0 }.strings.baseLanguageCode] + } + + let text = String(text.prefix(64)) + languageRecognizer.processString(text) + let hypotheses = languageRecognizer.languageHypotheses(withMaximum: 2) + languageRecognizer.reset() + + if let language = hypotheses.first(where: { supportedTranslationLanguages.contains($0.key.rawValue) }) { + return !dontTranslateLanguages.contains(language.key.rawValue) + } else { + return false + } + } else { + return false + } +} + +public func translateText(context: AccountContext, text: String) { + guard !text.isEmpty else { + return + } + if #available(iOS 15.0, *) { + let textView = UITextView() + textView.text = text + textView.isEditable = false + if let navigationController = context.sharedContext.mainWindow?.viewController as? NavigationController, let topController = navigationController.topViewController as? ViewController { + topController.view.addSubview(textView) + textView.selectAll(nil) + textView.perform(NSSelectorFromString(["_", "trans", "late:"].joined(separator: "")), with: nil) + + DispatchQueue.main.async { + textView.removeFromSuperview() + } + } + } +} diff --git a/submodules/UrlWhitelist/Sources/UrlWhitelist.swift b/submodules/UrlWhitelist/Sources/UrlWhitelist.swift index 3f12ac2e08..47bcf4ec20 100644 --- a/submodules/UrlWhitelist/Sources/UrlWhitelist.swift +++ b/submodules/UrlWhitelist/Sources/UrlWhitelist.swift @@ -2,7 +2,8 @@ import Foundation private let whitelistedHosts: Set = Set([ "t.me", - "telegram.me" + "telegram.me", + "telegra.ph" ]) public func isConcealedUrlWhitelisted(_ url: URL) -> Bool { diff --git a/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift b/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift index 1297e5144a..8105368973 100644 --- a/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift +++ b/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift @@ -56,6 +56,8 @@ public protocol WallpaperBackgroundNode: ASDisplayNode { func updateBubbleTheme(bubbleTheme: PresentationTheme, bubbleCorners: PresentationChatBubbleCorners) func hasBubbleBackground(for type: WallpaperBubbleType) -> Bool func makeBubbleBackground(for type: WallpaperBubbleType) -> WallpaperBubbleBackgroundNode? + + func makeDimmedNode() -> ASDisplayNode? } final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode { @@ -799,7 +801,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode if isFirstLayout && !self.frame.isEmpty { self.updateScale() - if false, self.newYearNode == nil { + if self.context.sharedContext.immediateExperimentalUISettings.snow, self.newYearNode == nil { let newYearNode = WallpaperNewYearNode() self.addSubnode(newYearNode) self.newYearNode = newYearNode @@ -897,6 +899,14 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode node.updateContents() return node } + + func makeDimmedNode() -> ASDisplayNode? { + if let gradientBackgroundNode = self.gradientBackgroundNode { + return GradientBackgroundNode.CloneNode(parentNode: gradientBackgroundNode) + } else { + return nil + } + } } private protocol WallpaperComponentView: AnyObject { @@ -1695,6 +1705,10 @@ final class WallpaperBackgroundNodeMergedImpl: ASDisplayNode, WallpaperBackgroun node.updateContents() return node } + + func makeDimmedNode() -> ASDisplayNode? { + return nil + } } private let sharedStorage = WallpaperBackgroundNodeMergedImpl.SharedStorage() @@ -1738,7 +1752,7 @@ private class WallpaperNewYearNode: ASDisplayNode { cell1.scale = 0.04 cell1.scaleRange = 0.15 cell1.color = UIColor.white.withAlphaComponent(0.88).cgColor - cell1.alphaRange = -0.2 +// cell1.alphaRange = -0.2 particlesLayer.emitterCells = [cell1] } diff --git a/submodules/WallpaperResources/Sources/WallpaperResources.swift b/submodules/WallpaperResources/Sources/WallpaperResources.swift index b6e7549af1..0f1479172e 100644 --- a/submodules/WallpaperResources/Sources/WallpaperResources.swift +++ b/submodules/WallpaperResources/Sources/WallpaperResources.swift @@ -1300,7 +1300,23 @@ public func themeImage(account: Account, accountManager: AccountManager, theme: PresentationThemeReference, color: PresentationThemeAccentColor?, wallpaper: TelegramWallpaper? = nil, nightMode: Bool? = nil, emoticon: Bool = false, large: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { +private let qrIconImage: UIImage = { + return generateImage(CGSize(width: 36.0, height: 36.0), rotatedContext: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + context.setFillColor(UIColor.white.cgColor) + context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: 9.0).cgPath) + context.fillPath() + + if let image = UIImage(bundleImageName: "Settings/QrButtonIcon")?.cgImage { + context.clip(to: CGRect(x: 6.0, y: 6.0, width: 24.0, height: 24.0), mask: image) + context.clear(bounds) + } + })! +}() + +public func themeIconImage(account: Account, accountManager: AccountManager, theme: PresentationThemeReference, color: PresentationThemeAccentColor?, wallpaper: TelegramWallpaper? = nil, nightMode: Bool? = nil, emoticon: Bool = false, large: Bool = false, qr: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { let colorsSignal: Signal<((UIColor, UIColor?, [UInt32]), [UIColor], [UIColor], UIImage?, Bool, Bool, CGFloat, Int32?), NoError> var reference: MediaResourceReference? @@ -1552,134 +1568,140 @@ public func themeIconImage(account: Account, accountManager: AccountManager 1 { - c.clip() + c.translateBy(x: 7.0, y: 27.0) + c.translateBy(x: 114.0, y: 32.0) + c.scaleBy(x: 1.0, y: -1.0) + c.translateBy(x: -114.0, y: -32.0) + + let _ = try? drawSvgPath(c, path: "M12.8304,29.8712 C10.0551,31.8416 6.6628,33 2.99998,33 C1.98426,33 0.989361,32.9109 0.022644,32.7402 C2.97318,31.9699 5.24596,29.5785 5.84625,26.5607 C5.99996,25.7879 5.99996,24.8586 5.99996,23 V16.0 H6.00743 C6.27176,7.11861 13.5546,0 22.5,0 H61.5 C70.6127,0 78,7.3873 78,16.5 C78,25.6127 70.6127,33 61.5,33 H22.5 C18.8883,33 15.5476,31.8396 12.8304,29.8712 ") + if Set(incomingColors.map(\.rgb)).count > 1 { + c.clip() - var colors: [CGColor] = [] - var locations: [CGFloat] = [] - for i in 0 ..< incomingColors.count { - let t = CGFloat(i) / CGFloat(incomingColors.count - 1) - locations.append(t) - colors.append(incomingColors[i].cgColor) + var colors: [CGColor] = [] + var locations: [CGFloat] = [] + for i in 0 ..< incomingColors.count { + let t = CGFloat(i) / CGFloat(incomingColors.count - 1) + locations.append(t) + colors.append(incomingColors[i].cgColor) + } + + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as NSArray, locations: &locations)! + c.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: 34.0), options: CGGradientDrawingOptions()) + } else { + c.setFillColor(incomingColors[0].cgColor) + c.fillPath() } - - let colorSpace = CGColorSpaceCreateDeviceRGB() - let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as NSArray, locations: &locations)! - c.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: 34.0), options: CGGradientDrawingOptions()) + + c.restoreGState() } else { - c.setFillColor(incomingColors[0].cgColor) - c.fillPath() - } - - c.restoreGState() - } else { - let rect = CGRect(x: 8.0, y: 44.0, width: 48.0, height: 24.0) - c.addPath(UIBezierPath(roundedRect: rect, cornerRadius: 12.0).cgPath) - c.clip() - - if incomingColors.count >= 2 { - let gradientColors = incomingColors.reversed().map { $0.cgColor } as CFArray + let rect = CGRect(x: 8.0, y: 44.0, width: 48.0, height: 24.0) + c.addPath(UIBezierPath(roundedRect: rect, cornerRadius: 12.0).cgPath) + c.clip() + + if incomingColors.count >= 2 { + let gradientColors = incomingColors.reversed().map { $0.cgColor } as CFArray - var locations: [CGFloat] = [] - for i in 0 ..< incomingColors.count { - let t = CGFloat(i) / CGFloat(incomingColors.count - 1) - locations.append(t) + var locations: [CGFloat] = [] + for i in 0 ..< incomingColors.count { + let t = CGFloat(i) / CGFloat(incomingColors.count - 1) + locations.append(t) + } + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! + + c.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: rect.minY), end: CGPoint(x: 0.0, y: rect.maxY), options: CGGradientDrawingOptions()) + } else if !incomingColors.isEmpty { + c.setFillColor(incomingColors[0].cgColor) + c.fill(rect) } - let colorSpace = CGColorSpaceCreateDeviceRGB() - let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! + + c.resetClip() + } + } else { + let incoming = generateGradientTintedImage(image: UIImage(bundleImageName: "Settings/ThemeBubble"), colors: incomingColors) + c.draw(incoming!.cgImage!, in: CGRect(x: 9.0, y: 34.0, width: 57.0, height: 16.0)) + } + + if !(emoticon && large) { + c.translateBy(x: drawingRect.width / 2.0, y: drawingRect.height / 2.0) + c.scaleBy(x: -1.0, y: 1.0) + c.translateBy(x: -drawingRect.width / 2.0, y: -drawingRect.height / 2.0) + } + + let outgoingColors = colors.2 + if emoticon { + if large { + c.saveGState() + + c.translateBy(x: (drawingRect.width - 120) - 71, y: 66.0) + c.translateBy(x: 114.0, y: 32.0) + c.scaleBy(x: 1.0, y: -1.0) + c.translateBy(x: 0.0, y: -32.0) + + let _ = try? drawSvgPath(c, path: "M57.1696,29.8712 C59.9449,31.8416 63.3372,33 67,33 C68.0157,33 69.0106,32.9109 69.9773,32.7402 C67.0268,31.9699 64.754,29.5786 64.1537,26.5607 C64,25.7879 64,24.8586 64,23 V16.5 V16 H63.9926 C63.7282,7.11861 56.4454,0 47.5,0 H16.5 C7.3873,0 0,7.3873 0,16.5 C0,25.6127 7.3873,33 16.5,33 H47.5 C51.1117,33 54.4524,31.8396 57.1696,29.8712 ") + if Set(outgoingColors.map(\.rgb)).count > 1 { + c.clip() - c.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: rect.minY), end: CGPoint(x: 0.0, y: rect.maxY), options: CGGradientDrawingOptions()) - } else if !incomingColors.isEmpty { - c.setFillColor(incomingColors[0].cgColor) - c.fill(rect) + var colors: [CGColor] = [] + var locations: [CGFloat] = [] + for i in 0 ..< outgoingColors.count { + let t = CGFloat(i) / CGFloat(outgoingColors.count - 1) + locations.append(t) + colors.append(outgoingColors[i].cgColor) + } + + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as NSArray, locations: &locations)! + c.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: 34.0), options: CGGradientDrawingOptions()) + } else { + c.setFillColor(outgoingColors[0].cgColor) + c.fillPath() + } + + c.restoreGState() + } else { + let rect = CGRect(x: 8.0, y: 72.0, width: 48.0, height: 24.0) + c.addPath(UIBezierPath(roundedRect: rect, cornerRadius: 12.0).cgPath) + c.clip() + + if outgoingColors.count >= 2 { + let gradientColors = outgoingColors.reversed().map { $0.cgColor } as CFArray + + var locations: [CGFloat] = [] + for i in 0 ..< outgoingColors.count { + let t = CGFloat(i) / CGFloat(outgoingColors.count - 1) + locations.append(t) + } + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! + + c.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: rect.minY), end: CGPoint(x: 0.0, y: rect.maxY), options: CGGradientDrawingOptions()) + } else if !outgoingColors.isEmpty { + c.setFillColor(outgoingColors[0].cgColor) + c.fill(rect) + } } c.resetClip() - } - } else { - let incoming = generateGradientTintedImage(image: UIImage(bundleImageName: "Settings/ThemeBubble"), colors: incomingColors) - c.draw(incoming!.cgImage!, in: CGRect(x: 9.0, y: 34.0, width: 57.0, height: 16.0)) - } - - if !(emoticon && large) { - c.translateBy(x: drawingRect.width / 2.0, y: drawingRect.height / 2.0) - c.scaleBy(x: -1.0, y: 1.0) - c.translateBy(x: -drawingRect.width / 2.0, y: -drawingRect.height / 2.0) - } - - let outgoingColors = colors.2 - if emoticon { - if large { - c.saveGState() - - c.translateBy(x: (drawingRect.width - 120) - 71, y: 66.0) - c.translateBy(x: 114.0, y: 32.0) - c.scaleBy(x: 1.0, y: -1.0) - c.translateBy(x: 0.0, y: -32.0) - - let _ = try? drawSvgPath(c, path: "M57.1696,29.8712 C59.9449,31.8416 63.3372,33 67,33 C68.0157,33 69.0106,32.9109 69.9773,32.7402 C67.0268,31.9699 64.754,29.5786 64.1537,26.5607 C64,25.7879 64,24.8586 64,23 V16.5 V16 H63.9926 C63.7282,7.11861 56.4454,0 47.5,0 H16.5 C7.3873,0 0,7.3873 0,16.5 C0,25.6127 7.3873,33 16.5,33 H47.5 C51.1117,33 54.4524,31.8396 57.1696,29.8712 ") - if Set(outgoingColors.map(\.rgb)).count > 1 { - c.clip() - - var colors: [CGColor] = [] - var locations: [CGFloat] = [] - for i in 0 ..< outgoingColors.count { - let t = CGFloat(i) / CGFloat(outgoingColors.count - 1) - locations.append(t) - colors.append(outgoingColors[i].cgColor) - } - - let colorSpace = CGColorSpaceCreateDeviceRGB() - let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as NSArray, locations: &locations)! - c.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: 34.0), options: CGGradientDrawingOptions()) - } else { - c.setFillColor(outgoingColors[0].cgColor) - c.fillPath() - } - - c.restoreGState() } else { - let rect = CGRect(x: 8.0, y: 72.0, width: 48.0, height: 24.0) - c.addPath(UIBezierPath(roundedRect: rect, cornerRadius: 12.0).cgPath) - c.clip() - - if outgoingColors.count >= 2 { - let gradientColors = outgoingColors.reversed().map { $0.cgColor } as CFArray - - var locations: [CGFloat] = [] - for i in 0 ..< outgoingColors.count { - let t = CGFloat(i) / CGFloat(outgoingColors.count - 1) - locations.append(t) - } - let colorSpace = CGColorSpaceCreateDeviceRGB() - let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! - - c.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: rect.minY), end: CGPoint(x: 0.0, y: rect.maxY), options: CGGradientDrawingOptions()) - } else if !outgoingColors.isEmpty { - c.setFillColor(outgoingColors[0].cgColor) - c.fill(rect) - } + let outgoing = generateGradientTintedImage(image: UIImage(bundleImageName: "Settings/ThemeBubble"), colors: outgoingColors) + c.draw(outgoing!.cgImage!, in: CGRect(x: 9.0, y: 12.0, width: 57.0, height: 16.0)) } - - c.resetClip() - } else { - let outgoing = generateGradientTintedImage(image: UIImage(bundleImageName: "Settings/ThemeBubble"), colors: outgoingColors) - c.draw(outgoing!.cgImage!, in: CGRect(x: 9.0, y: 12.0, width: 57.0, height: 16.0)) } } addCorners(context, arguments: arguments) diff --git a/versions.json b/versions.json index f4351a4b04..320ec2a314 100644 --- a/versions.json +++ b/versions.json @@ -1,5 +1,5 @@ { - "app": "8.3.1", + "app": "8.4", "bazel": "4.0.0", "xcode": "13.1" }