mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2026-04-09 06:32:01 +00:00
Fixes
fix localeWithStrings globally (#30)
Fix badge on zoomed devices. closes #9
Hide channel bottom panel closes #27
Another attempt to fix badge on some Zoomed devices
Force System Share sheet tg://sg/debug
fixes for device badge
New Crowdin updates (#34)
* New translations sglocalizable.strings (Chinese Traditional)
* New translations sglocalizable.strings (Chinese Simplified)
* New translations sglocalizable.strings (Chinese Traditional)
Fix input panel hidden on selection (#31)
* added if check for selectionState != nil
* same order of subnodes
Revert "Fix input panel hidden on selection (#31)"
This reverts commit e8a8bb1496.
Fix input panel for channels Closes #37
Quickly share links with system's share menu
force tabbar when editing
increase height for correct animation
New translations sglocalizable.strings (Ukrainian) (#38)
Hide Post Story button
Fix 10.15.1
Fix archive option for long-tap
Enable in-app Safari
Disable some unsupported purchases
disableDeleteChatSwipeOption + refactor restart alert
Hide bot in suggestions list
Fix merge v11.0
Fix exceptions for safari webview controller
New Crowdin updates (#47)
* New translations sglocalizable.strings (Romanian)
* New translations sglocalizable.strings (French)
* New translations sglocalizable.strings (Spanish)
* New translations sglocalizable.strings (Afrikaans)
* New translations sglocalizable.strings (Arabic)
* New translations sglocalizable.strings (Catalan)
* New translations sglocalizable.strings (Czech)
* New translations sglocalizable.strings (Danish)
* New translations sglocalizable.strings (German)
* New translations sglocalizable.strings (Greek)
* New translations sglocalizable.strings (Finnish)
* New translations sglocalizable.strings (Hebrew)
* New translations sglocalizable.strings (Hungarian)
* New translations sglocalizable.strings (Italian)
* New translations sglocalizable.strings (Japanese)
* New translations sglocalizable.strings (Korean)
* New translations sglocalizable.strings (Dutch)
* New translations sglocalizable.strings (Norwegian)
* New translations sglocalizable.strings (Polish)
* New translations sglocalizable.strings (Portuguese)
* New translations sglocalizable.strings (Serbian (Cyrillic))
* New translations sglocalizable.strings (Swedish)
* New translations sglocalizable.strings (Turkish)
* New translations sglocalizable.strings (Vietnamese)
* New translations sglocalizable.strings (Indonesian)
* New translations sglocalizable.strings (Hindi)
* New translations sglocalizable.strings (Uzbek)
New Crowdin updates (#49)
* New translations sglocalizable.strings (Arabic)
* New translations sglocalizable.strings (Arabic)
New translations sglocalizable.strings (Russian) (#51)
Call confirmation
WIP Settings search
Settings Search
Localize placeholder
Update AccountUtils.swift
mark mutual contact
Align back context action to left
New Crowdin updates (#54)
* New translations sglocalizable.strings (Chinese Simplified)
* New translations sglocalizable.strings (Chinese Traditional)
* New translations sglocalizable.strings (Ukrainian)
Independent Playground app for simulator
New translations sglocalizable.strings (Ukrainian) (#55)
Playground UIKit base and controllers
Inject SwiftUI view with overflow to AsyncDisplayKit
Launch Playgound project on simulator
Create .swiftformat
Move Playground to example
Update .swiftformat
Init SwiftUIViewController
wip
New translations sglocalizable.strings (Chinese Traditional) (#57)
Xcode 16 fixes
Fix
New translations sglocalizable.strings (Italian) (#59)
New translations sglocalizable.strings (Chinese Simplified) (#63)
Force disable CallKit integration due to missing NSE Entitlement
Fix merge
Fix whole chat translator
Sweetpad config
Bump version
11.3.1 fixes
Mutual contact placement fix
Disable Video PIP swipe
Update versions.json
Fix PIP crash
1307 lines
71 KiB
Swift
1307 lines
71 KiB
Swift
import SGSimpleSettings
|
|
import Foundation
|
|
import UIKit
|
|
import AsyncDisplayKit
|
|
import Postbox
|
|
import TelegramCore
|
|
import Display
|
|
import SwiftSignalKit
|
|
import TelegramPresentationData
|
|
import AccountContext
|
|
import AppBundle
|
|
import ReactionButtonListComponent
|
|
import ReactionImageComponent
|
|
import AnimationCache
|
|
import MultiAnimationRenderer
|
|
|
|
private func maybeAddRotationAnimation(_ layer: CALayer, duration: Double) {
|
|
if let _ = layer.animation(forKey: "clockFrameAnimation") {
|
|
return
|
|
}
|
|
|
|
let basicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
|
|
basicAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
|
|
basicAnimation.duration = duration
|
|
basicAnimation.fromValue = NSNumber(value: Float(0.0))
|
|
basicAnimation.toValue = NSNumber(value: Float(Double.pi * 2.0))
|
|
basicAnimation.repeatCount = Float.infinity
|
|
basicAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
|
|
basicAnimation.beginTime = 1.0
|
|
layer.add(basicAnimation, forKey: "clockFrameAnimation")
|
|
}
|
|
|
|
public enum ChatMessageDateAndStatusOutgoingType: Equatable {
|
|
case Sent(read: Bool)
|
|
case Sending
|
|
case Failed
|
|
}
|
|
|
|
public enum ChatMessageDateAndStatusType: Equatable {
|
|
case BubbleIncoming
|
|
case BubbleOutgoing(ChatMessageDateAndStatusOutgoingType)
|
|
case ImageIncoming
|
|
case ImageOutgoing(ChatMessageDateAndStatusOutgoingType)
|
|
case FreeIncoming
|
|
case FreeOutgoing(ChatMessageDateAndStatusOutgoingType)
|
|
}
|
|
|
|
private let reactionCountFont = Font.semibold(11.0)
|
|
private let reactionFont = Font.regular(12.0)
|
|
|
|
private final class StatusReactionNode: ASDisplayNode {
|
|
let iconView: ReactionIconView
|
|
|
|
private let iconImageDisposable = MetaDisposable()
|
|
|
|
private var theme: PresentationTheme?
|
|
private var value: MessageReaction.Reaction?
|
|
private var isSelected: Bool?
|
|
|
|
private var resolvedFile: TelegramMediaFile?
|
|
private var fileDisposable: Disposable?
|
|
|
|
private var alternativeTextView: ImmediateTextView?
|
|
|
|
override init() {
|
|
self.iconView = ReactionIconView()
|
|
|
|
super.init()
|
|
|
|
self.view.addSubview(self.iconView)
|
|
}
|
|
|
|
deinit {
|
|
self.iconImageDisposable.dispose()
|
|
self.fileDisposable?.dispose()
|
|
}
|
|
|
|
func update(context: AccountContext, type: ChatMessageDateAndStatusType, value: MessageReaction.Reaction, file: TelegramMediaFile?, fileId: Int64?, alternativeText: String, isSelected: Bool, count: Int, theme: PresentationTheme, wallpaper: TelegramWallpaper, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, animated: Bool) {
|
|
if self.value != value {
|
|
self.value = value
|
|
|
|
let boundingImageSize = CGSize(width: 8.0, height: 8.0)
|
|
|
|
let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((boundingImageSize.width - boundingImageSize.width) / 2.0), y: floorToScreenPixels((boundingImageSize.height - boundingImageSize.height) / 2.0)), size: boundingImageSize)
|
|
self.iconView.frame = iconFrame
|
|
if let fileId = fileId ?? file?.fileId.id {
|
|
let animateIdle: Bool
|
|
if case .custom = value {
|
|
animateIdle = true
|
|
} else {
|
|
animateIdle = false
|
|
}
|
|
|
|
let placeholderColor: UIColor
|
|
switch type {
|
|
case .BubbleIncoming:
|
|
placeholderColor = theme.chat.message.incoming.mediaPlaceholderColor
|
|
case .BubbleOutgoing:
|
|
placeholderColor = theme.chat.message.incoming.mediaPlaceholderColor
|
|
case .ImageIncoming:
|
|
placeholderColor = UIColor(white: 1.0, alpha: 0.1)
|
|
case .ImageOutgoing:
|
|
placeholderColor = UIColor(white: 1.0, alpha: 0.1)
|
|
case .FreeIncoming:
|
|
placeholderColor = UIColor(white: 0.0, alpha: 0.1)
|
|
case .FreeOutgoing:
|
|
placeholderColor = UIColor(white: 0.0, alpha: 0.1)
|
|
}
|
|
|
|
self.iconView.update(
|
|
size: boundingImageSize,
|
|
context: context,
|
|
file: file,
|
|
fileId: fileId,
|
|
animationCache: animationCache,
|
|
animationRenderer: animationRenderer,
|
|
tintColor: nil,
|
|
placeholderColor: placeholderColor,
|
|
animateIdle: animateIdle,
|
|
reaction: value,
|
|
transition: .immediate
|
|
)
|
|
if let alternativeTextView = self.alternativeTextView {
|
|
self.alternativeTextView = nil
|
|
alternativeTextView.removeFromSuperview()
|
|
}
|
|
} else {
|
|
let alternativeTextView: ImmediateTextView
|
|
if let current = self.alternativeTextView {
|
|
alternativeTextView = current
|
|
} else {
|
|
alternativeTextView = ImmediateTextView()
|
|
alternativeTextView.insets = UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0)
|
|
self.view.addSubview(alternativeTextView)
|
|
}
|
|
alternativeTextView.attributedText = NSAttributedString(string: alternativeText, font: Font.regular(10.0), textColor: .black)
|
|
let alternativeTextSize = alternativeTextView.updateLayout(CGSize(width: 100.0, height: 100.0))
|
|
alternativeTextView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((boundingImageSize.width - alternativeTextSize.width) / 2.0), y: floorToScreenPixels((boundingImageSize.height - alternativeTextSize.height) / 2.0)), size: alternativeTextSize)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public class ChatMessageDateAndStatusNode: ASDisplayNode {
|
|
public struct TrailingReactionSettings {
|
|
public var displayInline: Bool
|
|
public var preferAdditionalInset: Bool
|
|
|
|
public init(displayInline: Bool, preferAdditionalInset: Bool) {
|
|
self.displayInline = displayInline
|
|
self.preferAdditionalInset = preferAdditionalInset
|
|
}
|
|
}
|
|
|
|
public struct StandaloneReactionSettings {
|
|
public init() {
|
|
}
|
|
}
|
|
|
|
public enum LayoutInput {
|
|
case trailingContent(contentWidth: CGFloat?, reactionSettings: TrailingReactionSettings?)
|
|
case standalone(reactionSettings: StandaloneReactionSettings?)
|
|
|
|
public var displayInlineReactions: Bool {
|
|
switch self {
|
|
case let .trailingContent(_, reactionSettings):
|
|
if let reactionSettings = reactionSettings {
|
|
return reactionSettings.displayInline
|
|
} else {
|
|
return false
|
|
}
|
|
case let .standalone(reactionSettings):
|
|
if let _ = reactionSettings {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public struct Arguments {
|
|
var context: AccountContext
|
|
var presentationData: ChatPresentationData
|
|
var edited: Bool
|
|
var impressionCount: Int?
|
|
var dateText: String
|
|
var type: ChatMessageDateAndStatusType
|
|
var layoutInput: LayoutInput
|
|
var constrainedSize: CGSize
|
|
var availableReactions: AvailableReactions?
|
|
var savedMessageTags: SavedMessageTags?
|
|
var reactions: [MessageReaction]
|
|
var reactionPeers: [(MessageReaction.Reaction, EnginePeer)]
|
|
var displayAllReactionPeers: Bool
|
|
var areReactionsTags: Bool
|
|
var messageEffect: AvailableMessageEffects.MessageEffect?
|
|
var replyCount: Int
|
|
var isPinned: Bool
|
|
var hasAutoremove: Bool
|
|
var canViewReactionList: Bool
|
|
var animationCache: AnimationCache
|
|
var animationRenderer: MultiAnimationRenderer
|
|
|
|
public init(
|
|
context: AccountContext,
|
|
presentationData: ChatPresentationData,
|
|
edited: Bool,
|
|
impressionCount: Int?,
|
|
dateText: String,
|
|
type: ChatMessageDateAndStatusType,
|
|
layoutInput: LayoutInput,
|
|
constrainedSize: CGSize,
|
|
availableReactions: AvailableReactions?,
|
|
savedMessageTags: SavedMessageTags?,
|
|
reactions: [MessageReaction],
|
|
reactionPeers: [(MessageReaction.Reaction, EnginePeer)],
|
|
displayAllReactionPeers: Bool,
|
|
areReactionsTags: Bool,
|
|
messageEffect: AvailableMessageEffects.MessageEffect?,
|
|
replyCount: Int,
|
|
isPinned: Bool,
|
|
hasAutoremove: Bool,
|
|
canViewReactionList: Bool,
|
|
animationCache: AnimationCache,
|
|
animationRenderer: MultiAnimationRenderer
|
|
) {
|
|
self.context = context
|
|
self.presentationData = presentationData
|
|
self.edited = edited
|
|
self.impressionCount = impressionCount == 0 ? nil : impressionCount
|
|
self.dateText = dateText
|
|
self.type = type
|
|
self.layoutInput = layoutInput
|
|
self.availableReactions = availableReactions
|
|
self.savedMessageTags = savedMessageTags
|
|
self.constrainedSize = constrainedSize
|
|
self.reactions = reactions
|
|
self.reactionPeers = reactionPeers
|
|
self.displayAllReactionPeers = displayAllReactionPeers
|
|
self.areReactionsTags = areReactionsTags
|
|
self.messageEffect = messageEffect
|
|
self.replyCount = replyCount
|
|
self.isPinned = isPinned
|
|
self.hasAutoremove = hasAutoremove
|
|
self.canViewReactionList = canViewReactionList
|
|
self.animationCache = animationCache
|
|
self.animationRenderer = animationRenderer
|
|
}
|
|
}
|
|
|
|
private var backgroundNode: ASImageNode?
|
|
private var blurredBackgroundNode: NavigationBackgroundNode?
|
|
private var checkSentNode: ASImageNode?
|
|
private var checkReadNode: ASImageNode?
|
|
private var clockFrameNode: ASImageNode?
|
|
private var clockMinNode: ASImageNode?
|
|
private let dateNode: TextNode
|
|
private var impressionIcon: ASImageNode?
|
|
private var reactionNodes: [MessageReaction.Reaction: StatusReactionNode] = [:]
|
|
private let reactionButtonsContainer = ReactionButtonsAsyncLayoutContainer()
|
|
private var reactionButtonNode: HighlightTrackingButtonNode?
|
|
private var repliesIcon: ASImageNode?
|
|
private var selfExpiringIcon: ASImageNode?
|
|
private var replyCountNode: TextNode?
|
|
|
|
private var type: ChatMessageDateAndStatusType?
|
|
private var theme: ChatPresentationThemeData?
|
|
private var layoutSize: CGSize?
|
|
|
|
private var tapGestureRecognizer: UITapGestureRecognizer?
|
|
|
|
public var openReplies: (() -> Void)?
|
|
public var pressed: (() -> Void)? {
|
|
didSet {
|
|
if self.pressed != nil {
|
|
if self.tapGestureRecognizer == nil {
|
|
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
|
|
self.tapGestureRecognizer = tapGestureRecognizer
|
|
self.view.addGestureRecognizer(tapGestureRecognizer)
|
|
}
|
|
} else if let tapGestureRecognizer = self.tapGestureRecognizer{
|
|
self.tapGestureRecognizer = nil
|
|
self.view.removeGestureRecognizer(tapGestureRecognizer)
|
|
}
|
|
}
|
|
}
|
|
public var reactionSelected: ((ReactionButtonAsyncNode, MessageReaction.Reaction, ContextExtractedContentContainingView?) -> Void)?
|
|
public var openReactionPreview: ((ContextGesture?, ContextExtractedContentContainingView, MessageReaction.Reaction) -> Void)?
|
|
|
|
override public init() {
|
|
self.dateNode = TextNode()
|
|
self.dateNode.isUserInteractionEnabled = false
|
|
self.dateNode.displaysAsynchronously = false
|
|
self.dateNode.contentsScale = UIScreenScale
|
|
self.dateNode.contentMode = .topLeft
|
|
|
|
super.init()
|
|
|
|
self.addSubnode(self.dateNode)
|
|
}
|
|
|
|
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
|
if case .ended = recognizer.state {
|
|
self.pressed?()
|
|
}
|
|
}
|
|
|
|
public func asyncLayout() -> (_ arguments: Arguments) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void)) {
|
|
let dateLayout = TextNode.asyncLayout(self.dateNode)
|
|
|
|
var checkReadNode = self.checkReadNode
|
|
var checkSentNode = self.checkSentNode
|
|
var clockFrameNode = self.clockFrameNode
|
|
var clockMinNode = self.clockMinNode
|
|
|
|
var currentBackgroundNode = self.backgroundNode
|
|
var currentImpressionIcon = self.impressionIcon
|
|
var currentRepliesIcon = self.repliesIcon
|
|
|
|
let currentType = self.type
|
|
let currentTheme = self.theme
|
|
|
|
let makeReplyCountLayout = TextNode.asyncLayout(self.replyCountNode)
|
|
|
|
let reactionButtonsContainer = self.reactionButtonsContainer
|
|
|
|
return { [weak self] arguments in
|
|
let dateColor: UIColor
|
|
var backgroundImage: UIImage?
|
|
var blurredBackgroundColor: (UIColor, Bool)?
|
|
var outgoingStatus: ChatMessageDateAndStatusOutgoingType?
|
|
var leftInset: CGFloat
|
|
|
|
let loadedCheckFullImage: UIImage?
|
|
let loadedCheckPartialImage: UIImage?
|
|
let clockFrameImage: UIImage?
|
|
let clockMinImage: UIImage?
|
|
var impressionImage: UIImage?
|
|
var repliesImage: UIImage?
|
|
|
|
let themeUpdated = arguments.presentationData.theme != currentTheme || arguments.type != currentType
|
|
|
|
let graphics = PresentationResourcesChat.principalGraphics(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper, bubbleCorners: arguments.presentationData.chatBubbleCorners)
|
|
let isDefaultWallpaper = serviceMessageColorHasDefaultWallpaper(arguments.presentationData.theme.wallpaper)
|
|
let offset: CGFloat = -UIScreenPixel
|
|
|
|
let checkSize: CGFloat = floor(floor(arguments.presentationData.fontSize.baseDisplaySize * 11.0 / 17.0))
|
|
|
|
let reactionColors: ReactionButtonComponent.Colors
|
|
switch arguments.type {
|
|
case .BubbleIncoming, .ImageIncoming, .FreeIncoming:
|
|
let themeColors = bubbleColorComponents(theme: arguments.presentationData.theme.theme, incoming: true, wallpaper: !arguments.presentationData.theme.wallpaper.isEmpty)
|
|
|
|
reactionColors = ReactionButtonComponent.Colors(
|
|
deselectedBackground: themeColors.reactionInactiveBackground.argb,
|
|
selectedBackground: themeColors.reactionActiveBackground.argb,
|
|
deselectedForeground: themeColors.reactionInactiveForeground.argb,
|
|
selectedForeground: themeColors.reactionActiveForeground.argb,
|
|
deselectedStarsBackground: themeColors.reactionStarsInactiveBackground.argb,
|
|
selectedStarsBackground: themeColors.reactionStarsActiveBackground.argb,
|
|
deselectedStarsForeground: themeColors.reactionStarsInactiveForeground.argb,
|
|
selectedStarsForeground: themeColors.reactionStarsActiveForeground.argb,
|
|
extractedBackground: arguments.presentationData.theme.theme.contextMenu.backgroundColor.argb,
|
|
extractedForeground: arguments.presentationData.theme.theme.contextMenu.primaryColor.argb,
|
|
extractedSelectedForeground: arguments.presentationData.theme.theme.overallDarkAppearance ? themeColors.reactionActiveForeground.argb : arguments.presentationData.theme.theme.contextMenu.primaryColor.argb,
|
|
deselectedMediaPlaceholder: themeColors.reactionInactiveMediaPlaceholder.argb,
|
|
selectedMediaPlaceholder: themeColors.reactionActiveMediaPlaceholder.argb
|
|
)
|
|
case .BubbleOutgoing, .ImageOutgoing, .FreeOutgoing:
|
|
let themeColors = bubbleColorComponents(theme: arguments.presentationData.theme.theme, incoming: false, wallpaper: !arguments.presentationData.theme.wallpaper.isEmpty)
|
|
|
|
reactionColors = ReactionButtonComponent.Colors(
|
|
deselectedBackground: themeColors.reactionInactiveBackground.argb,
|
|
selectedBackground: themeColors.reactionActiveBackground.argb,
|
|
deselectedForeground: themeColors.reactionInactiveForeground.argb,
|
|
selectedForeground: themeColors.reactionActiveForeground.argb,
|
|
deselectedStarsBackground: themeColors.reactionStarsInactiveBackground.argb,
|
|
selectedStarsBackground: themeColors.reactionStarsActiveBackground.argb,
|
|
deselectedStarsForeground: themeColors.reactionStarsInactiveForeground.argb,
|
|
selectedStarsForeground: themeColors.reactionStarsActiveForeground.argb,
|
|
extractedBackground: arguments.presentationData.theme.theme.contextMenu.backgroundColor.argb,
|
|
extractedForeground: arguments.presentationData.theme.theme.contextMenu.primaryColor.argb,
|
|
extractedSelectedForeground: arguments.presentationData.theme.theme.overallDarkAppearance ? themeColors.reactionActiveForeground.argb : arguments.presentationData.theme.theme.list.itemCheckColors.foregroundColor.argb,
|
|
deselectedMediaPlaceholder: themeColors.reactionInactiveMediaPlaceholder.argb,
|
|
selectedMediaPlaceholder: themeColors.reactionActiveMediaPlaceholder.argb
|
|
)
|
|
}
|
|
|
|
switch arguments.type {
|
|
case .BubbleIncoming:
|
|
dateColor = arguments.presentationData.theme.theme.chat.message.incoming.secondaryTextColor
|
|
leftInset = 5.0
|
|
loadedCheckFullImage = PresentationResourcesChat.chatOutgoingFullCheck(arguments.presentationData.theme.theme, size: checkSize)
|
|
loadedCheckPartialImage = PresentationResourcesChat.chatOutgoingPartialCheck(arguments.presentationData.theme.theme, size: checkSize)
|
|
clockFrameImage = graphics.clockBubbleIncomingFrameImage
|
|
clockMinImage = graphics.clockBubbleIncomingMinImage
|
|
if arguments.impressionCount != nil {
|
|
impressionImage = graphics.incomingDateAndStatusImpressionIcon
|
|
}
|
|
if arguments.replyCount != 0 {
|
|
repliesImage = graphics.incomingDateAndStatusRepliesIcon
|
|
} else if arguments.isPinned {
|
|
repliesImage = graphics.incomingDateAndStatusPinnedIcon
|
|
}
|
|
case let .BubbleOutgoing(status):
|
|
dateColor = arguments.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor
|
|
outgoingStatus = status
|
|
leftInset = 5.0
|
|
loadedCheckFullImage = PresentationResourcesChat.chatOutgoingFullCheck(arguments.presentationData.theme.theme, size: checkSize)
|
|
loadedCheckPartialImage = PresentationResourcesChat.chatOutgoingPartialCheck(arguments.presentationData.theme.theme, size: checkSize)
|
|
clockFrameImage = graphics.clockBubbleOutgoingFrameImage
|
|
clockMinImage = graphics.clockBubbleOutgoingMinImage
|
|
if arguments.impressionCount != nil {
|
|
impressionImage = graphics.outgoingDateAndStatusImpressionIcon
|
|
}
|
|
if arguments.replyCount != 0 {
|
|
repliesImage = graphics.outgoingDateAndStatusRepliesIcon
|
|
} else if arguments.isPinned {
|
|
repliesImage = graphics.outgoingDateAndStatusPinnedIcon
|
|
}
|
|
case .ImageIncoming:
|
|
dateColor = arguments.presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor
|
|
backgroundImage = graphics.dateAndStatusMediaBackground
|
|
leftInset = 0.0
|
|
loadedCheckFullImage = PresentationResourcesChat.chatMediaFullCheck(arguments.presentationData.theme.theme, size: checkSize)
|
|
loadedCheckPartialImage = PresentationResourcesChat.chatMediaPartialCheck(arguments.presentationData.theme.theme, size: checkSize)
|
|
clockFrameImage = graphics.clockMediaFrameImage
|
|
clockMinImage = graphics.clockMediaMinImage
|
|
if arguments.impressionCount != nil {
|
|
impressionImage = graphics.mediaImpressionIcon
|
|
}
|
|
if arguments.replyCount != 0 {
|
|
repliesImage = graphics.mediaRepliesIcon
|
|
} else if arguments.isPinned {
|
|
repliesImage = graphics.mediaPinnedIcon
|
|
}
|
|
case let .ImageOutgoing(status):
|
|
dateColor = arguments.presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor
|
|
outgoingStatus = status
|
|
backgroundImage = graphics.dateAndStatusMediaBackground
|
|
leftInset = 0.0
|
|
loadedCheckFullImage = PresentationResourcesChat.chatMediaFullCheck(arguments.presentationData.theme.theme, size: checkSize)
|
|
loadedCheckPartialImage = PresentationResourcesChat.chatMediaPartialCheck(arguments.presentationData.theme.theme, size: checkSize)
|
|
clockFrameImage = graphics.clockMediaFrameImage
|
|
clockMinImage = graphics.clockMediaMinImage
|
|
if arguments.impressionCount != nil {
|
|
impressionImage = graphics.mediaImpressionIcon
|
|
}
|
|
if arguments.replyCount != 0 {
|
|
repliesImage = graphics.mediaRepliesIcon
|
|
} else if arguments.isPinned {
|
|
repliesImage = graphics.mediaPinnedIcon
|
|
}
|
|
case .FreeIncoming:
|
|
let serviceColor = serviceMessageColorComponents(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper)
|
|
dateColor = serviceColor.primaryText
|
|
|
|
blurredBackgroundColor = (selectDateFillStaticColor(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper), arguments.context.sharedContext.energyUsageSettings.fullTranslucency && dateFillNeedsBlur(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper))
|
|
leftInset = 0.0
|
|
loadedCheckFullImage = PresentationResourcesChat.chatFreeFullCheck(arguments.presentationData.theme.theme, size: checkSize, isDefaultWallpaper: isDefaultWallpaper)
|
|
loadedCheckPartialImage = PresentationResourcesChat.chatFreePartialCheck(arguments.presentationData.theme.theme, size: checkSize, isDefaultWallpaper: isDefaultWallpaper)
|
|
clockFrameImage = graphics.clockFreeFrameImage
|
|
clockMinImage = graphics.clockFreeMinImage
|
|
if arguments.impressionCount != nil {
|
|
impressionImage = graphics.freeImpressionIcon
|
|
}
|
|
if arguments.replyCount != 0 {
|
|
repliesImage = graphics.freeRepliesIcon
|
|
} else if arguments.isPinned {
|
|
repliesImage = graphics.freePinnedIcon
|
|
}
|
|
case let .FreeOutgoing(status):
|
|
let serviceColor = serviceMessageColorComponents(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper)
|
|
dateColor = serviceColor.primaryText
|
|
outgoingStatus = status
|
|
blurredBackgroundColor = (selectDateFillStaticColor(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper), arguments.context.sharedContext.energyUsageSettings.fullTranslucency && dateFillNeedsBlur(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper))
|
|
leftInset = 0.0
|
|
loadedCheckFullImage = PresentationResourcesChat.chatFreeFullCheck(arguments.presentationData.theme.theme, size: checkSize, isDefaultWallpaper: isDefaultWallpaper)
|
|
loadedCheckPartialImage = PresentationResourcesChat.chatFreePartialCheck(arguments.presentationData.theme.theme, size: checkSize, isDefaultWallpaper: isDefaultWallpaper)
|
|
clockFrameImage = graphics.clockFreeFrameImage
|
|
clockMinImage = graphics.clockFreeMinImage
|
|
if arguments.impressionCount != nil {
|
|
impressionImage = graphics.freeImpressionIcon
|
|
}
|
|
if arguments.replyCount != 0 {
|
|
repliesImage = graphics.freeRepliesIcon
|
|
} else if arguments.isPinned {
|
|
repliesImage = graphics.freePinnedIcon
|
|
}
|
|
}
|
|
|
|
var updatedDateText = arguments.dateText
|
|
if arguments.edited {
|
|
updatedDateText = "\(arguments.presentationData.strings.Conversation_MessageEditedLabel) \(updatedDateText)"
|
|
}
|
|
if let impressionCount = arguments.impressionCount {
|
|
updatedDateText = compactNumericCountString(impressionCount, decimalSeparator: arguments.presentationData.dateTimeFormat.decimalSeparator) + " " + updatedDateText
|
|
}
|
|
|
|
let dateFont = Font.regular(floor(arguments.presentationData.fontSize.baseDisplaySize * 11.0 / 17.0))
|
|
let (date, dateApply) = dateLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: updatedDateText, font: dateFont, textColor: dateColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: arguments.constrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
|
|
|
let checkOffset = floor(arguments.presentationData.fontSize.baseDisplaySize * 6.0 / 17.0)
|
|
|
|
let statusWidth: CGFloat
|
|
|
|
var checkSentFrame: CGRect?
|
|
var checkReadFrame: CGRect?
|
|
|
|
var clockPosition = CGPoint()
|
|
|
|
var impressionSize = CGSize()
|
|
var impressionWidth: CGFloat = 0.0
|
|
if let impressionImage = impressionImage {
|
|
if currentImpressionIcon == nil {
|
|
let iconNode = ASImageNode()
|
|
iconNode.isLayerBacked = true
|
|
iconNode.displayWithoutProcessing = true
|
|
iconNode.displaysAsynchronously = false
|
|
currentImpressionIcon = iconNode
|
|
}
|
|
impressionSize = impressionImage.size
|
|
impressionWidth = impressionSize.width + 3.0
|
|
} else {
|
|
currentImpressionIcon = nil
|
|
}
|
|
|
|
var repliesIconSize = CGSize()
|
|
if let repliesImage = repliesImage {
|
|
if currentRepliesIcon == nil {
|
|
let iconNode = ASImageNode()
|
|
iconNode.isLayerBacked = true
|
|
iconNode.displayWithoutProcessing = true
|
|
iconNode.displaysAsynchronously = false
|
|
currentRepliesIcon = iconNode
|
|
}
|
|
repliesIconSize = repliesImage.size
|
|
} else {
|
|
currentRepliesIcon = nil
|
|
}
|
|
|
|
if let outgoingStatus = outgoingStatus {
|
|
switch outgoingStatus {
|
|
case .Sending:
|
|
statusWidth = floor(floor(arguments.presentationData.fontSize.baseDisplaySize * 13.0 / 17.0))
|
|
|
|
if checkReadNode == nil {
|
|
checkReadNode = ASImageNode()
|
|
checkReadNode?.isLayerBacked = true
|
|
checkReadNode?.displaysAsynchronously = false
|
|
checkReadNode?.displayWithoutProcessing = true
|
|
}
|
|
|
|
if checkSentNode == nil {
|
|
checkSentNode = ASImageNode()
|
|
checkSentNode?.isLayerBacked = true
|
|
checkSentNode?.displaysAsynchronously = false
|
|
checkSentNode?.displayWithoutProcessing = true
|
|
}
|
|
|
|
if clockFrameNode == nil {
|
|
clockFrameNode = ASImageNode()
|
|
clockFrameNode?.isLayerBacked = true
|
|
clockFrameNode?.displaysAsynchronously = false
|
|
clockFrameNode?.displayWithoutProcessing = true
|
|
clockFrameNode?.frame = CGRect(origin: CGPoint(), size: clockFrameImage?.size ?? CGSize())
|
|
}
|
|
|
|
if clockMinNode == nil {
|
|
clockMinNode = ASImageNode()
|
|
clockMinNode?.isLayerBacked = true
|
|
clockMinNode?.displaysAsynchronously = false
|
|
clockMinNode?.displayWithoutProcessing = true
|
|
clockMinNode?.frame = CGRect(origin: CGPoint(), size: clockMinImage?.size ?? CGSize())
|
|
}
|
|
clockPosition = CGPoint(x: leftInset + date.size.width + 8.5, y: 7.5 + offset)
|
|
case let .Sent(read):
|
|
let hideStatus: Bool
|
|
switch arguments.type {
|
|
case .BubbleOutgoing, .FreeOutgoing, .ImageOutgoing:
|
|
hideStatus = false
|
|
default:
|
|
hideStatus = arguments.impressionCount != nil
|
|
}
|
|
|
|
if hideStatus {
|
|
statusWidth = 0.0
|
|
|
|
checkReadNode = nil
|
|
checkSentNode = nil
|
|
clockFrameNode = nil
|
|
clockMinNode = nil
|
|
} else {
|
|
statusWidth = floor(floor(arguments.presentationData.fontSize.baseDisplaySize * 13.0 / 17.0))
|
|
|
|
if checkReadNode == nil {
|
|
checkReadNode = ASImageNode()
|
|
checkReadNode?.isLayerBacked = true
|
|
checkReadNode?.displaysAsynchronously = false
|
|
checkReadNode?.displayWithoutProcessing = true
|
|
}
|
|
|
|
if checkSentNode == nil {
|
|
checkSentNode = ASImageNode()
|
|
checkSentNode?.isLayerBacked = true
|
|
checkSentNode?.displaysAsynchronously = false
|
|
checkSentNode?.displayWithoutProcessing = true
|
|
}
|
|
|
|
clockFrameNode = nil
|
|
clockMinNode = nil
|
|
|
|
let checkSize = loadedCheckFullImage!.size
|
|
|
|
if read {
|
|
checkReadFrame = CGRect(origin: CGPoint(x: leftInset + impressionWidth + date.size.width + 5.0 + statusWidth - checkSize.width, y: 3.0 + offset), size: checkSize)
|
|
}
|
|
checkSentFrame = CGRect(origin: CGPoint(x: leftInset + impressionWidth + date.size.width + 5.0 + statusWidth - checkSize.width - checkOffset, y: 3.0 + offset), size: checkSize)
|
|
}
|
|
case .Failed:
|
|
statusWidth = 0.0
|
|
|
|
checkReadNode = nil
|
|
checkSentNode = nil
|
|
clockFrameNode = nil
|
|
clockMinNode = nil
|
|
}
|
|
} else {
|
|
statusWidth = 0.0
|
|
|
|
checkReadNode = nil
|
|
checkSentNode = nil
|
|
clockFrameNode = nil
|
|
clockMinNode = nil
|
|
}
|
|
|
|
var backgroundInsets = UIEdgeInsets()
|
|
|
|
if let _ = backgroundImage {
|
|
if currentBackgroundNode == nil {
|
|
let backgroundNode = ASImageNode()
|
|
backgroundNode.isLayerBacked = true
|
|
backgroundNode.displayWithoutProcessing = true
|
|
backgroundNode.displaysAsynchronously = false
|
|
currentBackgroundNode = backgroundNode
|
|
}
|
|
backgroundInsets = UIEdgeInsets(top: 2.0, left: 7.0, bottom: 2.0, right: 7.0)
|
|
} else if blurredBackgroundColor != nil {
|
|
backgroundInsets = UIEdgeInsets(top: 2.0, left: 7.0, bottom: 2.0, right: 7.0)
|
|
}
|
|
|
|
var replyCountLayoutAndApply: (TextNodeLayout, () -> TextNode)?
|
|
|
|
let reactionSize: CGFloat = 8.0
|
|
let reactionSpacing: CGFloat = 2.0
|
|
let reactionTrailingSpacing: CGFloat = 6.0
|
|
|
|
var reactionInset: CGFloat = 0.0
|
|
|
|
if arguments.replyCount > 0 {
|
|
let countString: String
|
|
if arguments.replyCount > 1000000 {
|
|
countString = "\(arguments.replyCount / 1000000)M"
|
|
} else if arguments.replyCount > 1000 {
|
|
countString = "\(arguments.replyCount / 1000)K"
|
|
} else {
|
|
countString = "\(arguments.replyCount)"
|
|
}
|
|
|
|
let layoutAndApply = makeReplyCountLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: countString, font: dateFont, textColor: dateColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0)))
|
|
reactionInset += 14.0 + layoutAndApply.0.size.width + 4.0
|
|
replyCountLayoutAndApply = layoutAndApply
|
|
} else if arguments.isPinned {
|
|
reactionInset += 12.0
|
|
}
|
|
|
|
if arguments.messageEffect != nil {
|
|
reactionInset += 13.0
|
|
}
|
|
|
|
leftInset += reactionInset
|
|
|
|
let layoutSize = CGSize(width: leftInset + impressionWidth + date.size.width + statusWidth + backgroundInsets.left + backgroundInsets.right, height: date.size.height + backgroundInsets.top + backgroundInsets.bottom)
|
|
|
|
let verticalReactionsInset: CGFloat
|
|
let verticalInset: CGFloat
|
|
let resultingWidth: CGFloat
|
|
let resultingHeight: CGFloat
|
|
|
|
let reactionButtonsResult: ReactionButtonsAsyncLayoutContainer.Result
|
|
switch arguments.layoutInput {
|
|
case .standalone:
|
|
verticalReactionsInset = 0.0
|
|
verticalInset = 0.0
|
|
resultingWidth = layoutSize.width
|
|
resultingHeight = layoutSize.height
|
|
reactionButtonsResult = reactionButtonsContainer.update(
|
|
context: arguments.context,
|
|
action: { itemNode, value, sourceView in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.reactionSelected?(itemNode, value, sourceView)
|
|
},
|
|
reactions: [],
|
|
colors: reactionColors,
|
|
isTag: arguments.areReactionsTags,
|
|
constrainedWidth: arguments.constrainedSize.width
|
|
)
|
|
case let .trailingContent(contentWidth, reactionSettings):
|
|
if let reactionSettings = reactionSettings, !reactionSettings.displayInline {
|
|
var totalReactionCount: Int = 0
|
|
for reaction in arguments.reactions {
|
|
totalReactionCount += Int(reaction.count)
|
|
}
|
|
|
|
reactionButtonsResult = reactionButtonsContainer.update(
|
|
context: arguments.context,
|
|
action: { itemNode, value, sourceView in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.reactionSelected?(itemNode, value, sourceView)
|
|
},
|
|
reactions: arguments.reactions.map { reaction in
|
|
var centerAnimation: TelegramMediaFile?
|
|
var animationFileId: Int64?
|
|
|
|
switch reaction.value {
|
|
case .builtin, .stars:
|
|
if let availableReactions = arguments.availableReactions {
|
|
for availableReaction in availableReactions.reactions {
|
|
if availableReaction.value == reaction.value {
|
|
centerAnimation = availableReaction.centerAnimation
|
|
break
|
|
}
|
|
}
|
|
}
|
|
case let .custom(fileId):
|
|
animationFileId = fileId
|
|
}
|
|
|
|
var peers: [EnginePeer] = []
|
|
for (value, peer) in arguments.reactionPeers {
|
|
if value == reaction.value {
|
|
if !peers.contains(where: { $0.id == peer.id }) {
|
|
peers.append(peer)
|
|
}
|
|
}
|
|
}
|
|
if !arguments.displayAllReactionPeers {
|
|
if peers.count != Int(reaction.count) || arguments.reactionPeers.count != totalReactionCount {
|
|
peers.removeAll()
|
|
}
|
|
}
|
|
|
|
var title: String?
|
|
if arguments.areReactionsTags, let savedMessageTags = arguments.savedMessageTags {
|
|
for tag in savedMessageTags.tags {
|
|
if tag.reaction == reaction.value {
|
|
title = tag.title
|
|
}
|
|
}
|
|
}
|
|
|
|
return ReactionButtonsAsyncLayoutContainer.Reaction(
|
|
reaction: ReactionButtonComponent.Reaction(
|
|
value: reaction.value,
|
|
centerAnimation: centerAnimation,
|
|
animationFileId: animationFileId,
|
|
title: title
|
|
),
|
|
count: Int(reaction.count),
|
|
peers: arguments.areReactionsTags ? [] : peers,
|
|
chosenOrder: reaction.chosenOrder
|
|
)
|
|
},
|
|
colors: reactionColors,
|
|
isTag: arguments.areReactionsTags,
|
|
constrainedWidth: arguments.constrainedSize.width
|
|
)
|
|
} else {
|
|
reactionButtonsResult = reactionButtonsContainer.update(
|
|
context: arguments.context,
|
|
action: { itemNode, value, sourceView in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.reactionSelected?(itemNode, value, sourceView)
|
|
},
|
|
reactions: [],
|
|
colors: reactionColors,
|
|
isTag: arguments.areReactionsTags,
|
|
constrainedWidth: arguments.constrainedSize.width
|
|
)
|
|
}
|
|
|
|
var reactionButtonsSize = CGSize()
|
|
var currentRowWidth: CGFloat = 0.0
|
|
for item in reactionButtonsResult.items {
|
|
if currentRowWidth + item.size.width > arguments.constrainedSize.width {
|
|
reactionButtonsSize.width = max(reactionButtonsSize.width, currentRowWidth)
|
|
if !reactionButtonsSize.height.isZero {
|
|
reactionButtonsSize.height += 6.0
|
|
}
|
|
reactionButtonsSize.height += item.size.height
|
|
currentRowWidth = 0.0
|
|
}
|
|
|
|
if !currentRowWidth.isZero {
|
|
currentRowWidth += 6.0
|
|
}
|
|
currentRowWidth += item.size.width
|
|
}
|
|
if !currentRowWidth.isZero && !reactionButtonsResult.items.isEmpty {
|
|
reactionButtonsSize.width = max(reactionButtonsSize.width, currentRowWidth)
|
|
if !reactionButtonsSize.height.isZero {
|
|
reactionButtonsSize.height += 6.0
|
|
}
|
|
reactionButtonsSize.height += reactionButtonsResult.items[0].size.height
|
|
}
|
|
|
|
if reactionButtonsSize.width.isZero {
|
|
verticalReactionsInset = 0.0
|
|
if let contentWidth {
|
|
if contentWidth + layoutSize.width > arguments.constrainedSize.width {
|
|
resultingWidth = layoutSize.width
|
|
verticalInset = 0.0
|
|
resultingHeight = layoutSize.height + verticalInset
|
|
} else {
|
|
resultingWidth = contentWidth + layoutSize.width
|
|
verticalInset = -layoutSize.height
|
|
resultingHeight = 0.0
|
|
}
|
|
} else {
|
|
resultingWidth = layoutSize.width
|
|
verticalInset = 0.0
|
|
resultingHeight = layoutSize.height + verticalInset
|
|
}
|
|
} else {
|
|
var additionalVerticalInset: CGFloat = 0.0
|
|
if let reactionSettings = reactionSettings {
|
|
if reactionSettings.preferAdditionalInset {
|
|
verticalReactionsInset = 8.0
|
|
additionalVerticalInset += 1.0
|
|
} else {
|
|
verticalReactionsInset = 3.0
|
|
}
|
|
} else {
|
|
verticalReactionsInset = 0.0
|
|
}
|
|
|
|
if currentRowWidth + layoutSize.width > arguments.constrainedSize.width {
|
|
resultingWidth = max(layoutSize.width, reactionButtonsSize.width)
|
|
resultingHeight = verticalReactionsInset + reactionButtonsSize.height + 1.0 + layoutSize.height
|
|
verticalInset = verticalReactionsInset + reactionButtonsSize.height + 3.0
|
|
} else {
|
|
resultingWidth = max(layoutSize.width + currentRowWidth, reactionButtonsSize.width)
|
|
verticalInset = verticalReactionsInset + reactionButtonsSize.height - layoutSize.height + additionalVerticalInset
|
|
resultingHeight = verticalReactionsInset + reactionButtonsSize.height + 1.0
|
|
}
|
|
}
|
|
}
|
|
|
|
return (resultingWidth, { boundingWidth in
|
|
return (CGSize(width: boundingWidth, height: resultingHeight), { animation in
|
|
if let strongSelf = self {
|
|
let leftOffset = boundingWidth - layoutSize.width
|
|
|
|
strongSelf.theme = arguments.presentationData.theme
|
|
strongSelf.type = arguments.type
|
|
strongSelf.layoutSize = layoutSize
|
|
|
|
let reactionButtons = reactionButtonsResult.apply(
|
|
animation,
|
|
ReactionButtonsAsyncLayoutContainer.Arguments(
|
|
animationCache: arguments.animationCache,
|
|
animationRenderer: arguments.animationRenderer
|
|
)
|
|
)
|
|
|
|
var reactionButtonPosition = CGPoint(x: -1.0, y: verticalReactionsInset)
|
|
for item in reactionButtons.items {
|
|
if reactionButtonPosition.x + item.size.width > boundingWidth {
|
|
reactionButtonPosition.x = -1.0
|
|
reactionButtonPosition.y += item.size.height + 6.0
|
|
}
|
|
|
|
if item.node.view.superview != strongSelf.view {
|
|
assert(item.node.view.superview == nil)
|
|
strongSelf.view.addSubview(item.node.view)
|
|
item.node.view.frame = CGRect(origin: reactionButtonPosition, size: item.size)
|
|
|
|
if animation.isAnimated {
|
|
item.node.view.layer.animateScale(from: 0.01, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
|
|
item.node.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
}
|
|
} else {
|
|
animation.animator.updateFrame(layer: item.node.view.layer, frame: CGRect(origin: reactionButtonPosition, size: item.size), completion: nil)
|
|
}
|
|
|
|
let itemValue = item.value
|
|
let itemNode = item.node
|
|
item.node.view.isGestureEnabled = true
|
|
let canViewReactionList = arguments.canViewReactionList
|
|
item.node.view.activateAfterCompletion = !canViewReactionList
|
|
item.node.view.activated = { [weak itemNode] gesture, _ in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
guard let itemNode = itemNode else {
|
|
return
|
|
}
|
|
|
|
if let openReactionPreview = strongSelf.openReactionPreview {
|
|
openReactionPreview(gesture, itemNode.view.containerView, itemValue)
|
|
} else {
|
|
gesture.cancel()
|
|
}
|
|
}
|
|
|
|
reactionButtonPosition.x += item.size.width + 6.0
|
|
}
|
|
|
|
for node in reactionButtons.removedNodes {
|
|
if animation.isAnimated {
|
|
node.view.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
|
|
node.view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
|
|
node.view.removeFromSuperview()
|
|
})
|
|
} else {
|
|
node.view.removeFromSuperview()
|
|
}
|
|
}
|
|
|
|
if backgroundImage != nil {
|
|
if let currentBackgroundNode = currentBackgroundNode {
|
|
if currentBackgroundNode.supernode == nil {
|
|
strongSelf.backgroundNode = currentBackgroundNode
|
|
currentBackgroundNode.image = backgroundImage
|
|
strongSelf.insertSubnode(currentBackgroundNode, at: 0)
|
|
} else if themeUpdated {
|
|
currentBackgroundNode.image = backgroundImage
|
|
}
|
|
}
|
|
if let backgroundNode = strongSelf.backgroundNode {
|
|
animation.animator.updateFrame(layer: backgroundNode.layer, frame: CGRect(origin: CGPoint(), size: layoutSize), completion: nil)
|
|
}
|
|
} else {
|
|
if let backgroundNode = strongSelf.backgroundNode {
|
|
backgroundNode.removeFromSupernode()
|
|
strongSelf.backgroundNode = nil
|
|
}
|
|
}
|
|
|
|
if let blurredBackgroundColor = blurredBackgroundColor {
|
|
if let blurredBackgroundNode = strongSelf.blurredBackgroundNode {
|
|
blurredBackgroundNode.updateColor(color: blurredBackgroundColor.0, enableBlur: blurredBackgroundColor.1, transition: .immediate)
|
|
animation.animator.updateFrame(layer: blurredBackgroundNode.layer, frame: CGRect(origin: CGPoint(), size: layoutSize), completion: nil)
|
|
blurredBackgroundNode.update(size: blurredBackgroundNode.bounds.size, cornerRadius: blurredBackgroundNode.bounds.height / 2.0, animator: animation.animator)
|
|
} else {
|
|
let blurredBackgroundNode = NavigationBackgroundNode(color: blurredBackgroundColor.0, enableBlur: blurredBackgroundColor.1)
|
|
strongSelf.blurredBackgroundNode = blurredBackgroundNode
|
|
strongSelf.insertSubnode(blurredBackgroundNode, at: 0)
|
|
blurredBackgroundNode.frame = CGRect(origin: CGPoint(), size: layoutSize)
|
|
blurredBackgroundNode.update(size: blurredBackgroundNode.bounds.size, cornerRadius: blurredBackgroundNode.bounds.height / 2.0, transition: .immediate)
|
|
}
|
|
} else if let blurredBackgroundNode = strongSelf.blurredBackgroundNode {
|
|
strongSelf.blurredBackgroundNode = nil
|
|
blurredBackgroundNode.removeFromSupernode()
|
|
}
|
|
|
|
let _ = dateApply()
|
|
|
|
if let currentImpressionIcon = currentImpressionIcon {
|
|
let impressionIconFrame = CGRect(origin: CGPoint(x: leftOffset + leftInset + backgroundInsets.left, y: backgroundInsets.top + 1.0 + offset + verticalInset + floor((date.size.height - impressionSize.height) / 2.0)), size: impressionSize)
|
|
currentImpressionIcon.displaysAsynchronously = false
|
|
if currentImpressionIcon.image !== impressionImage {
|
|
currentImpressionIcon.image = impressionImage
|
|
}
|
|
if currentImpressionIcon.supernode == nil {
|
|
strongSelf.impressionIcon = currentImpressionIcon
|
|
strongSelf.addSubnode(currentImpressionIcon)
|
|
currentImpressionIcon.frame = impressionIconFrame
|
|
} else {
|
|
animation.animator.updateFrame(layer: currentImpressionIcon.layer, frame: impressionIconFrame, completion: nil)
|
|
}
|
|
} else if let impressionIcon = strongSelf.impressionIcon {
|
|
impressionIcon.removeFromSupernode()
|
|
strongSelf.impressionIcon = nil
|
|
}
|
|
|
|
animation.animator.updateFrame(layer: strongSelf.dateNode.layer, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset + backgroundInsets.left + impressionWidth, y: backgroundInsets.top + 1.0 + offset + verticalInset), size: date.size), completion: nil)
|
|
|
|
if let clockFrameNode = clockFrameNode {
|
|
let clockPosition = CGPoint(x: leftOffset + backgroundInsets.left + clockPosition.x + reactionInset, y: backgroundInsets.top + clockPosition.y + verticalInset)
|
|
if strongSelf.clockFrameNode == nil {
|
|
strongSelf.clockFrameNode = clockFrameNode
|
|
clockFrameNode.image = clockFrameImage
|
|
strongSelf.addSubnode(clockFrameNode)
|
|
|
|
clockFrameNode.position = clockPosition
|
|
} else {
|
|
if themeUpdated {
|
|
clockFrameNode.image = clockFrameImage
|
|
}
|
|
animation.animator.updatePosition(layer: clockFrameNode.layer, position: clockPosition, completion: nil)
|
|
}
|
|
if let clockFrameNode = strongSelf.clockFrameNode {
|
|
maybeAddRotationAnimation(clockFrameNode.layer, duration: 6.0)
|
|
}
|
|
} else if let clockFrameNode = strongSelf.clockFrameNode {
|
|
clockFrameNode.removeFromSupernode()
|
|
strongSelf.clockFrameNode = nil
|
|
}
|
|
|
|
if let clockMinNode = clockMinNode {
|
|
let clockMinPosition = CGPoint(x: leftOffset + backgroundInsets.left + clockPosition.x + reactionInset, y: backgroundInsets.top + clockPosition.y + verticalInset)
|
|
if strongSelf.clockMinNode == nil {
|
|
strongSelf.clockMinNode = clockMinNode
|
|
clockMinNode.image = clockMinImage
|
|
strongSelf.addSubnode(clockMinNode)
|
|
|
|
clockMinNode.position = clockMinPosition
|
|
} else {
|
|
if themeUpdated {
|
|
clockMinNode.image = clockMinImage
|
|
}
|
|
animation.animator.updatePosition(layer: clockMinNode.layer, position: clockMinPosition, completion: nil)
|
|
}
|
|
if let clockMinNode = strongSelf.clockMinNode {
|
|
maybeAddRotationAnimation(clockMinNode.layer, duration: 1.0)
|
|
}
|
|
} else if let clockMinNode = strongSelf.clockMinNode {
|
|
clockMinNode.removeFromSupernode()
|
|
strongSelf.clockMinNode = nil
|
|
}
|
|
|
|
if let checkSentNode = checkSentNode, let checkReadNode = checkReadNode {
|
|
var animateSentNode = false
|
|
if strongSelf.checkSentNode == nil {
|
|
checkSentNode.image = loadedCheckFullImage
|
|
strongSelf.checkSentNode = checkSentNode
|
|
strongSelf.addSubnode(checkSentNode)
|
|
animateSentNode = animation.isAnimated
|
|
} else if themeUpdated {
|
|
checkSentNode.image = loadedCheckFullImage
|
|
}
|
|
|
|
if let checkSentFrame = checkSentFrame {
|
|
let actualCheckSentFrame = checkSentFrame.offsetBy(dx: leftOffset + backgroundInsets.left + reactionInset, dy: backgroundInsets.top + verticalInset)
|
|
|
|
if checkSentNode.isHidden {
|
|
animateSentNode = animation.isAnimated
|
|
checkSentNode.isHidden = false
|
|
checkSentNode.frame = actualCheckSentFrame
|
|
} else {
|
|
animation.animator.updateFrame(layer: checkSentNode.layer, frame: actualCheckSentFrame, completion: nil)
|
|
}
|
|
} else {
|
|
checkSentNode.isHidden = true
|
|
}
|
|
|
|
var animateReadNode = false
|
|
if strongSelf.checkReadNode == nil {
|
|
animateReadNode = animation.isAnimated
|
|
checkReadNode.image = loadedCheckPartialImage
|
|
strongSelf.checkReadNode = checkReadNode
|
|
strongSelf.addSubnode(checkReadNode)
|
|
} else if themeUpdated {
|
|
checkReadNode.image = loadedCheckPartialImage
|
|
}
|
|
|
|
if let checkReadFrame = checkReadFrame {
|
|
if checkReadNode.isHidden {
|
|
animateReadNode = animation.isAnimated
|
|
checkReadNode.frame = checkReadFrame.offsetBy(dx: leftOffset + backgroundInsets.left + reactionInset, dy: backgroundInsets.top + verticalInset)
|
|
} else {
|
|
animation.animator.updateFrame(layer: checkReadNode.layer, frame: checkReadFrame.offsetBy(dx: leftOffset + backgroundInsets.left + reactionInset, dy: backgroundInsets.top + verticalInset), completion: nil)
|
|
}
|
|
checkReadNode.isHidden = false
|
|
} else {
|
|
checkReadNode.isHidden = true
|
|
}
|
|
|
|
if animateSentNode {
|
|
strongSelf.checkSentNode?.layer.animateScale(from: 1.3, to: 1.0, duration: 0.1)
|
|
}
|
|
if animateReadNode {
|
|
strongSelf.checkReadNode?.layer.animateScale(from: 1.3, to: 1.0, duration: 0.1)
|
|
}
|
|
} else if let checkSentNode = strongSelf.checkSentNode, let checkReadNode = strongSelf.checkReadNode {
|
|
checkSentNode.removeFromSupernode()
|
|
checkReadNode.removeFromSupernode()
|
|
strongSelf.checkSentNode = nil
|
|
strongSelf.checkReadNode = nil
|
|
}
|
|
|
|
var reactionOffset: CGFloat = leftOffset + leftInset - reactionInset + backgroundInsets.left
|
|
|
|
if let messageEffect = arguments.messageEffect {
|
|
var validReactions = Set<MessageReaction.Reaction>()
|
|
do {
|
|
let node: StatusReactionNode
|
|
var animateNode = true
|
|
if let current = strongSelf.reactionNodes[.custom(messageEffect.id)] {
|
|
node = current
|
|
} else {
|
|
animateNode = false
|
|
node = StatusReactionNode()
|
|
strongSelf.reactionNodes[.custom(messageEffect.id)] = node
|
|
}
|
|
validReactions.insert(.custom(messageEffect.id))
|
|
|
|
var centerAnimation: TelegramMediaFile?
|
|
|
|
centerAnimation = messageEffect.staticIcon
|
|
|
|
node.update(
|
|
context: arguments.context,
|
|
type: arguments.type,
|
|
value: .custom(messageEffect.id),
|
|
file: centerAnimation,
|
|
fileId: centerAnimation?.fileId.id,
|
|
alternativeText: messageEffect.emoticon,
|
|
isSelected: false,
|
|
count: 0,
|
|
theme: arguments.presentationData.theme.theme,
|
|
wallpaper: arguments.presentationData.theme.wallpaper,
|
|
animationCache: arguments.animationCache,
|
|
animationRenderer: arguments.animationRenderer,
|
|
animated: false
|
|
)
|
|
if node.supernode == nil {
|
|
strongSelf.addSubnode(node)
|
|
if animation.isAnimated {
|
|
node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
}
|
|
}
|
|
let nodeFrame = CGRect(origin: CGPoint(x: reactionOffset, y: backgroundInsets.top + offset + 3.0 + UIScreenPixel + verticalInset), size: CGSize(width: reactionSize, height: reactionSize))
|
|
if animateNode {
|
|
animation.animator.updateFrame(layer: node.layer, frame: nodeFrame, completion: nil)
|
|
} else {
|
|
node.frame = nodeFrame
|
|
}
|
|
reactionOffset += reactionSize + reactionSpacing
|
|
}
|
|
if !arguments.reactions.isEmpty {
|
|
reactionOffset += reactionTrailingSpacing
|
|
}
|
|
|
|
var removeIds: [MessageReaction.Reaction] = []
|
|
for (id, node) in strongSelf.reactionNodes {
|
|
if !validReactions.contains(id) {
|
|
removeIds.append(id)
|
|
if animation.isAnimated {
|
|
node.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false)
|
|
node.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak node] _ in
|
|
node?.layer.removeAllAnimations()
|
|
node?.removeFromSupernode()
|
|
})
|
|
} else {
|
|
node.removeFromSupernode()
|
|
}
|
|
}
|
|
}
|
|
for id in removeIds {
|
|
strongSelf.reactionNodes.removeValue(forKey: id)
|
|
}
|
|
} else {
|
|
var removeIds: [MessageReaction.Reaction] = []
|
|
for (id, node) in strongSelf.reactionNodes {
|
|
removeIds.append(id)
|
|
if animation.isAnimated {
|
|
node.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false)
|
|
node.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak node] _ in
|
|
node?.layer.removeAllAnimations()
|
|
node?.removeFromSupernode()
|
|
})
|
|
} else {
|
|
node.removeFromSupernode()
|
|
}
|
|
}
|
|
for id in removeIds {
|
|
strongSelf.reactionNodes.removeValue(forKey: id)
|
|
}
|
|
}
|
|
|
|
if let currentRepliesIcon = currentRepliesIcon {
|
|
currentRepliesIcon.displaysAsynchronously = false
|
|
if currentRepliesIcon.image !== repliesImage {
|
|
currentRepliesIcon.image = repliesImage
|
|
}
|
|
if currentRepliesIcon.supernode == nil {
|
|
strongSelf.repliesIcon = currentRepliesIcon
|
|
strongSelf.addSubnode(currentRepliesIcon)
|
|
if animation.isAnimated {
|
|
currentRepliesIcon.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
|
}
|
|
}
|
|
let repliesIconFrame = CGRect(origin: CGPoint(x: reactionOffset - 2.0, y: backgroundInsets.top + offset + verticalInset + floor((date.size.height - repliesIconSize.height) / 2.0)), size: repliesIconSize)
|
|
animation.animator.updateFrame(layer: currentRepliesIcon.layer, frame: repliesIconFrame, completion: nil)
|
|
reactionOffset += 9.0
|
|
} else if let repliesIcon = strongSelf.repliesIcon {
|
|
strongSelf.repliesIcon = nil
|
|
if animation.isAnimated {
|
|
repliesIcon.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak repliesIcon] _ in
|
|
repliesIcon?.removeFromSupernode()
|
|
})
|
|
} else {
|
|
repliesIcon.removeFromSupernode()
|
|
}
|
|
}
|
|
|
|
if let (layout, apply) = replyCountLayoutAndApply {
|
|
let node = apply()
|
|
if strongSelf.replyCountNode !== node {
|
|
strongSelf.replyCountNode?.removeFromSupernode()
|
|
strongSelf.addSubnode(node)
|
|
strongSelf.replyCountNode = node
|
|
if animation.isAnimated {
|
|
node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
|
}
|
|
}
|
|
let replyCountFrame = CGRect(origin: CGPoint(x: reactionOffset + 4.0, y: backgroundInsets.top + 1.0 + offset + verticalInset), size: layout.size)
|
|
animation.animator.updateFrame(layer: node.layer, frame: replyCountFrame, completion: nil)
|
|
reactionOffset += 4.0 + layout.size.width
|
|
} else if let replyCountNode = strongSelf.replyCountNode {
|
|
strongSelf.replyCountNode = nil
|
|
if animation.isAnimated {
|
|
replyCountNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak replyCountNode] _ in
|
|
replyCountNode?.removeFromSupernode()
|
|
})
|
|
} else {
|
|
replyCountNode.removeFromSupernode()
|
|
}
|
|
}
|
|
}
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
public static func asyncLayout(_ node: ChatMessageDateAndStatusNode?) -> (_ arguments: Arguments) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageDateAndStatusNode)) {
|
|
let currentLayout = node?.asyncLayout()
|
|
return { arguments in
|
|
let resultNode: ChatMessageDateAndStatusNode
|
|
let resultSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))
|
|
if let node = node, let currentLayout = currentLayout {
|
|
resultNode = node
|
|
resultSuggestedWidthAndContinue = currentLayout(arguments)
|
|
} else {
|
|
resultNode = ChatMessageDateAndStatusNode()
|
|
resultSuggestedWidthAndContinue = resultNode.asyncLayout()(arguments)
|
|
}
|
|
|
|
return (resultSuggestedWidthAndContinue.0, { boundingWidth in
|
|
let (size, apply) = resultSuggestedWidthAndContinue.1(boundingWidth)
|
|
return (size, { animation in
|
|
apply(animation)
|
|
|
|
return resultNode
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
public func reactionView(value: MessageReaction.Reaction) -> UIView? {
|
|
for (key, button) in self.reactionButtonsContainer.buttons {
|
|
if key == value {
|
|
return button.view.iconView
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
public func messageEffectTargetView() -> UIView? {
|
|
for (_, node) in self.reactionNodes {
|
|
return node.iconView
|
|
}
|
|
return nil
|
|
}
|
|
|
|
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
for (_, button) in self.reactionButtonsContainer.buttons {
|
|
if button.view.frame.contains(point) {
|
|
if let result = button.view.hitTest(self.view.convert(point, to: button.view), with: event) {
|
|
return result
|
|
}
|
|
}
|
|
}
|
|
if self.pressed != nil {
|
|
if self.bounds.contains(point) {
|
|
return self.view
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
public func shouldDisplayInlineDateReactions(message: Message, isPremium: Bool, forceInline: Bool) -> Bool {
|
|
// MARK: Swiftgram
|
|
// With 10.13 it now hides reactions in favor of message effect badge
|
|
return SGSimpleSettings.shared.hideReactions
|
|
}
|