mirror of
https://github.com/GLEGram/GLEGram-iOS.git
synced 2026-04-29 22:37:53 +02:00
4647310322
Based on Swiftgram 12.5 (Telegram iOS 12.5). All GLEGram features ported and organized in GLEGram/ folder. Features: Ghost Mode, Saved Deleted Messages, Content Protection Bypass, Font Replacement, Fake Profile, Chat Export, Plugin System, and more. See CHANGELOG_12.5.md for full details.
2647 lines
144 KiB
Swift
2647 lines
144 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import AsyncDisplayKit
|
|
import Display
|
|
import Postbox
|
|
import TelegramCore
|
|
import SwiftSignalKit
|
|
import Photos
|
|
import TelegramPresentationData
|
|
import TelegramUIPreferences
|
|
import TextFormat
|
|
import TelegramStringFormatting
|
|
import AccountContext
|
|
import RadialStatusNode
|
|
import ShareController
|
|
import OpenInExternalAppUI
|
|
import AppBundle
|
|
import LocalizedPeerData
|
|
import TextSelectionNode
|
|
import UrlEscaping
|
|
import UndoUI
|
|
import ManagedAnimationNode
|
|
import TelegramUniversalVideoContent
|
|
import InvisibleInkDustNode
|
|
import TextNodeWithEntities
|
|
import AnimationCache
|
|
import MultiAnimationRenderer
|
|
import Pasteboard
|
|
import Speak
|
|
import TranslateUI
|
|
import TelegramNotices
|
|
import SolidRoundedButtonNode
|
|
import UrlHandling
|
|
import GlassControls
|
|
import ComponentFlow
|
|
import ComponentDisplayAdapters
|
|
import EdgeEffect
|
|
import RasterizedCompositionComponent
|
|
import BadgeComponent
|
|
import UIKitRuntimeUtils
|
|
|
|
#if canImport(SGSimpleSettings)
|
|
import SGSimpleSettings
|
|
#endif
|
|
|
|
private let deleteImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: .white)
|
|
private let actionImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: .white)
|
|
private let editImage = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/Draw"), color: .white)
|
|
|
|
private let backwardImage = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/BackwardButton"), color: .white)
|
|
private let forwardImage = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/ForwardButton"), color: .white)
|
|
|
|
private let cloudFetchIcon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/FileCloudFetch"), color: UIColor.white)
|
|
|
|
enum ChatItemGalleryFooterContent: Equatable {
|
|
case info
|
|
case fetch(status: MediaResourceStatus, seekable: Bool)
|
|
case playback(paused: Bool, seekable: Bool)
|
|
|
|
static func ==(lhs: ChatItemGalleryFooterContent, rhs: ChatItemGalleryFooterContent) -> Bool {
|
|
switch lhs {
|
|
case .info:
|
|
if case .info = rhs {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .fetch(lhsStatus, lhsSeekable):
|
|
if case let .fetch(rhsStatus, rhsSeekable) = rhs, lhsStatus == rhsStatus, lhsSeekable == rhsSeekable {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .playback(lhsPaused, lhsSeekable):
|
|
if case let .playback(rhsPaused, rhsSeekable) = rhs, lhsPaused == rhsPaused, lhsSeekable == rhsSeekable {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
enum ChatItemGalleryFooterContentTapAction {
|
|
case none
|
|
case url(url: String, concealed: Bool)
|
|
case textMention(String)
|
|
case peerMention(PeerId, String)
|
|
case botCommand(String)
|
|
case hashtag(String?, String)
|
|
case instantPage
|
|
case call(PeerId)
|
|
case openMessage
|
|
case ignore
|
|
}
|
|
|
|
class CaptionScrollWrapperNode: ASDisplayNode {
|
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
let result = super.hitTest(point, with: event)
|
|
if result == self.view, let subnode = self.subnodes?.first {
|
|
let convertedPoint = self.view.convert(point, to: subnode.view)
|
|
if let subnodes = subnode.subnodes {
|
|
for node in subnodes.reversed() {
|
|
if node.frame.contains(convertedPoint) && node.isUserInteractionEnabled {
|
|
if let dustNode = node as? InvisibleInkDustNode, dustNode.isRevealed {
|
|
continue
|
|
}
|
|
return node.view
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
return result
|
|
}
|
|
}
|
|
|
|
final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScrollViewDelegate {
|
|
public final class SettingsButtonState: Equatable {
|
|
public let speed: String?
|
|
public let quality: String?
|
|
|
|
public init(speed: String?, quality: String?) {
|
|
self.speed = speed
|
|
self.quality = quality
|
|
}
|
|
|
|
public static func ==(lhs: SettingsButtonState, rhs: SettingsButtonState) -> Bool {
|
|
if lhs.speed != rhs.speed {
|
|
return false
|
|
}
|
|
if lhs.quality != rhs.quality {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|
|
private let context: AccountContext
|
|
private var presentationData: PresentationData
|
|
private var theme: PresentationTheme
|
|
private var strings: PresentationStrings
|
|
private var nameOrder: PresentationPersonNameOrder
|
|
private var dateTimeFormat: PresentationDateTimeFormat
|
|
|
|
private let contentNode: ASDisplayNode
|
|
private let maskNode: ASDisplayNode
|
|
private let textSelectionKnobContainer: UIView
|
|
private let textSelectionKnobSurface: UIView
|
|
private let scrollWrapperNode: CaptionScrollWrapperNode
|
|
private let scrollWrapperMask: EdgeMaskView
|
|
private let scrollWrapperEffect: VariableBlurEffect
|
|
private let scrollNode: ASScrollNode
|
|
|
|
private let textNode: ImmediateTextNodeWithEntities
|
|
private var spoilerTextNode: ImmediateTextNodeWithEntities?
|
|
private var dustNode: InvisibleInkDustNode?
|
|
private var buttonNode: SolidRoundedButtonNode?
|
|
private var buttonIconNode: ASImageNode?
|
|
private let buttonPanel = ComponentView<Empty>()
|
|
|
|
private var textSelectionNode: TextSelectionNode?
|
|
|
|
private let animationCache: AnimationCache
|
|
private let animationRenderer: MultiAnimationRenderer
|
|
|
|
private let backwardButton: PlaybackButtonNode
|
|
private let forwardButton: PlaybackButtonNode
|
|
private let playbackControlButton: HighlightableButtonNode
|
|
private let playPauseIconNode: PlayPauseIconNode
|
|
|
|
private let statusButtonNode: HighlightTrackingButtonNode
|
|
private let statusNode: RadialStatusNode
|
|
|
|
private var currentMessageText: NSAttributedString?
|
|
|
|
private var currentMessage: Message?
|
|
private var currentWebPageAndMedia: (TelegramMediaWebpage, Media)?
|
|
private let messageContextDisposable = MetaDisposable()
|
|
|
|
private var videoFramePreviewNode: (ASImageNode, ImmediateTextNode)?
|
|
|
|
private var validLayout: (CGSize, LayoutMetrics, CGFloat, CGFloat, CGFloat, CGFloat)?
|
|
private var buttonsState: (
|
|
displayDeleteButton: Bool,
|
|
displayFullscreenButton: Bool,
|
|
displayActionButton: Bool,
|
|
displayEditButton: Bool,
|
|
displayPictureInPictureButton: Bool,
|
|
settingsButtonState: SettingsButtonState?,
|
|
displayTextRecognitionButton: Bool,
|
|
displayStickersButton: Bool
|
|
)?
|
|
|
|
private var codeHighlightState: (id: EngineMessage.Id, specs: [CachedMessageSyntaxHighlight.Spec], disposable: Disposable)?
|
|
|
|
var playbackControl: (() -> Void)?
|
|
var seekBackward: ((Double) -> Void)?
|
|
var seekForward: ((Double) -> Void)?
|
|
var setPlayRate: ((Double) -> Void)?
|
|
var toggleFullscreen: (() -> Void)?
|
|
var fetchControl: (() -> Void)?
|
|
|
|
var interacting: ((Bool) -> Void)?
|
|
|
|
var shareMediaParameters: (() -> ShareControllerSubject.MediaParameters?)?
|
|
|
|
private var seekTimer: SwiftSignalKit.Timer?
|
|
private var currentIsPaused: Bool = true
|
|
private var seekRate: Double = 1.0
|
|
|
|
private var currentSpeechHolder: SpeechSynthesizerHolder?
|
|
|
|
var performAction: ((GalleryControllerInteractionTapAction) -> Void)?
|
|
var openActionOptions: ((GalleryControllerInteractionTapAction, Message) -> Void)?
|
|
|
|
private var isAd: Bool {
|
|
if self.currentMessage?.adAttribute != nil {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
var content: ChatItemGalleryFooterContent = .info {
|
|
didSet {
|
|
if self.content != oldValue {
|
|
switch self.content {
|
|
case .info:
|
|
self.hasSeekControls = false
|
|
self.playbackControlButton.isHidden = true
|
|
self.statusButtonNode.isHidden = true
|
|
self.statusNode.isHidden = true
|
|
case let .fetch(status, seekable):
|
|
self.currentIsPaused = true
|
|
self.hasSeekControls = seekable && !self.isAd
|
|
|
|
if status == .Local && !self.isAd {
|
|
self.playbackControlButton.isHidden = false
|
|
self.playPauseIconNode.enqueueState(.play, animated: true)
|
|
} else {
|
|
self.playbackControlButton.isHidden = true
|
|
}
|
|
self.statusButtonNode.isHidden = false
|
|
self.statusNode.isHidden = false
|
|
|
|
var statusState: RadialStatusNodeState = .none
|
|
switch status {
|
|
case let .Fetching(_, progress):
|
|
let adjustedProgress = max(progress, 0.027)
|
|
statusState = .cloudProgress(color: UIColor.white, strokeBackgroundColor: UIColor.white.withAlphaComponent(0.5), lineWidth: 2.0, value: CGFloat(adjustedProgress))
|
|
case .Local:
|
|
break
|
|
case .Remote, .Paused:
|
|
var isHLS = false
|
|
if let message = self.currentMessage {
|
|
for media in message.media {
|
|
if let file = media as? TelegramMediaFile {
|
|
isHLS = NativeVideoContent.isHLSVideo(file: file)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if isHLS {
|
|
statusState = .none
|
|
} else {
|
|
if let image = cloudFetchIcon {
|
|
statusState = .customIcon(image)
|
|
}
|
|
}
|
|
}
|
|
self.statusNode.transitionToState(statusState, completion: {})
|
|
self.statusButtonNode.isUserInteractionEnabled = statusState != .none
|
|
case let .playback(paused, seekable):
|
|
self.currentIsPaused = paused
|
|
|
|
if !self.isAd {
|
|
self.playbackControlButton.isHidden = false
|
|
let icon: PlayPauseIconNodeState
|
|
if let wasPlaying = self.wasPlaying {
|
|
icon = wasPlaying ? .pause : .play
|
|
} else {
|
|
icon = paused ? .play : .pause
|
|
}
|
|
self.playPauseIconNode.enqueueState(icon, animated: true)
|
|
self.hasSeekControls = seekable
|
|
} else {
|
|
self.hasSeekControls = false
|
|
}
|
|
self.statusButtonNode.isHidden = true
|
|
self.statusNode.isHidden = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var hasSeekControls: Bool = false {
|
|
didSet {
|
|
let alpha = self.hasSeekControls ? 1.0 : 0.0
|
|
let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
|
|
transition.updateAlpha(node: self.backwardButton, alpha: alpha)
|
|
transition.updateAlpha(node: self.forwardButton, alpha: alpha)
|
|
self.backwardButton.isUserInteractionEnabled = self.hasSeekControls
|
|
self.forwardButton.isUserInteractionEnabled = self.hasSeekControls
|
|
}
|
|
}
|
|
|
|
private var scrubbingHandleRelativePosition: CGFloat = 0.0
|
|
private var scrubbingVisualTimestamp: Double?
|
|
|
|
var scrubberView: ChatVideoGalleryItemScrubberView? = nil {
|
|
willSet {
|
|
if let scrubberView = self.scrubberView, scrubberView.superview == self.view {
|
|
scrubberView.removeFromSuperview()
|
|
}
|
|
}
|
|
didSet {
|
|
if let scrubberView = self.scrubberView {
|
|
self.view.addSubview(scrubberView)
|
|
scrubberView.updateScrubbingVisual = { [weak self] value in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
if let value = value {
|
|
strongSelf.scrubbingVisualTimestamp = value
|
|
if let (videoFramePreviewNode, videoFrameTextNode) = strongSelf.videoFramePreviewNode {
|
|
videoFrameTextNode.attributedText = NSAttributedString(string: stringForDuration(Int32(value)), font: Font.regular(13.0), textColor: .white)
|
|
let textSize = videoFrameTextNode.updateLayout(CGSize(width: 100.0, height: 100.0))
|
|
let imageFrame = videoFramePreviewNode.frame
|
|
let textOffset = (Int((imageFrame.size.width - videoFrameTextNode.bounds.width) / 2) / 2) * 2
|
|
videoFrameTextNode.frame = CGRect(origin: CGPoint(x: CGFloat(textOffset), y: imageFrame.size.height - videoFrameTextNode.bounds.height - 5.0), size: textSize)
|
|
}
|
|
} else {
|
|
strongSelf.scrubbingVisualTimestamp = nil
|
|
}
|
|
}
|
|
scrubberView.updateScrubbingHandlePosition = { [weak self] value in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.scrubbingHandleRelativePosition = value
|
|
if let validLayout = strongSelf.validLayout {
|
|
let _ = strongSelf.updateLayout(size: validLayout.0, metrics: validLayout.1, leftInset: validLayout.2, rightInset: validLayout.3, bottomInset: validLayout.4, contentInset: validLayout.5, transition: .immediate)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
override func setVisibilityAlpha(_ alpha: CGFloat, animated: Bool) {
|
|
self.visibilityAlpha = alpha
|
|
|
|
let transition: ComponentTransition = animated ? .easeInOut(duration: 0.2) : .immediate
|
|
transition.animateView {
|
|
self.contentNode.alpha = alpha
|
|
}
|
|
//transition.setAlpha(view: self.contentNode.view, alpha: alpha)
|
|
|
|
if let validLayout = self.validLayout {
|
|
let _ = self.updateLayout(size: validLayout.0, metrics: validLayout.1, leftInset: validLayout.2, rightInset: validLayout.3, bottomInset: validLayout.4, contentInset: validLayout.5, transition: animated ? .animated(duration: 0.4, curve: .spring) : .immediate)
|
|
}
|
|
}
|
|
|
|
private var hasExpandedCaptionPromise = ValuePromise<Bool>(false)
|
|
var hasExpandedCaption: Signal<Bool, NoError> {
|
|
return hasExpandedCaptionPromise.get()
|
|
}
|
|
|
|
init(context: AccountContext, presentationData: PresentationData, present: @escaping (ViewController, Any?) -> Void = { _, _ in }) {
|
|
self.context = context
|
|
self.presentationData = presentationData
|
|
self.theme = presentationData.theme
|
|
self.strings = presentationData.strings
|
|
self.nameOrder = presentationData.nameDisplayOrder
|
|
self.dateTimeFormat = presentationData.dateTimeFormat
|
|
|
|
self.contentNode = ASDisplayNode()
|
|
|
|
self.textSelectionKnobContainer = UIView()
|
|
self.textSelectionKnobSurface = UIView()
|
|
self.textSelectionKnobContainer.addSubview(self.textSelectionKnobSurface)
|
|
|
|
self.scrollWrapperNode = CaptionScrollWrapperNode()
|
|
self.scrollWrapperNode.layer.allowsGroupOpacity = true
|
|
self.scrollWrapperNode.clipsToBounds = true
|
|
self.scrollWrapperEffect = VariableBlurEffect(layer: self.scrollWrapperNode.layer, isTransparent: true, maxBlurRadius: 1.0)
|
|
self.scrollWrapperNode.backgroundColor = .clear
|
|
self.scrollWrapperNode.isOpaque = false
|
|
|
|
self.scrollWrapperMask = EdgeMaskView()
|
|
//self.scrollWrapperNode.view.addSubview(self.scrollWrapperMask)
|
|
|
|
self.scrollNode = ASScrollNode()
|
|
self.scrollNode.clipsToBounds = false
|
|
|
|
self.maskNode = ASDisplayNode()
|
|
|
|
self.textNode = ImmediateTextNodeWithEntities()
|
|
self.textNode.maximumNumberOfLines = 0
|
|
self.textNode.linkHighlightColor = UIColor(rgb: 0x5ac8fa, alpha: 0.2)
|
|
self.textNode.displaySpoilerEffect = false
|
|
|
|
self.backwardButton = PlaybackButtonNode()
|
|
self.backwardButton.alpha = 0.0
|
|
self.backwardButton.isUserInteractionEnabled = false
|
|
self.backwardButton.backgroundIconNode.image = backwardImage
|
|
|
|
self.forwardButton = PlaybackButtonNode()
|
|
self.forwardButton.alpha = 0.0
|
|
self.forwardButton.isUserInteractionEnabled = false
|
|
self.forwardButton.forward = true
|
|
self.forwardButton.backgroundIconNode.image = forwardImage
|
|
|
|
self.playbackControlButton = HighlightableButtonNode()
|
|
self.playbackControlButton.isHidden = true
|
|
|
|
self.playPauseIconNode = PlayPauseIconNode()
|
|
|
|
self.statusButtonNode = HighlightTrackingButtonNode()
|
|
self.statusNode = RadialStatusNode(backgroundNodeColor: .clear)
|
|
self.statusNode.isUserInteractionEnabled = false
|
|
|
|
self.animationCache = context.animationCache
|
|
self.animationRenderer = context.animationRenderer
|
|
|
|
super.init()
|
|
|
|
self.addSubnode(self.contentNode)
|
|
|
|
self.textNode.highlightAttributeAction = { attributes in
|
|
let highlightedAttributes = [TelegramTextAttributes.URL,
|
|
TelegramTextAttributes.PeerMention,
|
|
TelegramTextAttributes.PeerTextMention,
|
|
TelegramTextAttributes.BotCommand,
|
|
TelegramTextAttributes.Hashtag,
|
|
TelegramTextAttributes.Timecode]
|
|
|
|
for attribute in highlightedAttributes {
|
|
if let _ = attributes[NSAttributedString.Key(rawValue: attribute)] {
|
|
return NSAttributedString.Key(rawValue: attribute)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
self.textNode.tapAttributeAction = { [weak self] attributes, index in
|
|
if let strongSelf = self, let action = strongSelf.actionForAttributes(attributes, index) {
|
|
strongSelf.performAction?(action)
|
|
}
|
|
}
|
|
self.textNode.longTapAttributeAction = { [weak self] attributes, index in
|
|
if let strongSelf = self, let action = strongSelf.actionForAttributes(attributes, index), let message = strongSelf.currentMessage {
|
|
strongSelf.openActionOptions?(action, message)
|
|
} else {
|
|
|
|
}
|
|
}
|
|
|
|
self.textNode.arguments = TextNodeWithEntities.Arguments(
|
|
context: self.context,
|
|
cache: self.animationCache,
|
|
renderer: self.animationRenderer,
|
|
placeholderColor: defaultDarkPresentationTheme.list.mediaPlaceholderColor,
|
|
attemptSynchronous: false
|
|
)
|
|
self.textNode.visibility = true
|
|
|
|
let textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: defaultDarkPresentationTheme.list.itemAccentColor.withMultipliedAlpha(0.5), knob: defaultDarkPresentationTheme.list.itemAccentColor, isDark: true), strings: presentationData.strings, textNode: self.textNode, updateIsActive: { [weak self] value in
|
|
guard let self else {
|
|
return
|
|
}
|
|
let _ = self
|
|
}, present: { c, a in
|
|
present(c, a)
|
|
}, rootNode: { [weak self] in
|
|
guard let self else {
|
|
return nil
|
|
}
|
|
return self.controllerInteraction?.controller()?.displayNode
|
|
}, externalKnobSurface: self.textSelectionKnobSurface, performAction: { [weak self] text, action in
|
|
guard let self else {
|
|
return
|
|
}
|
|
|
|
switch action {
|
|
case .copy:
|
|
storeAttributedTextInPasteboard(text)
|
|
|
|
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
|
let undoController = UndoOverlayController(presentationData: presentationData, content: .copy(text: presentationData.strings.Conversation_TextCopied), elevatedLayout: false, animateInAsReplacement: false, appearance: UndoOverlayController.Appearance(isBlurred: true), action: { _ in true })
|
|
|
|
self.controllerInteraction?.presentController(undoController, nil)
|
|
case .share:
|
|
let theme = defaultDarkPresentationTheme
|
|
let updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>) = (self.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), self.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) })
|
|
|
|
let shareController = ShareController(context: self.context, subject: .text(text.string), externalShare: true, immediateExternalShare: false, updatedPresentationData: updatedPresentationData)
|
|
|
|
self.controllerInteraction?.presentController(shareController, nil)
|
|
case .lookup:
|
|
let controller = UIReferenceLibraryViewController(term: text.string)
|
|
if let window = self.controllerInteraction?.controller()?.view.window {
|
|
controller.popoverPresentationController?.sourceView = window
|
|
controller.popoverPresentationController?.sourceRect = CGRect(origin: CGPoint(x: window.bounds.width / 2.0, y: window.bounds.size.height - 1.0), size: CGSize(width: 1.0, height: 1.0))
|
|
window.rootViewController?.present(controller, animated: true)
|
|
}
|
|
case .speak:
|
|
if let speechHolder = speakText(context: self.context, text: text.string) {
|
|
speechHolder.completion = { [weak self, weak speechHolder] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
if self.currentSpeechHolder === speechHolder {
|
|
self.currentSpeechHolder = nil
|
|
}
|
|
}
|
|
self.currentSpeechHolder = speechHolder
|
|
}
|
|
case .translate:
|
|
let _ = (self.context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.translationSettings])
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { [weak self] sharedData in
|
|
guard let self else {
|
|
return
|
|
}
|
|
//let peer = component.slice.peer
|
|
|
|
let translationSettings: TranslationSettings
|
|
if let current = sharedData.entries[ApplicationSpecificSharedDataKeys.translationSettings]?.get(TranslationSettings.self) {
|
|
translationSettings = current
|
|
} else {
|
|
translationSettings = TranslationSettings.defaultSettings
|
|
}
|
|
|
|
/*var showTranslateIfTopical = false
|
|
if case let .channel(channel) = peer, !(channel.addressName ?? "").isEmpty {
|
|
showTranslateIfTopical = true
|
|
}*/
|
|
let showTranslateIfTopical = false
|
|
|
|
let (_, language) = canTranslateText(context: self.context, text: text.string, showTranslate: translationSettings.showTranslate, showTranslateIfTopical: showTranslateIfTopical, ignoredLanguages: translationSettings.ignoredLanguages)
|
|
|
|
let _ = ApplicationSpecificNotice.incrementTranslationSuggestion(accountManager: self.context.sharedContext.accountManager, timestamp: Int32(Date().timeIntervalSince1970)).start()
|
|
|
|
let translateController = TranslateScreen(context: self.context, forceTheme: defaultDarkPresentationTheme, text: text.string, canCopy: true, fromLanguage: language, ignoredLanguages: translationSettings.ignoredLanguages)
|
|
translateController.pushController = { [weak self] c in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.controllerInteraction?.pushController(c)
|
|
}
|
|
translateController.presentController = { [weak self] c in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.controllerInteraction?.presentController(c, nil)
|
|
}
|
|
|
|
//self.actionSheet = translateController
|
|
//view.updateIsProgressPaused()
|
|
|
|
/*translateController.wasDismissed = { [weak self, weak view] in
|
|
guard let self, let view else {
|
|
return
|
|
}
|
|
self.actionSheet = nil
|
|
view.updateIsProgressPaused()
|
|
}*/
|
|
|
|
//component.controller()?.present(translateController, in: .window(.root))
|
|
self.controllerInteraction?.presentController(translateController, nil)
|
|
})
|
|
case .quote:
|
|
break
|
|
}
|
|
})
|
|
|
|
textSelectionNode.enableLookup = true
|
|
textSelectionNode.canBeginSelection = { [weak self] location in
|
|
guard let self else {
|
|
return false
|
|
}
|
|
|
|
let textNode = self.textNode
|
|
|
|
let titleFrame = textNode.view.bounds
|
|
|
|
if let (index, attributes) = textNode.attributesAtPoint(CGPoint(x: location.x - titleFrame.minX, y: location.y - titleFrame.minY)) {
|
|
let _ = index
|
|
|
|
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Spoiler)] {
|
|
return false
|
|
} else if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
|
|
return false
|
|
} else if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Timecode)] {
|
|
return false
|
|
} else if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention {
|
|
return false
|
|
} else if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String {
|
|
return false
|
|
} else if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag {
|
|
return false
|
|
} else if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BankCard)] as? String {
|
|
return false
|
|
} else if let emoji = attributes[NSAttributedString.Key(rawValue: ChatTextInputAttributes.customEmoji.rawValue)] as? ChatTextInputTextCustomEmojiAttribute, let _ = emoji.file {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
self.textSelectionNode = textSelectionNode
|
|
|
|
self.contentNode.addSubnode(self.scrollWrapperNode)
|
|
self.scrollWrapperNode.addSubnode(self.scrollNode)
|
|
self.contentNode.view.addSubview(self.textSelectionKnobContainer)
|
|
self.scrollNode.addSubnode(textSelectionNode.highlightAreaNode)
|
|
self.scrollNode.addSubnode(self.textNode)
|
|
self.scrollNode.addSubnode(textSelectionNode)
|
|
|
|
//self.contentNode.addSubnode(self.backwardButton)
|
|
//self.contentNode.addSubnode(self.forwardButton)
|
|
//self.contentNode.addSubnode(self.playbackControlButton)
|
|
self.playbackControlButton.addSubnode(self.playPauseIconNode)
|
|
|
|
self.contentNode.addSubnode(self.statusNode)
|
|
self.contentNode.addSubnode(self.statusButtonNode)
|
|
|
|
/*self.deleteButton.addTarget(self, action: #selector(self.deleteButtonPressed), for: [.touchUpInside])
|
|
self.deleteButton.accessibilityTraits = [.button]
|
|
self.deleteButton.accessibilityLabel = presentationData.strings.Gallery_VoiceOver_Delete
|
|
|
|
self.fullscreenButton.addTarget(self, action: #selector(self.fullscreenButtonPressed), for: [.touchUpInside])
|
|
self.fullscreenButton.accessibilityTraits = [.button]
|
|
self.fullscreenButton.accessibilityLabel = presentationData.strings.Gallery_VoiceOver_Fullscreen
|
|
|
|
self.actionButton.addTarget(self, action: #selector(self.actionButtonPressed), for: [.touchUpInside])
|
|
self.actionButton.accessibilityTraits = [.button]
|
|
self.actionButton.accessibilityLabel = presentationData.strings.Gallery_VoiceOver_Share
|
|
|
|
self.editButton.addTarget(self, action: #selector(self.editButtonPressed), for: [.touchUpInside])
|
|
self.editButton.accessibilityTraits = [.button]
|
|
self.editButton.accessibilityLabel = presentationData.strings.Gallery_VoiceOver_Edit*/
|
|
|
|
self.backwardButton.addTarget(self, action: #selector(self.backwardButtonPressed), forControlEvents: .touchUpInside)
|
|
self.forwardButton.addTarget(self, action: #selector(self.forwardButtonPressed), forControlEvents: .touchUpInside)
|
|
self.playbackControlButton.addTarget(self, action: #selector(self.playbackControlPressed), forControlEvents: .touchUpInside)
|
|
|
|
self.statusButtonNode.highligthedChanged = { [weak self] highlighted in
|
|
if let strongSelf = self {
|
|
if highlighted {
|
|
strongSelf.statusNode.layer.removeAnimation(forKey: "opacity")
|
|
strongSelf.statusNode.alpha = 0.4
|
|
} else {
|
|
strongSelf.statusNode.alpha = 1.0
|
|
strongSelf.statusNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
|
}
|
|
}
|
|
}
|
|
self.statusButtonNode.addTarget(self, action: #selector(self.statusPressed), forControlEvents: .touchUpInside)
|
|
}
|
|
|
|
deinit {
|
|
self.messageContextDisposable.dispose()
|
|
self.codeHighlightState?.disposable.dispose()
|
|
}
|
|
|
|
override func didLoad() {
|
|
super.didLoad()
|
|
self.scrollNode.view.delegate = self.wrappedScrollViewDelegate
|
|
self.scrollNode.view.showsVerticalScrollIndicator = false
|
|
|
|
let backwardLongPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.seekBackwardLongPress(_:)))
|
|
backwardLongPressGestureRecognizer.minimumPressDuration = 0.3
|
|
self.backwardButton.view.addGestureRecognizer(backwardLongPressGestureRecognizer)
|
|
|
|
let forwardLongPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.seekForwardLongPress(_:)))
|
|
forwardLongPressGestureRecognizer.minimumPressDuration = 0.3
|
|
self.forwardButton.view.addGestureRecognizer(forwardLongPressGestureRecognizer)
|
|
}
|
|
|
|
private var wasPlaying: Bool?
|
|
@objc private func seekBackwardLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) {
|
|
switch gestureRecognizer.state {
|
|
case .began:
|
|
self.interacting?(true)
|
|
self.backwardButton.isPressing = true
|
|
self.wasPlaying = !self.currentIsPaused
|
|
if self.wasPlaying == true {
|
|
self.playbackControl?()
|
|
}
|
|
|
|
var time: Double = 0.0
|
|
let seekTimer = SwiftSignalKit.Timer(timeout: 0.1, repeat: true, completion: { [weak self] in
|
|
if let strongSelf = self {
|
|
var delta: Double = 0.8
|
|
if time >= 4.0 {
|
|
delta = 3.2
|
|
} else if time >= 2.0 {
|
|
delta = 1.6
|
|
}
|
|
time += 0.1
|
|
|
|
strongSelf.seekBackward?(delta)
|
|
}
|
|
}, queue: Queue.mainQueue())
|
|
self.seekTimer = seekTimer
|
|
seekTimer.start()
|
|
case .ended, .cancelled:
|
|
self.interacting?(false)
|
|
self.backwardButton.isPressing = false
|
|
self.seekTimer?.invalidate()
|
|
self.seekTimer = nil
|
|
if self.wasPlaying == true {
|
|
self.playbackControl?()
|
|
}
|
|
self.wasPlaying = nil
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
@objc private func seekForwardLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) {
|
|
switch gestureRecognizer.state {
|
|
case .began:
|
|
self.interacting?(true)
|
|
self.forwardButton.isPressing = true
|
|
self.wasPlaying = !self.currentIsPaused
|
|
if self.wasPlaying == false {
|
|
self.playbackControl?()
|
|
}
|
|
|
|
self.seekRate = 4.0
|
|
self.setPlayRate?(self.seekRate)
|
|
let seekTimer = SwiftSignalKit.Timer(timeout: 2.0, repeat: true, completion: { [weak self] in
|
|
if let strongSelf = self {
|
|
if strongSelf.seekRate == 4.0 {
|
|
strongSelf.seekRate = 8.0
|
|
}
|
|
strongSelf.setPlayRate?(strongSelf.seekRate)
|
|
if strongSelf.seekRate == 8.0 {
|
|
strongSelf.seekTimer?.invalidate()
|
|
strongSelf.seekTimer = nil
|
|
}
|
|
}
|
|
}, queue: Queue.mainQueue())
|
|
self.seekTimer = seekTimer
|
|
seekTimer.start()
|
|
case .ended, .cancelled:
|
|
self.interacting?(false)
|
|
self.forwardButton.isPressing = false
|
|
self.setPlayRate?(1.0)
|
|
self.seekTimer?.invalidate()
|
|
self.seekTimer = nil
|
|
|
|
if self.wasPlaying == false {
|
|
self.playbackControl?()
|
|
}
|
|
self.wasPlaying = nil
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
let result = super.hitTest(point, with: event)
|
|
|
|
if let textSelectionNode = self.textSelectionNode, result === textSelectionNode.view {
|
|
let localPoint = self.view.convert(point, to: textSelectionNode.view)
|
|
if !textSelectionNode.canBeginSelection(localPoint) {
|
|
if let result = self.textNode.view.hitTest(self.view.convert(point, to: self.textNode.view), with: event) {
|
|
return result
|
|
}
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
private func actionForAttributes(_ attributes: [NSAttributedString.Key: Any], _ index: Int) -> GalleryControllerInteractionTapAction? {
|
|
if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
|
|
var concealed = true
|
|
if let (attributeText, fullText) = self.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) {
|
|
concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText)
|
|
}
|
|
return .url(url: url, concealed: concealed, forceExternal: false, dismiss: true)
|
|
} else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention {
|
|
return .peerMention(peerMention.peerId, peerMention.mention)
|
|
} else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String {
|
|
return .textMention(peerName)
|
|
} else if let botCommand = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand)] as? String {
|
|
return .botCommand(botCommand)
|
|
} else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag {
|
|
return .hashtag(hashtag.peerName, hashtag.hashtag)
|
|
} else if let timecode = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Timecode)] as? TelegramTimecode {
|
|
return .timecode(timecode.time, timecode.text)
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func setup(origin: GalleryItemOriginData?, caption: NSAttributedString, isAd: Bool = false) {
|
|
var titleText = origin?.title
|
|
|
|
let caption = caption.mutableCopy() as! NSMutableAttributedString
|
|
if isAd {
|
|
if let titleText, !titleText.isEmpty {
|
|
caption.insert(NSAttributedString(string: titleText + "\n", font: Font.semibold(17.0), textColor: .white), at: 0)
|
|
}
|
|
titleText = nil
|
|
}
|
|
|
|
if origin == nil {
|
|
self.buttonsState = (
|
|
displayDeleteButton: false,
|
|
displayFullscreenButton: false,
|
|
displayActionButton: false,
|
|
displayEditButton: false,
|
|
displayPictureInPictureButton: false,
|
|
settingsButtonState: nil,
|
|
displayTextRecognitionButton: false,
|
|
displayStickersButton: false
|
|
)
|
|
}
|
|
|
|
if self.currentMessageText != caption {
|
|
self.currentMessageText = caption
|
|
|
|
if caption.length == 0 {
|
|
self.textNode.isHidden = true
|
|
self.textNode.attributedText = nil
|
|
} else {
|
|
self.textNode.isHidden = false
|
|
self.textNode.attributedText = caption
|
|
}
|
|
self.textSelectionNode?.isHidden = self.textNode.isHidden
|
|
|
|
self.requestLayout?(.immediate)
|
|
}
|
|
}
|
|
|
|
func setMessage(_ message: Message, displayInfo: Bool = true, translateToLanguage: String? = nil, peerIsCopyProtected: Bool = false, displayPictureInPictureButton: Bool = false, settingsButtonState: SettingsButtonState? = nil, displayTextRecognitionButton: Bool = false, displayStickersButton: Bool = false, animated: Bool = false) {
|
|
self.currentMessage = message
|
|
|
|
var displayInfo = displayInfo
|
|
if Namespaces.Message.allNonRegular.contains(message.id.namespace) || message.timestamp == 0 {
|
|
displayInfo = false
|
|
}
|
|
var canFullscreen = false
|
|
var canDelete: Bool
|
|
// MARK: - GLEGram - Allow sharing self-destructing messages if enabled
|
|
let canSaveSecretMedia: Bool
|
|
#if canImport(SGSimpleSettings)
|
|
canSaveSecretMedia = SGSimpleSettings.shared.enableSavingSelfDestructingMessages
|
|
#else
|
|
canSaveSecretMedia = false
|
|
#endif
|
|
var canShare = (!message.containsSecretMedia || canSaveSecretMedia) && !Namespaces.Message.allNonRegular.contains(message.id.namespace) && message.adAttribute == nil
|
|
|
|
var canEdit = false
|
|
var isImage = false
|
|
var isVideo = false
|
|
for media in message.media {
|
|
if media is TelegramMediaImage {
|
|
canEdit = true
|
|
isImage = true
|
|
} else if let media = media as? TelegramMediaFile, !media.isAnimated {
|
|
for attribute in media.attributes {
|
|
switch attribute {
|
|
case let .Video(_, dimensions, _, _, _, _):
|
|
isVideo = true
|
|
if dimensions.height > 0 {
|
|
if CGFloat(dimensions.width) / CGFloat(dimensions.height) > 1.33 {
|
|
canFullscreen = true
|
|
}
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
if !isVideo {
|
|
canEdit = true
|
|
isImage = true
|
|
}
|
|
} else if let media = media as? TelegramMediaWebpage, case let .Loaded(content) = media.content {
|
|
let type = webEmbedType(content: content)
|
|
switch type {
|
|
case .youtube, .vimeo:
|
|
canFullscreen = true
|
|
default:
|
|
break
|
|
}
|
|
if let file = content.file, !file.isAnimated, file.isVideo {
|
|
canFullscreen = true
|
|
}
|
|
if content.type == "photo", let _ = content.image {
|
|
canEdit = true
|
|
}
|
|
}
|
|
}
|
|
|
|
canEdit = canEdit && (!message.containsSecretMedia || canSaveSecretMedia)
|
|
if let peer = message.peers[message.id.peerId] {
|
|
if peer is TelegramUser || peer is TelegramSecretChat {
|
|
canDelete = true
|
|
} else if let _ = peer as? TelegramGroup {
|
|
canDelete = true
|
|
} else if let channel = peer as? TelegramChannel {
|
|
if message.flags.contains(.Incoming) {
|
|
canDelete = channel.hasPermission(.deleteAllMessages)
|
|
if canEdit {
|
|
if isImage {
|
|
if !channel.hasPermission(.sendPhoto) {
|
|
canEdit = false
|
|
}
|
|
} else if isVideo {
|
|
if !channel.hasPermission(.sendVideo) {
|
|
canEdit = false
|
|
}
|
|
} else {
|
|
canEdit = false
|
|
}
|
|
}
|
|
} else {
|
|
canDelete = true
|
|
}
|
|
} else {
|
|
canDelete = false
|
|
canEdit = false
|
|
}
|
|
} else {
|
|
canDelete = false
|
|
canShare = false
|
|
canEdit = false
|
|
}
|
|
|
|
// MARK: - GLEGram - Allow saving protected content and self-destructing messages if enabled
|
|
let canSaveProtectedContent: Bool
|
|
#if canImport(SGSimpleSettings)
|
|
canSaveProtectedContent = SGSimpleSettings.shared.enableSavingProtectedContent || SGSimpleSettings.shared.enableSavingSelfDestructingMessages
|
|
#else
|
|
canSaveProtectedContent = false
|
|
#endif
|
|
|
|
let canSaveSelfDestructing: Bool
|
|
#if canImport(SGSimpleSettings)
|
|
canSaveSelfDestructing = SGSimpleSettings.shared.enableSavingSelfDestructingMessages
|
|
#else
|
|
canSaveSelfDestructing = false
|
|
#endif
|
|
|
|
let isCopyProtected = message.isCopyProtected() || peerIsCopyProtected || message.paidContent != nil
|
|
let isSelfDestructing = message.containsSecretMedia
|
|
|
|
if isCopyProtected && !canSaveProtectedContent {
|
|
canShare = false
|
|
canEdit = false
|
|
}
|
|
|
|
if isSelfDestructing && !canSaveSelfDestructing {
|
|
canShare = false
|
|
canEdit = false
|
|
canDelete = false
|
|
} else if isSelfDestructing && canSaveSelfDestructing {
|
|
if !isCopyProtected || canSaveProtectedContent {
|
|
canShare = true
|
|
}
|
|
}
|
|
// MARK: - End GLEGram
|
|
|
|
if let _ = message.adAttribute {
|
|
displayInfo = false
|
|
canFullscreen = false
|
|
}
|
|
|
|
var authorNameText: String?
|
|
if let forwardInfo = message.forwardInfo, forwardInfo.flags.contains(.isImported), let authorSignature = forwardInfo.authorSignature {
|
|
authorNameText = authorSignature
|
|
} else if let author = message.effectiveAuthor {
|
|
authorNameText = EnginePeer(author).displayTitle(strings: self.strings, displayOrder: self.nameOrder)
|
|
} else if let peer = message.peers[message.id.peerId] {
|
|
authorNameText = EnginePeer(peer).displayTitle(strings: self.strings, displayOrder: self.nameOrder)
|
|
}
|
|
|
|
var messageText = NSMutableAttributedString(string: "")
|
|
var hasCaption = false
|
|
var mediaDuration: Double?
|
|
for media in message.media {
|
|
if media is TelegramMediaPaidContent {
|
|
hasCaption = true
|
|
} else if media is TelegramMediaImage {
|
|
hasCaption = true
|
|
} else if let file = media as? TelegramMediaFile {
|
|
hasCaption = file.mimeType.hasPrefix("image/") || file.mimeType.hasPrefix("video/")
|
|
mediaDuration = file.duration
|
|
} else if media is TelegramMediaInvoice {
|
|
hasCaption = true
|
|
}
|
|
}
|
|
if hasCaption {
|
|
var entities: [MessageTextEntity] = []
|
|
for attribute in message.attributes {
|
|
if let attribute = attribute as? TextEntitiesMessageAttribute {
|
|
entities = attribute.entities
|
|
break
|
|
}
|
|
}
|
|
var text = message.text
|
|
if let result = addLocallyGeneratedEntities(text, enabledTypes: [.timecode], entities: entities, mediaDuration: mediaDuration) {
|
|
entities = result
|
|
}
|
|
if let translateToLanguage, !text.isEmpty {
|
|
for attribute in message.attributes {
|
|
if let attribute = attribute as? TranslationMessageAttribute, !attribute.text.isEmpty, attribute.toLang == translateToLanguage {
|
|
text = attribute.text
|
|
entities = attribute.entities
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
let codeHighlightSpecs = extractMessageSyntaxHighlightSpecs(text: text, entities: entities)
|
|
var cachedMessageSyntaxHighlight: CachedMessageSyntaxHighlight?
|
|
|
|
if !codeHighlightSpecs.isEmpty {
|
|
for attribute in message.attributes {
|
|
if let attribute = attribute as? DerivedDataMessageAttribute {
|
|
if let value = attribute.data["code"]?.get(CachedMessageSyntaxHighlight.self) {
|
|
cachedMessageSyntaxHighlight = value
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if !codeHighlightSpecs.isEmpty {
|
|
if let current = self.codeHighlightState, current.id == message.id, current.specs == codeHighlightSpecs {
|
|
} else {
|
|
if let codeHighlightState = self.codeHighlightState {
|
|
self.codeHighlightState = nil
|
|
codeHighlightState.disposable.dispose()
|
|
}
|
|
|
|
let disposable = MetaDisposable()
|
|
self.codeHighlightState = (message.id, codeHighlightSpecs, disposable)
|
|
disposable.set(asyncUpdateMessageSyntaxHighlight(engine: self.context.engine, messageId: message.id, current: cachedMessageSyntaxHighlight, specs: codeHighlightSpecs).startStrict(completed: {
|
|
}))
|
|
}
|
|
} else if let codeHighlightState = self.codeHighlightState {
|
|
self.codeHighlightState = nil
|
|
codeHighlightState.disposable.dispose()
|
|
}
|
|
|
|
messageText = galleryCaptionStringWithAppliedEntities(context: self.context, text: text, entities: entities, message: message, cachedMessageSyntaxHighlight: cachedMessageSyntaxHighlight).mutableCopy() as! NSMutableAttributedString
|
|
if let _ = message.adAttribute {
|
|
messageText.insert(NSAttributedString(string: (authorNameText ?? "") + "\n", font: Font.semibold(17.0), textColor: .white), at: 0)
|
|
}
|
|
}
|
|
|
|
if !displayInfo {
|
|
canEdit = false
|
|
}
|
|
|
|
var displayDeleteButton = false
|
|
var displayFullscreenButton = false
|
|
var displayActionButton = false
|
|
var displayEditButton = false
|
|
|
|
do {
|
|
self.currentMessageText = messageText
|
|
|
|
if messageText.length == 0 {
|
|
self.textNode.isHidden = true
|
|
self.textNode.attributedText = nil
|
|
} else {
|
|
self.textNode.isHidden = false
|
|
self.textNode.attributedText = messageText
|
|
}
|
|
|
|
self.textSelectionNode?.isHidden = self.textNode.isHidden
|
|
|
|
if canFullscreen {
|
|
displayFullscreenButton = true
|
|
}
|
|
displayDeleteButton = canDelete
|
|
|
|
displayActionButton = canShare
|
|
displayEditButton = canEdit
|
|
|
|
if let adAttribute = message.adAttribute {
|
|
if self.buttonNode == nil {
|
|
let buttonNode = SolidRoundedButtonNode(title: adAttribute.buttonText, theme: SolidRoundedButtonTheme(backgroundColor: UIColor(rgb: 0xffffff, alpha: 0.15), foregroundColor: UIColor(rgb: 0xffffff)), height: 50.0, cornerRadius: 11.0)
|
|
buttonNode.pressed = { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.performAction?(.ad(message.id))
|
|
}
|
|
self.contentNode.addSubnode(buttonNode)
|
|
self.buttonNode = buttonNode
|
|
|
|
if !isTelegramMeLink(adAttribute.url) {
|
|
let buttonIconNode = ASImageNode()
|
|
buttonIconNode.displaysAsynchronously = false
|
|
buttonIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotLink"), color: .white)
|
|
buttonNode.addSubnode(buttonIconNode)
|
|
self.buttonIconNode = buttonIconNode
|
|
}
|
|
}
|
|
} else if let buttonNode = self.buttonNode {
|
|
buttonNode.removeFromSupernode()
|
|
}
|
|
|
|
self.buttonsState = (
|
|
displayDeleteButton: displayDeleteButton,
|
|
displayFullscreenButton: displayFullscreenButton,
|
|
displayActionButton: displayActionButton,
|
|
displayEditButton: displayEditButton,
|
|
displayPictureInPictureButton: displayPictureInPictureButton,
|
|
settingsButtonState: settingsButtonState,
|
|
displayTextRecognitionButton: displayTextRecognitionButton,
|
|
displayStickersButton: displayStickersButton
|
|
)
|
|
|
|
self.requestLayout?(animated ? .animated(duration: 0.4, curve: .spring) : .immediate)
|
|
}
|
|
}
|
|
|
|
private func updateSpoilers(textFrame: CGRect) {
|
|
if let textLayout = self.textNode.cachedLayout, !textLayout.spoilers.isEmpty {
|
|
if self.spoilerTextNode == nil {
|
|
let spoilerTextNode = ImmediateTextNodeWithEntities()
|
|
spoilerTextNode.displaySpoilerEffect = false
|
|
spoilerTextNode.attributedText = textNode.attributedText
|
|
spoilerTextNode.maximumNumberOfLines = 0
|
|
spoilerTextNode.linkHighlightColor = UIColor(rgb: 0x5ac8fa, alpha: 0.2)
|
|
spoilerTextNode.displaySpoilers = true
|
|
spoilerTextNode.isHidden = false
|
|
spoilerTextNode.alpha = 0.0
|
|
spoilerTextNode.isUserInteractionEnabled = false
|
|
|
|
self.spoilerTextNode = spoilerTextNode
|
|
self.textNode.supernode?.insertSubnode(spoilerTextNode, aboveSubnode: self.textNode)
|
|
|
|
let dustNode = InvisibleInkDustNode(textNode: spoilerTextNode, enableAnimations: self.context.sharedContext.energyUsageSettings.fullTranslucency)
|
|
self.dustNode = dustNode
|
|
if let textSelectionNode = self.textSelectionNode {
|
|
spoilerTextNode.supernode?.insertSubnode(dustNode, aboveSubnode: textSelectionNode)
|
|
} else {
|
|
spoilerTextNode.supernode?.insertSubnode(dustNode, aboveSubnode: spoilerTextNode)
|
|
}
|
|
}
|
|
if let dustNode = self.dustNode {
|
|
dustNode.update(size: textFrame.size, color: .white, textColor: .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 {
|
|
if let spoilerTextNode = self.spoilerTextNode {
|
|
self.spoilerTextNode = nil
|
|
spoilerTextNode.removeFromSupernode()
|
|
}
|
|
if let dustNode = self.dustNode {
|
|
self.dustNode = nil
|
|
dustNode.removeFromSupernode()
|
|
}
|
|
}
|
|
}
|
|
|
|
func setWebPage(_ webPage: TelegramMediaWebpage, media: Media) {
|
|
self.currentWebPageAndMedia = (webPage, media)
|
|
}
|
|
|
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
self.textSelectionKnobContainer.frame = CGRect(origin: CGPoint(x: 0.0, y: -scrollView.bounds.origin.y), size: CGSize())
|
|
|
|
self.requestLayout?(.immediate)
|
|
}
|
|
|
|
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
|
self.hasExpandedCaptionPromise.set(true)
|
|
}
|
|
|
|
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
|
if !decelerate {
|
|
self.hasExpandedCaptionPromise.set(scrollView.contentOffset.y > 1.0)
|
|
}
|
|
}
|
|
|
|
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
|
self.hasExpandedCaptionPromise.set(scrollView.contentOffset.y > 1.0)
|
|
}
|
|
|
|
override func updateLayout(size: CGSize, metrics: LayoutMetrics, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, contentInset: CGFloat, transition: ContainedViewLayoutTransition) -> LayoutInfo {
|
|
self.validLayout = (size, metrics, leftInset, rightInset, bottomInset, contentInset)
|
|
|
|
let width = size.width
|
|
var panelHeight = 54.0 + bottomInset
|
|
panelHeight += contentInset
|
|
|
|
let isLandscape = size.width > size.height
|
|
|
|
let displayCaption: Bool
|
|
if case .compact = metrics.widthClass {
|
|
displayCaption = !self.textNode.isHidden && !isLandscape
|
|
} else {
|
|
displayCaption = !self.textNode.isHidden
|
|
}
|
|
|
|
var buttonPanelInsets = UIEdgeInsets()
|
|
buttonPanelInsets.left = 8.0
|
|
buttonPanelInsets.right = 8.0
|
|
buttonPanelInsets.bottom = bottomInset + 8.0
|
|
if bottomInset <= 32.0 {
|
|
buttonPanelInsets.left += 18.0
|
|
buttonPanelInsets.right += 18.0
|
|
}
|
|
|
|
var needsShadow = false
|
|
if !self.textNode.isHidden {
|
|
var textFrame = CGRect()
|
|
var visibleTextHeight: CGFloat = 0.0
|
|
|
|
let sideInset: CGFloat = 16.0 + leftInset
|
|
let topInset: CGFloat = 8.0
|
|
let textBottomInset: CGFloat = 8.0 + 14.0
|
|
|
|
let constrainSize = CGSize(width: width - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude)
|
|
let textSize = self.textNode.updateLayout(constrainSize)
|
|
|
|
var textOffset: CGFloat = 0.0
|
|
var additionalTextHeight: CGFloat = 0.0
|
|
if displayCaption {
|
|
needsShadow = true
|
|
|
|
visibleTextHeight = textSize.height
|
|
if visibleTextHeight > 100.0 {
|
|
visibleTextHeight = 80.0
|
|
self.scrollNode.view.isScrollEnabled = true
|
|
} else {
|
|
self.scrollNode.view.isScrollEnabled = false
|
|
}
|
|
|
|
let visibleTextPanelHeight = visibleTextHeight + topInset + textBottomInset
|
|
let scrollViewContentSize = CGSize(width: width, height: textSize.height + topInset + textBottomInset)
|
|
if self.scrollNode.view.contentSize != scrollViewContentSize {
|
|
self.scrollNode.view.contentSize = scrollViewContentSize
|
|
}
|
|
let scrollNodeFrame = CGRect(x: 0.0, y: 0.0, width: width, height: visibleTextPanelHeight)
|
|
if self.scrollNode.frame != scrollNodeFrame {
|
|
self.scrollNode.frame = scrollNodeFrame
|
|
}
|
|
|
|
var maxTextOffset: CGFloat = size.height - bottomInset - 238.0 - UIScreenPixel
|
|
if let _ = self.scrubberView {
|
|
//maxTextOffset -= 100.0
|
|
maxTextOffset += 0.0
|
|
additionalTextHeight += 44.0
|
|
}
|
|
textOffset = min(maxTextOffset, self.scrollNode.view.contentOffset.y)
|
|
let originalPanelHeight = panelHeight
|
|
panelHeight = max(0.0, panelHeight + visibleTextPanelHeight + textOffset)
|
|
|
|
if self.scrollNode.view.isScrollEnabled {
|
|
if self.scrollWrapperNode.view.mask == nil {
|
|
self.scrollWrapperNode.view.mask = self.scrollWrapperMask
|
|
}
|
|
} else {
|
|
self.scrollWrapperNode.view.mask = nil
|
|
}
|
|
|
|
let scrollWrapperNodeFrame = CGRect(x: 0.0, y: 0.0, width: width, height: max(0.0, visibleTextPanelHeight + textOffset + originalPanelHeight - 14.0 + additionalTextHeight))
|
|
if self.scrollWrapperNode.frame != scrollWrapperNodeFrame {
|
|
self.scrollWrapperNode.frame = scrollWrapperNodeFrame
|
|
do {
|
|
let mask = self.scrollWrapperMask
|
|
mask.update(size: self.scrollWrapperNode.bounds.size, color: UIColor.blue, gradientHeight: 90.0 + additionalTextHeight + contentInset, extensionHeight: 0.0, transition: ComponentTransition(transition))
|
|
mask.frame = self.scrollWrapperNode.bounds
|
|
mask.layer.removeAllAnimations()
|
|
}
|
|
self.scrollWrapperEffect.update(size: CGSize(width: scrollWrapperNodeFrame.width, height: scrollWrapperNodeFrame.height), constantHeight: 90.0, placement: VariableBlurEffect.Placement(position: .bottom, inwardsExtension: additionalTextHeight + contentInset), gradient: EdgeEffectView.generateEdgeGradientData(baseHeight: 90.0), transition: transition)
|
|
}
|
|
|
|
if let buttonNode = self.buttonNode {
|
|
let buttonHeight = buttonNode.updateLayout(width: constrainSize.width, transition: transition)
|
|
transition.updateFrame(node: buttonNode, frame: CGRect(origin: CGPoint(x: sideInset, y: scrollWrapperNodeFrame.maxY + 8.0), size: CGSize(width: constrainSize.width, height: buttonHeight)))
|
|
|
|
if let buttonIconNode = self.buttonIconNode, let icon = buttonIconNode.image {
|
|
transition.updateFrame(node: buttonIconNode, frame: CGRect(origin: CGPoint(x: constrainSize.width - icon.size.width - 9.0, y: 9.0), size: icon.size))
|
|
}
|
|
|
|
if let _ = self.scrubberView {
|
|
panelHeight += 68.0
|
|
} else {
|
|
panelHeight += 22.0
|
|
}
|
|
}
|
|
}
|
|
textFrame = CGRect(origin: CGPoint(x: sideInset, y: topInset + textOffset), size: textSize)
|
|
|
|
self.updateSpoilers(textFrame: textFrame)
|
|
|
|
let _ = self.spoilerTextNode?.updateLayout(constrainSize)
|
|
|
|
if self.textNode.frame != textFrame {
|
|
self.textNode.frame = textFrame
|
|
self.spoilerTextNode?.frame = textFrame
|
|
|
|
self.textSelectionNode?.frame = textFrame
|
|
self.textSelectionNode?.highlightAreaNode.frame = textFrame
|
|
self.textSelectionKnobSurface.frame = textFrame
|
|
}
|
|
}
|
|
|
|
if let scrubberView = self.scrubberView, scrubberView.superview == self.view {
|
|
panelHeight += 10.0
|
|
if isLandscape {
|
|
panelHeight += 34.0
|
|
} else {
|
|
panelHeight += 34.0
|
|
}
|
|
|
|
var scrubberY: CGFloat = 0.0
|
|
if self.textNode.isHidden || !displayCaption {
|
|
panelHeight += 8.0
|
|
} else {
|
|
scrubberY = panelHeight - buttonPanelInsets.bottom - 44.0 - 44.0 - 10.0
|
|
if contentInset > 0.0 {
|
|
scrubberY -= contentInset
|
|
}
|
|
}
|
|
|
|
if let _ = self.buttonNode {
|
|
panelHeight -= 44.0
|
|
}
|
|
|
|
let scrubberFrame = CGRect(origin: CGPoint(x: buttonPanelInsets.left, y: scrubberY), size: CGSize(width: width - buttonPanelInsets.left - buttonPanelInsets.right, height: 44.0))
|
|
scrubberView.updateLayout(size: scrubberFrame.size, leftInset: 0.0, rightInset: 0.0, isCollapsed: self.visibilityAlpha < 1.0, transition: transition)
|
|
transition.updateBounds(layer: scrubberView.layer, bounds: CGRect(origin: CGPoint(), size: scrubberFrame.size))
|
|
transition.updatePosition(layer: scrubberView.layer, position: CGPoint(x: scrubberFrame.midX, y: scrubberFrame.midY))
|
|
}
|
|
transition.updateAlpha(node: self.scrollWrapperNode, alpha: displayCaption ? 1.0 : 0.0)
|
|
|
|
var leftControlItems: [GlassControlGroupComponent.Item] = []
|
|
var rightControlItems: [GlassControlGroupComponent.Item] = []
|
|
var centerControlItems: [GlassControlGroupComponent.Item] = []
|
|
|
|
if let buttonsState = self.buttonsState {
|
|
if buttonsState.displayActionButton {
|
|
leftControlItems.append(GlassControlGroupComponent.Item(
|
|
id: AnyHashable("forward"),
|
|
content: .icon("Chat/Input/Accessory Panels/MessageSelectionForward"),
|
|
action: { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.actionButtonPressed()
|
|
}
|
|
))
|
|
}
|
|
if buttonsState.displayPictureInPictureButton {
|
|
centerControlItems.append(GlassControlGroupComponent.Item(
|
|
id: AnyHashable("pip"),
|
|
content: .icon("Media Gallery/PictureInPictureButton"),
|
|
action: { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.pipButtonPressed()
|
|
}
|
|
))
|
|
}
|
|
if let settingsButtonState = buttonsState.settingsButtonState {
|
|
centerControlItems.append(GlassControlGroupComponent.Item(
|
|
id: AnyHashable("settings"),
|
|
content: .customIcon(id: AnyHashable("settings"), component: AnyComponent(SettingsNavigationIconComponent(
|
|
speed: settingsButtonState.speed,
|
|
quality: settingsButtonState.quality,
|
|
isOpen: false
|
|
))),
|
|
action: { [weak self] in
|
|
guard let self, let buttonPanelView = self.buttonPanel.view as? GlassControlPanelComponent.View else {
|
|
return
|
|
}
|
|
if let centerItemView = buttonPanelView.centerItemView, let itemView = centerItemView.itemView(id: AnyHashable("settings")) {
|
|
self.settingsButtonPressed(sourceView: itemView)
|
|
} else if let rightItemView = buttonPanelView.rightItemView, let itemView = rightItemView.itemView(id: AnyHashable("settings")) {
|
|
self.settingsButtonPressed(sourceView: itemView)
|
|
}
|
|
}
|
|
))
|
|
}
|
|
if buttonsState.displayEditButton {
|
|
centerControlItems.append(GlassControlGroupComponent.Item(
|
|
id: AnyHashable("edit"),
|
|
content: .icon("Media Gallery/Draw"),
|
|
action: { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.editButtonPressed()
|
|
}
|
|
))
|
|
}
|
|
if buttonsState.displayTextRecognitionButton {
|
|
centerControlItems.append(GlassControlGroupComponent.Item(
|
|
id: AnyHashable("textRecognition"),
|
|
content: .icon("Media Gallery/LiveTextIcon"),
|
|
action: { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.textRecognitionButtonPressed()
|
|
}
|
|
))
|
|
}
|
|
if buttonsState.displayStickersButton {
|
|
centerControlItems.append(GlassControlGroupComponent.Item(
|
|
id: AnyHashable("stickers"),
|
|
content: .icon("Media Gallery/Stickers"),
|
|
action: { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.stickersButtonPressed()
|
|
}
|
|
))
|
|
}
|
|
if buttonsState.displayFullscreenButton && !metrics.isTablet {
|
|
centerControlItems.append(GlassControlGroupComponent.Item(
|
|
id: AnyHashable("fullscreen"),
|
|
content: .icon(isLandscape ? "Chat/Context Menu/Collapse" : "Chat/Context Menu/Expand"),
|
|
action: { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.fullscreenButtonPressed()
|
|
}
|
|
))
|
|
}
|
|
if buttonsState.displayDeleteButton {
|
|
rightControlItems.append(GlassControlGroupComponent.Item(
|
|
id: AnyHashable("delete"),
|
|
content: .icon("Chat/Input/Accessory Panels/MessageSelectionTrash"),
|
|
action: { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.deleteButtonPressed()
|
|
}
|
|
))
|
|
}
|
|
}
|
|
|
|
if !centerControlItems.isEmpty && rightControlItems.isEmpty {
|
|
rightControlItems = centerControlItems
|
|
centerControlItems = []
|
|
}
|
|
|
|
let buttonPanelSize = self.buttonPanel.update(
|
|
transition: ComponentTransition(transition),
|
|
component: AnyComponent(GlassControlPanelComponent(
|
|
theme: defaultDarkColorPresentationTheme,
|
|
leftItem: leftControlItems.isEmpty ? nil : GlassControlPanelComponent.Item(
|
|
items: leftControlItems,
|
|
background: .panel
|
|
),
|
|
centralItem: centerControlItems.isEmpty ? nil : GlassControlPanelComponent.Item(
|
|
items: centerControlItems,
|
|
background: .panel
|
|
),
|
|
rightItem: rightControlItems.isEmpty ? nil : GlassControlPanelComponent.Item(
|
|
items: rightControlItems,
|
|
background: .panel
|
|
),
|
|
centerAlignmentIfPossible: true
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: size.width - buttonPanelInsets.left - buttonPanelInsets.right, height: 44.0)
|
|
)
|
|
let buttonPanelFrame = CGRect(origin: CGPoint(x: buttonPanelInsets.left, y: panelHeight - buttonPanelInsets.bottom - buttonPanelSize.height), size: buttonPanelSize)
|
|
if let buttonPanelView = self.buttonPanel.view {
|
|
if buttonPanelView.superview == nil {
|
|
self.contentNode.view.addSubview(buttonPanelView)
|
|
}
|
|
ComponentTransition(transition).setFrame(view: buttonPanelView, frame: buttonPanelFrame)
|
|
}
|
|
|
|
if let image = self.backwardButton.backgroundIconNode.image {
|
|
self.backwardButton.frame = CGRect(origin: CGPoint(x: floor((width - image.size.width) / 2.0) - 66.0, y: panelHeight - buttonPanelInsets.bottom - 44.0 + floorToScreenPixels((44.0 - image.size.height) * 0.5)), size: image.size)
|
|
}
|
|
if let image = self.forwardButton.backgroundIconNode.image {
|
|
self.forwardButton.frame = CGRect(origin: CGPoint(x: floor((width - image.size.width) / 2.0) + 66.0, y: panelHeight - buttonPanelInsets.bottom - 44.0 + floorToScreenPixels((44.0 - image.size.height) * 0.5)), size: image.size)
|
|
}
|
|
|
|
self.playbackControlButton.frame = CGRect(origin: CGPoint(x: floor((width - 44.0) / 2.0), y: panelHeight - buttonPanelInsets.bottom - 44.0 + floorToScreenPixels((44.0 - 44.0) * 0.5)), size: CGSize(width: 44.0, height: 44.0))
|
|
self.playPauseIconNode.frame = self.playbackControlButton.bounds.offsetBy(dx: 2.0, dy: -2.0)
|
|
|
|
let statusSize = CGSize(width: 28.0, height: 28.0)
|
|
transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: floor((width - statusSize.width) / 2.0), y: panelHeight - bottomInset - statusSize.height - 8.0), size: statusSize))
|
|
|
|
self.statusButtonNode.frame = CGRect(origin: CGPoint(x: floor((width - 44.0) / 2.0), y: panelHeight - bottomInset - 44.0), size: CGSize(width: 44.0, height: 44.0))
|
|
|
|
if let (videoFramePreviewNode, videoFrameTextNode) = self.videoFramePreviewNode {
|
|
let intrinsicImageSize = videoFramePreviewNode.image?.size ?? CGSize(width: 320.0, height: 240.0)
|
|
let fitSize: CGSize
|
|
if intrinsicImageSize.width < intrinsicImageSize.height {
|
|
fitSize = CGSize(width: 90.0, height: 160.0)
|
|
} else {
|
|
fitSize = CGSize(width: 160.0, height: 90.0)
|
|
}
|
|
let scrubberInset: CGFloat
|
|
if size.width > size.height {
|
|
scrubberInset = 58.0
|
|
} else {
|
|
scrubberInset = 14.0 + 8.0
|
|
}
|
|
|
|
let imageSize = intrinsicImageSize.aspectFitted(fitSize)
|
|
var imageFrame = CGRect(origin: CGPoint(x: leftInset + scrubberInset + floor(self.scrubbingHandleRelativePosition * (width - leftInset - rightInset - scrubberInset * 2.0) - imageSize.width / 2.0), y: self.scrollNode.frame.minY - 6.0 - imageSize.height), size: imageSize)
|
|
imageFrame.origin.x = min(imageFrame.origin.x, width - rightInset - 10.0 - imageSize.width)
|
|
imageFrame.origin.x = max(imageFrame.origin.x, leftInset + 10.0)
|
|
|
|
videoFramePreviewNode.frame = imageFrame
|
|
videoFramePreviewNode.subnodes?.first?.frame = CGRect(origin: CGPoint(), size: imageFrame.size)
|
|
|
|
let textOffset = (Int((imageFrame.size.width - videoFrameTextNode.bounds.width) / 2) / 2) * 2
|
|
videoFrameTextNode.frame = CGRect(origin: CGPoint(x: CGFloat(textOffset), y: imageFrame.size.height - videoFrameTextNode.bounds.height - 5.0), size: videoFrameTextNode.bounds.size)
|
|
}
|
|
|
|
self.contentNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: panelHeight))
|
|
|
|
return LayoutInfo(height: panelHeight, needsShadow: needsShadow)
|
|
}
|
|
|
|
override func animateIn(transition: ContainedViewLayoutTransition) {
|
|
self.contentNode.alpha = 0.0
|
|
transition.updateAlpha(node: self.contentNode, alpha: self.visibilityAlpha)
|
|
}
|
|
|
|
override func animateIn(fromHeight: CGFloat, previousContentNode: GalleryFooterContentNode, transition: ContainedViewLayoutTransition) {
|
|
if let scrubberView = self.scrubberView, scrubberView.superview == self.view {
|
|
if let previousContentNode = previousContentNode as? ChatItemGalleryFooterContentNode, previousContentNode.scrubberView != nil {
|
|
} else {
|
|
transition.animatePositionAdditive(layer: scrubberView.layer, offset: CGPoint(x: 0.0, y: self.bounds.height - fromHeight))
|
|
}
|
|
scrubberView.alpha = 1.0
|
|
if transition.isAnimated {
|
|
scrubberView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
|
}
|
|
}
|
|
transition.animatePositionAdditive(node: self.scrollWrapperNode, offset: CGPoint(x: 0.0, y: self.bounds.height - fromHeight))
|
|
self.scrollNode.alpha = 0.0
|
|
ComponentTransition(transition).setAlpha(view: self.scrollNode.view, alpha: 1.0)
|
|
if let buttonPanelView = self.buttonPanel.view {
|
|
buttonPanelView.alpha = 1.0
|
|
}
|
|
|
|
self.backwardButton.alpha = self.hasSeekControls ? 1.0 : 0.0
|
|
self.forwardButton.alpha = self.hasSeekControls ? 1.0 : 0.0
|
|
self.statusNode.alpha = 1.0
|
|
self.playbackControlButton.alpha = 1.0
|
|
self.buttonNode?.alpha = 1.0
|
|
}
|
|
|
|
override func animateOut(transition: ContainedViewLayoutTransition) {
|
|
transition.updateAlpha(node: self.contentNode, alpha: 0.0)
|
|
}
|
|
|
|
override func animateOut(toHeight: CGFloat, nextContentNode: GalleryFooterContentNode, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
|
|
if let scrubberView = self.scrubberView, scrubberView.superview == self.view {
|
|
if let nextContentNode = nextContentNode as? ChatItemGalleryFooterContentNode, nextContentNode.scrubberView != nil {
|
|
} else {
|
|
transition.updateFrame(view: scrubberView, frame: scrubberView.frame.offsetBy(dx: 0.0, dy: self.bounds.height - toHeight))
|
|
}
|
|
scrubberView.alpha = 0.0
|
|
scrubberView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15)
|
|
}
|
|
transition.updateFrame(node: self.scrollWrapperNode, frame: self.scrollWrapperNode.frame.offsetBy(dx: 0.0, dy: self.bounds.height - toHeight))
|
|
ComponentTransition(transition).setAlpha(view: self.scrollNode.view, alpha: 0.0, completion: { _ in
|
|
completion()
|
|
})
|
|
|
|
if let buttonPanelView = self.buttonPanel.view {
|
|
buttonPanelView.alpha = 0.0
|
|
}
|
|
|
|
self.backwardButton.alpha = 0.0
|
|
self.forwardButton.alpha = 0.0
|
|
self.statusNode.alpha = 0.0
|
|
self.playbackControlButton.alpha = 0.0
|
|
self.buttonNode?.alpha = 0.0
|
|
}
|
|
|
|
@objc func fullscreenButtonPressed() {
|
|
self.toggleFullscreen?()
|
|
}
|
|
|
|
@objc func deleteButtonPressed() {
|
|
if let currentMessage = self.currentMessage {
|
|
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Messages.MessageGroup(id: currentMessage.id))
|
|
|> deliverOnMainQueue).start(next: { [weak self] messages in
|
|
if let strongSelf = self, !messages.isEmpty {
|
|
if messages.count == 1 {
|
|
strongSelf.commitDeleteMessages(messages, ask: true)
|
|
} else {
|
|
strongSelf.interacting?(true)
|
|
|
|
var presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
|
if !presentationData.theme.overallDarkAppearance {
|
|
presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)
|
|
}
|
|
|
|
var generalMessageContentKind: MessageContentKind?
|
|
for message in messages {
|
|
let currentKind = messageContentKind(contentSettings: strongSelf.context.currentContentSettings.with { $0 }, message: message, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat, accountPeerId: strongSelf.context.account.peerId)
|
|
if generalMessageContentKind == nil || generalMessageContentKind == currentKind {
|
|
generalMessageContentKind = currentKind
|
|
} else {
|
|
generalMessageContentKind = nil
|
|
break
|
|
}
|
|
}
|
|
|
|
var singleText = presentationData.strings.Media_ShareItem(1)
|
|
var multipleText = presentationData.strings.Media_ShareItem(Int32(messages.count))
|
|
|
|
if let generalMessageContentKind = generalMessageContentKind {
|
|
switch generalMessageContentKind {
|
|
case .image:
|
|
singleText = presentationData.strings.Media_ShareThisPhoto
|
|
multipleText = presentationData.strings.Media_SharePhoto(Int32(messages.count))
|
|
case .video:
|
|
singleText = presentationData.strings.Media_ShareThisVideo
|
|
multipleText = presentationData.strings.Media_ShareVideo(Int32(messages.count))
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
let deleteAction: ([EngineMessage]) -> Void = { messages in
|
|
if let strongSelf = self {
|
|
strongSelf.commitDeleteMessages(messages, ask: false)
|
|
}
|
|
}
|
|
|
|
let actionSheet = ActionSheetController(presentationData: presentationData)
|
|
actionSheet.dismissed = { [weak self] _ in
|
|
self?.interacting?(false)
|
|
}
|
|
let items: [ActionSheetItem] = [
|
|
ActionSheetButtonItem(title: singleText, color: .destructive, action: { [weak actionSheet] in
|
|
actionSheet?.dismissAnimated()
|
|
deleteAction([EngineMessage(currentMessage)])
|
|
}),
|
|
ActionSheetButtonItem(title: multipleText, color: .destructive, action: { [weak actionSheet] in
|
|
actionSheet?.dismissAnimated()
|
|
deleteAction(messages)
|
|
})
|
|
]
|
|
|
|
actionSheet.setItemGroups([
|
|
ActionSheetItemGroup(items: items),
|
|
ActionSheetItemGroup(items: [
|
|
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
|
actionSheet?.dismissAnimated()
|
|
})
|
|
])
|
|
])
|
|
strongSelf.controllerInteraction?.presentController(actionSheet, nil)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
private func commitDeleteMessages(_ messages: [EngineMessage], ask: Bool) {
|
|
self.messageContextDisposable.set((self.context.sharedContext.chatAvailableMessageActions(engine: self.context.engine, accountPeerId: self.context.account.peerId, messageIds: Set(messages.map { $0.id }), keepUpdated: false) |> deliverOnMainQueue).start(next: { [weak self] actions in
|
|
if let strongSelf = self, let controllerInteration = strongSelf.controllerInteraction, !actions.options.isEmpty {
|
|
var presentationData = strongSelf.presentationData
|
|
if !presentationData.theme.overallDarkAppearance {
|
|
presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)
|
|
}
|
|
let actionSheet = ActionSheetController(presentationData: presentationData)
|
|
var items: [ActionSheetItem] = []
|
|
var personalPeerName: String?
|
|
var isChannel = false
|
|
let peerId: PeerId = messages[0].id.peerId
|
|
if let user = messages[0].peers[messages[0].id.peerId] as? TelegramUser {
|
|
personalPeerName = EnginePeer(user).compactDisplayTitle
|
|
} else if let channel = messages[0].peers[messages[0].id.peerId] as? TelegramChannel, case .broadcast = channel.info {
|
|
isChannel = true
|
|
}
|
|
|
|
if actions.options.contains(.deleteGlobally) {
|
|
let globalTitle: String
|
|
if isChannel {
|
|
globalTitle = strongSelf.strings.Common_Delete
|
|
} else if let personalPeerName = personalPeerName {
|
|
globalTitle = strongSelf.strings.Conversation_DeleteMessagesFor(personalPeerName).string
|
|
} else {
|
|
globalTitle = strongSelf.strings.Conversation_DeleteMessagesForEveryone
|
|
}
|
|
items.append(ActionSheetButtonItem(title: globalTitle, color: .destructive, action: { [weak actionSheet] in
|
|
actionSheet?.dismissAnimated()
|
|
if let strongSelf = self {
|
|
let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: messages.map { $0.id }, type: .forEveryone).start()
|
|
strongSelf.controllerInteraction?.dismissController()
|
|
}
|
|
}))
|
|
}
|
|
if actions.options.contains(.deleteLocally) {
|
|
var localOptionText = strongSelf.strings.Conversation_DeleteMessagesForMe
|
|
if let messageId = messages.first?.id, Namespaces.Message.allScheduled.contains(messageId.namespace) {
|
|
localOptionText = messages.count > 1 ? strongSelf.strings.ScheduledMessages_DeleteMany : strongSelf.strings.ScheduledMessages_Delete
|
|
} else if strongSelf.context.account.peerId == peerId {
|
|
localOptionText = strongSelf.strings.Conversation_Moderate_Delete
|
|
}
|
|
items.append(ActionSheetButtonItem(title: localOptionText, color: .destructive, action: { [weak actionSheet] in
|
|
actionSheet?.dismissAnimated()
|
|
if let strongSelf = self {
|
|
let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: messages.map { $0.id }, type: .forLocalPeer).start()
|
|
strongSelf.controllerInteraction?.dismissController()
|
|
}
|
|
}))
|
|
}
|
|
if !ask && items.count == 1 {
|
|
let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: messages.map { $0.id }, type: .forEveryone).start()
|
|
strongSelf.controllerInteraction?.dismissController()
|
|
} else if !items.isEmpty {
|
|
strongSelf.interacting?(true)
|
|
actionSheet.dismissed = { [weak self] _ in
|
|
self?.interacting?(false)
|
|
}
|
|
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
|
|
ActionSheetButtonItem(title: strongSelf.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
|
actionSheet?.dismissAnimated()
|
|
})
|
|
])])
|
|
controllerInteration.presentController(actionSheet, nil)
|
|
}
|
|
}
|
|
}))
|
|
}
|
|
|
|
@objc func actionButtonPressed() {
|
|
self.interacting?(true)
|
|
|
|
if let currentMessage = self.currentMessage {
|
|
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Messages.MessageGroup(id: currentMessage.id))
|
|
|> deliverOnMainQueue).start(next: { [weak self] messages in
|
|
if let strongSelf = self, !messages.isEmpty {
|
|
var presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
|
var forceTheme: PresentationTheme?
|
|
if !presentationData.theme.overallDarkAppearance {
|
|
forceTheme = defaultDarkColorPresentationTheme
|
|
presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)
|
|
}
|
|
var generalMessageContentKind: MessageContentKind?
|
|
var beganContentKindScanning = false
|
|
var messageContentKinds = Set<MessageContentKindKey>()
|
|
|
|
for message in messages {
|
|
let currentKind = messageContentKind(contentSettings: strongSelf.context.currentContentSettings.with { $0 }, message: message, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat, accountPeerId: strongSelf.context.account.peerId)
|
|
if beganContentKindScanning, let messageContentKind = generalMessageContentKind, !messageContentKind.isSemanticallyEqual(to: currentKind) {
|
|
generalMessageContentKind = nil
|
|
} else if !beganContentKindScanning || currentKind == generalMessageContentKind {
|
|
beganContentKindScanning = true
|
|
generalMessageContentKind = currentKind
|
|
}
|
|
messageContentKinds.insert(currentKind.key)
|
|
}
|
|
|
|
var preferredAction = ShareControllerPreferredAction.default
|
|
var actionCompletionText: String?
|
|
if let generalMessageContentKind = generalMessageContentKind {
|
|
switch generalMessageContentKind {
|
|
case .image:
|
|
preferredAction = .saveToCameraRoll
|
|
actionCompletionText = strongSelf.presentationData.strings.Gallery_ImageSaved
|
|
case .video:
|
|
if let message = messages.first, let channel = message.peers[message.id.peerId] as? TelegramChannel, channel.addressName != nil {
|
|
} else {
|
|
preferredAction = .saveToCameraRoll
|
|
}
|
|
actionCompletionText = strongSelf.presentationData.strings.Gallery_VideoSaved
|
|
case .file:
|
|
preferredAction = .saveToCameraRoll
|
|
default:
|
|
break
|
|
}
|
|
} else if messageContentKinds.count == 2 && messageContentKinds.contains(.image) && messageContentKinds.contains(.video) {
|
|
preferredAction = .saveToCameraRoll
|
|
actionCompletionText = strongSelf.presentationData.strings.Gallery_ImagesAndVideosSaved
|
|
}
|
|
|
|
if messages.count == 1 {
|
|
var subject: ShareControllerSubject = ShareControllerSubject.messages(messages.map { $0._asMessage() })
|
|
for m in messages[0].media {
|
|
if let image = m as? TelegramMediaImage {
|
|
subject = .image(image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: .media(media: .message(message: MessageReference(messages[0]._asMessage()), media: m), resource: $0.resource)) }))
|
|
} else if let webpage = m as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content {
|
|
if content.embedType == "iframe" {
|
|
let item = OpenInItem.url(url: content.url)
|
|
if availableOpenInOptions(context: strongSelf.context, item: item).count > 1 {
|
|
preferredAction = .custom(action: ShareControllerAction(title: presentationData.strings.Conversation_FileOpenIn, action: { [weak self] in
|
|
if let strongSelf = self {
|
|
let openInController = OpenInActionSheetController(context: strongSelf.context, forceTheme: defaultDarkColorPresentationTheme, item: item, additionalAction: nil, openUrl: { [weak self] url in
|
|
if let strongSelf = self {
|
|
strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {})
|
|
}
|
|
})
|
|
strongSelf.controllerInteraction?.presentController(openInController, nil)
|
|
}
|
|
}))
|
|
} else {
|
|
preferredAction = .custom(action: ShareControllerAction(title: presentationData.strings.Web_OpenExternal, action: { [weak self] in
|
|
if let strongSelf = self {
|
|
strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: content.url, forceExternal: false, presentationData: presentationData, navigationController: nil, dismissInput: {})
|
|
}
|
|
}))
|
|
}
|
|
} else {
|
|
if let file = content.file {
|
|
subject = .media(.webPage(webPage: WebpageReference(webpage), media: file), nil)
|
|
preferredAction = .saveToCameraRoll
|
|
} else if let image = content.image {
|
|
subject = .media(.webPage(webPage: WebpageReference(webpage), media: image), nil)
|
|
preferredAction = .saveToCameraRoll
|
|
actionCompletionText = strongSelf.presentationData.strings.Gallery_ImageSaved
|
|
}
|
|
}
|
|
} else if let file = m as? TelegramMediaFile {
|
|
subject = .media(.message(message: MessageReference(messages[0]._asMessage()), media: file), strongSelf.shareMediaParameters?())
|
|
if file.isAnimated {
|
|
if messages[0].id.peerId.namespace == Namespaces.Peer.SecretChat {
|
|
preferredAction = .default
|
|
} else {
|
|
preferredAction = .custom(action: ShareControllerAction(title: presentationData.strings.Preview_SaveGif, action: { [weak self] in
|
|
if let strongSelf = self {
|
|
let message = messages[0]
|
|
|
|
let context = strongSelf.context
|
|
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
|
let controllerInteraction = strongSelf.controllerInteraction
|
|
let _ = (toggleGifSaved(account: context.account, fileReference: .message(message: MessageReference(message._asMessage()), media: file), saved: true)
|
|
|> deliverOnMainQueue).start(next: { result in
|
|
switch result {
|
|
case .generic:
|
|
controllerInteraction?.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: nil, text: presentationData.strings.Gallery_GifSaved, customUndoText: nil, timeout: nil), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), nil)
|
|
case let .limitExceeded(limit, premiumLimit):
|
|
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
|
|
let text: String
|
|
if limit == premiumLimit || premiumConfiguration.isPremiumDisabled {
|
|
text = presentationData.strings.Premium_MaxSavedGifsFinalText
|
|
} else {
|
|
text = presentationData.strings.Premium_MaxSavedGifsText("\(premiumLimit)").string
|
|
}
|
|
controllerInteraction?.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: presentationData.strings.Premium_MaxSavedGifsTitle("\(limit)").string, text: text, customUndoText: nil, timeout: nil), elevatedLayout: true, animateInAsReplacement: false, action: { action in
|
|
if case .info = action {
|
|
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .savedGifs, forceDark: false, dismissed: nil)
|
|
controllerInteraction?.pushController(controller)
|
|
return true
|
|
}
|
|
return false
|
|
}), nil)
|
|
}
|
|
})
|
|
}
|
|
}))
|
|
}
|
|
} else if file.mimeType.hasPrefix("image/") {
|
|
preferredAction = .saveToCameraRoll
|
|
actionCompletionText = strongSelf.presentationData.strings.Gallery_ImageSaved
|
|
} else if file.mimeType.hasPrefix("video/") {
|
|
preferredAction = .saveToCameraRoll
|
|
actionCompletionText = strongSelf.presentationData.strings.Gallery_VideoSaved
|
|
}
|
|
}
|
|
}
|
|
|
|
var hasExternalShare = true
|
|
for media in currentMessage.media {
|
|
if let _ = media as? TelegramMediaPaidContent {
|
|
hasExternalShare = false
|
|
break
|
|
} else if let invoice = media as? TelegramMediaInvoice, let _ = invoice.extendedMedia {
|
|
hasExternalShare = false
|
|
break
|
|
}
|
|
}
|
|
|
|
let shareController = ShareController(context: strongSelf.context, subject: subject, preferredAction: preferredAction, externalShare: hasExternalShare, forceTheme: forceTheme)
|
|
shareController.dismissed = { [weak self] _ in
|
|
self?.interacting?(false)
|
|
}
|
|
shareController.onMediaTimestampLinkCopied = { [weak self] timestamp in
|
|
guard let self else {
|
|
return
|
|
}
|
|
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
|
let text: String
|
|
if let timestamp {
|
|
let startTimeString: String
|
|
let hours = timestamp / (60 * 60)
|
|
let minutes = timestamp % (60 * 60) / 60
|
|
let seconds = timestamp % 60
|
|
if hours != 0 {
|
|
startTimeString = String(format: "%d:%02d:%02d", hours, minutes, seconds)
|
|
} else {
|
|
startTimeString = String(format: "%d:%02d", minutes, seconds)
|
|
}
|
|
text = presentationData.strings.Conversation_VideoTimeLinkCopied(startTimeString).string
|
|
} else {
|
|
text = presentationData.strings.Conversation_LinkCopied
|
|
}
|
|
|
|
self.controllerInteraction?.presentController(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: text), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return true }), nil)
|
|
}
|
|
|
|
shareController.actionCompleted = { [weak self] in
|
|
if let strongSelf = self, let actionCompletionText = actionCompletionText {
|
|
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
|
strongSelf.controllerInteraction?.presentController(UndoOverlayController(presentationData: presentationData, content: .mediaSaved(text: actionCompletionText), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return true }), nil)
|
|
}
|
|
}
|
|
shareController.completed = { [weak self] peerIds in
|
|
if let strongSelf = self {
|
|
let _ = (strongSelf.context.engine.data.get(
|
|
EngineDataList(
|
|
peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init)
|
|
)
|
|
)
|
|
|> deliverOnMainQueue).start(next: { [weak self] peerList in
|
|
if let strongSelf = self {
|
|
let peers = peerList.compactMap { $0 }
|
|
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
|
|
|
let text: String
|
|
var savedMessages = false
|
|
if peerIds.count == 1, let peerId = peerIds.first, peerId == strongSelf.context.account.peerId {
|
|
text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One : presentationData.strings.Conversation_ForwardTooltip_SavedMessages_Many
|
|
savedMessages = true
|
|
} else {
|
|
if peers.count == 1, let peer = peers.first {
|
|
var peerName = peer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
|
|
peerName = peerName.replacingOccurrences(of: "**", with: "")
|
|
text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_Chat_One(peerName).string : presentationData.strings.Conversation_ForwardTooltip_Chat_Many(peerName).string
|
|
} else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last {
|
|
var firstPeerName = firstPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
|
|
firstPeerName = firstPeerName.replacingOccurrences(of: "**", with: "")
|
|
var secondPeerName = secondPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
|
|
secondPeerName = secondPeerName.replacingOccurrences(of: "**", with: "")
|
|
text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string : presentationData.strings.Conversation_ForwardTooltip_TwoChats_Many(firstPeerName, secondPeerName).string
|
|
} else if let peer = peers.first {
|
|
var peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
|
|
peerName = peerName.replacingOccurrences(of: "**", with: "")
|
|
text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").string : presentationData.strings.Conversation_ForwardTooltip_ManyChats_Many(peerName, "\(peers.count - 1)").string
|
|
} else {
|
|
text = ""
|
|
}
|
|
}
|
|
|
|
strongSelf.controllerInteraction?.presentController(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: true, animateInAsReplacement: true, action: { _ in return false }), nil)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
strongSelf.controllerInteraction?.presentController(shareController, nil)
|
|
} else {
|
|
var singleText = presentationData.strings.Media_ShareItem(1)
|
|
var multipleText = presentationData.strings.Media_ShareItem(Int32(messages.count))
|
|
|
|
if let generalMessageContentKind = generalMessageContentKind {
|
|
switch generalMessageContentKind {
|
|
case .image:
|
|
singleText = presentationData.strings.Media_ShareThisPhoto
|
|
multipleText = presentationData.strings.Media_SharePhoto(Int32(messages.count))
|
|
case .video:
|
|
singleText = presentationData.strings.Media_ShareThisVideo
|
|
multipleText = presentationData.strings.Media_ShareVideo(Int32(messages.count))
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
let shareAction: ([Message]) -> Void = { messages in
|
|
if let strongSelf = self {
|
|
let shareController = ShareController(context: strongSelf.context, subject: .messages(messages), preferredAction: preferredAction, forceTheme: forceTheme)
|
|
shareController.dismissed = { [weak self] _ in
|
|
self?.interacting?(false)
|
|
}
|
|
shareController.actionCompleted = { [weak self, weak shareController] in
|
|
if let strongSelf = self, let shareController = shareController, shareController.actionIsMediaSaving {
|
|
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
|
strongSelf.controllerInteraction?.presentController(UndoOverlayController(presentationData: presentationData, content: .mediaSaved(text: presentationData.strings.Gallery_ImageSaved), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return true }), nil)
|
|
}
|
|
}
|
|
shareController.completed = { [weak self] peerIds in
|
|
if let strongSelf = self {
|
|
let _ = (strongSelf.context.engine.data.get(
|
|
EngineDataList(
|
|
peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init)
|
|
)
|
|
)
|
|
|> deliverOnMainQueue).start(next: { [weak self] peerList in
|
|
if let strongSelf = self {
|
|
let peers = peerList.compactMap { $0 }
|
|
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
|
|
|
let text: String
|
|
var savedMessages = false
|
|
if peerIds.count == 1, let peerId = peerIds.first, peerId == strongSelf.context.account.peerId {
|
|
text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One : presentationData.strings.Conversation_ForwardTooltip_SavedMessages_Many
|
|
savedMessages = true
|
|
} else {
|
|
if peers.count == 1, let peer = peers.first {
|
|
var peerName = peer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
|
|
peerName = peerName.replacingOccurrences(of: "**", with: "")
|
|
text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_Chat_One(peerName).string : presentationData.strings.Conversation_ForwardTooltip_Chat_Many(peerName).string
|
|
} else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last {
|
|
var firstPeerName = firstPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
|
|
firstPeerName = firstPeerName.replacingOccurrences(of: "**", with: "")
|
|
var secondPeerName = secondPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
|
|
secondPeerName = secondPeerName.replacingOccurrences(of: "**", with: "")
|
|
text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string : presentationData.strings.Conversation_ForwardTooltip_TwoChats_Many(firstPeerName, secondPeerName).string
|
|
} else if let peer = peers.first {
|
|
var peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
|
|
peerName = peerName.replacingOccurrences(of: "**", with: "")
|
|
text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").string : presentationData.strings.Conversation_ForwardTooltip_ManyChats_Many(peerName, "\(peers.count - 1)").string
|
|
} else {
|
|
text = ""
|
|
}
|
|
}
|
|
|
|
strongSelf.controllerInteraction?.presentController(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: true, animateInAsReplacement: true, action: { _ in return false }), nil)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
strongSelf.controllerInteraction?.presentController(shareController, nil)
|
|
}
|
|
}
|
|
|
|
let actionSheet = ActionSheetController(presentationData: presentationData)
|
|
let items: [ActionSheetItem] = [
|
|
ActionSheetButtonItem(title: singleText, color: .accent, action: { [weak actionSheet] in
|
|
actionSheet?.dismissAnimated()
|
|
shareAction([currentMessage])
|
|
}),
|
|
ActionSheetButtonItem(title: multipleText, color: .accent, action: { [weak actionSheet] in
|
|
actionSheet?.dismissAnimated()
|
|
shareAction(messages.map { $0._asMessage() })
|
|
})
|
|
]
|
|
|
|
actionSheet.setItemGroups([ActionSheetItemGroup(items: items),
|
|
ActionSheetItemGroup(items: [
|
|
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
|
actionSheet?.dismissAnimated()
|
|
})
|
|
])
|
|
])
|
|
strongSelf.controllerInteraction?.presentController(actionSheet, nil)
|
|
}
|
|
}
|
|
})
|
|
} else if let (webPage, media) = self.currentWebPageAndMedia {
|
|
var presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
|
var forceTheme: PresentationTheme?
|
|
if !presentationData.theme.overallDarkAppearance {
|
|
forceTheme = defaultDarkColorPresentationTheme
|
|
presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)
|
|
}
|
|
|
|
var preferredAction = ShareControllerPreferredAction.default
|
|
var subject = ShareControllerSubject.media(.webPage(webPage: WebpageReference(webPage), media: media), self.shareMediaParameters?())
|
|
|
|
if let file = media as? TelegramMediaFile {
|
|
if file.isAnimated {
|
|
preferredAction = .custom(action: ShareControllerAction(title: presentationData.strings.Preview_SaveGif, action: { [weak self] in
|
|
if let strongSelf = self {
|
|
let context = strongSelf.context
|
|
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
|
let controllerInteraction = strongSelf.controllerInteraction
|
|
let _ = (toggleGifSaved(account: context.account, fileReference: .webPage(webPage: WebpageReference(webPage), media: file), saved: true)
|
|
|> deliverOnMainQueue).start(next: { result in
|
|
switch result {
|
|
case .generic:
|
|
controllerInteraction?.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: nil, text: presentationData.strings.Gallery_GifSaved, customUndoText: nil, timeout: nil), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), nil)
|
|
case let .limitExceeded(limit, premiumLimit):
|
|
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
|
|
let text: String
|
|
if limit == premiumLimit || premiumConfiguration.isPremiumDisabled {
|
|
text = presentationData.strings.Premium_MaxSavedGifsFinalText
|
|
} else {
|
|
text = presentationData.strings.Premium_MaxSavedGifsText("\(premiumLimit)").string
|
|
}
|
|
controllerInteraction?.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: presentationData.strings.Premium_MaxSavedGifsTitle("\(limit)").string, text: text, customUndoText: nil, timeout: nil), elevatedLayout: true, animateInAsReplacement: false, action: { action in
|
|
if case .info = action {
|
|
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .savedGifs, forceDark: false, dismissed: nil)
|
|
controllerInteraction?.pushController(controller)
|
|
return true
|
|
}
|
|
return false
|
|
}), nil)
|
|
}
|
|
})
|
|
}
|
|
}))
|
|
} else if file.mimeType.hasPrefix("image/") || file.mimeType.hasPrefix("video/") {
|
|
preferredAction = .saveToCameraRoll
|
|
}
|
|
} else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content {
|
|
if content.embedType == "iframe" || content.embedType == "video" {
|
|
subject = .url(content.url)
|
|
|
|
let item = OpenInItem.url(url: content.url)
|
|
if availableOpenInOptions(context: self.context, item: item).count > 1 {
|
|
preferredAction = .custom(action: ShareControllerAction(title: presentationData.strings.Conversation_FileOpenIn, action: { [weak self] in
|
|
if let strongSelf = self {
|
|
let openInController = OpenInActionSheetController(context: strongSelf.context, forceTheme: forceTheme, item: item, additionalAction: nil, openUrl: { [weak self] url in
|
|
if let strongSelf = self {
|
|
strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {})
|
|
}
|
|
})
|
|
strongSelf.controllerInteraction?.presentController(openInController, nil)
|
|
}
|
|
}))
|
|
} else {
|
|
preferredAction = .custom(action: ShareControllerAction(title: presentationData.strings.Web_OpenExternal, action: { [weak self] in
|
|
if let strongSelf = self {
|
|
strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: content.url, forceExternal: false, presentationData: presentationData, navigationController: nil, dismissInput: {})
|
|
}
|
|
}))
|
|
}
|
|
} else {
|
|
if let file = content.file {
|
|
subject = .media(.webPage(webPage: WebpageReference(webpage), media: file), self.shareMediaParameters?())
|
|
preferredAction = .saveToCameraRoll
|
|
} else if let image = content.image {
|
|
subject = .media(.webPage(webPage: WebpageReference(webpage), media: image), self.shareMediaParameters?())
|
|
preferredAction = .saveToCameraRoll
|
|
}
|
|
}
|
|
}
|
|
let shareController = ShareController(context: self.context, subject: subject, preferredAction: preferredAction, forceTheme: forceTheme)
|
|
shareController.dismissed = { [weak self] _ in
|
|
self?.interacting?(false)
|
|
}
|
|
shareController.completed = { [weak self] peerIds in
|
|
if let strongSelf = self {
|
|
let _ = (strongSelf.context.engine.data.get(
|
|
EngineDataList(
|
|
peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init)
|
|
)
|
|
)
|
|
|> deliverOnMainQueue).start(next: { [weak self] peerList in
|
|
if let strongSelf = self {
|
|
let peers = peerList.compactMap { $0 }
|
|
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
|
|
|
let text: String
|
|
var savedMessages = false
|
|
if peerIds.count == 1, let peerId = peerIds.first, peerId == strongSelf.context.account.peerId {
|
|
text = presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One
|
|
savedMessages = true
|
|
} else {
|
|
if peers.count == 1, let peer = peers.first {
|
|
var peerName = peer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
|
|
peerName = peerName.replacingOccurrences(of: "**", with: "")
|
|
text = presentationData.strings.Conversation_ForwardTooltip_Chat_One(peerName).string
|
|
} else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last {
|
|
var firstPeerName = firstPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
|
|
firstPeerName = firstPeerName.replacingOccurrences(of: "**", with: "")
|
|
var secondPeerName = secondPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
|
|
secondPeerName = secondPeerName.replacingOccurrences(of: "**", with: "")
|
|
text = presentationData.strings.Conversation_ForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string
|
|
} else if let peer = peers.first {
|
|
var peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
|
|
peerName = peerName.replacingOccurrences(of: "**", with: "")
|
|
text = presentationData.strings.Conversation_ForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").string
|
|
} else {
|
|
text = ""
|
|
}
|
|
}
|
|
|
|
strongSelf.controllerInteraction?.presentController(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: true, animateInAsReplacement: true, action: { _ in return false }), nil)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
self.controllerInteraction?.presentController(shareController, nil)
|
|
}
|
|
}
|
|
|
|
@objc func editButtonPressed() {
|
|
guard let message = self.currentMessage else {
|
|
return
|
|
}
|
|
self.controllerInteraction?.editMedia(message.id)
|
|
}
|
|
|
|
private func textRecognitionButtonPressed() {
|
|
guard let currentItemNode = self.controllerInteraction?.currentItemNode() as? ChatImageGalleryItemNode else {
|
|
return
|
|
}
|
|
currentItemNode.textRecognitionButtonPressed()
|
|
}
|
|
|
|
private func stickersButtonPressed() {
|
|
if let currentItemNode = self.controllerInteraction?.currentItemNode() as? ChatImageGalleryItemNode {
|
|
currentItemNode.openStickersButtonPressed()
|
|
} else if let currentItemNode = self.controllerInteraction?.currentItemNode() as? UniversalVideoGalleryItemNode {
|
|
currentItemNode.openStickersButtonPressed()
|
|
}
|
|
}
|
|
|
|
private func pipButtonPressed() {
|
|
guard let currentItemNode = self.controllerInteraction?.currentItemNode() as? UniversalVideoGalleryItemNode else {
|
|
return
|
|
}
|
|
currentItemNode.pictureInPictureButtonPressed()
|
|
}
|
|
|
|
private func settingsButtonPressed(sourceView: UIView) {
|
|
guard let currentItemNode = self.controllerInteraction?.currentItemNode() as? UniversalVideoGalleryItemNode else {
|
|
return
|
|
}
|
|
currentItemNode.settingsButtonPressed(sourceView: sourceView)
|
|
}
|
|
|
|
@objc func playbackControlPressed() {
|
|
self.playbackControl?()
|
|
}
|
|
|
|
@objc func backwardButtonPressed() {
|
|
self.interacting?(true)
|
|
self.seekBackward?(15.0)
|
|
self.interacting?(false)
|
|
}
|
|
|
|
@objc func forwardButtonPressed() {
|
|
self.interacting?(true)
|
|
self.seekForward?(15.0)
|
|
self.interacting?(false)
|
|
}
|
|
|
|
@objc private func statusPressed() {
|
|
self.fetchControl?()
|
|
}
|
|
|
|
func setFramePreviewImageIsLoading() {
|
|
if self.videoFramePreviewNode?.0.image != nil {
|
|
//self.videoFramePreviewNode?.subnodes?.first?.alpha = 1.0
|
|
}
|
|
}
|
|
|
|
func setFramePreviewImage(image: UIImage?, isSecret: Bool) {
|
|
if let image = image {
|
|
let videoFramePreviewNode: ASImageNode
|
|
let videoFrameTextNode: ImmediateTextNode
|
|
var animateIn = false
|
|
if let current = self.videoFramePreviewNode {
|
|
videoFramePreviewNode = current.0
|
|
videoFrameTextNode = current.1
|
|
} else {
|
|
videoFramePreviewNode = ASImageNode()
|
|
videoFramePreviewNode.displaysAsynchronously = false
|
|
videoFramePreviewNode.displayWithoutProcessing = true
|
|
videoFramePreviewNode.clipsToBounds = true
|
|
videoFramePreviewNode.cornerRadius = 6.0
|
|
|
|
let dimNode = ASDisplayNode()
|
|
dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
|
|
videoFramePreviewNode.addSubnode(dimNode)
|
|
|
|
videoFrameTextNode = ImmediateTextNode()
|
|
videoFrameTextNode.displaysAsynchronously = false
|
|
videoFrameTextNode.maximumNumberOfLines = 1
|
|
videoFrameTextNode.textShadowColor = .black
|
|
if let scrubbingVisualTimestamp = self.scrubbingVisualTimestamp {
|
|
videoFrameTextNode.attributedText = NSAttributedString(string: stringForDuration(Int32(scrubbingVisualTimestamp)), font: Font.regular(13.0), textColor: .white)
|
|
}
|
|
let textSize = videoFrameTextNode.updateLayout(CGSize(width: 100.0, height: 100.0))
|
|
videoFrameTextNode.frame = CGRect(origin: CGPoint(), size: textSize)
|
|
// videoFramePreviewNode.addSubnode(videoFrameTextNode)
|
|
|
|
self.videoFramePreviewNode = (videoFramePreviewNode, videoFrameTextNode)
|
|
self.addSubnode(videoFramePreviewNode)
|
|
animateIn = true
|
|
}
|
|
videoFramePreviewNode.subnodes?.first?.alpha = 0.0
|
|
let updateLayout = videoFramePreviewNode.image?.size != image.size
|
|
videoFramePreviewNode.image = image
|
|
setLayerDisableScreenshots(videoFramePreviewNode.layer, isSecret)
|
|
if updateLayout, let validLayout = self.validLayout {
|
|
let _ = self.updateLayout(size: validLayout.0, metrics: validLayout.1, leftInset: validLayout.2, rightInset: validLayout.3, bottomInset: validLayout.4, contentInset: validLayout.5, transition: .immediate)
|
|
}
|
|
if animateIn {
|
|
videoFramePreviewNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
|
|
}
|
|
} else if let (videoFramePreviewNode, _) = self.videoFramePreviewNode {
|
|
self.videoFramePreviewNode = nil
|
|
videoFramePreviewNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, removeOnCompletion: false, completion: { [weak videoFramePreviewNode] _ in
|
|
videoFramePreviewNode?.removeFromSupernode()
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
private enum PlayPauseIconNodeState: Equatable {
|
|
case play
|
|
case pause
|
|
}
|
|
|
|
private final class PlayPauseIconNode: ManagedAnimationNode {
|
|
private let duration: Double = 0.35
|
|
private var iconState: PlayPauseIconNodeState = .pause
|
|
|
|
init() {
|
|
super.init(size: CGSize(width: 40.0, height: 40.0))
|
|
|
|
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 41), duration: 0.01))
|
|
}
|
|
|
|
func enqueueState(_ state: PlayPauseIconNodeState, animated: Bool) {
|
|
guard self.iconState != state else {
|
|
return
|
|
}
|
|
|
|
let previousState = self.iconState
|
|
self.iconState = state
|
|
|
|
switch previousState {
|
|
case .pause:
|
|
switch state {
|
|
case .play:
|
|
if animated {
|
|
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 83), duration: self.duration))
|
|
} else {
|
|
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01))
|
|
}
|
|
case .pause:
|
|
break
|
|
}
|
|
case .play:
|
|
switch state {
|
|
case .pause:
|
|
if animated {
|
|
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 41), duration: self.duration))
|
|
} else {
|
|
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 41), duration: 0.01))
|
|
}
|
|
case .play:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private let circleDiameter: CGFloat = 80.0
|
|
|
|
private final class PlaybackButtonNode: HighlightTrackingButtonNode {
|
|
let backgroundIconNode: ASImageNode
|
|
let textNode: ImmediateTextNode
|
|
|
|
var forward: Bool = false
|
|
|
|
var isPressing = false {
|
|
didSet {
|
|
if self.isPressing != oldValue && !self.isPressing {
|
|
self.highligthedChanged(false)
|
|
}
|
|
}
|
|
}
|
|
|
|
init() {
|
|
self.backgroundIconNode = ASImageNode()
|
|
self.backgroundIconNode.isLayerBacked = true
|
|
self.backgroundIconNode.displaysAsynchronously = false
|
|
self.backgroundIconNode.displayWithoutProcessing = true
|
|
|
|
self.textNode = ImmediateTextNode()
|
|
self.textNode.attributedText = NSAttributedString(string: "15", font: Font.with(size: 11.0, design: .round, weight: .semibold, traits: []), textColor: .white)
|
|
|
|
super.init(pointerStyle: nil)
|
|
|
|
self.addSubnode(self.backgroundIconNode)
|
|
self.addSubnode(self.textNode)
|
|
|
|
self.highligthedChanged = { [weak self] highlighted in
|
|
if let strongSelf = self {
|
|
if highlighted {
|
|
strongSelf.backgroundIconNode.layer.removeAnimation(forKey: "opacity")
|
|
strongSelf.backgroundIconNode.alpha = 0.4
|
|
|
|
strongSelf.textNode.layer.removeAnimation(forKey: "opacity")
|
|
strongSelf.textNode.alpha = 0.4
|
|
|
|
let transition: ContainedViewLayoutTransition = .animated(duration: 0.18, curve: .linear)
|
|
let angle = CGFloat.pi / 4.0 + 0.226
|
|
transition.updateTransformRotation(node: strongSelf.backgroundIconNode, angle: strongSelf.forward ? angle : -angle)
|
|
} else if !strongSelf.isPressing {
|
|
strongSelf.backgroundIconNode.alpha = 1.0
|
|
strongSelf.backgroundIconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
|
|
|
strongSelf.textNode.alpha = 1.0
|
|
strongSelf.textNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
|
|
|
let transition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .linear)
|
|
transition.updateTransformRotation(node: strongSelf.backgroundIconNode, angle: 0.0)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
override func layout() {
|
|
super.layout()
|
|
self.backgroundIconNode.frame = self.bounds
|
|
|
|
let size = self.bounds.size
|
|
let textSize = self.textNode.updateLayout(size)
|
|
self.textNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: floorToScreenPixels((size.height - textSize.height) / 2.0) + UIScreenPixel), size: textSize)
|
|
}
|
|
}
|
|
|
|
final class SettingsNavigationIconComponent: Component {
|
|
let speed: String?
|
|
let quality: String?
|
|
let isOpen: Bool
|
|
|
|
init(speed: String?, quality: String?, isOpen: Bool) {
|
|
self.speed = speed
|
|
self.quality = quality
|
|
self.isOpen = isOpen
|
|
}
|
|
|
|
static func ==(lhs: SettingsNavigationIconComponent, rhs: SettingsNavigationIconComponent) -> Bool {
|
|
if lhs.speed != rhs.speed {
|
|
return false
|
|
}
|
|
if lhs.quality != rhs.quality {
|
|
return false
|
|
}
|
|
if lhs.isOpen != rhs.isOpen {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
final class View: UIView {
|
|
private let iconLayer: RasterizedCompositionMonochromeLayer
|
|
|
|
private let gearsLayer: RasterizedCompositionImageLayer
|
|
private let dotLayer: RasterizedCompositionImageLayer
|
|
|
|
private var speedBadge: ComponentView<Empty>?
|
|
private var qualityBadge: ComponentView<Empty>?
|
|
|
|
private var speedBadgeText: String?
|
|
private var qualityBadgeText: String?
|
|
|
|
private let badgeFont: UIFont
|
|
|
|
private var isMenuOpen: Bool = false
|
|
|
|
override init(frame: CGRect) {
|
|
self.iconLayer = RasterizedCompositionMonochromeLayer()
|
|
|
|
self.gearsLayer = RasterizedCompositionImageLayer()
|
|
self.gearsLayer.image = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettingsNoDot"), color: .white)
|
|
|
|
self.dotLayer = RasterizedCompositionImageLayer()
|
|
self.dotLayer.image = generateFilledCircleImage(diameter: 4.0, color: .white)
|
|
|
|
self.iconLayer.contentsLayer.addSublayer(self.gearsLayer)
|
|
self.iconLayer.contentsLayer.addSublayer(self.dotLayer)
|
|
|
|
self.badgeFont = Font.with(size: 8.0, design: .round, weight: .bold)
|
|
|
|
super.init(frame: frame)
|
|
|
|
self.layer.addSublayer(self.iconLayer)
|
|
|
|
let size = CGSize(width: 44.0, height: 44.0)
|
|
|
|
if let image = self.gearsLayer.image {
|
|
let iconInnerInsets = UIEdgeInsets(top: 4.0, left: 8.0, bottom: 4.0, right: 6.0)
|
|
let iconSize = CGSize(width: image.size.width + iconInnerInsets.left + iconInnerInsets.right, height: image.size.height + iconInnerInsets.top + iconInnerInsets.bottom)
|
|
let iconFrame = CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) / 2.0), y: floor((size.height - iconSize.height) / 2.0)), size: iconSize)
|
|
self.iconLayer.position = iconFrame.center
|
|
self.iconLayer.bounds = CGRect(origin: CGPoint(), size: iconFrame.size)
|
|
|
|
self.iconLayer.contentsLayer.position = CGRect(origin: CGPoint(), size: iconFrame.size).center
|
|
self.iconLayer.contentsLayer.bounds = CGRect(origin: CGPoint(), size: iconFrame.size)
|
|
|
|
self.iconLayer.maskedLayer.position = CGRect(origin: CGPoint(), size: iconFrame.size).center
|
|
self.iconLayer.maskedLayer.bounds = CGRect(origin: CGPoint(), size: iconFrame.size)
|
|
self.iconLayer.maskedLayer.backgroundColor = UIColor.white.cgColor
|
|
|
|
let gearsFrame = CGRect(origin: CGPoint(x: floor((iconSize.width - image.size.width) * 0.5), y: floor((iconSize.height - image.size.height) * 0.5)), size: image.size)
|
|
self.gearsLayer.position = gearsFrame.center
|
|
self.gearsLayer.bounds = CGRect(origin: CGPoint(), size: gearsFrame.size)
|
|
|
|
if let dotImage = self.dotLayer.image {
|
|
let dotFrame = CGRect(origin: CGPoint(x: gearsFrame.minX + floorToScreenPixels((gearsFrame.width - dotImage.size.width) * 0.5), y: gearsFrame.minY + floorToScreenPixels((gearsFrame.height - dotImage.size.height) * 0.5)), size: dotImage.size)
|
|
self.dotLayer.position = dotFrame.center
|
|
self.dotLayer.bounds = CGRect(origin: CGPoint(), size: dotFrame.size)
|
|
}
|
|
}
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
func setIsMenuOpen(isMenuOpen: Bool) {
|
|
if self.isMenuOpen == isMenuOpen {
|
|
return
|
|
}
|
|
self.isMenuOpen = isMenuOpen
|
|
|
|
let rotationTransition: ContainedViewLayoutTransition = .animated(duration: 0.35, curve: .spring)
|
|
rotationTransition.updateTransform(layer: self.gearsLayer, transform: CGAffineTransformMakeRotation(isMenuOpen ? (CGFloat.pi * 2.0 / 6.0) : 0.0))
|
|
self.gearsLayer.animateScale(from: 1.0, to: 1.07, duration: 0.1, removeOnCompletion: false, completion: { [weak self] finished in
|
|
guard let self, finished else {
|
|
return
|
|
}
|
|
self.gearsLayer.animateScale(from: 1.07, to: 1.0, duration: 0.1, removeOnCompletion: true)
|
|
})
|
|
|
|
self.dotLayer.animateScale(from: 1.0, to: 0.8, duration: 0.1, removeOnCompletion: false, completion: { [weak self] finished in
|
|
guard let self, finished else {
|
|
return
|
|
}
|
|
self.dotLayer.animateScale(from: 0.8, to: 1.0, duration: 0.1, removeOnCompletion: true)
|
|
})
|
|
}
|
|
|
|
func setBadges(speed: String?, quality: String?, transition: ComponentTransition) {
|
|
if self.speedBadgeText == speed && self.qualityBadgeText == quality {
|
|
return
|
|
}
|
|
self.speedBadgeText = speed
|
|
self.qualityBadgeText = quality
|
|
|
|
if let badgeText = speed {
|
|
var badgeTransition = transition
|
|
let speedBadge: ComponentView<Empty>
|
|
if let current = self.speedBadge {
|
|
speedBadge = current
|
|
} else {
|
|
speedBadge = ComponentView()
|
|
self.speedBadge = speedBadge
|
|
badgeTransition = badgeTransition.withAnimation(.none)
|
|
}
|
|
let badgeSize = speedBadge.update(
|
|
transition: badgeTransition,
|
|
component: AnyComponent(BadgeComponent(
|
|
text: badgeText,
|
|
font: self.badgeFont,
|
|
cornerRadius: .custom(3.0),
|
|
insets: UIEdgeInsets(top: 1.33, left: 1.66, bottom: 1.33, right: 1.66),
|
|
outerInsets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0)
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: 100.0, height: 100.0)
|
|
)
|
|
if let speedBadgeView = speedBadge.view {
|
|
if speedBadgeView.layer.superlayer == nil {
|
|
self.iconLayer.contentsLayer.addSublayer(speedBadgeView.layer)
|
|
|
|
transition.animateAlpha(layer: speedBadgeView.layer, from: 0.0, to: 1.0)
|
|
transition.animateScale(layer: speedBadgeView.layer, from: 0.001, to: 1.0)
|
|
}
|
|
badgeTransition.setFrame(layer: speedBadgeView.layer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: badgeSize))
|
|
}
|
|
} else if let speedBadge = self.speedBadge {
|
|
self.speedBadge = nil
|
|
if let speedBadgeView = speedBadge.view {
|
|
let speedBadgeLayer = speedBadgeView.layer
|
|
transition.setAlpha(layer: speedBadgeLayer, alpha: 0.0, completion: { [weak speedBadgeLayer] _ in
|
|
speedBadgeLayer?.removeFromSuperlayer()
|
|
})
|
|
transition.setScale(layer: speedBadgeLayer, scale: 0.001)
|
|
}
|
|
}
|
|
|
|
if let badgeText = quality {
|
|
var badgeTransition = transition
|
|
let qualityBadge: ComponentView<Empty>
|
|
if let current = self.qualityBadge {
|
|
qualityBadge = current
|
|
} else {
|
|
qualityBadge = ComponentView()
|
|
self.qualityBadge = qualityBadge
|
|
badgeTransition = badgeTransition.withAnimation(.none)
|
|
}
|
|
let badgeSize = qualityBadge.update(
|
|
transition: badgeTransition,
|
|
component: AnyComponent(BadgeComponent(
|
|
text: badgeText,
|
|
font: self.badgeFont,
|
|
cornerRadius: .custom(3.0),
|
|
insets: UIEdgeInsets(top: 1.33, left: 1.66, bottom: 1.33, right: 1.66),
|
|
outerInsets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0)
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: 100.0, height: 100.0)
|
|
)
|
|
if let qualityBadgeView = qualityBadge.view {
|
|
if qualityBadgeView.layer.superlayer == nil {
|
|
self.iconLayer.contentsLayer.addSublayer(qualityBadgeView.layer)
|
|
|
|
transition.animateAlpha(layer: qualityBadgeView.layer, from: 0.0, to: 1.0)
|
|
transition.animateScale(layer: qualityBadgeView.layer, from: 0.001, to: 1.0)
|
|
}
|
|
badgeTransition.setFrame(layer: qualityBadgeView.layer, frame: CGRect(origin: CGPoint(x: self.iconLayer.bounds.width - badgeSize.width, y: self.iconLayer.bounds.height - badgeSize.height), size: badgeSize))
|
|
}
|
|
} else if let qualityBadge = self.qualityBadge {
|
|
self.qualityBadge = nil
|
|
if let qualityBadgeView = qualityBadge.view {
|
|
let qualityBadgeLayer = qualityBadgeView.layer
|
|
transition.setAlpha(layer: qualityBadgeLayer, alpha: 0.0, completion: { [weak qualityBadgeLayer] _ in
|
|
qualityBadgeLayer?.removeFromSuperlayer()
|
|
})
|
|
transition.setScale(layer: qualityBadgeLayer, scale: 0.001)
|
|
}
|
|
}
|
|
}
|
|
|
|
func update(component: SettingsNavigationIconComponent, availableSize: CGSize, state: State, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
|
self.setBadges(speed: component.speed, quality: component.quality, transition: transition)
|
|
self.setIsMenuOpen(isMenuOpen: component.isOpen)
|
|
|
|
return CGSize(width: 44.0, height: 44.0)
|
|
}
|
|
}
|
|
|
|
func makeView() -> View {
|
|
return View(frame: CGRect())
|
|
}
|
|
|
|
func update(view: View, availableSize: CGSize, state: State, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
|
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
|
}
|
|
}
|