mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-06 03:33:41 +02:00
Merge commit '3db9593f49' into temp
# Conflicts: # submodules/TelegramCore/Sources/Utils/MessageUtils.swift
This commit is contained in:
commit
840e7c807a
151 changed files with 7335 additions and 1505 deletions
|
|
@ -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)!)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
BIN
Telegram/Telegram-iOS/Resources/PlaneLogoPlain.tgs
Normal file
BIN
Telegram/Telegram-iOS/Resources/PlaneLogoPlain.tgs
Normal file
Binary file not shown.
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ swift_library(
|
|||
"//submodules/TelegramStringFormatting:TelegramStringFormatting",
|
||||
"//submodules/TelegramCallsUI:TelegramCallsUI",
|
||||
"//submodules/StickerResources:StickerResources",
|
||||
"//submodules/TextFormat:TextFormat",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
26
submodules/Display/Source/SimpleLayer.swift
Normal file
26
submodules/Display/Source/SimpleLayer.swift
Normal 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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ swift_library(
|
|||
"//submodules/Speak:Speak",
|
||||
"//submodules/UndoUI:UndoUI",
|
||||
"//submodules/InvisibleInkDustNode:InvisibleInkDustNode",
|
||||
"//submodules/Translate:Translate",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -91,6 +91,8 @@ swift_library(
|
|||
"//submodules/UIKitRuntimeUtils:UIKitRuntimeUtils",
|
||||
"//submodules/DebugSettingsUI:DebugSettingsUI",
|
||||
"//submodules/WallpaperBackgroundNode:WallpaperBackgroundNode",
|
||||
"//submodules/WebPBinding:WebPBinding",
|
||||
"//submodules/Translate:Translate",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -1043,7 +1043,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate
|
|||
return state
|
||||
}, animated: true)
|
||||
}, clickThroughMessage: {
|
||||
}, backgroundNode: self.backgroundNode)
|
||||
}, backgroundNode: self.backgroundNode, availableReactions: nil)
|
||||
return item
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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] = []
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>) {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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? {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
BIN
submodules/TelegramUI/Images.xcassets/Avatar/SampleAvatar1.imageset/Avatar8.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Avatar/SampleAvatar1.imageset/Avatar8.pdf
vendored
Normal file
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Avatar/SampleAvatar1.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Avatar/SampleAvatar1.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Avatar8.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
12
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Translate.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Translate.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "translate_24.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
127
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Translate.imageset/translate_24.pdf
vendored
Normal file
127
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Translate.imageset/translate_24.pdf
vendored
Normal 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
|
||||
12
submodules/TelegramUI/Images.xcassets/Settings/Menu/Reactions.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Settings/Menu/Reactions.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "reactions_30.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
180
submodules/TelegramUI/Images.xcassets/Settings/Menu/Reactions.imageset/reactions_30.pdf
vendored
Normal file
180
submodules/TelegramUI/Images.xcassets/Settings/Menu/Reactions.imageset/reactions_30.pdf
vendored
Normal 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
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue