Merge commit '3db9593f49' into temp

# Conflicts:
#	submodules/TelegramCore/Sources/Utils/MessageUtils.swift
This commit is contained in:
overtake 2021-12-22 15:26:16 +04:00
commit 840e7c807a
151 changed files with 7335 additions and 1505 deletions

View file

@ -613,7 +613,7 @@ private final class NotificationServiceHandler {
let appVersion = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "unknown"
self.accountManager = AccountManager<TelegramAccountManagerTypes>(basePath: rootPath + "/accounts-metadata", isTemporary: true, isReadOnly: false, useCaches: false)
self.accountManager = AccountManager<TelegramAccountManagerTypes>(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)!)

View file

@ -121,7 +121,7 @@ class DefaultIntentHandler: INExtension, INSendMessageIntentHandling, INSearchFo
let appVersion = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "unknown"
initializeAccountManagement()
let accountManager = AccountManager<TelegramAccountManagerTypes>(basePath: rootPath + "/accounts-metadata", isTemporary: true, isReadOnly: false, useCaches: false)
let accountManager = AccountManager<TelegramAccountManagerTypes>(basePath: rootPath + "/accounts-metadata", isTemporary: true, isReadOnly: false, useCaches: false, removeDatabaseOnError: false)
self.accountManager = accountManager
let deviceSpecificEncryptionParameters = BuildConfig.deviceSpecificEncryptionParameters(rootPath, baseAppBundleId: baseAppBundleId)

Binary file not shown.

View file

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

View file

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

View file

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

View file

@ -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<EnginePeer.Id> = 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<EnginePeer.Id> = 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?
}

View file

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

View file

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

View file

@ -61,6 +61,7 @@ swift_library(
"//submodules/TelegramStringFormatting:TelegramStringFormatting",
"//submodules/TelegramCallsUI:TelegramCallsUI",
"//submodules/StickerResources:StickerResources",
"//submodules/TextFormat:TextFormat",
],
visibility = [
"//visibility:public",

View file

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

View file

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

View file

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

View file

@ -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<String>()
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<Empty>
public var size: CGSize
}
public var items: [Item]
public var removedViews: [ComponentHostView<Empty>]
}
public private(set) var buttons: [String: ComponentHostView<Empty>] = [:]
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<Empty>] = []
var validIds = Set<String>()
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<Empty>
var itemTransition = transition
if let current = self.buttons[reaction.reaction.value] {
itemTransition = .immediate
view = current
} else {
view = ComponentHostView<Empty>()
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
)
}
}

View file

@ -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<Int>()
var validPlaceholderIds = Set<Int>()
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,

View file

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

View file

@ -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<Bool, NoError> { get }
func takeView() -> ContextControllerTakeViewInfo?
@ -2043,6 +2075,10 @@ public extension ContextExtractedContentSource {
var centerVertically: Bool {
return false
}
var centerActionsHorizontally: Bool {
return false
}
var shouldBeDismissed: Signal<Bool, NoError> {
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)
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -39,6 +39,7 @@ swift_library(
"//submodules/Speak:Speak",
"//submodules/UndoUI:UndoUI",
"//submodules/InvisibleInkDustNode:InvisibleInkDustNode",
"//submodules/Translate:Translate",
],
visibility = [
"//visibility:public",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<T: ItemListNodeEntry>(_ f: @escaping (Int, Int, [T]) -> Signal<Bool, NoError>) {
@ -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)
}

View file

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

View file

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

View file

@ -9,6 +9,8 @@
@interface TGMediaAssetsModernLibrary () <PHPhotoLibraryChangeObserver>
{
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

View file

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

View file

@ -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<PostboxResult, NoError> {
public func openPostbox(basePath: String, seedConfiguration: SeedConfiguration, encryptionParameters: ValueBoxEncryptionParameters, timestampForAbsoluteTimeBasedOperations: Int32, isTemporary: Bool, isReadOnly: Bool, useCopy: Bool, useCaches: Bool, removeDatabaseOnError: Bool) -> Signal<PostboxResult, NoError> {
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)

View file

@ -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<Void, SqlError> {
func tryStep(handle: OpaquePointer?, _ initial: Bool = false, pathToRemoveOnError: String?) -> Result<Void, SqlError> {
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<Bool>(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)
}

View file

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

View file

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

View file

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

View file

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

View file

@ -178,6 +178,12 @@ public func combineLatest<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13
}, initialValues: [:], queue: queue)
}
public func combineLatest<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, E>(queue: Queue? = nil, _ s1: Signal<T1, E>, _ s2: Signal<T2, E>, _ s3: Signal<T3, E>, _ s4: Signal<T4, E>, _ s5: Signal<T5, E>, _ s6: Signal<T6, E>, _ s7: Signal<T7, E>, _ s8: Signal<T8, E>, _ s9: Signal<T9, E>, _ s10: Signal<T10, E>, _ s11: Signal<T11, E>, _ s12: Signal<T12, E>, _ s13: Signal<T13, E>, _ s14: Signal<T14, E>, _ s15: Signal<T15, E>, _ s16: Signal<T16, E>) -> 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<T, E>(queue: Queue? = nil, _ signals: [Signal<T, E>]) -> Signal<[T], E> {
if signals.count == 0 {
return single([T](), E.self)

View file

@ -91,6 +91,8 @@ swift_library(
"//submodules/UIKitRuntimeUtils:UIKitRuntimeUtils",
"//submodules/DebugSettingsUI:DebugSettingsUI",
"//submodules/WallpaperBackgroundNode:WallpaperBackgroundNode",
"//submodules/WebPBinding:WebPBinding",
"//submodules/Translate:Translate",
],
visibility = [
"//visibility:public",

View file

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

View file

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

View file

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

View file

@ -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<Bool>()
@ -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<LocalizationListState?>(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<String>()
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()

View file

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

View file

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

View file

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

View file

@ -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<PresentationData, NoError>)? = 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
}

View file

@ -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<Void, NoError>?, (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<PeerId, Peer>()
let messages = SimpleDictionary<MessageId, Message>()
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)
}
}

View file

@ -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<ItemCollectionId>()
@ -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<UIImage?, NoError>
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<UIImage?, NoError> 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 {

View file

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

View file

@ -1043,7 +1043,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate
return state
}, animated: true)
}, clickThroughMessage: {
}, backgroundNode: self.backgroundNode)
}, backgroundNode: self.backgroundNode, availableReactions: nil)
return item
}

View file

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

View file

@ -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] = []

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4591,6 +4591,20 @@ public extension Api {
return result
})
}
public static func setDefaultReaction(emoji: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
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<Api.Bool>) {

View file

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

View file

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

View file

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

View file

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

View file

@ -173,7 +173,8 @@ public func accountWithId(accountManager: AccountManager<TelegramAccountManagerT
isTemporary: false,
isReadOnly: false,
useCopy: false,
useCaches: !supplementary
useCaches: !supplementary,
removeDatabaseOnError: !supplementary
)
return postbox
@ -1295,7 +1296,8 @@ public func standaloneStateManager(
isTemporary: false,
isReadOnly: false,
useCopy: false,
useCaches: false
useCaches: false,
removeDatabaseOnError: false
)
return postbox

View file

@ -73,7 +73,7 @@ final class AccountManagerImpl<Types: AccountManagerTypes> {
}
}
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<Types: AccountManagerTypes> {
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<Types: AccountManagerTypes> {
return AccountManagerImpl<Types>.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<Types: AccountManagerTypes> {
self.temporarySessionId = temporarySessionId
let queue = self.queue
self.impl = QueueLocalObject(queue: queue, generate: {
if let value = AccountManagerImpl<Types>(queue: queue, basePath: basePath, isTemporary: isTemporary, isReadOnly: isReadOnly, useCaches: useCaches, temporarySessionId: temporarySessionId) {
if let value = AccountManagerImpl<Types>(queue: queue, basePath: basePath, isTemporary: isTemporary, isReadOnly: isReadOnly, useCaches: useCaches, removeDatabaseOnError: removeDatabaseOnError, temporarySessionId: temporarySessionId) {
return value
} else {
preconditionFailure()

View file

@ -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<Never, NoError> {
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
}

View file

@ -812,13 +812,13 @@ public final class AccountViewTracker {
}
}
public func updateReactionsForMessageIds(messageIds: Set<MessageId>) {
public func updateReactionsForMessageIds(messageIds: Set<MessageId>, 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)
}

View file

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

View file

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

View file

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

View file

@ -97,9 +97,9 @@ public enum AccountTransactionError {
case couldNotOpen
}
public func accountTransaction<T>(rootPath: String, id: AccountRecordId, encryptionParameters: ValueBoxEncryptionParameters, isReadOnly: Bool, useCopy: Bool = false, useCaches: Bool = true, transaction: @escaping (Postbox, Transaction) -> T) -> Signal<T, AccountTransactionError> {
public func accountTransaction<T>(rootPath: String, id: AccountRecordId, encryptionParameters: ValueBoxEncryptionParameters, isReadOnly: Bool, useCopy: Bool = false, useCaches: Bool = true, removeDatabaseOnError: Bool = true, transaction: @escaping (Postbox, Transaction) -> T) -> Signal<T, AccountTransactionError> {
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<T, AccountTransactionError> in

View file

@ -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<State, NoError> 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)
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "Avatar8.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "translate_24.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

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

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "reactions_30.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

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

View file

@ -705,7 +705,7 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
UINavigationController.attemptRotationToDeviceOrientation()
})
let accountManager = AccountManager<TelegramAccountManagerTypes>(basePath: rootPath + "/accounts-metadata", isTemporary: false, isReadOnly: false, useCaches: true)
let accountManager = AccountManager<TelegramAccountManagerTypes>(basePath: rootPath + "/accounts-metadata", isTemporary: false, isReadOnly: false, useCaches: true, removeDatabaseOnError: true)
self.accountManager = accountManager
telegramUIDeclareEncodables()

View file

@ -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<String>? 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<String>)
case all
}
func peerAllowedReactions(context: AccountContext, peerId: PeerId) -> Signal<AllowedReactions?, NoError> {
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
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

Some files were not shown because too many files have changed in this diff Show more