mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-08-02 00:17:02 +00:00
Merge commit 'f59abe16896937b2dd8f885a859f9352cd76bf28'
This commit is contained in:
commit
f11ebd9559
@ -20,11 +20,13 @@ public struct MessageHistoryScrollToSubject: Equatable {
|
|||||||
|
|
||||||
public var index: MessageHistoryAnchorIndex
|
public var index: MessageHistoryAnchorIndex
|
||||||
public var quote: Quote?
|
public var quote: Quote?
|
||||||
|
public var todoTaskId: Int32?
|
||||||
public var setupReply: Bool
|
public var setupReply: Bool
|
||||||
|
|
||||||
public init(index: MessageHistoryAnchorIndex, quote: Quote?, setupReply: Bool = false) {
|
public init(index: MessageHistoryAnchorIndex, quote: Quote?, todoTaskId: Int32? = nil, setupReply: Bool = false) {
|
||||||
self.index = index
|
self.index = index
|
||||||
self.quote = quote
|
self.quote = quote
|
||||||
|
self.todoTaskId = todoTaskId
|
||||||
self.setupReply = setupReply
|
self.setupReply = setupReply
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -837,7 +837,7 @@ private final class MainButtonNode: HighlightTrackingButtonNode {
|
|||||||
transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: size.width - statusSize.width - 15.0, y: floorToScreenPixels((size.height - statusSize.height) / 2.0)), size: statusSize))
|
transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: size.width - statusSize.width - 15.0, y: floorToScreenPixels((size.height - statusSize.height) / 2.0)), size: statusSize))
|
||||||
|
|
||||||
self.statusNode.foregroundNodeColor = state.textColor
|
self.statusNode.foregroundNodeColor = state.textColor
|
||||||
self.statusNode.transitionToState(state.progress == .side ? .progress(value: nil, cancelEnabled: false, appearance: SemanticStatusNodeState.ProgressAppearance(inset: 0.0, lineWidth: 2.0)) : .none)
|
self.statusNode.transitionToState(state.progress == .side ? .progress(value: nil, cancelEnabled: false, appearance: SemanticStatusNodeState.ProgressAppearance(inset: 0.0, lineWidth: 2.0), animateRotation: true) : .none)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -530,6 +530,8 @@ final class ComposePollScreenComponent: Component {
|
|||||||
let themeUpdated = self.environment?.theme !== environment.theme
|
let themeUpdated = self.environment?.theme !== environment.theme
|
||||||
self.environment = environment
|
self.environment = environment
|
||||||
|
|
||||||
|
let theme = environment.theme.withModalBlocksBackground()
|
||||||
|
|
||||||
if self.component == nil {
|
if self.component == nil {
|
||||||
self.isQuiz = component.isQuiz ?? false
|
self.isQuiz = component.isQuiz ?? false
|
||||||
|
|
||||||
@ -682,7 +684,7 @@ final class ComposePollScreenComponent: Component {
|
|||||||
let sectionSpacing: CGFloat = 24.0
|
let sectionSpacing: CGFloat = 24.0
|
||||||
|
|
||||||
if themeUpdated {
|
if themeUpdated {
|
||||||
self.backgroundColor = environment.theme.list.blocksBackgroundColor
|
self.backgroundColor = theme.list.blocksBackgroundColor
|
||||||
}
|
}
|
||||||
|
|
||||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||||
@ -695,7 +697,7 @@ final class ComposePollScreenComponent: Component {
|
|||||||
pollTextSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListComposePollOptionComponent(
|
pollTextSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListComposePollOptionComponent(
|
||||||
externalState: self.pollTextInputState,
|
externalState: self.pollTextInputState,
|
||||||
context: component.context,
|
context: component.context,
|
||||||
theme: environment.theme,
|
theme: theme,
|
||||||
strings: environment.strings,
|
strings: environment.strings,
|
||||||
resetText: self.resetPollText.flatMap { resetText in
|
resetText: self.resetPollText.flatMap { resetText in
|
||||||
return ListComposePollOptionComponent.ResetText(value: NSAttributedString(string: resetText))
|
return ListComposePollOptionComponent.ResetText(value: NSAttributedString(string: resetText))
|
||||||
@ -737,12 +739,12 @@ final class ComposePollScreenComponent: Component {
|
|||||||
let pollTextSectionSize = self.pollTextSection.update(
|
let pollTextSectionSize = self.pollTextSection.update(
|
||||||
transition: transition,
|
transition: transition,
|
||||||
component: AnyComponent(ListSectionComponent(
|
component: AnyComponent(ListSectionComponent(
|
||||||
theme: environment.theme,
|
theme: theme,
|
||||||
header: AnyComponent(MultilineTextComponent(
|
header: AnyComponent(MultilineTextComponent(
|
||||||
text: .plain(NSAttributedString(
|
text: .plain(NSAttributedString(
|
||||||
string: environment.strings.CreatePoll_TextHeader,
|
string: environment.strings.CreatePoll_TextHeader,
|
||||||
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
|
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
|
||||||
textColor: environment.theme.list.freeTextColor
|
textColor: theme.list.freeTextColor
|
||||||
)),
|
)),
|
||||||
maximumNumberOfLines: 0
|
maximumNumberOfLines: 0
|
||||||
)),
|
)),
|
||||||
@ -790,7 +792,7 @@ final class ComposePollScreenComponent: Component {
|
|||||||
pollOptionsSectionItems.append(AnyComponentWithIdentity(id: pollOption.id, component: AnyComponent(ListComposePollOptionComponent(
|
pollOptionsSectionItems.append(AnyComponentWithIdentity(id: pollOption.id, component: AnyComponent(ListComposePollOptionComponent(
|
||||||
externalState: pollOption.textInputState,
|
externalState: pollOption.textInputState,
|
||||||
context: component.context,
|
context: component.context,
|
||||||
theme: environment.theme,
|
theme: theme,
|
||||||
strings: environment.strings,
|
strings: environment.strings,
|
||||||
resetText: pollOption.resetText.flatMap { resetText in
|
resetText: pollOption.resetText.flatMap { resetText in
|
||||||
return ListComposePollOptionComponent.ResetText(value: NSAttributedString(string: resetText))
|
return ListComposePollOptionComponent.ResetText(value: NSAttributedString(string: resetText))
|
||||||
@ -916,7 +918,7 @@ final class ComposePollScreenComponent: Component {
|
|||||||
|
|
||||||
let pollOptionsSectionUpdateResult = self.pollOptionsSectionContainer.update(
|
let pollOptionsSectionUpdateResult = self.pollOptionsSectionContainer.update(
|
||||||
configuration: ListSectionContentView.Configuration(
|
configuration: ListSectionContentView.Configuration(
|
||||||
theme: environment.theme,
|
theme: theme,
|
||||||
displaySeparators: true,
|
displaySeparators: true,
|
||||||
extendsItemHighlightToSection: false,
|
extendsItemHighlightToSection: false,
|
||||||
background: .all
|
background: .all
|
||||||
@ -934,7 +936,7 @@ final class ComposePollScreenComponent: Component {
|
|||||||
text: .plain(NSAttributedString(
|
text: .plain(NSAttributedString(
|
||||||
string: environment.strings.CreatePoll_OptionsHeader,
|
string: environment.strings.CreatePoll_OptionsHeader,
|
||||||
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
|
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
|
||||||
textColor: environment.theme.list.freeTextColor
|
textColor: theme.list.freeTextColor
|
||||||
)),
|
)),
|
||||||
maximumNumberOfLines: 0
|
maximumNumberOfLines: 0
|
||||||
)),
|
)),
|
||||||
@ -983,7 +985,7 @@ final class ComposePollScreenComponent: Component {
|
|||||||
if pollOptionsLimitReached {
|
if pollOptionsLimitReached {
|
||||||
pollOptionsFooterTransition = pollOptionsFooterTransition.withAnimation(.none)
|
pollOptionsFooterTransition = pollOptionsFooterTransition.withAnimation(.none)
|
||||||
pollOptionsComponent = AnyComponent(MultilineTextComponent(
|
pollOptionsComponent = AnyComponent(MultilineTextComponent(
|
||||||
text: .plain(NSAttributedString(string: environment.strings.CreatePoll_AllOptionsAdded, font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.freeTextColor)),
|
text: .plain(NSAttributedString(string: environment.strings.CreatePoll_AllOptionsAdded, font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: theme.list.freeTextColor)),
|
||||||
maximumNumberOfLines: 0
|
maximumNumberOfLines: 0
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
@ -1015,7 +1017,7 @@ final class ComposePollScreenComponent: Component {
|
|||||||
|
|
||||||
pollOptionsComponent = AnyComponent(AnimatedTextComponent(
|
pollOptionsComponent = AnyComponent(AnimatedTextComponent(
|
||||||
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
|
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
|
||||||
color: environment.theme.list.freeTextColor,
|
color: theme.list.freeTextColor,
|
||||||
items: pollOptionsFooterItems
|
items: pollOptionsFooterItems
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
@ -1055,13 +1057,13 @@ final class ComposePollScreenComponent: Component {
|
|||||||
var pollSettingsSectionItems: [AnyComponentWithIdentity<Empty>] = []
|
var pollSettingsSectionItems: [AnyComponentWithIdentity<Empty>] = []
|
||||||
if canBePublic {
|
if canBePublic {
|
||||||
pollSettingsSectionItems.append(AnyComponentWithIdentity(id: "anonymous", component: AnyComponent(ListActionItemComponent(
|
pollSettingsSectionItems.append(AnyComponentWithIdentity(id: "anonymous", component: AnyComponent(ListActionItemComponent(
|
||||||
theme: environment.theme,
|
theme: theme,
|
||||||
title: AnyComponent(VStack([
|
title: AnyComponent(VStack([
|
||||||
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
|
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
|
||||||
text: .plain(NSAttributedString(
|
text: .plain(NSAttributedString(
|
||||||
string: environment.strings.CreatePoll_Anonymous,
|
string: environment.strings.CreatePoll_Anonymous,
|
||||||
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
|
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
|
||||||
textColor: environment.theme.list.itemPrimaryTextColor
|
textColor: theme.list.itemPrimaryTextColor
|
||||||
)),
|
)),
|
||||||
maximumNumberOfLines: 1
|
maximumNumberOfLines: 1
|
||||||
))),
|
))),
|
||||||
@ -1077,13 +1079,13 @@ final class ComposePollScreenComponent: Component {
|
|||||||
))))
|
))))
|
||||||
}
|
}
|
||||||
pollSettingsSectionItems.append(AnyComponentWithIdentity(id: "multiAnswer", component: AnyComponent(ListActionItemComponent(
|
pollSettingsSectionItems.append(AnyComponentWithIdentity(id: "multiAnswer", component: AnyComponent(ListActionItemComponent(
|
||||||
theme: environment.theme,
|
theme: theme,
|
||||||
title: AnyComponent(VStack([
|
title: AnyComponent(VStack([
|
||||||
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
|
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
|
||||||
text: .plain(NSAttributedString(
|
text: .plain(NSAttributedString(
|
||||||
string: environment.strings.CreatePoll_MultipleChoice,
|
string: environment.strings.CreatePoll_MultipleChoice,
|
||||||
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
|
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
|
||||||
textColor: environment.theme.list.itemPrimaryTextColor
|
textColor: theme.list.itemPrimaryTextColor
|
||||||
)),
|
)),
|
||||||
maximumNumberOfLines: 1
|
maximumNumberOfLines: 1
|
||||||
))),
|
))),
|
||||||
@ -1101,13 +1103,13 @@ final class ComposePollScreenComponent: Component {
|
|||||||
action: nil
|
action: nil
|
||||||
))))
|
))))
|
||||||
pollSettingsSectionItems.append(AnyComponentWithIdentity(id: "quiz", component: AnyComponent(ListActionItemComponent(
|
pollSettingsSectionItems.append(AnyComponentWithIdentity(id: "quiz", component: AnyComponent(ListActionItemComponent(
|
||||||
theme: environment.theme,
|
theme: theme,
|
||||||
title: AnyComponent(VStack([
|
title: AnyComponent(VStack([
|
||||||
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
|
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
|
||||||
text: .plain(NSAttributedString(
|
text: .plain(NSAttributedString(
|
||||||
string: environment.strings.CreatePoll_Quiz,
|
string: environment.strings.CreatePoll_Quiz,
|
||||||
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
|
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
|
||||||
textColor: environment.theme.list.itemPrimaryTextColor
|
textColor: theme.list.itemPrimaryTextColor
|
||||||
)),
|
)),
|
||||||
maximumNumberOfLines: 1
|
maximumNumberOfLines: 1
|
||||||
))),
|
))),
|
||||||
@ -1128,13 +1130,13 @@ final class ComposePollScreenComponent: Component {
|
|||||||
let pollSettingsSectionSize = self.pollSettingsSection.update(
|
let pollSettingsSectionSize = self.pollSettingsSection.update(
|
||||||
transition: transition,
|
transition: transition,
|
||||||
component: AnyComponent(ListSectionComponent(
|
component: AnyComponent(ListSectionComponent(
|
||||||
theme: environment.theme,
|
theme: theme,
|
||||||
header: nil,
|
header: nil,
|
||||||
footer: AnyComponent(MultilineTextComponent(
|
footer: AnyComponent(MultilineTextComponent(
|
||||||
text: .plain(NSAttributedString(
|
text: .plain(NSAttributedString(
|
||||||
string: environment.strings.CreatePoll_QuizInfo,
|
string: environment.strings.CreatePoll_QuizInfo,
|
||||||
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
|
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
|
||||||
textColor: environment.theme.list.freeTextColor
|
textColor: theme.list.freeTextColor
|
||||||
)),
|
)),
|
||||||
maximumNumberOfLines: 0
|
maximumNumberOfLines: 0
|
||||||
)),
|
)),
|
||||||
@ -1158,12 +1160,12 @@ final class ComposePollScreenComponent: Component {
|
|||||||
let quizAnswerSectionSize = self.quizAnswerSection.update(
|
let quizAnswerSectionSize = self.quizAnswerSection.update(
|
||||||
transition: transition,
|
transition: transition,
|
||||||
component: AnyComponent(ListSectionComponent(
|
component: AnyComponent(ListSectionComponent(
|
||||||
theme: environment.theme,
|
theme: theme,
|
||||||
header: AnyComponent(MultilineTextComponent(
|
header: AnyComponent(MultilineTextComponent(
|
||||||
text: .plain(NSAttributedString(
|
text: .plain(NSAttributedString(
|
||||||
string: environment.strings.CreatePoll_ExplanationHeader,
|
string: environment.strings.CreatePoll_ExplanationHeader,
|
||||||
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
|
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
|
||||||
textColor: environment.theme.list.freeTextColor
|
textColor: theme.list.freeTextColor
|
||||||
)),
|
)),
|
||||||
maximumNumberOfLines: 0
|
maximumNumberOfLines: 0
|
||||||
)),
|
)),
|
||||||
@ -1171,7 +1173,7 @@ final class ComposePollScreenComponent: Component {
|
|||||||
text: .plain(NSAttributedString(
|
text: .plain(NSAttributedString(
|
||||||
string: environment.strings.CreatePoll_ExplanationInfo,
|
string: environment.strings.CreatePoll_ExplanationInfo,
|
||||||
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
|
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
|
||||||
textColor: environment.theme.list.freeTextColor
|
textColor: theme.list.freeTextColor
|
||||||
)),
|
)),
|
||||||
maximumNumberOfLines: 0
|
maximumNumberOfLines: 0
|
||||||
)),
|
)),
|
||||||
@ -1179,7 +1181,7 @@ final class ComposePollScreenComponent: Component {
|
|||||||
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListComposePollOptionComponent(
|
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListComposePollOptionComponent(
|
||||||
externalState: self.quizAnswerTextInputState,
|
externalState: self.quizAnswerTextInputState,
|
||||||
context: component.context,
|
context: component.context,
|
||||||
theme: environment.theme,
|
theme: theme,
|
||||||
strings: environment.strings,
|
strings: environment.strings,
|
||||||
resetText: self.resetQuizAnswerText.flatMap { resetText in
|
resetText: self.resetQuizAnswerText.flatMap { resetText in
|
||||||
return ListComposePollOptionComponent.ResetText(value: NSAttributedString(string: resetText))
|
return ListComposePollOptionComponent.ResetText(value: NSAttributedString(string: resetText))
|
||||||
@ -1326,7 +1328,7 @@ final class ComposePollScreenComponent: Component {
|
|||||||
component: AnyComponent(EmojiSuggestionsComponent(
|
component: AnyComponent(EmojiSuggestionsComponent(
|
||||||
context: component.context,
|
context: component.context,
|
||||||
userLocation: .other,
|
userLocation: .other,
|
||||||
theme: EmojiSuggestionsComponent.Theme(theme: environment.theme, backgroundColor: environment.theme.list.itemBlocksBackgroundColor),
|
theme: EmojiSuggestionsComponent.Theme(theme: theme, backgroundColor: theme.list.itemBlocksBackgroundColor),
|
||||||
animationCache: component.context.animationCache,
|
animationCache: component.context.animationCache,
|
||||||
animationRenderer: component.context.animationRenderer,
|
animationRenderer: component.context.animationRenderer,
|
||||||
files: value,
|
files: value,
|
||||||
|
@ -66,6 +66,9 @@ swift_library(
|
|||||||
"//submodules/ComponentFlow",
|
"//submodules/ComponentFlow",
|
||||||
"//submodules/TelegramUI/Components/ToastComponent",
|
"//submodules/TelegramUI/Components/ToastComponent",
|
||||||
"//submodules/SemanticStatusNode",
|
"//submodules/SemanticStatusNode",
|
||||||
|
"//submodules/TelegramUI/Components/PlainButtonComponent",
|
||||||
|
"//submodules/AvatarNode",
|
||||||
|
"//submodules/PhotoResources",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
@ -0,0 +1,212 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import AsyncDisplayKit
|
||||||
|
import Display
|
||||||
|
import SwiftSignalKit
|
||||||
|
import ComponentFlow
|
||||||
|
import SemanticStatusNode
|
||||||
|
import AnimatedTextComponent
|
||||||
|
|
||||||
|
public final class AdRemainingProgressComponent: Component {
|
||||||
|
public let initialTimestamp: Int32
|
||||||
|
public let minDisplayDuration: Int32
|
||||||
|
public let maxDisplayDuration: Int32
|
||||||
|
public let action: (Bool) -> Void
|
||||||
|
|
||||||
|
public init(
|
||||||
|
initialTimestamp: Int32,
|
||||||
|
minDisplayDuration: Int32,
|
||||||
|
maxDisplayDuration: Int32,
|
||||||
|
action: @escaping (Bool) -> Void
|
||||||
|
) {
|
||||||
|
self.initialTimestamp = initialTimestamp
|
||||||
|
self.minDisplayDuration = minDisplayDuration
|
||||||
|
self.maxDisplayDuration = maxDisplayDuration
|
||||||
|
self.action = action
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func ==(lhs: AdRemainingProgressComponent, rhs: AdRemainingProgressComponent) -> Bool {
|
||||||
|
if lhs.initialTimestamp != rhs.initialTimestamp {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.minDisplayDuration != rhs.minDisplayDuration {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.maxDisplayDuration != rhs.maxDisplayDuration {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class View: HighlightTrackingButton {
|
||||||
|
private var component: AdRemainingProgressComponent?
|
||||||
|
private weak var componentState: EmptyComponentState?
|
||||||
|
|
||||||
|
private let node: SemanticStatusNode
|
||||||
|
private var textComponent = ComponentView<Empty>()
|
||||||
|
private var cancelIcon = UIImageView()
|
||||||
|
|
||||||
|
private var progress: Double = 1.0
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
self.node = SemanticStatusNode(backgroundNodeColor: .clear, foregroundNodeColor: .white)
|
||||||
|
self.node.isUserInteractionEnabled = false
|
||||||
|
|
||||||
|
self.cancelIcon.alpha = 0.0
|
||||||
|
self.cancelIcon.isUserInteractionEnabled = false
|
||||||
|
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.addSubview(self.node.view)
|
||||||
|
self.addSubview(self.cancelIcon)
|
||||||
|
|
||||||
|
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
|
||||||
|
|
||||||
|
self.highligthedChanged = { [weak self] highlighted in
|
||||||
|
if let self {
|
||||||
|
if highlighted {
|
||||||
|
self.layer.removeAnimation(forKey: "opacity")
|
||||||
|
self.alpha = 0.7
|
||||||
|
} else {
|
||||||
|
self.alpha = 1.0
|
||||||
|
self.layer.animateAlpha(from: 7, to: 1.0, duration: 0.2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
required init(coder: NSCoder) {
|
||||||
|
preconditionFailure()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func pressed() {
|
||||||
|
guard let component = self.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
component.action(self.progress < .ulpOfOne)
|
||||||
|
}
|
||||||
|
|
||||||
|
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||||
|
return super.hitTest(point, with: event)
|
||||||
|
}
|
||||||
|
|
||||||
|
var timer: SwiftSignalKit.Timer?
|
||||||
|
|
||||||
|
func update(component: AdRemainingProgressComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||||
|
if self.component == nil {
|
||||||
|
self.timer = SwiftSignalKit.Timer(timeout: 0.25, repeat: true, completion: { [weak self] progress in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.componentState?.updated(transition: .easeInOut(duration: 0.2))
|
||||||
|
}, queue: Queue.mainQueue())
|
||||||
|
self.timer?.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
self.component = component
|
||||||
|
self.componentState = state
|
||||||
|
|
||||||
|
let size = CGSize(width: 24.0, height: 24.0)
|
||||||
|
let color = UIColor(rgb: 0x64d2ff)
|
||||||
|
|
||||||
|
if self.cancelIcon.image == nil {
|
||||||
|
self.cancelIcon.image = generateCancelIcon(color: color)
|
||||||
|
self.node.foregroundNodeColor = color
|
||||||
|
}
|
||||||
|
|
||||||
|
var progress = 0.0
|
||||||
|
let currentTimestamp = CFAbsoluteTimeGetCurrent()
|
||||||
|
let minTimestamp = Double(component.initialTimestamp + component.minDisplayDuration)
|
||||||
|
let initialTimestamp = Double(component.initialTimestamp)
|
||||||
|
|
||||||
|
let remaining = min(9, max(1, minTimestamp - currentTimestamp))
|
||||||
|
|
||||||
|
var textIsHidden = false
|
||||||
|
if currentTimestamp >= initialTimestamp && currentTimestamp <= minTimestamp {
|
||||||
|
progress = (minTimestamp - currentTimestamp) / (minTimestamp - initialTimestamp)
|
||||||
|
} else {
|
||||||
|
progress = 0
|
||||||
|
textIsHidden = true
|
||||||
|
}
|
||||||
|
self.progress = progress
|
||||||
|
|
||||||
|
let textSize = self.textComponent.update(
|
||||||
|
transition: transition,
|
||||||
|
component: AnyComponent(
|
||||||
|
AnimatedTextComponent(
|
||||||
|
font: Font.regular(14.0),
|
||||||
|
color: color,
|
||||||
|
items: [AnimatedTextComponent.Item(id: 0, content: .number(Int(remaining), minDigits: 1))]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
environment: {},
|
||||||
|
containerSize: size
|
||||||
|
)
|
||||||
|
|
||||||
|
let iconTransition = ComponentTransition(animation: .curve(duration: 0.25, curve: .spring))
|
||||||
|
if let textView = self.textComponent.view {
|
||||||
|
if textView.superview == nil {
|
||||||
|
textView.isUserInteractionEnabled = false
|
||||||
|
self.addSubview(textView)
|
||||||
|
}
|
||||||
|
textView.frame = CGRect(origin: CGPoint(x: (size.width - textSize.width) / 2.0, y: (size.height - textSize.height) / 2.0), size: textSize)
|
||||||
|
iconTransition.setAlpha(view: textView, alpha: textIsHidden ? 0.0 : 1.0)
|
||||||
|
iconTransition.setAlpha(view: self.cancelIcon, alpha: textIsHidden ? 1.0 : 0.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let icon = self.cancelIcon.image {
|
||||||
|
self.cancelIcon.bounds = CGRect(origin: .zero, size: icon.size)
|
||||||
|
|
||||||
|
var iconScale = 0.7
|
||||||
|
var iconAlpha = 1.0
|
||||||
|
var iconPosition = CGPoint(x: size.width / 2.0 + 10.0, y: size.height / 2.0 - 10.0)
|
||||||
|
if progress > 0.8 {
|
||||||
|
iconScale = 0.01
|
||||||
|
iconAlpha = 0.0
|
||||||
|
} else if progress < .ulpOfOne {
|
||||||
|
iconScale = 1.0
|
||||||
|
iconPosition = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
|
||||||
|
}
|
||||||
|
iconTransition.setAlpha(view: self.cancelIcon, alpha: iconAlpha)
|
||||||
|
iconTransition.setScale(view: self.cancelIcon, scale: iconScale)
|
||||||
|
iconTransition.setPosition(view: self.cancelIcon, position: iconPosition)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.node.frame = CGRect(origin: .zero, size: size)
|
||||||
|
self.node.transitionToState(.progress(value: max(0.0, min(1.0, progress)), cancelEnabled: false, appearance: SemanticStatusNodeState.ProgressAppearance(inset: 1.0, lineWidth: 1.0 + UIScreenPixel), animateRotation: false), updateCutout: false)
|
||||||
|
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func makeView() -> View {
|
||||||
|
return View(frame: CGRect())
|
||||||
|
}
|
||||||
|
|
||||||
|
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||||
|
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func generateCancelIcon(color: UIColor) -> UIImage? {
|
||||||
|
return generateImage(CGSize(width: 12.0, height: 12.0), rotatedContext: { size, context in
|
||||||
|
let bounds = CGRect(origin: CGPoint(), size: size)
|
||||||
|
context.clear(bounds)
|
||||||
|
|
||||||
|
let lineWidth = 2.0 - UIScreenPixel
|
||||||
|
context.setLineWidth(lineWidth)
|
||||||
|
context.setLineCap(.round)
|
||||||
|
context.setStrokeColor(color.cgColor)
|
||||||
|
|
||||||
|
context.move(to: CGPoint(x: lineWidth / 2.0, y: lineWidth / 2.0))
|
||||||
|
context.addLine(to: CGPoint(x: size.width - lineWidth / 2.0, y: size.height - lineWidth / 2.0))
|
||||||
|
|
||||||
|
context.strokePath()
|
||||||
|
|
||||||
|
context.move(to: CGPoint(x: size.width - lineWidth / 2.0, y: lineWidth / 2.0))
|
||||||
|
context.addLine(to: CGPoint(x: lineWidth / 2.0, y: size.height - lineWidth / 2.0))
|
||||||
|
|
||||||
|
context.strokePath()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -756,7 +756,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||||||
guard let controller = self.baseNavigationController()?.topViewController as? ViewController else {
|
guard let controller = self.baseNavigationController()?.topViewController as? ViewController else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let contextController = ContextController(presentationData: self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme), source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: self.moreBarButton.referenceNode)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture)
|
let contextController = ContextController(presentationData: self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme), source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: self.moreBarButton.referenceNode, actionsOnTop: false)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture)
|
||||||
controller.presentInGlobalOverlay(contextController)
|
controller.presentInGlobalOverlay(contextController)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -223,85 +223,190 @@ private final class UniversalVideoGalleryItemPictureInPictureNode: ASDisplayNode
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private let fullscreenImage = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/Fullscreen"), color: .white)
|
|
||||||
private let minimizeImage = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/Minimize"), color: .white)
|
|
||||||
|
|
||||||
private final class UniversalVideoGalleryItemOverlayNode: GalleryOverlayContentNode {
|
private final class UniversalVideoGalleryItemOverlayNode: GalleryOverlayContentNode {
|
||||||
private let wrapperNode: ASDisplayNode
|
private var context: AccountContext?
|
||||||
private let fullscreenNode: HighlightableButtonNode
|
|
||||||
private var validLayout: (CGSize, LayoutMetrics, UIEdgeInsets)?
|
|
||||||
|
|
||||||
var action: ((Bool) -> Void)?
|
private var adView = ComponentView<Empty>()
|
||||||
|
|
||||||
override init() {
|
private var message: Message?
|
||||||
self.wrapperNode = ASDisplayNode()
|
private var adContext: AdMessagesHistoryContext?
|
||||||
self.wrapperNode.alpha = 0.0
|
private var adState: (startDelay: Int32?, betweenDelay: Int32?, messages: [Message])?
|
||||||
|
private let adDisposable = MetaDisposable()
|
||||||
|
|
||||||
self.fullscreenNode = HighlightableButtonNode()
|
private var program: [(Int32, Message?)] = []
|
||||||
self.fullscreenNode.setImage(fullscreenImage, for: .normal)
|
|
||||||
self.fullscreenNode.setImage(minimizeImage, for: .selected)
|
|
||||||
self.fullscreenNode.setImage(minimizeImage, for: [.selected, .highlighted])
|
|
||||||
|
|
||||||
super.init()
|
var performAction: ((GalleryControllerInteractionTapAction) -> Void)?
|
||||||
|
var presentPremiumDemo: (() -> Void)?
|
||||||
|
var openMoreMenu: ((ContextReferenceContentNode, Message) -> Void)?
|
||||||
|
|
||||||
self.addSubnode(self.wrapperNode)
|
private var validLayout: (size: CGSize, metrics: LayoutMetrics, insets: UIEdgeInsets)?
|
||||||
self.wrapperNode.addSubnode(self.fullscreenNode)
|
|
||||||
|
|
||||||
self.fullscreenNode.addTarget(self, action: #selector(self.toggleFullscreenPressed), forControlEvents: .touchUpInside)
|
deinit {
|
||||||
|
self.adDisposable.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setMessage(context: AccountContext, message: Message) {
|
||||||
|
self.context = context
|
||||||
|
guard self.message?.id != message.id else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.message = message
|
||||||
|
|
||||||
|
let adContext = context.engine.messages.adMessages(peerId: message.id.peerId, messageId: message.id)
|
||||||
|
self.adContext = adContext
|
||||||
|
self.adDisposable.set((adContext.state
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] state in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !state.messages.isEmpty {
|
||||||
|
self.adState = (state.startDelay, state.betweenDelay, state.messages)
|
||||||
|
|
||||||
|
var startTime = Int32(CFAbsoluteTimeGetCurrent()) // + (state.startDelay ?? 0)
|
||||||
|
var program: [(Int32, Message?)] = []
|
||||||
|
var maxDisplayDuration: Int32 = 30
|
||||||
|
for message in state.messages {
|
||||||
|
if !program.isEmpty {
|
||||||
|
program.append((startTime, nil))
|
||||||
|
startTime += (state.betweenDelay ?? 0)
|
||||||
|
}
|
||||||
|
program.append((startTime, message))
|
||||||
|
|
||||||
|
if let adAttribute = message.adAttribute {
|
||||||
|
maxDisplayDuration = adAttribute.maxDisplayDuration ?? 30
|
||||||
|
startTime += maxDisplayDuration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
program.append((startTime + maxDisplayDuration, nil))
|
||||||
|
self.program = program
|
||||||
|
} else {
|
||||||
|
self.adState = nil
|
||||||
|
|
||||||
|
self.program = []
|
||||||
|
}
|
||||||
|
|
||||||
|
if let validLayout = self.validLayout {
|
||||||
|
self.updateLayout(size: validLayout.size, metrics: validLayout.metrics, insets: validLayout.insets, isHidden: false, transition: .immediate)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
var timer: SwiftSignalKit.Timer?
|
||||||
|
var hiddenMessages = Set<MessageId>()
|
||||||
|
var isAnimatingOut = false
|
||||||
|
var reportedMessages = Set<Data>()
|
||||||
|
|
||||||
override func updateLayout(size: CGSize, metrics: LayoutMetrics, insets: UIEdgeInsets, isHidden: Bool, transition: ContainedViewLayoutTransition) {
|
override func updateLayout(size: CGSize, metrics: LayoutMetrics, insets: UIEdgeInsets, isHidden: Bool, transition: ContainedViewLayoutTransition) {
|
||||||
self.validLayout = (size, metrics, insets)
|
self.validLayout = (size, metrics, insets)
|
||||||
|
|
||||||
let isLandscape = size.width > size.height
|
if self.timer == nil {
|
||||||
self.fullscreenNode.isSelected = isLandscape
|
self.timer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] progress in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if let validLayout = self.validLayout {
|
||||||
|
self.updateLayout(size: validLayout.size, metrics: validLayout.metrics, insets: validLayout.insets, isHidden: false, transition: .immediate)
|
||||||
|
}
|
||||||
|
}, queue: Queue.mainQueue())
|
||||||
|
self.timer?.start()
|
||||||
|
}
|
||||||
|
|
||||||
let iconSize: CGFloat = 42.0
|
let isLandscape = size.width > size.height
|
||||||
let inset: CGFloat = 4.0
|
let _ = isLandscape
|
||||||
let buttonFrame = CGRect(origin: CGPoint(x: size.width - iconSize - inset - insets.right, y: size.height - iconSize - inset - insets.bottom), size: CGSize(width: iconSize, height: iconSize))
|
|
||||||
transition.updateFrame(node: self.wrapperNode, frame: buttonFrame)
|
let currentTime = Int32(CFAbsoluteTimeGetCurrent())
|
||||||
transition.updateFrame(node: self.fullscreenNode, frame: CGRect(origin: CGPoint(), size: buttonFrame.size))
|
var currentAd: (Int32, Message?)?
|
||||||
|
|
||||||
|
for (time, maybeMessage) in program {
|
||||||
|
if currentTime > time {
|
||||||
|
currentAd = (time, maybeMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let context = self.context, let (initialTimestamp, maybeMessage) = currentAd, let adMessage = maybeMessage, !self.hiddenMessages.contains(adMessage.id) {
|
||||||
|
if let adAttribute = adMessage.adAttribute {
|
||||||
|
if !self.reportedMessages.contains(adAttribute.opaqueId) {
|
||||||
|
self.reportedMessages.insert(adAttribute.opaqueId)
|
||||||
|
context.engine.messages.markAdAsSeen(opaqueId: adAttribute.opaqueId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let sideInset: CGFloat = 16.0
|
||||||
|
|
||||||
|
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||||
|
let adSize = self.adView.update(
|
||||||
|
transition: .immediate,
|
||||||
|
component: AnyComponent(
|
||||||
|
VideoAdComponent(
|
||||||
|
context: context,
|
||||||
|
theme: presentationData.theme,
|
||||||
|
strings: presentationData.strings,
|
||||||
|
message: EngineMessage(adMessage),
|
||||||
|
initialTimestamp: initialTimestamp,
|
||||||
|
action: { [weak self] available in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if available {
|
||||||
|
self.hiddenMessages.insert(adMessage.id)
|
||||||
|
if let validLayout = self.validLayout {
|
||||||
|
self.updateLayout(size: validLayout.size, metrics: validLayout.metrics, insets: validLayout.insets, isHidden: false, transition: .immediate)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.presentPremiumDemo?()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
adAction: { [weak self] in
|
||||||
|
if let self, let ad = adMessage.adAttribute {
|
||||||
|
context.engine.messages.markAdAction(opaqueId: ad.opaqueId, media: false, fullscreen: false)
|
||||||
|
self.performAction?(.url(url: ad.url, concealed: false))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
moreAction: { [weak self] sourceNode in
|
||||||
|
if let self {
|
||||||
|
self.openMoreMenu?(sourceNode, adMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: size.width - sideInset * 2.0, height: 200.0)
|
||||||
|
)
|
||||||
|
if let adView = self.adView.view {
|
||||||
|
if adView.superview == nil {
|
||||||
|
self.view.addSubview(adView)
|
||||||
|
|
||||||
|
adView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||||
|
adView.layer.animatePosition(from: CGPoint(x: 0.0, y: 64.0), to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||||
|
}
|
||||||
|
transition.updateFrame(view: adView, frame: CGRect(origin: CGPoint(x: floor((size.width - adSize.width) / 2.0), y: size.height - adSize.height - insets.bottom), size: adSize))
|
||||||
|
}
|
||||||
|
} else if let adView = self.adView.view, !self.isAnimatingOut {
|
||||||
|
self.isAnimatingOut = true
|
||||||
|
adView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false)
|
||||||
|
adView.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: 64.0), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true, completion: { _ in
|
||||||
|
adView.removeFromSuperview()
|
||||||
|
Queue.mainQueue().after(0.1) {
|
||||||
|
adView.layer.removeAllAnimations()
|
||||||
|
}
|
||||||
|
self.isAnimatingOut = false
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func animateIn(previousContentNode: GalleryOverlayContentNode?, transition: ContainedViewLayoutTransition) {
|
override func animateIn(previousContentNode: GalleryOverlayContentNode?, transition: ContainedViewLayoutTransition) {
|
||||||
if !self.visibilityAlpha.isZero {
|
|
||||||
transition.updateAlpha(node: self.wrapperNode, alpha: 1.0)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func animateOut(nextContentNode: GalleryOverlayContentNode?, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
|
override func animateOut(nextContentNode: GalleryOverlayContentNode?, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
|
||||||
transition.updateAlpha(node: self.wrapperNode, alpha: 0.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func setVisibilityAlpha(_ alpha: CGFloat) {
|
|
||||||
super.setVisibilityAlpha(alpha)
|
|
||||||
self.updateFullscreenButtonVisibility()
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateFullscreenButtonVisibility() {
|
|
||||||
self.wrapperNode.alpha = self.visibilityAlpha
|
|
||||||
|
|
||||||
if let validLayout = self.validLayout {
|
|
||||||
self.updateLayout(size: validLayout.0, metrics: validLayout.1, insets: validLayout.2, isHidden: false, transition: .animated(duration: 0.3, curve: .easeInOut))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc func toggleFullscreenPressed() {
|
|
||||||
var toLandscape = false
|
|
||||||
if let (size, _, _) = self.validLayout, size.width < size.height {
|
|
||||||
toLandscape = true
|
|
||||||
}
|
|
||||||
if toLandscape {
|
|
||||||
self.wrapperNode.alpha = 0.0
|
|
||||||
}
|
|
||||||
self.action?(toLandscape)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||||
if !self.wrapperNode.frame.contains(point) {
|
if let adView = self.adView.view, adView.frame.contains(point) {
|
||||||
return nil
|
return super.hitTest(point, with: event)
|
||||||
}
|
}
|
||||||
return super.hitTest(point, with: event)
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1150,14 +1255,6 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||||||
self?.isInteractingPromise.set(value)
|
self?.isInteractingPromise.set(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.overlayContentNode.action = { [weak self] toLandscape in
|
|
||||||
guard let self else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
self.updateControlsVisibility(!toLandscape)
|
|
||||||
self.updateOrientation(toLandscape ? .landscapeRight : .portrait)
|
|
||||||
}
|
|
||||||
|
|
||||||
self.statusButtonNode.addSubnode(self.statusNode)
|
self.statusButtonNode.addSubnode(self.statusNode)
|
||||||
self.statusButtonNode.addTarget(self, action: #selector(self.statusButtonPressed), forControlEvents: .touchUpInside)
|
self.statusButtonNode.addTarget(self, action: #selector(self.statusButtonPressed), forControlEvents: .touchUpInside)
|
||||||
|
|
||||||
@ -1262,7 +1359,11 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.openMoreMenu(sourceNode: self.moreBarButton.referenceNode, gesture: gesture, isSettings: false)
|
var adMessage: Message?
|
||||||
|
if case let .message(message, _) = self.item?.contentInfo, let _ = message.adAttribute {
|
||||||
|
adMessage = message
|
||||||
|
}
|
||||||
|
self.openMoreMenu(sourceNode: self.moreBarButton.referenceNode, gesture: gesture, adMessage: adMessage, isSettings: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.titleContentView = GalleryTitleView(frame: CGRect())
|
self.titleContentView = GalleryTitleView(frame: CGRect())
|
||||||
@ -1524,13 +1625,6 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||||||
|
|
||||||
let _ = isAdaptive
|
let _ = isAdaptive
|
||||||
|
|
||||||
let dimensions = item.content.dimensions
|
|
||||||
if dimensions.height > 0.0 {
|
|
||||||
if dimensions.width / dimensions.height < 1.33 || isAnimated {
|
|
||||||
self.overlayContentNode.isHidden = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let videoNode = self.videoNode {
|
if let videoNode = self.videoNode {
|
||||||
videoNode.canAttachContent = false
|
videoNode.canAttachContent = false
|
||||||
videoNode.removeFromSupernode()
|
videoNode.removeFromSupernode()
|
||||||
@ -1993,6 +2087,17 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.footerContentNode.setup(origin: item.originData, caption: item.caption, isAd: isAd)
|
self.footerContentNode.setup(origin: item.originData, caption: item.caption, isAd: isAd)
|
||||||
|
|
||||||
|
if let contentInfo = item.contentInfo, case let .message(message, _) = contentInfo {
|
||||||
|
self.overlayContentNode.performAction = item.performAction
|
||||||
|
self.overlayContentNode.presentPremiumDemo = { [weak self] in
|
||||||
|
self?.presentPremiumDemo()
|
||||||
|
}
|
||||||
|
self.overlayContentNode.openMoreMenu = { [weak self] sourceNode, adMessage in
|
||||||
|
self?.openMoreMenu(sourceNode: sourceNode, gesture: nil, adMessage: adMessage, isSettings: false, actionsOnTop: true)
|
||||||
|
}
|
||||||
|
self.overlayContentNode.setMessage(context: item.context, message: message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func controlsVisibilityUpdated(isVisible: Bool) {
|
override func controlsVisibilityUpdated(isVisible: Bool) {
|
||||||
@ -3109,15 +3214,15 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||||||
self.moreBarButton.contextAction?(self.moreBarButton.containerNode, nil)
|
self.moreBarButton.contextAction?(self.moreBarButton.containerNode, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func openMoreMenu(sourceNode: ContextReferenceContentNode, gesture: ContextGesture?, isSettings: Bool) {
|
private func openMoreMenu(sourceNode: ContextReferenceContentNode, gesture: ContextGesture?, adMessage: Message?, isSettings: Bool, actionsOnTop: Bool = false) {
|
||||||
guard let controller = self.baseNavigationController()?.topViewController as? ViewController else {
|
guard let controller = self.baseNavigationController()?.topViewController as? ViewController else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var dismissImpl: (() -> Void)?
|
var dismissImpl: (() -> Void)?
|
||||||
let items: Signal<(items: [ContextMenuItem], topItems: [ContextMenuItem]), NoError>
|
let items: Signal<(items: [ContextMenuItem], topItems: [ContextMenuItem]), NoError>
|
||||||
if case let .message(message, _) = self.item?.contentInfo, let _ = message.adAttribute {
|
if let adMessage {
|
||||||
items = self.adMenuMainItems() |> map { items in
|
items = self.adMenuMainItems(message: adMessage) |> map { items in
|
||||||
return (items, [])
|
return (items, [])
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -3126,7 +3231,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
let contextController = ContextController(presentationData: self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme), source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: sourceNode)), items: items |> map { items in
|
let contextController = ContextController(presentationData: self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme), source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: sourceNode, actionsOnTop: actionsOnTop)), items: items |> map { items in
|
||||||
if !items.topItems.isEmpty {
|
if !items.topItems.isEmpty {
|
||||||
return ContextController.Items(id: AnyHashable(0), content: .twoLists(items.items, items.topItems))
|
return ContextController.Items(id: AnyHashable(0), content: .twoLists(items.items, items.topItems))
|
||||||
} else {
|
} else {
|
||||||
@ -3164,8 +3269,22 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||||||
return speedList
|
return speedList
|
||||||
}
|
}
|
||||||
|
|
||||||
private func adMenuMainItems() -> Signal<[ContextMenuItem], NoError> {
|
private func presentPremiumDemo() {
|
||||||
guard case let .message(message, _) = self.item?.contentInfo, let adAttribute = message.adAttribute else {
|
var replaceImpl: ((ViewController) -> Void)?
|
||||||
|
let controller = self.context.sharedContext.makePremiumDemoController(context: self.context, subject: .noAds, forceDark: true, action: {
|
||||||
|
let controller = self.context.sharedContext.makePremiumIntroController(context: self.context, source: .ads, forceDark: true, dismissed: nil)
|
||||||
|
replaceImpl?(controller)
|
||||||
|
}, dismissed: nil)
|
||||||
|
replaceImpl = { [weak controller] c in
|
||||||
|
controller?.replace(with: c)
|
||||||
|
}
|
||||||
|
if let navigationController = self.baseNavigationController() {
|
||||||
|
navigationController.pushViewController(controller)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func adMenuMainItems(message: Message) -> Signal<[ContextMenuItem], NoError> {
|
||||||
|
guard let adAttribute = message.adAttribute else {
|
||||||
return .single([])
|
return .single([])
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3244,18 +3363,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||||||
actions.append(.action(ContextMenuActionItem(text: presentationData.strings.SponsoredMessageMenu_Hide, textColor: .primary, textLayout: .twoLinesMax, textFont: .custom(font: Font.regular(presentationData.listsFontSize.baseDisplaySize - 1.0), height: nil, verticalOffset: nil), badge: nil, icon: { theme in
|
actions.append(.action(ContextMenuActionItem(text: presentationData.strings.SponsoredMessageMenu_Hide, textColor: .primary, textLayout: .twoLinesMax, textFont: .custom(font: Font.regular(presentationData.listsFontSize.baseDisplaySize - 1.0), height: nil, verticalOffset: nil), badge: nil, icon: { theme in
|
||||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.primaryTextColor)
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.primaryTextColor)
|
||||||
}, iconSource: nil, action: { [weak self] c, _ in
|
}, iconSource: nil, action: { [weak self] c, _ in
|
||||||
c?.dismiss(completion: {
|
c?.dismiss(completion: { [weak self] in
|
||||||
var replaceImpl: ((ViewController) -> Void)?
|
self?.presentPremiumDemo()
|
||||||
let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .noAds, forceDark: true, action: {
|
|
||||||
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .ads, forceDark: true, dismissed: nil)
|
|
||||||
replaceImpl?(controller)
|
|
||||||
}, dismissed: nil)
|
|
||||||
replaceImpl = { [weak controller] c in
|
|
||||||
controller?.replace(with: c)
|
|
||||||
}
|
|
||||||
if let navigationController = self?.baseNavigationController() as? NavigationController {
|
|
||||||
navigationController.pushViewController(controller)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
@ -3765,7 +3874,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc private func settingsButtonPressed() {
|
@objc private func settingsButtonPressed() {
|
||||||
self.openMoreMenu(sourceNode: self.settingsBarButton.referenceNode, gesture: nil, isSettings: true)
|
self.openMoreMenu(sourceNode: self.settingsBarButton.referenceNode, gesture: nil, adMessage: nil, isSettings: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func adjustForPreviewing() {
|
override func adjustForPreviewing() {
|
||||||
@ -3775,7 +3884,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override func footerContent() -> Signal<(GalleryFooterContentNode?, GalleryOverlayContentNode?), NoError> {
|
override func footerContent() -> Signal<(GalleryFooterContentNode?, GalleryOverlayContentNode?), NoError> {
|
||||||
return .single((self.footerContentNode, nil))
|
return .single((self.footerContentNode, self.overlayContentNode))
|
||||||
}
|
}
|
||||||
|
|
||||||
func updatePlaybackRate(_ playbackRate: Double?) {
|
func updatePlaybackRate(_ playbackRate: Double?) {
|
||||||
@ -3940,14 +4049,16 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||||||
final class HeaderContextReferenceContentSource: ContextReferenceContentSource {
|
final class HeaderContextReferenceContentSource: ContextReferenceContentSource {
|
||||||
private let controller: ViewController
|
private let controller: ViewController
|
||||||
private let sourceNode: ContextReferenceContentNode
|
private let sourceNode: ContextReferenceContentNode
|
||||||
|
private let actionsOnTop: Bool
|
||||||
|
|
||||||
init(controller: ViewController, sourceNode: ContextReferenceContentNode) {
|
init(controller: ViewController, sourceNode: ContextReferenceContentNode, actionsOnTop: Bool) {
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
self.sourceNode = sourceNode
|
self.sourceNode = sourceNode
|
||||||
|
self.actionsOnTop = actionsOnTop
|
||||||
}
|
}
|
||||||
|
|
||||||
func transitionInfo() -> ContextControllerReferenceViewInfo? {
|
func transitionInfo() -> ContextControllerReferenceViewInfo? {
|
||||||
return ContextControllerReferenceViewInfo(referenceView: self.sourceNode.view, contentAreaInScreenSpace: UIScreen.main.bounds)
|
return ContextControllerReferenceViewInfo(referenceView: self.sourceNode.view, contentAreaInScreenSpace: UIScreen.main.bounds, actionsPosition: self.actionsOnTop ? .top : .bottom)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
289
submodules/GalleryUI/Sources/Items/VideoAdComponent.swift
Normal file
289
submodules/GalleryUI/Sources/Items/VideoAdComponent.swift
Normal file
@ -0,0 +1,289 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Display
|
||||||
|
import SwiftSignalKit
|
||||||
|
import ComponentFlow
|
||||||
|
import MultilineTextComponent
|
||||||
|
import Postbox
|
||||||
|
import TelegramCore
|
||||||
|
import TelegramPresentationData
|
||||||
|
import ContextUI
|
||||||
|
import PlainButtonComponent
|
||||||
|
import AvatarNode
|
||||||
|
import AccountContext
|
||||||
|
import PhotoResources
|
||||||
|
|
||||||
|
final class VideoAdComponent: Component {
|
||||||
|
let context: AccountContext
|
||||||
|
let theme: PresentationTheme
|
||||||
|
let strings: PresentationStrings
|
||||||
|
let message: EngineMessage
|
||||||
|
let initialTimestamp: Int32
|
||||||
|
let action: (Bool) -> Void
|
||||||
|
let adAction: () -> Void
|
||||||
|
let moreAction: (ContextReferenceContentNode) -> Void
|
||||||
|
|
||||||
|
init(
|
||||||
|
context: AccountContext,
|
||||||
|
theme: PresentationTheme,
|
||||||
|
strings: PresentationStrings,
|
||||||
|
message: EngineMessage,
|
||||||
|
initialTimestamp: Int32,
|
||||||
|
action: @escaping (Bool) -> Void,
|
||||||
|
adAction: @escaping () -> Void,
|
||||||
|
moreAction: @escaping (ContextReferenceContentNode) -> Void
|
||||||
|
) {
|
||||||
|
self.context = context
|
||||||
|
self.theme = theme
|
||||||
|
self.strings = strings
|
||||||
|
self.message = message
|
||||||
|
self.initialTimestamp = initialTimestamp
|
||||||
|
self.action = action
|
||||||
|
self.adAction = adAction
|
||||||
|
self.moreAction = moreAction
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: VideoAdComponent, rhs: VideoAdComponent) -> Bool {
|
||||||
|
if lhs.message != rhs.message {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.initialTimestamp != rhs.initialTimestamp {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
final class View: UIView {
|
||||||
|
private var component: VideoAdComponent?
|
||||||
|
private weak var componentState: EmptyComponentState?
|
||||||
|
|
||||||
|
private let wrapperView: UIView
|
||||||
|
private let backgroundView: UIView
|
||||||
|
private let imageNode: TransformImageNode
|
||||||
|
private let title = ComponentView<Empty>()
|
||||||
|
private let text = ComponentView<Empty>()
|
||||||
|
private let button = ComponentView<Empty>()
|
||||||
|
private let buttonNode: ContextReferenceContentNode
|
||||||
|
private let progress = ComponentView<Empty>()
|
||||||
|
|
||||||
|
private var adIcon: UIImage?
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
self.wrapperView = UIView()
|
||||||
|
self.wrapperView.clipsToBounds = true
|
||||||
|
self.wrapperView.layer.cornerRadius = 14.0
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
self.wrapperView.layer.cornerCurve = .continuous
|
||||||
|
}
|
||||||
|
|
||||||
|
self.backgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .dark))
|
||||||
|
|
||||||
|
self.imageNode = TransformImageNode()
|
||||||
|
self.imageNode.isUserInteractionEnabled = false
|
||||||
|
|
||||||
|
self.buttonNode = ContextReferenceContentNode()
|
||||||
|
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.addSubview(self.wrapperView)
|
||||||
|
self.wrapperView.addSubview(self.backgroundView)
|
||||||
|
self.wrapperView.addSubview(self.buttonNode.view)
|
||||||
|
self.wrapperView.addSubview(self.imageNode.view)
|
||||||
|
|
||||||
|
self.backgroundView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapped)))
|
||||||
|
}
|
||||||
|
|
||||||
|
required init(coder: NSCoder) {
|
||||||
|
preconditionFailure()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func tapped() {
|
||||||
|
if let component = self.component {
|
||||||
|
component.adAction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(component: VideoAdComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||||
|
let isFirstTime = self.component == nil
|
||||||
|
self.component = component
|
||||||
|
|
||||||
|
let titleString = component.message.author?.compactDisplayTitle ?? ""
|
||||||
|
|
||||||
|
var media: Media?
|
||||||
|
if let photo = component.message.media.first as? TelegramMediaImage {
|
||||||
|
media = photo
|
||||||
|
} else if let file = component.message.media.first as? TelegramMediaFile {
|
||||||
|
media = file
|
||||||
|
}
|
||||||
|
if isFirstTime {
|
||||||
|
let signal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>
|
||||||
|
if let photo = media as? TelegramMediaImage {
|
||||||
|
signal = mediaGridMessagePhoto(account: component.context.account, userLocation: .other, photoReference: .standalone(media: photo))
|
||||||
|
} else if let file = media as? TelegramMediaFile {
|
||||||
|
signal = mediaGridMessageVideo(postbox: component.context.account.postbox, userLocation: .other, videoReference: .standalone(media: file))
|
||||||
|
} else {
|
||||||
|
signal = .complete()
|
||||||
|
}
|
||||||
|
self.imageNode.setSignal(signal)
|
||||||
|
}
|
||||||
|
|
||||||
|
let leftInset: CGFloat = media != nil ? 51.0 : 16.0
|
||||||
|
let rightInset: CGFloat = 60.0
|
||||||
|
|
||||||
|
let titleSize = self.title.update(
|
||||||
|
transition: .immediate,
|
||||||
|
component: AnyComponent(
|
||||||
|
MultilineTextComponent(text: .plain(NSAttributedString(string: titleString, font: Font.semibold(14.0), textColor: .white)))
|
||||||
|
),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: availableSize.height)
|
||||||
|
)
|
||||||
|
let textSize = self.text.update(
|
||||||
|
transition: .immediate,
|
||||||
|
component: AnyComponent(
|
||||||
|
MultilineTextComponent(
|
||||||
|
text: .plain(NSAttributedString(string: component.message.text, font: Font.regular(14.0), textColor: .white)),
|
||||||
|
maximumNumberOfLines: 2
|
||||||
|
)
|
||||||
|
),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: availableSize.height)
|
||||||
|
)
|
||||||
|
|
||||||
|
let contentHeight = titleSize.height + 3.0 + textSize.height
|
||||||
|
|
||||||
|
let size = CGSize(width: availableSize.width, height: contentHeight + 24.0)
|
||||||
|
|
||||||
|
let imageSize = CGSize(width: 30.0, height: 30.0)
|
||||||
|
self.imageNode.frame = CGRect(origin: CGPoint(x: 10.0, y: floor((size.height - imageSize.height) / 2.0)), size: imageSize)
|
||||||
|
|
||||||
|
let makeLayout = self.imageNode.asyncLayout()
|
||||||
|
let apply = makeLayout(TransformImageArguments(corners: ImageCorners(radius: imageSize.width / 2.0), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: .zero))
|
||||||
|
apply()
|
||||||
|
|
||||||
|
let contentOriginY = floor((size.height - contentHeight) / 2.0)
|
||||||
|
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: contentOriginY), size: titleSize)
|
||||||
|
if let titleView = self.title.view {
|
||||||
|
if titleView.superview == nil {
|
||||||
|
titleView.isUserInteractionEnabled = false
|
||||||
|
self.wrapperView.addSubview(titleView)
|
||||||
|
}
|
||||||
|
titleView.frame = titleFrame
|
||||||
|
}
|
||||||
|
|
||||||
|
let textFrame = CGRect(origin: CGPoint(x: leftInset, y: contentOriginY + contentHeight - textSize.height), size: textSize)
|
||||||
|
if let textView = self.text.view {
|
||||||
|
if textView.superview == nil {
|
||||||
|
textView.isUserInteractionEnabled = false
|
||||||
|
self.wrapperView.addSubview(textView)
|
||||||
|
}
|
||||||
|
textView.frame = textFrame
|
||||||
|
}
|
||||||
|
|
||||||
|
let color = UIColor(rgb: 0x64d2ff)
|
||||||
|
if self.adIcon == nil {
|
||||||
|
self.adIcon = generateAdIcon(color: color, strings: component.strings)
|
||||||
|
}
|
||||||
|
|
||||||
|
let buttonSize = self.button.update(
|
||||||
|
transition: .immediate,
|
||||||
|
component: AnyComponent(
|
||||||
|
PlainButtonComponent(
|
||||||
|
content: AnyComponent(
|
||||||
|
Image(image: self.adIcon, contentMode: .center)
|
||||||
|
),
|
||||||
|
effectAlignment: .center,
|
||||||
|
action: { [weak self] in
|
||||||
|
if let self {
|
||||||
|
component.moreAction(self.buttonNode)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
animateScale: false
|
||||||
|
)
|
||||||
|
),
|
||||||
|
environment: {},
|
||||||
|
containerSize: availableSize
|
||||||
|
)
|
||||||
|
let buttonFrame = CGRect(origin: CGPoint(x: titleFrame.maxX + 4.0, y: floor(titleFrame.midY - buttonSize.height / 2.0) + 1.0), size: buttonSize)
|
||||||
|
if let buttonView = self.button.view {
|
||||||
|
if buttonView.superview == nil {
|
||||||
|
self.wrapperView.addSubview(buttonView)
|
||||||
|
}
|
||||||
|
buttonView.frame = buttonFrame
|
||||||
|
}
|
||||||
|
self.buttonNode.frame = buttonFrame
|
||||||
|
|
||||||
|
let progressSize = self.progress.update(
|
||||||
|
transition: .immediate,
|
||||||
|
component: AnyComponent(
|
||||||
|
AdRemainingProgressComponent(
|
||||||
|
initialTimestamp: component.initialTimestamp,
|
||||||
|
minDisplayDuration: 10,
|
||||||
|
maxDisplayDuration: 30,
|
||||||
|
action: { [weak self] available in
|
||||||
|
guard let self, let component = self.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
component.action(available)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
),
|
||||||
|
environment: {},
|
||||||
|
containerSize: availableSize
|
||||||
|
)
|
||||||
|
|
||||||
|
let progressFrame = CGRect(origin: CGPoint(x: size.width - progressSize.width - 16.0, y: floor((size.height - progressSize.height) / 2.0)), size: progressSize)
|
||||||
|
if let progressView = self.progress.view {
|
||||||
|
if progressView.superview == nil {
|
||||||
|
self.wrapperView.addSubview(progressView)
|
||||||
|
}
|
||||||
|
progressView.frame = progressFrame
|
||||||
|
}
|
||||||
|
|
||||||
|
self.wrapperView.frame = CGRect(origin: .zero, size: size)
|
||||||
|
self.backgroundView.frame = CGRect(origin: .zero, size: size)
|
||||||
|
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeView() -> View {
|
||||||
|
return View(frame: CGRect())
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||||
|
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func generateAdIcon(color: UIColor, strings: PresentationStrings) -> UIImage? {
|
||||||
|
let titleString = NSAttributedString(string: strings.ChatList_Search_Ad, font: Font.regular(11.0), textColor: color, paragraphAlignment: .center)
|
||||||
|
let stringRect = titleString.boundingRect(with: CGSize(width: 200.0, height: 20.0), options: .usesLineFragmentOrigin, context: nil)
|
||||||
|
|
||||||
|
return generateImage(CGSize(width: floor(stringRect.width) + 18.0, height: 15.0), rotatedContext: { size, context in
|
||||||
|
let bounds = CGRect(origin: CGPoint(), size: size)
|
||||||
|
context.clear(bounds)
|
||||||
|
|
||||||
|
context.setFillColor(color.withMultipliedAlpha(0.1).cgColor)
|
||||||
|
context.addPath(UIBezierPath(roundedRect: bounds, cornerRadius: size.height / 2.0).cgPath)
|
||||||
|
context.fillPath()
|
||||||
|
|
||||||
|
context.setFillColor(color.cgColor)
|
||||||
|
|
||||||
|
let circleSize = CGSize(width: 2.0 - UIScreenPixel, height: 2.0 - UIScreenPixel)
|
||||||
|
context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - 8.0, y: 3.0 + UIScreenPixel), size: circleSize))
|
||||||
|
context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - 8.0, y: 7.0 - UIScreenPixel), size: circleSize))
|
||||||
|
context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - 8.0, y: 10.0), size: circleSize))
|
||||||
|
|
||||||
|
let textRect = CGRect(
|
||||||
|
x: 5.0,
|
||||||
|
y: (size.height - stringRect.height) / 2.0 - UIScreenPixel,
|
||||||
|
width: stringRect.width,
|
||||||
|
height: stringRect.height
|
||||||
|
)
|
||||||
|
|
||||||
|
UIGraphicsPushContext(context)
|
||||||
|
titleString.draw(in: textRect)
|
||||||
|
UIGraphicsPopContext()
|
||||||
|
})
|
||||||
|
}
|
@ -1360,7 +1360,7 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
|||||||
switch fetchStatus {
|
switch fetchStatus {
|
||||||
case let .Fetching(_, progress):
|
case let .Fetching(_, progress):
|
||||||
if item.isDownloadList {
|
if item.isDownloadList {
|
||||||
iconStatusState = .progress(value: CGFloat(progress), cancelEnabled: true, appearance: nil)
|
iconStatusState = .progress(value: CGFloat(progress), cancelEnabled: true, appearance: nil, animateRotation: true)
|
||||||
}
|
}
|
||||||
case .Local:
|
case .Local:
|
||||||
if isAudio || isInstantVideo {
|
if isAudio || isInstantVideo {
|
||||||
|
@ -32,7 +32,7 @@ public enum SemanticStatusNodeState: Equatable {
|
|||||||
case play
|
case play
|
||||||
case pause
|
case pause
|
||||||
case check(appearance: CheckAppearance?)
|
case check(appearance: CheckAppearance?)
|
||||||
case progress(value: CGFloat?, cancelEnabled: Bool, appearance: ProgressAppearance?)
|
case progress(value: CGFloat?, cancelEnabled: Bool, appearance: ProgressAppearance?, animateRotation: Bool)
|
||||||
case secretTimeout(position: Double, duration: Double, generationTimestamp: Double, appearance: ProgressAppearance?)
|
case secretTimeout(position: Double, duration: Double, generationTimestamp: Double, appearance: ProgressAppearance?)
|
||||||
case customIcon(UIImage)
|
case customIcon(UIImage)
|
||||||
}
|
}
|
||||||
@ -136,12 +136,12 @@ private extension SemanticStatusNodeState {
|
|||||||
} else {
|
} else {
|
||||||
return SemanticStatusNodeSecretTimeoutContext(position: position, duration: duration, generationTimestamp: generationTimestamp, appearance: appearance)
|
return SemanticStatusNodeSecretTimeoutContext(position: position, duration: duration, generationTimestamp: generationTimestamp, appearance: appearance)
|
||||||
}
|
}
|
||||||
case let .progress(value, cancelEnabled, appearance):
|
case let .progress(value, cancelEnabled, appearance, animateRotation):
|
||||||
if let current = current as? SemanticStatusNodeProgressContext, current.displayCancel == cancelEnabled {
|
if let current = current as? SemanticStatusNodeProgressContext, current.displayCancel == cancelEnabled {
|
||||||
current.updateValue(value: value)
|
current.updateValue(value: value)
|
||||||
return current
|
return current
|
||||||
} else {
|
} else {
|
||||||
return SemanticStatusNodeProgressContext(value: value, displayCancel: cancelEnabled, appearance: appearance)
|
return SemanticStatusNodeProgressContext(value: value, displayCancel: cancelEnabled, appearance: appearance, animateRotation: animateRotation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,13 +25,15 @@ final class SemanticStatusNodeProgressContext: SemanticStatusNodeStateContext {
|
|||||||
let value: CGFloat?
|
let value: CGFloat?
|
||||||
let displayCancel: Bool
|
let displayCancel: Bool
|
||||||
let appearance: SemanticStatusNodeState.ProgressAppearance?
|
let appearance: SemanticStatusNodeState.ProgressAppearance?
|
||||||
|
let animateRotation: Bool
|
||||||
let timestamp: Double
|
let timestamp: Double
|
||||||
|
|
||||||
init(transitionFraction: CGFloat, value: CGFloat?, displayCancel: Bool, appearance: SemanticStatusNodeState.ProgressAppearance?, timestamp: Double) {
|
init(transitionFraction: CGFloat, value: CGFloat?, displayCancel: Bool, appearance: SemanticStatusNodeState.ProgressAppearance?, animateRotation: Bool, timestamp: Double) {
|
||||||
self.transitionFraction = transitionFraction
|
self.transitionFraction = transitionFraction
|
||||||
self.value = value
|
self.value = value
|
||||||
self.displayCancel = displayCancel
|
self.displayCancel = displayCancel
|
||||||
self.appearance = appearance
|
self.appearance = appearance
|
||||||
|
self.animateRotation = animateRotation
|
||||||
self.timestamp = timestamp
|
self.timestamp = timestamp
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
@ -59,6 +61,10 @@ final class SemanticStatusNodeProgressContext: SemanticStatusNodeStateContext {
|
|||||||
var endAngle: CGFloat
|
var endAngle: CGFloat
|
||||||
if let value = self.value {
|
if let value = self.value {
|
||||||
progress = value
|
progress = value
|
||||||
|
if !self.animateRotation {
|
||||||
|
progress = 1.0 - progress
|
||||||
|
}
|
||||||
|
|
||||||
startAngle = -CGFloat.pi / 2.0
|
startAngle = -CGFloat.pi / 2.0
|
||||||
endAngle = CGFloat(progress) * 2.0 * CGFloat.pi + startAngle
|
endAngle = CGFloat(progress) * 2.0 * CGFloat.pi + startAngle
|
||||||
|
|
||||||
@ -98,14 +104,16 @@ final class SemanticStatusNodeProgressContext: SemanticStatusNodeStateContext {
|
|||||||
pathDiameter = diameter - lineWidth - 2.5 * 2.0
|
pathDiameter = diameter - lineWidth - 2.5 * 2.0
|
||||||
}
|
}
|
||||||
|
|
||||||
var angle = self.timestamp.truncatingRemainder(dividingBy: Double.pi * 2.0)
|
if self.animateRotation {
|
||||||
angle *= 4.0
|
var angle = self.timestamp.truncatingRemainder(dividingBy: Double.pi * 2.0)
|
||||||
|
angle *= 4.0
|
||||||
|
|
||||||
context.translateBy(x: diameter / 2.0, y: diameter / 2.0)
|
context.translateBy(x: diameter / 2.0, y: diameter / 2.0)
|
||||||
context.rotate(by: CGFloat(angle.truncatingRemainder(dividingBy: Double.pi * 2.0)))
|
context.rotate(by: CGFloat(angle.truncatingRemainder(dividingBy: Double.pi * 2.0)))
|
||||||
context.translateBy(x: -diameter / 2.0, y: -diameter / 2.0)
|
context.translateBy(x: -diameter / 2.0, y: -diameter / 2.0)
|
||||||
|
}
|
||||||
|
|
||||||
let path = UIBezierPath(arcCenter: CGPoint(x: diameter / 2.0, y: diameter / 2.0), radius: pathDiameter / 2.0, startAngle: startAngle, endAngle: endAngle, clockwise: true)
|
let path = UIBezierPath(arcCenter: CGPoint(x: diameter / 2.0, y: diameter / 2.0), radius: pathDiameter / 2.0, startAngle: startAngle, endAngle: endAngle, clockwise: self.animateRotation)
|
||||||
path.lineWidth = lineWidth
|
path.lineWidth = lineWidth
|
||||||
path.lineCapStyle = .round
|
path.lineCapStyle = .round
|
||||||
path.stroke()
|
path.stroke()
|
||||||
@ -147,6 +155,7 @@ final class SemanticStatusNodeProgressContext: SemanticStatusNodeStateContext {
|
|||||||
var value: CGFloat?
|
var value: CGFloat?
|
||||||
let displayCancel: Bool
|
let displayCancel: Bool
|
||||||
let appearance: SemanticStatusNodeState.ProgressAppearance?
|
let appearance: SemanticStatusNodeState.ProgressAppearance?
|
||||||
|
let animateRotation: Bool
|
||||||
var transition: SemanticStatusNodeProgressTransition?
|
var transition: SemanticStatusNodeProgressTransition?
|
||||||
|
|
||||||
var isAnimating: Bool {
|
var isAnimating: Bool {
|
||||||
@ -155,10 +164,11 @@ final class SemanticStatusNodeProgressContext: SemanticStatusNodeStateContext {
|
|||||||
|
|
||||||
var requestUpdate: () -> Void = {}
|
var requestUpdate: () -> Void = {}
|
||||||
|
|
||||||
init(value: CGFloat?, displayCancel: Bool, appearance: SemanticStatusNodeState.ProgressAppearance?) {
|
init(value: CGFloat?, displayCancel: Bool, appearance: SemanticStatusNodeState.ProgressAppearance?, animateRotation: Bool) {
|
||||||
self.value = value
|
self.value = value
|
||||||
self.displayCancel = displayCancel
|
self.displayCancel = displayCancel
|
||||||
self.appearance = appearance
|
self.appearance = appearance
|
||||||
|
self.animateRotation = animateRotation
|
||||||
}
|
}
|
||||||
|
|
||||||
func drawingState(transitionFraction: CGFloat) -> SemanticStatusNodeStateDrawingState {
|
func drawingState(transitionFraction: CGFloat) -> SemanticStatusNodeStateDrawingState {
|
||||||
@ -178,7 +188,7 @@ final class SemanticStatusNodeProgressContext: SemanticStatusNodeStateContext {
|
|||||||
} else {
|
} else {
|
||||||
resolvedValue = nil
|
resolvedValue = nil
|
||||||
}
|
}
|
||||||
return DrawingState(transitionFraction: transitionFraction, value: resolvedValue, displayCancel: self.displayCancel, appearance: self.appearance, timestamp: timestamp)
|
return DrawingState(transitionFraction: transitionFraction, value: resolvedValue, displayCancel: self.displayCancel, appearance: self.appearance, animateRotation: self.animateRotation, timestamp: timestamp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func maskView() -> UIView? {
|
func maskView() -> UIView? {
|
||||||
|
@ -160,7 +160,7 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode {
|
|||||||
let statusNode = SemanticStatusNode(backgroundNodeColor: .white, foregroundNodeColor: .clear, cutout: statusFrame.insetBy(dx: 8.0, dy: 8.0))
|
let statusNode = SemanticStatusNode(backgroundNodeColor: .white, foregroundNodeColor: .clear, cutout: statusFrame.insetBy(dx: 8.0, dy: 8.0))
|
||||||
self.statusNode = statusNode
|
self.statusNode = statusNode
|
||||||
self.contentContainer.insertSubnode(statusNode, belowSubnode: self.contentNode)
|
self.contentContainer.insertSubnode(statusNode, belowSubnode: self.contentNode)
|
||||||
statusNode.transitionToState(.progress(value: nil, cancelEnabled: false, appearance: SemanticStatusNodeState.ProgressAppearance(inset: 4.0, lineWidth: 3.0)), animated: false, completion: {})
|
statusNode.transitionToState(.progress(value: nil, cancelEnabled: false, appearance: SemanticStatusNodeState.ProgressAppearance(inset: 4.0, lineWidth: 3.0), animateRotation: true), animated: false, completion: {})
|
||||||
}
|
}
|
||||||
if let statusNode = self.statusNode {
|
if let statusNode = self.statusNode {
|
||||||
statusNode.frame = statusFrame
|
statusNode.frame = statusFrame
|
||||||
|
@ -133,7 +133,16 @@ public struct PresentationResourcesItemList {
|
|||||||
|
|
||||||
public static func itemListReorderIndicatorIcon(_ theme: PresentationTheme) -> UIImage? {
|
public static func itemListReorderIndicatorIcon(_ theme: PresentationTheme) -> UIImage? {
|
||||||
return theme.image(PresentationResourceKey.itemListReorderIndicatorIcon.rawValue, { theme in
|
return theme.image(PresentationResourceKey.itemListReorderIndicatorIcon.rawValue, { theme in
|
||||||
return generateTintedImage(image: UIImage(bundleImageName: "Item List/Reorder"), color: theme.list.controlSecondaryColor)
|
return generateImage(CGSize(width: 17.0, height: 14.0), rotatedContext: { size, context in
|
||||||
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||||
|
context.setFillColor(theme.list.itemBlocksSeparatorColor.cgColor)
|
||||||
|
|
||||||
|
let lineHeight = 1.0 + UIScreenPixel
|
||||||
|
context.addPath(CGPath(roundedRect: CGRect(x: 0.0, y: UIScreenPixel, width: 17.0, height: lineHeight), cornerWidth: lineHeight / 2.0, cornerHeight: lineHeight / 2.0, transform: nil))
|
||||||
|
context.addPath(CGPath(roundedRect: CGRect(x: 0.0, y: UIScreenPixel + 6.0, width: 17.0, height: lineHeight), cornerWidth: lineHeight / 2.0, cornerHeight: lineHeight / 2.0, transform: nil))
|
||||||
|
context.addPath(CGPath(roundedRect: CGRect(x: 0.0, y: UIScreenPixel + 12.0, width: 17.0, height: lineHeight), cornerWidth: lineHeight / 2.0, cornerHeight: lineHeight / 2.0, transform: nil))
|
||||||
|
context.fillPath()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -323,7 +323,7 @@ private final class ScrollContent: CombinedComponent {
|
|||||||
|
|
||||||
let infoBackground = infoBackground.update(
|
let infoBackground = infoBackground.update(
|
||||||
component: RoundedRectangle(
|
component: RoundedRectangle(
|
||||||
color: theme.list.blocksBackgroundColor,
|
color: theme.overallDarkAppearance ? theme.list.itemModalBlocksBackgroundColor : theme.list.blocksBackgroundColor,
|
||||||
cornerRadius: 10.0
|
cornerRadius: 10.0
|
||||||
),
|
),
|
||||||
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: totalInfoHeight),
|
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: totalInfoHeight),
|
||||||
@ -423,11 +423,13 @@ private final class ContainerComponent: CombinedComponent {
|
|||||||
let environment = context.environment[EnvironmentType.self]
|
let environment = context.environment[EnvironmentType.self]
|
||||||
let state = context.state
|
let state = context.state
|
||||||
|
|
||||||
|
let theme = environment.theme
|
||||||
|
|
||||||
let openContextMenu = context.component.openContextMenu
|
let openContextMenu = context.component.openContextMenu
|
||||||
let dismiss = context.component.dismiss
|
let dismiss = context.component.dismiss
|
||||||
|
|
||||||
let background = background.update(
|
let background = background.update(
|
||||||
component: Rectangle(color: environment.theme.list.plainBackgroundColor),
|
component: Rectangle(color: theme.overallDarkAppearance ? theme.list.modalBlocksBackgroundColor : theme.list.plainBackgroundColor),
|
||||||
environment: {},
|
environment: {},
|
||||||
availableSize: context.availableSize,
|
availableSize: context.availableSize,
|
||||||
transition: context.transition
|
transition: context.transition
|
||||||
@ -696,7 +698,7 @@ public class AdsInfoScreen: ViewController {
|
|||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
self.containerView.clipsToBounds = true
|
self.containerView.clipsToBounds = true
|
||||||
self.containerView.backgroundColor = self.presentationData.theme.overallDarkAppearance ? self.presentationData.theme.list.blocksBackgroundColor : self.presentationData.theme.list.plainBackgroundColor
|
self.containerView.backgroundColor = self.presentationData.theme.overallDarkAppearance ? self.presentationData.theme.list.modalBlocksBackgroundColor : self.presentationData.theme.list.plainBackgroundColor
|
||||||
|
|
||||||
self.addSubnode(self.dim)
|
self.addSubnode(self.dim)
|
||||||
|
|
||||||
|
@ -92,8 +92,8 @@ private final class SheetPageContent: CombinedComponent {
|
|||||||
let state = context.state
|
let state = context.state
|
||||||
|
|
||||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||||
let theme = presentationData.theme
|
let theme = environment.theme
|
||||||
let strings = presentationData.strings
|
let strings = environment.strings
|
||||||
|
|
||||||
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
|
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
|
||||||
|
|
||||||
|
@ -684,6 +684,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||||||
|
|
||||||
private struct HighlightedState: Equatable {
|
private struct HighlightedState: Equatable {
|
||||||
var quote: ChatInterfaceHighlightedState.Quote?
|
var quote: ChatInterfaceHighlightedState.Quote?
|
||||||
|
var todoTaskId: Int32?
|
||||||
}
|
}
|
||||||
private var highlightedState: HighlightedState?
|
private var highlightedState: HighlightedState?
|
||||||
|
|
||||||
@ -5903,7 +5904,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||||||
if let highlightedStateValue = item.controllerInteraction.highlightedState {
|
if let highlightedStateValue = item.controllerInteraction.highlightedState {
|
||||||
for (message, _) in item.content {
|
for (message, _) in item.content {
|
||||||
if highlightedStateValue.messageStableId == message.stableId {
|
if highlightedStateValue.messageStableId == message.stableId {
|
||||||
highlightedState = HighlightedState(quote: highlightedStateValue.quote)
|
highlightedState = HighlightedState(quote: highlightedStateValue.quote, todoTaskId: highlightedStateValue.todoTaskId)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -5915,6 +5916,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||||||
for contentNode in self.contentNodes {
|
for contentNode in self.contentNodes {
|
||||||
if let contentNode = contentNode as? ChatMessageTextBubbleContentNode {
|
if let contentNode = contentNode as? ChatMessageTextBubbleContentNode {
|
||||||
contentNode.updateQuoteTextHighlightState(text: nil, offset: nil, color: .clear, animated: true)
|
contentNode.updateQuoteTextHighlightState(text: nil, offset: nil, color: .clear, animated: true)
|
||||||
|
} else if let _ = contentNode as? ChatMessageTodoBubbleContentNode {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -5998,6 +6001,34 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
} else if highlightedState?.todoTaskId != nil {
|
||||||
|
Queue.mainQueue().after(0.3, { [weak self] in
|
||||||
|
guard let self, let _ = self.item, let backgroundHighlightNode = self.backgroundHighlightNode else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if let highlightedState = self.highlightedState, let todoTaskId = highlightedState.todoTaskId {
|
||||||
|
let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring)
|
||||||
|
|
||||||
|
var taskFrame: CGRect?
|
||||||
|
for contentNode in self.contentNodes {
|
||||||
|
if let contentNode = contentNode as? ChatMessageTodoBubbleContentNode, let localFrame = contentNode.taskItemFrame(id: todoTaskId) {
|
||||||
|
taskFrame = contentNode.view.convert(localFrame, to: backgroundHighlightNode.view.superview)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let taskFrame {
|
||||||
|
self.backgroundHighlightNode = nil
|
||||||
|
|
||||||
|
backgroundHighlightNode.updateLayout(size: taskFrame.size, transition: transition)
|
||||||
|
transition.updateFrame(node: backgroundHighlightNode, frame: taskFrame)
|
||||||
|
backgroundHighlightNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, delay: 0.05, removeOnCompletion: false, completion: { [weak backgroundHighlightNode] _ in
|
||||||
|
backgroundHighlightNode?.removeFromSupernode()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -1645,7 +1645,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
|
|
||||||
if let updatingMedia = arguments.attributes.updatingMedia, case .update = updatingMedia.media {
|
if let updatingMedia = arguments.attributes.updatingMedia, case .update = updatingMedia.media {
|
||||||
let adjustedProgress = max(CGFloat(updatingMedia.progress), 0.027)
|
let adjustedProgress = max(CGFloat(updatingMedia.progress), 0.027)
|
||||||
state = .progress(value: CGFloat(adjustedProgress), cancelEnabled: true, appearance: nil)
|
state = .progress(value: CGFloat(adjustedProgress), cancelEnabled: true, appearance: nil, animateRotation: true)
|
||||||
} else {
|
} else {
|
||||||
switch resourceStatus.mediaStatus {
|
switch resourceStatus.mediaStatus {
|
||||||
case var .fetchStatus(fetchStatus):
|
case var .fetchStatus(fetchStatus):
|
||||||
@ -1668,7 +1668,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
if message.groupingKey != nil, adjustedProgress.isEqual(to: 1.0), (message.flags.contains(.Unsent) || wasCheck) {
|
if message.groupingKey != nil, adjustedProgress.isEqual(to: 1.0), (message.flags.contains(.Unsent) || wasCheck) {
|
||||||
state = .check(appearance: nil)
|
state = .check(appearance: nil)
|
||||||
} else {
|
} else {
|
||||||
state = .progress(value: CGFloat(adjustedProgress), cancelEnabled: true, appearance: nil)
|
state = .progress(value: CGFloat(adjustedProgress), cancelEnabled: true, appearance: nil, animateRotation: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case .Local:
|
case .Local:
|
||||||
@ -1712,7 +1712,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
switch resourceStatus.fetchStatus {
|
switch resourceStatus.fetchStatus {
|
||||||
case let .Fetching(_, progress):
|
case let .Fetching(_, progress):
|
||||||
let adjustedProgress = max(progress, 0.027)
|
let adjustedProgress = max(progress, 0.027)
|
||||||
streamingState = .progress(value: CGFloat(adjustedProgress), cancelEnabled: true, appearance: .init(inset: 1.0, lineWidth: 2.0))
|
streamingState = .progress(value: CGFloat(adjustedProgress), cancelEnabled: true, appearance: .init(inset: 1.0, lineWidth: 2.0), animateRotation: true)
|
||||||
case .Local:
|
case .Local:
|
||||||
streamingState = .none
|
streamingState = .none
|
||||||
case .Remote, .Paused:
|
case .Remote, .Paused:
|
||||||
@ -1730,7 +1730,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
} else if case .check = state {
|
} else if case .check = state {
|
||||||
} else {
|
} else {
|
||||||
let adjustedProgress: CGFloat = 0.027
|
let adjustedProgress: CGFloat = 0.027
|
||||||
state = .progress(value: CGFloat(adjustedProgress), cancelEnabled: true, appearance: .init(inset: 1.0, lineWidth: 2.0))
|
state = .progress(value: CGFloat(adjustedProgress), cancelEnabled: true, appearance: .init(inset: 1.0, lineWidth: 2.0), animateRotation: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1323,13 +1323,13 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
|||||||
case let .Fetching(_, progress):
|
case let .Fetching(_, progress):
|
||||||
if let isBuffering = isBuffering {
|
if let isBuffering = isBuffering {
|
||||||
if isBuffering {
|
if isBuffering {
|
||||||
state = .progress(value: nil, cancelEnabled: true, appearance: nil)
|
state = .progress(value: nil, cancelEnabled: true, appearance: nil, animateRotation: true)
|
||||||
} else {
|
} else {
|
||||||
state = .none
|
state = .none
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let adjustedProgress = max(progress, 0.027)
|
let adjustedProgress = max(progress, 0.027)
|
||||||
state = .progress(value: CGFloat(adjustedProgress), cancelEnabled: true, appearance: nil)
|
state = .progress(value: CGFloat(adjustedProgress), cancelEnabled: true, appearance: nil, animateRotation: true)
|
||||||
}
|
}
|
||||||
case .Local:
|
case .Local:
|
||||||
if isViewOnceMessage {
|
if isViewOnceMessage {
|
||||||
@ -1355,7 +1355,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
|||||||
isLocal = true
|
isLocal = true
|
||||||
}
|
}
|
||||||
if (isBuffering ?? false) && !isLocal {
|
if (isBuffering ?? false) && !isLocal {
|
||||||
state = .progress(value: nil, cancelEnabled: true, appearance: nil)
|
state = .progress(value: nil, cancelEnabled: true, appearance: nil, animateRotation: true)
|
||||||
} else {
|
} else {
|
||||||
state = .none
|
state = .none
|
||||||
}
|
}
|
||||||
|
@ -1297,7 +1297,6 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
public func updateQuoteTextHighlightState(text: String?, offset: Int?, color: UIColor, animated: Bool) {
|
public func updateQuoteTextHighlightState(text: String?, offset: Int?, color: UIColor, animated: Bool) {
|
||||||
var rectsSet: [CGRect] = []
|
var rectsSet: [CGRect] = []
|
||||||
if let text = text, !text.isEmpty, let cachedLayout = self.textNode.textNode.cachedLayout, let string = cachedLayout.attributedString?.string {
|
if let text = text, !text.isEmpty, let cachedLayout = self.textNode.textNode.cachedLayout, let string = cachedLayout.attributedString?.string {
|
||||||
|
|
||||||
let quoteRange = findQuoteRange(string: string, quoteText: text, offset: offset)
|
let quoteRange = findQuoteRange(string: string, quoteText: text, offset: offset)
|
||||||
if let quoteRange, let rects = cachedLayout.rangeRects(in: quoteRange)?.rects, !rects.isEmpty {
|
if let quoteRange, let rects = cachedLayout.rangeRects(in: quoteRange)?.rects, !rects.isEmpty {
|
||||||
rectsSet = rects
|
rectsSet = rects
|
||||||
|
@ -1241,4 +1241,13 @@ public class ChatMessageTodoBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func taskItemFrame(id: Int32) -> CGRect? {
|
||||||
|
for node in self.optionNodes {
|
||||||
|
if node.option?.id == id {
|
||||||
|
return node.frame
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,10 +31,12 @@ public struct ChatInterfaceHighlightedState: Equatable {
|
|||||||
|
|
||||||
public let messageStableId: UInt32
|
public let messageStableId: UInt32
|
||||||
public let quote: Quote?
|
public let quote: Quote?
|
||||||
|
public let todoTaskId: Int32?
|
||||||
|
|
||||||
public init(messageStableId: UInt32, quote: Quote?) {
|
public init(messageStableId: UInt32, quote: Quote?, todoTaskId: Int32?) {
|
||||||
self.messageStableId = messageStableId
|
self.messageStableId = messageStableId
|
||||||
self.quote = quote
|
self.quote = quote
|
||||||
|
self.todoTaskId = todoTaskId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,13 +98,15 @@ public struct NavigateToMessageParams {
|
|||||||
|
|
||||||
public var timestamp: Double?
|
public var timestamp: Double?
|
||||||
public var quote: Quote?
|
public var quote: Quote?
|
||||||
|
public var todoTaskId: Int32?
|
||||||
public var progress: Promise<Bool>?
|
public var progress: Promise<Bool>?
|
||||||
public var forceNew: Bool
|
public var forceNew: Bool
|
||||||
public var setupReply: Bool
|
public var setupReply: Bool
|
||||||
|
|
||||||
public init(timestamp: Double?, quote: Quote?, progress: Promise<Bool>? = nil, forceNew: Bool = false, setupReply: Bool = false) {
|
public init(timestamp: Double?, quote: Quote?, todoTaskId: Int32? = nil, progress: Promise<Bool>? = nil, forceNew: Bool = false, setupReply: Bool = false) {
|
||||||
self.timestamp = timestamp
|
self.timestamp = timestamp
|
||||||
self.quote = quote
|
self.quote = quote
|
||||||
|
self.todoTaskId = todoTaskId
|
||||||
self.progress = progress
|
self.progress = progress
|
||||||
self.forceNew = forceNew
|
self.forceNew = forceNew
|
||||||
self.setupReply = setupReply
|
self.setupReply = setupReply
|
||||||
|
@ -109,6 +109,9 @@ final class ComposeTodoScreenComponent: Component {
|
|||||||
|
|
||||||
private var currentEditingTag: AnyObject?
|
private var currentEditingTag: AnyObject?
|
||||||
|
|
||||||
|
private var reorderRecognizer: ReorderGestureRecognizer?
|
||||||
|
private var reorderingItem: (id: AnyHashable, snapshotView: UIView, backgroundView: UIView, initialPosition: CGPoint, position: CGPoint)?
|
||||||
|
|
||||||
var isAppendableByOthers = false
|
var isAppendableByOthers = false
|
||||||
var isCompletableByOthers = false
|
var isCompletableByOthers = false
|
||||||
|
|
||||||
@ -129,6 +132,39 @@ final class ComposeTodoScreenComponent: Component {
|
|||||||
|
|
||||||
self.scrollView.delegate = self
|
self.scrollView.delegate = self
|
||||||
self.addSubview(self.scrollView)
|
self.addSubview(self.scrollView)
|
||||||
|
|
||||||
|
let reorderRecognizer = ReorderGestureRecognizer(
|
||||||
|
shouldBegin: { [weak self] point in
|
||||||
|
guard let self, let (id, item) = self.item(at: point) else {
|
||||||
|
return (allowed: false, requiresLongPress: false, id: nil, item: nil)
|
||||||
|
}
|
||||||
|
return (allowed: true, requiresLongPress: false, id: id, item: item)
|
||||||
|
},
|
||||||
|
willBegin: { point in
|
||||||
|
},
|
||||||
|
began: { [weak self] item in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.setReorderingItem(item: item)
|
||||||
|
},
|
||||||
|
ended: { [weak self] in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.setReorderingItem(item: nil)
|
||||||
|
},
|
||||||
|
moved: { [weak self] distance in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.moveReorderingItem(distance: distance)
|
||||||
|
},
|
||||||
|
isActiveUpdated: { _ in
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.reorderRecognizer = reorderRecognizer
|
||||||
|
self.addGestureRecognizer(reorderRecognizer)
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
@ -143,6 +179,124 @@ final class ComposeTodoScreenComponent: Component {
|
|||||||
self.scrollView.setContentOffset(CGPoint(), animated: true)
|
self.scrollView.setContentOffset(CGPoint(), animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func item(at point: CGPoint) -> (AnyHashable, ComponentView<Empty>)? {
|
||||||
|
let localPoint = self.todoItemsSectionContainer.convert(point, from: self)
|
||||||
|
for (id, itemView) in self.todoItemsSectionContainer.itemViews {
|
||||||
|
if let view = itemView.contents.view {
|
||||||
|
let viewFrame = view.convert(view.bounds, to: self.todoItemsSectionContainer)
|
||||||
|
let iconFrame = CGRect(origin: CGPoint(x: viewFrame.maxX - viewFrame.height, y: viewFrame.minY), size: CGSize(width: viewFrame.height, height: viewFrame.height))
|
||||||
|
if iconFrame.contains(localPoint) {
|
||||||
|
return (id, itemView.contents)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setReorderingItem(item: AnyHashable?) {
|
||||||
|
guard let environment = self.environment else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var mappedItem: (AnyHashable, ComponentView<Empty>)?
|
||||||
|
for (id, itemView) in self.todoItemsSectionContainer.itemViews {
|
||||||
|
if id == item {
|
||||||
|
mappedItem = (id, itemView.contents)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if self.reorderingItem?.id != mappedItem?.0 {
|
||||||
|
if let (id, visibleItem) = mappedItem, let view = visibleItem.view, !view.isHidden, let viewSuperview = view.superview, let snapshotView = view.snapshotView(afterScreenUpdates: false) {
|
||||||
|
let mappedCenter = viewSuperview.convert(view.center, to: self.scrollView)
|
||||||
|
|
||||||
|
let wrapperView = UIView()
|
||||||
|
wrapperView.alpha = 0.8
|
||||||
|
wrapperView.frame = CGRect(origin: mappedCenter.offsetBy(dx: -snapshotView.bounds.width / 2.0, dy: -snapshotView.bounds.height / 2.0), size: snapshotView.bounds.size)
|
||||||
|
|
||||||
|
let theme = environment.theme.withModalBlocksBackground()
|
||||||
|
let backgroundView = UIImageView(image: generateReorderingBackgroundImage(backgroundColor: theme.list.itemBlocksBackgroundColor))
|
||||||
|
backgroundView.frame = wrapperView.bounds.insetBy(dx: -10.0, dy: -10.0)
|
||||||
|
snapshotView.frame = snapshotView.bounds
|
||||||
|
|
||||||
|
wrapperView.addSubview(backgroundView)
|
||||||
|
wrapperView.addSubview(snapshotView)
|
||||||
|
|
||||||
|
backgroundView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||||
|
wrapperView.transform = CGAffineTransformMakeScale(1.04, 1.04)
|
||||||
|
wrapperView.layer.animateScale(from: 1.0, to: 1.04, duration: 0.2)
|
||||||
|
|
||||||
|
self.scrollView.addSubview(wrapperView)
|
||||||
|
self.reorderingItem = (id, wrapperView, backgroundView, mappedCenter, mappedCenter)
|
||||||
|
self.state?.updated()
|
||||||
|
} else {
|
||||||
|
if let reorderingItem = self.reorderingItem {
|
||||||
|
self.reorderingItem = nil
|
||||||
|
for (itemId, itemView) in self.todoItemsSectionContainer.itemViews {
|
||||||
|
if itemId == reorderingItem.id, let view = itemView.contents.view {
|
||||||
|
let viewFrame = view.convert(view.bounds, to: self)
|
||||||
|
let transition = ComponentTransition.spring(duration: 0.3)
|
||||||
|
transition.setPosition(view: reorderingItem.snapshotView, position: viewFrame.center)
|
||||||
|
transition.setAlpha(view: reorderingItem.backgroundView, alpha: 0.0, completion: { _ in
|
||||||
|
reorderingItem.snapshotView.removeFromSuperview()
|
||||||
|
self.state?.updated()
|
||||||
|
})
|
||||||
|
transition.setScale(view: reorderingItem.snapshotView, scale: 1.0)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func moveReorderingItem(distance: CGPoint) {
|
||||||
|
if let (id, snapshotView, backgroundView, initialPosition, _) = self.reorderingItem {
|
||||||
|
let targetPosition = CGPoint(x: initialPosition.x + distance.x, y: initialPosition.y + distance.y)
|
||||||
|
self.reorderingItem = (id, snapshotView, backgroundView, initialPosition, targetPosition)
|
||||||
|
|
||||||
|
snapshotView.center = targetPosition
|
||||||
|
|
||||||
|
for (itemId, itemView) in self.todoItemsSectionContainer.itemViews {
|
||||||
|
if itemId == id {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if let view = itemView.contents.view {
|
||||||
|
let viewFrame = view.convert(view.bounds, to: self)
|
||||||
|
if viewFrame.contains(targetPosition) {
|
||||||
|
if let targetIndex = self.todoItems.firstIndex(where: { AnyHashable($0.id) == itemId }), let reorderingItem = self.todoItems.first(where: { AnyHashable($0.id) == id }) {
|
||||||
|
self.reorderIfPossible(item: reorderingItem, toIndex: targetIndex)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func reorderIfPossible(item: TodoItem, toIndex: Int) {
|
||||||
|
guard let component = self.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let targetItem = self.todoItems[toIndex]
|
||||||
|
guard targetItem.textInputState.hasText else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var canEdit = true
|
||||||
|
if let _ = component.initialData.existingTodo, !component.initialData.canEdit {
|
||||||
|
canEdit = false
|
||||||
|
}
|
||||||
|
if !canEdit, let existingTodo = component.initialData.existingTodo, existingTodo.items.contains(where: { $0.id == targetItem.id }) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if let fromIndex = self.todoItems.firstIndex(where: { $0.id == item.id }) {
|
||||||
|
self.todoItems[toIndex] = item
|
||||||
|
self.todoItems[fromIndex] = targetItem
|
||||||
|
|
||||||
|
HapticFeedback().tap()
|
||||||
|
|
||||||
|
self.state?.updated(transition: .spring(duration: 0.4))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func validatedInput() -> TelegramMediaTodo? {
|
func validatedInput() -> TelegramMediaTodo? {
|
||||||
if self.todoTextInputState.text.length == 0 {
|
if self.todoTextInputState.text.length == 0 {
|
||||||
return nil
|
return nil
|
||||||
@ -432,6 +586,8 @@ final class ComposeTodoScreenComponent: Component {
|
|||||||
let themeUpdated = self.environment?.theme !== environment.theme
|
let themeUpdated = self.environment?.theme !== environment.theme
|
||||||
self.environment = environment
|
self.environment = environment
|
||||||
|
|
||||||
|
let theme = environment.theme.withModalBlocksBackground()
|
||||||
|
|
||||||
let isFirstTime = self.component == nil
|
let isFirstTime = self.component == nil
|
||||||
if self.component == nil {
|
if self.component == nil {
|
||||||
if let existingTodo = component.initialData.existingTodo {
|
if let existingTodo = component.initialData.existingTodo {
|
||||||
@ -599,7 +755,7 @@ final class ComposeTodoScreenComponent: Component {
|
|||||||
let sectionSpacing: CGFloat = 24.0
|
let sectionSpacing: CGFloat = 24.0
|
||||||
|
|
||||||
if themeUpdated {
|
if themeUpdated {
|
||||||
self.backgroundColor = environment.theme.list.blocksBackgroundColor
|
self.backgroundColor = theme.list.blocksBackgroundColor
|
||||||
}
|
}
|
||||||
|
|
||||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||||
@ -617,7 +773,7 @@ final class ComposeTodoScreenComponent: Component {
|
|||||||
todoTextSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListComposePollOptionComponent(
|
todoTextSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListComposePollOptionComponent(
|
||||||
externalState: self.todoTextInputState,
|
externalState: self.todoTextInputState,
|
||||||
context: component.context,
|
context: component.context,
|
||||||
theme: environment.theme,
|
theme: theme,
|
||||||
strings: environment.strings,
|
strings: environment.strings,
|
||||||
isEnabled: canEdit,
|
isEnabled: canEdit,
|
||||||
resetText: self.resetTodoText.flatMap { resetText in
|
resetText: self.resetTodoText.flatMap { resetText in
|
||||||
@ -625,7 +781,6 @@ final class ComposeTodoScreenComponent: Component {
|
|||||||
},
|
},
|
||||||
assumeIsEditing: self.inputMediaNodeTargetTag === self.todoTextFieldTag,
|
assumeIsEditing: self.inputMediaNodeTargetTag === self.todoTextFieldTag,
|
||||||
characterLimit: component.initialData.maxTodoTextLength,
|
characterLimit: component.initialData.maxTodoTextLength,
|
||||||
canReorder: canEdit,
|
|
||||||
emptyLineHandling: .allowed,
|
emptyLineHandling: .allowed,
|
||||||
returnKeyAction: { [weak self] in
|
returnKeyAction: { [weak self] in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
@ -661,7 +816,7 @@ final class ComposeTodoScreenComponent: Component {
|
|||||||
let todoTextSectionSize = self.todoTextSection.update(
|
let todoTextSectionSize = self.todoTextSection.update(
|
||||||
transition: transition,
|
transition: transition,
|
||||||
component: AnyComponent(ListSectionComponent(
|
component: AnyComponent(ListSectionComponent(
|
||||||
theme: environment.theme,
|
theme: theme,
|
||||||
header: nil,
|
header: nil,
|
||||||
footer: nil,
|
footer: nil,
|
||||||
items: todoTextSectionItems
|
items: todoTextSectionItems
|
||||||
@ -701,7 +856,7 @@ final class ComposeTodoScreenComponent: Component {
|
|||||||
todoItemsSectionItems.append(AnyComponentWithIdentity(id: todoItem.id, component: AnyComponent(ListComposePollOptionComponent(
|
todoItemsSectionItems.append(AnyComponentWithIdentity(id: todoItem.id, component: AnyComponent(ListComposePollOptionComponent(
|
||||||
externalState: todoItem.textInputState,
|
externalState: todoItem.textInputState,
|
||||||
context: component.context,
|
context: component.context,
|
||||||
theme: environment.theme,
|
theme: theme,
|
||||||
strings: environment.strings,
|
strings: environment.strings,
|
||||||
isEnabled: isEnabled,
|
isEnabled: isEnabled,
|
||||||
resetText: todoItem.resetText.flatMap { resetText in
|
resetText: todoItem.resetText.flatMap { resetText in
|
||||||
@ -709,6 +864,7 @@ final class ComposeTodoScreenComponent: Component {
|
|||||||
},
|
},
|
||||||
assumeIsEditing: self.inputMediaNodeTargetTag === todoItem.textFieldTag,
|
assumeIsEditing: self.inputMediaNodeTargetTag === todoItem.textFieldTag,
|
||||||
characterLimit: component.initialData.maxTodoItemLength,
|
characterLimit: component.initialData.maxTodoItemLength,
|
||||||
|
canReorder: isEnabled,
|
||||||
emptyLineHandling: .notAllowed,
|
emptyLineHandling: .notAllowed,
|
||||||
returnKeyAction: { [weak self] in
|
returnKeyAction: { [weak self] in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
@ -788,6 +944,12 @@ final class ComposeTodoScreenComponent: Component {
|
|||||||
size: itemSize,
|
size: itemSize,
|
||||||
transition: itemTransition
|
transition: itemTransition
|
||||||
))
|
))
|
||||||
|
|
||||||
|
var isReordering = false
|
||||||
|
if let reorderingItem = self.reorderingItem, itemId == reorderingItem.id {
|
||||||
|
isReordering = true
|
||||||
|
}
|
||||||
|
itemView.contents.view?.isHidden = isReordering
|
||||||
}
|
}
|
||||||
|
|
||||||
for i in 0 ..< self.todoItems.count {
|
for i in 0 ..< self.todoItems.count {
|
||||||
@ -836,7 +998,7 @@ final class ComposeTodoScreenComponent: Component {
|
|||||||
|
|
||||||
let todoItemsSectionUpdateResult = self.todoItemsSectionContainer.update(
|
let todoItemsSectionUpdateResult = self.todoItemsSectionContainer.update(
|
||||||
configuration: ListSectionContentView.Configuration(
|
configuration: ListSectionContentView.Configuration(
|
||||||
theme: environment.theme,
|
theme: theme,
|
||||||
displaySeparators: true,
|
displaySeparators: true,
|
||||||
extendsItemHighlightToSection: false,
|
extendsItemHighlightToSection: false,
|
||||||
background: .all
|
background: .all
|
||||||
@ -854,7 +1016,7 @@ final class ComposeTodoScreenComponent: Component {
|
|||||||
text: .plain(NSAttributedString(
|
text: .plain(NSAttributedString(
|
||||||
string: "TO DO LIST",
|
string: "TO DO LIST",
|
||||||
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
|
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
|
||||||
textColor: environment.theme.list.freeTextColor
|
textColor: theme.list.freeTextColor
|
||||||
)),
|
)),
|
||||||
maximumNumberOfLines: 0
|
maximumNumberOfLines: 0
|
||||||
)),
|
)),
|
||||||
@ -900,19 +1062,19 @@ final class ComposeTodoScreenComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let todoItemsComponent: AnyComponent<Empty>
|
let todoItemsComponent: AnyComponent<Empty>
|
||||||
if !"".isEmpty, todoItemsLimitReached {
|
if todoItemsLimitReached {
|
||||||
todoItemsFooterTransition = todoItemsFooterTransition.withAnimation(.none)
|
todoItemsFooterTransition = todoItemsFooterTransition.withAnimation(.none)
|
||||||
|
|
||||||
let textFont = Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize)
|
let textFont = Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize)
|
||||||
let boldTextFont = Font.semibold(presentationData.listsFontSize.itemListBaseHeaderFontSize)
|
let boldTextFont = Font.semibold(presentationData.listsFontSize.itemListBaseHeaderFontSize)
|
||||||
let textColor = environment.theme.list.freeTextColor
|
let textColor = theme.list.freeTextColor
|
||||||
todoItemsComponent = AnyComponent(MultilineTextComponent(
|
todoItemsComponent = AnyComponent(MultilineTextComponent(
|
||||||
text: .markdown(
|
text: .markdown(
|
||||||
text: "Limit of tasks reached. You can increase the limit to **20 tasks** by subscribing to [Telegram Premium]().",
|
text: "Maximum number of tasks reached.",
|
||||||
attributes: MarkdownAttributes(
|
attributes: MarkdownAttributes(
|
||||||
body: MarkdownAttributeSet(font: textFont, textColor: textColor),
|
body: MarkdownAttributeSet(font: textFont, textColor: textColor),
|
||||||
bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor),
|
bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor),
|
||||||
link: MarkdownAttributeSet(font: textFont, textColor: environment.theme.list.itemAccentColor),
|
link: MarkdownAttributeSet(font: textFont, textColor: theme.list.itemAccentColor),
|
||||||
linkAttribute: { contents in
|
linkAttribute: { contents in
|
||||||
return (TelegramTextAttributes.URL, contents)
|
return (TelegramTextAttributes.URL, contents)
|
||||||
}
|
}
|
||||||
@ -969,7 +1131,7 @@ final class ComposeTodoScreenComponent: Component {
|
|||||||
|
|
||||||
todoItemsComponent = AnyComponent(AnimatedTextComponent(
|
todoItemsComponent = AnyComponent(AnimatedTextComponent(
|
||||||
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
|
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
|
||||||
color: environment.theme.list.freeTextColor,
|
color: theme.list.freeTextColor,
|
||||||
items: todoItemsFooterItems
|
items: todoItemsFooterItems
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
@ -1004,13 +1166,13 @@ final class ComposeTodoScreenComponent: Component {
|
|||||||
var todoSettingsSectionItems: [AnyComponentWithIdentity<Empty>] = []
|
var todoSettingsSectionItems: [AnyComponentWithIdentity<Empty>] = []
|
||||||
if canEdit && component.peer.id != component.context.account.peerId {
|
if canEdit && component.peer.id != component.context.account.peerId {
|
||||||
todoSettingsSectionItems.append(AnyComponentWithIdentity(id: "completable", component: AnyComponent(ListActionItemComponent(
|
todoSettingsSectionItems.append(AnyComponentWithIdentity(id: "completable", component: AnyComponent(ListActionItemComponent(
|
||||||
theme: environment.theme,
|
theme: theme,
|
||||||
title: AnyComponent(VStack([
|
title: AnyComponent(VStack([
|
||||||
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
|
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
|
||||||
text: .plain(NSAttributedString(
|
text: .plain(NSAttributedString(
|
||||||
string: "Allow Others to Mark as Done",
|
string: "Allow Others to Mark as Done",
|
||||||
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
|
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
|
||||||
textColor: environment.theme.list.itemPrimaryTextColor
|
textColor: theme.list.itemPrimaryTextColor
|
||||||
)),
|
)),
|
||||||
maximumNumberOfLines: 1
|
maximumNumberOfLines: 1
|
||||||
))),
|
))),
|
||||||
@ -1027,13 +1189,13 @@ final class ComposeTodoScreenComponent: Component {
|
|||||||
|
|
||||||
if self.isCompletableByOthers {
|
if self.isCompletableByOthers {
|
||||||
todoSettingsSectionItems.append(AnyComponentWithIdentity(id: "editable", component: AnyComponent(ListActionItemComponent(
|
todoSettingsSectionItems.append(AnyComponentWithIdentity(id: "editable", component: AnyComponent(ListActionItemComponent(
|
||||||
theme: environment.theme,
|
theme: theme,
|
||||||
title: AnyComponent(VStack([
|
title: AnyComponent(VStack([
|
||||||
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
|
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
|
||||||
text: .plain(NSAttributedString(
|
text: .plain(NSAttributedString(
|
||||||
string: "Allow Others to Add Tasks",
|
string: "Allow Others to Add Tasks",
|
||||||
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
|
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
|
||||||
textColor: environment.theme.list.itemPrimaryTextColor
|
textColor: theme.list.itemPrimaryTextColor
|
||||||
)),
|
)),
|
||||||
maximumNumberOfLines: 1
|
maximumNumberOfLines: 1
|
||||||
))),
|
))),
|
||||||
@ -1054,7 +1216,7 @@ final class ComposeTodoScreenComponent: Component {
|
|||||||
let todoSettingsSectionSize = self.todoSettingsSection.update(
|
let todoSettingsSectionSize = self.todoSettingsSection.update(
|
||||||
transition: transition,
|
transition: transition,
|
||||||
component: AnyComponent(ListSectionComponent(
|
component: AnyComponent(ListSectionComponent(
|
||||||
theme: environment.theme,
|
theme: theme,
|
||||||
header: nil,
|
header: nil,
|
||||||
footer: nil,
|
footer: nil,
|
||||||
items: todoSettingsSectionItems
|
items: todoSettingsSectionItems
|
||||||
@ -1164,7 +1326,7 @@ final class ComposeTodoScreenComponent: Component {
|
|||||||
component: AnyComponent(EmojiSuggestionsComponent(
|
component: AnyComponent(EmojiSuggestionsComponent(
|
||||||
context: component.context,
|
context: component.context,
|
||||||
userLocation: .other,
|
userLocation: .other,
|
||||||
theme: EmojiSuggestionsComponent.Theme(theme: environment.theme, backgroundColor: environment.theme.list.itemBlocksBackgroundColor),
|
theme: EmojiSuggestionsComponent.Theme(theme: theme, backgroundColor: theme.list.itemBlocksBackgroundColor),
|
||||||
animationCache: component.context.animationCache,
|
animationCache: component.context.animationCache,
|
||||||
animationRenderer: component.context.animationRenderer,
|
animationRenderer: component.context.animationRenderer,
|
||||||
files: value,
|
files: value,
|
||||||
@ -1530,3 +1692,212 @@ public class ComposeTodoScreen: ViewControllerComponentContainer, AttachmentCont
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final class ReorderGestureRecognizer: UIGestureRecognizer {
|
||||||
|
private let shouldBegin: (CGPoint) -> (allowed: Bool, requiresLongPress: Bool, id: AnyHashable?, item: ComponentView<Empty>?)
|
||||||
|
private let willBegin: (CGPoint) -> Void
|
||||||
|
private let began: (AnyHashable) -> Void
|
||||||
|
private let ended: () -> Void
|
||||||
|
private let moved: (CGPoint) -> Void
|
||||||
|
private let isActiveUpdated: (Bool) -> Void
|
||||||
|
|
||||||
|
private var initialLocation: CGPoint?
|
||||||
|
private var longTapTimer: SwiftSignalKit.Timer?
|
||||||
|
private var longPressTimer: SwiftSignalKit.Timer?
|
||||||
|
|
||||||
|
private var id: AnyHashable?
|
||||||
|
private var itemView: ComponentView<Empty>?
|
||||||
|
|
||||||
|
public init(shouldBegin: @escaping (CGPoint) -> (allowed: Bool, requiresLongPress: Bool, id: AnyHashable?, item: ComponentView<Empty>?), willBegin: @escaping (CGPoint) -> Void, began: @escaping (AnyHashable) -> Void, ended: @escaping () -> Void, moved: @escaping (CGPoint) -> Void, isActiveUpdated: @escaping (Bool) -> Void) {
|
||||||
|
self.shouldBegin = shouldBegin
|
||||||
|
self.willBegin = willBegin
|
||||||
|
self.began = began
|
||||||
|
self.ended = ended
|
||||||
|
self.moved = moved
|
||||||
|
self.isActiveUpdated = isActiveUpdated
|
||||||
|
|
||||||
|
super.init(target: nil, action: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
self.longTapTimer?.invalidate()
|
||||||
|
self.longPressTimer?.invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func startLongTapTimer() {
|
||||||
|
self.longTapTimer?.invalidate()
|
||||||
|
let longTapTimer = SwiftSignalKit.Timer(timeout: 0.25, repeat: false, completion: { [weak self] in
|
||||||
|
self?.longTapTimerFired()
|
||||||
|
}, queue: Queue.mainQueue())
|
||||||
|
self.longTapTimer = longTapTimer
|
||||||
|
longTapTimer.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func stopLongTapTimer() {
|
||||||
|
self.itemView = nil
|
||||||
|
self.longTapTimer?.invalidate()
|
||||||
|
self.longTapTimer = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func startLongPressTimer() {
|
||||||
|
self.longPressTimer?.invalidate()
|
||||||
|
let longPressTimer = SwiftSignalKit.Timer(timeout: 0.6, repeat: false, completion: { [weak self] in
|
||||||
|
self?.longPressTimerFired()
|
||||||
|
}, queue: Queue.mainQueue())
|
||||||
|
self.longPressTimer = longPressTimer
|
||||||
|
longPressTimer.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func stopLongPressTimer() {
|
||||||
|
self.itemView = nil
|
||||||
|
self.longPressTimer?.invalidate()
|
||||||
|
self.longPressTimer = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
override public func reset() {
|
||||||
|
super.reset()
|
||||||
|
|
||||||
|
self.itemView = nil
|
||||||
|
self.stopLongTapTimer()
|
||||||
|
self.stopLongPressTimer()
|
||||||
|
self.initialLocation = nil
|
||||||
|
|
||||||
|
self.isActiveUpdated(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func longTapTimerFired() {
|
||||||
|
guard let location = self.initialLocation else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.longTapTimer?.invalidate()
|
||||||
|
self.longTapTimer = nil
|
||||||
|
|
||||||
|
self.willBegin(location)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func longPressTimerFired() {
|
||||||
|
guard let _ = self.initialLocation else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.isActiveUpdated(true)
|
||||||
|
self.state = .began
|
||||||
|
self.longPressTimer?.invalidate()
|
||||||
|
self.longPressTimer = nil
|
||||||
|
self.longTapTimer?.invalidate()
|
||||||
|
self.longTapTimer = nil
|
||||||
|
if let id = self.id {
|
||||||
|
self.began(id)
|
||||||
|
}
|
||||||
|
self.isActiveUpdated(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override public func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||||
|
super.touchesBegan(touches, with: event)
|
||||||
|
|
||||||
|
if self.numberOfTouches > 1 {
|
||||||
|
self.isActiveUpdated(false)
|
||||||
|
self.state = .failed
|
||||||
|
self.ended()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.state == .possible {
|
||||||
|
if let location = touches.first?.location(in: self.view) {
|
||||||
|
let (allowed, requiresLongPress, id, itemView) = self.shouldBegin(location)
|
||||||
|
if allowed {
|
||||||
|
self.isActiveUpdated(true)
|
||||||
|
|
||||||
|
self.id = id
|
||||||
|
self.itemView = itemView
|
||||||
|
self.initialLocation = location
|
||||||
|
if requiresLongPress {
|
||||||
|
self.startLongTapTimer()
|
||||||
|
self.startLongPressTimer()
|
||||||
|
} else {
|
||||||
|
self.state = .began
|
||||||
|
if let id = self.id {
|
||||||
|
self.began(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.isActiveUpdated(false)
|
||||||
|
self.state = .failed
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.isActiveUpdated(false)
|
||||||
|
self.state = .failed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override public func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||||
|
super.touchesEnded(touches, with: event)
|
||||||
|
|
||||||
|
self.initialLocation = nil
|
||||||
|
|
||||||
|
self.stopLongTapTimer()
|
||||||
|
if self.longPressTimer != nil {
|
||||||
|
self.stopLongPressTimer()
|
||||||
|
self.isActiveUpdated(false)
|
||||||
|
self.state = .failed
|
||||||
|
}
|
||||||
|
if self.state == .began || self.state == .changed {
|
||||||
|
self.isActiveUpdated(false)
|
||||||
|
self.ended()
|
||||||
|
self.state = .failed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override public func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||||
|
super.touchesCancelled(touches, with: event)
|
||||||
|
|
||||||
|
self.initialLocation = nil
|
||||||
|
|
||||||
|
self.stopLongTapTimer()
|
||||||
|
if self.longPressTimer != nil {
|
||||||
|
self.isActiveUpdated(false)
|
||||||
|
self.stopLongPressTimer()
|
||||||
|
self.state = .failed
|
||||||
|
}
|
||||||
|
if self.state == .began || self.state == .changed {
|
||||||
|
self.isActiveUpdated(false)
|
||||||
|
self.ended()
|
||||||
|
self.state = .failed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override public func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||||
|
super.touchesMoved(touches, with: event)
|
||||||
|
|
||||||
|
if (self.state == .began || self.state == .changed), let initialLocation = self.initialLocation, let location = touches.first?.location(in: self.view) {
|
||||||
|
self.state = .changed
|
||||||
|
let offset = CGPoint(x: location.x - initialLocation.x, y: location.y - initialLocation.y)
|
||||||
|
self.moved(offset)
|
||||||
|
} else if let touch = touches.first, let initialTapLocation = self.initialLocation, self.longPressTimer != nil {
|
||||||
|
let touchLocation = touch.location(in: self.view)
|
||||||
|
let dX = touchLocation.x - initialTapLocation.x
|
||||||
|
let dY = touchLocation.y - initialTapLocation.y
|
||||||
|
|
||||||
|
if dX * dX + dY * dY > 3.0 * 3.0 {
|
||||||
|
self.stopLongTapTimer()
|
||||||
|
self.stopLongPressTimer()
|
||||||
|
self.initialLocation = nil
|
||||||
|
self.isActiveUpdated(false)
|
||||||
|
self.state = .failed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func generateReorderingBackgroundImage(backgroundColor: UIColor) -> UIImage? {
|
||||||
|
return generateImage(CGSize(width: 64.0, height: 64.0), contextGenerator: { size, context in
|
||||||
|
context.clear(CGRect(origin: .zero, size: size))
|
||||||
|
|
||||||
|
context.addPath(UIBezierPath(roundedRect: CGRect(x: 10, y: 10, width: 44, height: 44), cornerRadius: 10).cgPath)
|
||||||
|
context.setShadow(offset: CGSize(width: 0.0, height: 0.0), blur: 24.0, color: UIColor(white: 0.0, alpha: 0.35).cgColor)
|
||||||
|
context.setFillColor(backgroundColor.cgColor)
|
||||||
|
context.fillPath()
|
||||||
|
})?.stretchableImage(withLeftCapWidth: 32, topCapHeight: 32)
|
||||||
|
}
|
||||||
|
@ -257,6 +257,7 @@ public final class ListComposePollOptionComponent: Component {
|
|||||||
private let textField = ComponentView<Empty>()
|
private let textField = ComponentView<Empty>()
|
||||||
|
|
||||||
private var modeSelector: ComponentView<Empty>?
|
private var modeSelector: ComponentView<Empty>?
|
||||||
|
private var reorderIconView: UIImageView?
|
||||||
|
|
||||||
private var checkView: CheckView?
|
private var checkView: CheckView?
|
||||||
|
|
||||||
@ -455,6 +456,43 @@ public final class ListComposePollOptionComponent: Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var rightIconsInset: CGFloat = 0.0
|
||||||
|
if component.canReorder, let externalState = component.externalState, externalState.hasText {
|
||||||
|
var reorderIconTransition = transition
|
||||||
|
let reorderIconView: UIImageView
|
||||||
|
if let current = self.reorderIconView {
|
||||||
|
reorderIconView = current
|
||||||
|
} else {
|
||||||
|
reorderIconTransition = reorderIconTransition.withAnimation(.none)
|
||||||
|
reorderIconView = UIImageView()
|
||||||
|
self.reorderIconView = reorderIconView
|
||||||
|
self.addSubview(reorderIconView)
|
||||||
|
}
|
||||||
|
reorderIconView.image = PresentationResourcesItemList.itemListReorderIndicatorIcon(component.theme)
|
||||||
|
|
||||||
|
var reorderIconSize = CGSize()
|
||||||
|
if let icon = reorderIconView.image {
|
||||||
|
reorderIconSize = icon.size
|
||||||
|
}
|
||||||
|
|
||||||
|
let reorderIconFrame = CGRect(origin: CGPoint(x: size.width - 14.0 - reorderIconSize.width, y: floor((size.height - reorderIconSize.height) * 0.5)), size: reorderIconSize)
|
||||||
|
reorderIconTransition.setPosition(view: reorderIconView, position: reorderIconFrame.center)
|
||||||
|
reorderIconTransition.setBounds(view: reorderIconView, bounds: CGRect(origin: CGPoint(), size: reorderIconFrame.size))
|
||||||
|
|
||||||
|
rightIconsInset += 36.0
|
||||||
|
} else if let reorderIconView = self.reorderIconView {
|
||||||
|
self.reorderIconView = nil
|
||||||
|
if !transition.animation.isImmediate {
|
||||||
|
let alphaTransition: ComponentTransition = .easeInOut(duration: 0.2)
|
||||||
|
alphaTransition.setAlpha(view: reorderIconView, alpha: 0.0, completion: { [weak reorderIconView] _ in
|
||||||
|
reorderIconView?.removeFromSuperview()
|
||||||
|
})
|
||||||
|
alphaTransition.setScale(view: reorderIconView, scale: 0.001)
|
||||||
|
} else {
|
||||||
|
reorderIconView.removeFromSuperview()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let inputMode = component.inputMode {
|
if let inputMode = component.inputMode {
|
||||||
var modeSelectorTransition = transition
|
var modeSelectorTransition = transition
|
||||||
let modeSelector: ComponentView<Empty>
|
let modeSelector: ComponentView<Empty>
|
||||||
@ -501,7 +539,7 @@ public final class ListComposePollOptionComponent: Component {
|
|||||||
environment: {},
|
environment: {},
|
||||||
containerSize: modeSelectorSize
|
containerSize: modeSelectorSize
|
||||||
)
|
)
|
||||||
let modeSelectorFrame = CGRect(origin: CGPoint(x: size.width - 4.0 - modeSelectorSize.width, y: floor((size.height - modeSelectorSize.height) * 0.5)), size: modeSelectorSize)
|
let modeSelectorFrame = CGRect(origin: CGPoint(x: size.width - rightIconsInset - 4.0 - modeSelectorSize.width, y: floor((size.height - modeSelectorSize.height) * 0.5)), size: modeSelectorSize)
|
||||||
if let modeSelectorView = modeSelector.view as? PlainButtonComponent.View {
|
if let modeSelectorView = modeSelector.view as? PlainButtonComponent.View {
|
||||||
let alphaTransition: ComponentTransition = .easeInOut(duration: 0.2)
|
let alphaTransition: ComponentTransition = .easeInOut(duration: 0.2)
|
||||||
|
|
||||||
|
@ -336,7 +336,7 @@ public final class StoryFooterPanelComponent: Component {
|
|||||||
statusNode.view.frame = CGRect(origin: CGPoint(x: innerLeftOffset, y: floor((size.height - statusSize.height) * 0.5)), size: statusSize)
|
statusNode.view.frame = CGRect(origin: CGPoint(x: innerLeftOffset, y: floor((size.height - statusSize.height) * 0.5)), size: statusSize)
|
||||||
innerLeftOffset += statusSize.width + 10.0
|
innerLeftOffset += statusSize.width + 10.0
|
||||||
|
|
||||||
statusNode.transitionToState(.progress(value: CGFloat(max(0.08, self.uploadProgress)), cancelEnabled: true, appearance: SemanticStatusNodeState.ProgressAppearance(inset: 0.0, lineWidth: 2.0)))
|
statusNode.transitionToState(.progress(value: CGFloat(max(0.08, self.uploadProgress)), cancelEnabled: true, appearance: SemanticStatusNodeState.ProgressAppearance(inset: 0.0, lineWidth: 2.0), animateRotation: true))
|
||||||
|
|
||||||
let uploadingTextSize = uploadingText.update(
|
let uploadingTextSize = uploadingText.update(
|
||||||
transition: .immediate,
|
transition: .immediate,
|
||||||
|
@ -9,20 +9,17 @@ public final class ToastContentComponent: Component {
|
|||||||
public let content: AnyComponent<Empty>
|
public let content: AnyComponent<Empty>
|
||||||
public let insets: UIEdgeInsets
|
public let insets: UIEdgeInsets
|
||||||
public let iconSpacing: CGFloat
|
public let iconSpacing: CGFloat
|
||||||
public let action: (() -> Void)?
|
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
icon: AnyComponent<Empty>,
|
icon: AnyComponent<Empty>,
|
||||||
content: AnyComponent<Empty>,
|
content: AnyComponent<Empty>,
|
||||||
insets: UIEdgeInsets = UIEdgeInsets(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0),
|
insets: UIEdgeInsets = UIEdgeInsets(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0),
|
||||||
iconSpacing: CGFloat = 10.0,
|
iconSpacing: CGFloat = 10.0
|
||||||
action: (() -> Void)? = nil
|
|
||||||
) {
|
) {
|
||||||
self.icon = icon
|
self.icon = icon
|
||||||
self.content = content
|
self.content = content
|
||||||
self.insets = insets
|
self.insets = insets
|
||||||
self.iconSpacing = iconSpacing
|
self.iconSpacing = iconSpacing
|
||||||
self.action = action
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func ==(lhs: ToastContentComponent, rhs: ToastContentComponent) -> Bool {
|
public static func ==(lhs: ToastContentComponent, rhs: ToastContentComponent) -> Bool {
|
||||||
@ -69,18 +66,9 @@ public final class ToastContentComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@objc private func tapped() {
|
|
||||||
self.component?.action?()
|
|
||||||
}
|
|
||||||
|
|
||||||
func update(component: ToastContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
func update(component: ToastContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||||
var contentHeight: CGFloat = 0.0
|
var contentHeight: CGFloat = 0.0
|
||||||
|
|
||||||
if self.component == nil {
|
|
||||||
if let _ = component.action {
|
|
||||||
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapped)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.component = component
|
self.component = component
|
||||||
|
|
||||||
let leftInset: CGFloat = component.insets.left
|
let leftInset: CGFloat = component.insets.left
|
||||||
|
@ -4766,7 +4766,7 @@ extension ChatControllerImpl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let highlightedState = ChatInterfaceHighlightedState(messageStableId: message.stableId, quote: toSubject.quote.flatMap { quote in ChatInterfaceHighlightedState.Quote(string: quote.string, offset: quote.offset) })
|
let highlightedState = ChatInterfaceHighlightedState(messageStableId: message.stableId, quote: toSubject.quote.flatMap { quote in ChatInterfaceHighlightedState.Quote(string: quote.string, offset: quote.offset) }, todoTaskId: toSubject.todoTaskId)
|
||||||
controllerInteraction.highlightedState = highlightedState
|
controllerInteraction.highlightedState = highlightedState
|
||||||
strongSelf.updateItemNodesHighlightedStates(animated: initial)
|
strongSelf.updateItemNodesHighlightedStates(animated: initial)
|
||||||
strongSelf.contentData?.scrolledToMessageIdValue = ScrolledToMessageId(id: mappedId, allowedReplacementDirection: [])
|
strongSelf.contentData?.scrolledToMessageIdValue = ScrolledToMessageId(id: mappedId, allowedReplacementDirection: [])
|
||||||
|
@ -340,13 +340,15 @@ extension ChatControllerImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var quote: (string: String, offset: Int?)?
|
var quote: (string: String, offset: Int?)?
|
||||||
|
var todoTaskId: Int32?
|
||||||
var setupReply = false
|
var setupReply = false
|
||||||
if case let .id(_, params) = messageLocation {
|
if case let .id(_, params) = messageLocation {
|
||||||
quote = params.quote.flatMap { quote in (string: quote.string, offset: quote.offset) }
|
quote = params.quote.flatMap { quote in (string: quote.string, offset: quote.offset) }
|
||||||
setupReply = params.setupReply
|
setupReply = params.setupReply
|
||||||
|
todoTaskId = params.todoTaskId
|
||||||
}
|
}
|
||||||
|
|
||||||
self.chatDisplayNode.historyNode.scrollToMessage(from: scrollFromIndex, to: message.index, animated: animated, quote: quote, scrollPosition: scrollPosition, setupReply: setupReply)
|
self.chatDisplayNode.historyNode.scrollToMessage(from: scrollFromIndex, to: message.index, animated: animated, quote: quote, todoTaskId: todoTaskId, scrollPosition: scrollPosition, setupReply: setupReply)
|
||||||
|
|
||||||
if delayCompletion {
|
if delayCompletion {
|
||||||
Queue.mainQueue().after(0.25, {
|
Queue.mainQueue().after(0.25, {
|
||||||
|
@ -970,7 +970,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
case .pinnedMessageUpdated, .gameScore, .setSameChatWallpaper, .giveawayResults, .customText, .todoCompletions, .todoAppendTasks:
|
case .pinnedMessageUpdated, .gameScore, .setSameChatWallpaper, .giveawayResults, .customText, .todoCompletions, .todoAppendTasks:
|
||||||
for attribute in message.attributes {
|
for attribute in message.attributes {
|
||||||
if let attribute = attribute as? ReplyMessageAttribute {
|
if let attribute = attribute as? ReplyMessageAttribute {
|
||||||
self.navigateToMessage(from: message.id, to: .id(attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.isQuote ? attribute.quote.flatMap { quote in NavigateToMessageParams.Quote(string: quote.text, offset: quote.offset) } : nil)))
|
var todoTaskId: Int32?
|
||||||
|
if case let .todoCompletions(completed, incompleted) = action.action {
|
||||||
|
if let completedTaskId = completed.first {
|
||||||
|
todoTaskId = completedTaskId
|
||||||
|
} else if let incompletedTaskId = incompleted.first {
|
||||||
|
todoTaskId = incompletedTaskId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.navigateToMessage(from: message.id, to: .id(attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.isQuote ? attribute.quote.flatMap { quote in NavigateToMessageParams.Quote(string: quote.text, offset: quote.offset) } : nil, todoTaskId: todoTaskId)))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -4840,7 +4848,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.dismissAllUndoControllers()
|
self.dismissAllTooltips()
|
||||||
//TODO:localize
|
//TODO:localize
|
||||||
if !self.context.isPremium {
|
if !self.context.isPremium {
|
||||||
let controller = UndoOverlayController(
|
let controller = UndoOverlayController(
|
||||||
|
@ -3340,8 +3340,8 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func scrollToMessage(from fromIndex: MessageIndex, to toIndex: MessageIndex, animated: Bool, highlight: Bool = true, quote: (string: String, offset: Int?)? = nil, scrollPosition: ListViewScrollPosition = .center(.bottom), setupReply: Bool = false) {
|
public func scrollToMessage(from fromIndex: MessageIndex, to toIndex: MessageIndex, animated: Bool, highlight: Bool = true, quote: (string: String, offset: Int?)? = nil, todoTaskId: Int32? = nil, scrollPosition: ListViewScrollPosition = .center(.bottom), setupReply: Bool = false) {
|
||||||
self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Scroll(subject: MessageHistoryScrollToSubject(index: .message(toIndex), quote: quote.flatMap { quote in MessageHistoryScrollToSubject.Quote(string: quote.string, offset: quote.offset) }, setupReply: setupReply), anchorIndex: .message(toIndex), sourceIndex: .message(fromIndex), scrollPosition: scrollPosition, animated: animated, highlight: highlight, setupReply: setupReply), id: self.takeNextHistoryLocationId())
|
self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Scroll(subject: MessageHistoryScrollToSubject(index: .message(toIndex), quote: quote.flatMap { quote in MessageHistoryScrollToSubject.Quote(string: quote.string, offset: quote.offset) }, todoTaskId: todoTaskId, setupReply: setupReply), anchorIndex: .message(toIndex), sourceIndex: .message(fromIndex), scrollPosition: scrollPosition, animated: animated, highlight: highlight, setupReply: setupReply), id: self.takeNextHistoryLocationId())
|
||||||
}
|
}
|
||||||
|
|
||||||
public func anchorMessageInCurrentHistoryView() -> Message? {
|
public func anchorMessageInCurrentHistoryView() -> Message? {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user