mirror of
https://github.com/ichmagmaus111/ghostgram.git
synced 2026-06-08 02:53:56 +02:00
Update Ghostgram features
This commit is contained in:
@@ -5,6 +5,36 @@ load(
|
||||
"telegram_bundle_id",
|
||||
)
|
||||
|
||||
sgdeps = [
|
||||
"//Swiftgram/SGSettingsUI:SGSettingsUI",
|
||||
"//Swiftgram/SGConfig:SGConfig",
|
||||
"//Swiftgram/SGAPIWebSettings:SGAPIWebSettings",
|
||||
"//Swiftgram/SGAppGroupIdentifier:SGAppGroupIdentifier",
|
||||
"//Swiftgram/SGSimpleSettings:SGSimpleSettings",
|
||||
"//Swiftgram/SFSafariViewControllerPlus:SFSafariViewControllerPlus",
|
||||
"//Swiftgram/SGLogging:SGLogging",
|
||||
"//Swiftgram/SGStrings:SGStrings",
|
||||
"//Swiftgram/SGActionRequestHandlerSanitizer:SGActionRequestHandlerSanitizer",
|
||||
"//Swiftgram/Wrap:Wrap",
|
||||
"//Swiftgram/SGDeviceToken:SGDeviceToken",
|
||||
"//Swiftgram/SGDebugUI:SGDebugUI",
|
||||
"//Swiftgram/SGInputToolbar:SGInputToolbar",
|
||||
"//Swiftgram/SGIAP:SGIAP",
|
||||
"//Swiftgram/SGPayWall:SGPayWall",
|
||||
"//Swiftgram/SGStatus:SGStatus",
|
||||
"//Swiftgram/SGSwiftSignalKit:SGSwiftSignalKit",
|
||||
"//Swiftgram/SGProUI:SGProUI",
|
||||
"//Swiftgram/SGKeychainBackupManager:SGKeychainBackupManager",
|
||||
"//Swiftgram/SGGHSettings:SGGHSettings",
|
||||
# "//Swiftgram/SGContentAnalysis:SGContentAnalysis"
|
||||
]
|
||||
|
||||
sgsrcs = [
|
||||
"//Swiftgram/SGDBReset:SGDBReset",
|
||||
"//Swiftgram/SGShowMessageJson:SGShowMessageJson",
|
||||
"//Swiftgram/ChatControllerImplExtension:ChatControllerImplExtension",
|
||||
"//Swiftgram/SGSharedAccountContextMigration:SGSharedAccountContextMigration"
|
||||
]
|
||||
|
||||
filegroup(
|
||||
name = "TelegramUIResources",
|
||||
@@ -44,12 +74,12 @@ swift_library(
|
||||
module_name = "TelegramUI",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
]) + sgsrcs,
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//third-party/recaptcha:RecaptchaEnterpriseSDK",
|
||||
deps = sgdeps + [
|
||||
"//third-party/recaptcha:RecaptchaEnterprise",
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/SSignalKit/SSignalKit:SSignalKit",
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||
@@ -71,6 +101,7 @@ swift_library(
|
||||
"//submodules/TelegramVoip:TelegramVoip",
|
||||
"//submodules/DeviceAccess:DeviceAccess",
|
||||
"//submodules/Utils/DeviceModel",
|
||||
"//submodules/WatchCommon/Host:WatchCommon",
|
||||
"//submodules/BuildConfig:BuildConfig",
|
||||
"//submodules/BuildConfigExtra:BuildConfigExtra",
|
||||
"//submodules/rlottie:RLottieBinding",
|
||||
@@ -191,6 +222,8 @@ swift_library(
|
||||
"//submodules/RaiseToListen:RaiseToListen",
|
||||
"//submodules/OpusBinding:OpusBinding",
|
||||
"//third-party/opus:opus",
|
||||
"//submodules/WatchBridgeAudio:WatchBridgeAudio",
|
||||
"//submodules/WatchBridge:WatchBridge",
|
||||
"//submodules/ShareItems:ShareItems",
|
||||
"//submodules/ShareItems/Impl:ShareItemsImpl",
|
||||
"//submodules/SettingsUI:SettingsUI",
|
||||
@@ -465,6 +498,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/Gifts/GiftOptionsScreen",
|
||||
"//submodules/TelegramUI/Components/Gifts/GiftStoreScreen",
|
||||
"//submodules/TelegramUI/Components/Gifts/GiftSetupScreen",
|
||||
"//submodules/TelegramUI/Components/Gifts/GiftCraftScreen",
|
||||
"//submodules/TelegramUI/Components/ContentReportScreen",
|
||||
"//submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen",
|
||||
"//submodules/TelegramUI/Components/Stars/StarsBalanceOverlayComponent",
|
||||
@@ -513,10 +547,12 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/AlertComponent",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatAgeRestrictionAlertController",
|
||||
"//submodules/TelegramUI/Components/CocoonInfoScreen",
|
||||
"//submodules/TelegramUI/Components/ProxyServerPreviewScreen",
|
||||
"//submodules/TelegramUI/Components/ContextControllerImpl",
|
||||
"//submodules/TelegramUI/Components/AuthConfirmationScreen",
|
||||
] + select({
|
||||
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
|
||||
"//build-system:ios_sim_arm64": [],
|
||||
"//conditions:default": [],
|
||||
}),
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "AuthConfirmationScreen",
|
||||
module_name = "AuthConfirmationScreen",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Display",
|
||||
"//submodules/Postbox",
|
||||
"//submodules/TelegramCore",
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/Components/ViewControllerComponent",
|
||||
"//submodules/Components/ComponentDisplayAdapters",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/TelegramUI/Components/ButtonComponent",
|
||||
"//submodules/TelegramUI/Components/GlassBarButtonComponent",
|
||||
"//submodules/Components/SheetComponent",
|
||||
"//submodules/PresentationDataUtils",
|
||||
"//submodules/Components/BundleIconComponent",
|
||||
"//submodules/TelegramUI/Components/ListSectionComponent",
|
||||
"//submodules/TelegramUI/Components/ListActionItemComponent",
|
||||
"//submodules/TelegramUI/Components/AvatarComponent",
|
||||
"//submodules/Markdown",
|
||||
"//submodules/PhoneNumberFormat",
|
||||
"//submodules/ContextUI",
|
||||
"//submodules/AccountUtils",
|
||||
"//submodules/TelegramUI/Components/PeerInfo/AccountPeerContextItem",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import GlassBackgroundComponent
|
||||
import AvatarComponent
|
||||
import BundleIconComponent
|
||||
import AccountContext
|
||||
|
||||
final class AccountSwitchComponent: Component {
|
||||
let context: AccountContext
|
||||
let theme: PresentationTheme
|
||||
let peer: EnginePeer
|
||||
let canSwitch: Bool
|
||||
let action: () -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
theme: PresentationTheme,
|
||||
peer: EnginePeer,
|
||||
canSwitch: Bool,
|
||||
action: @escaping () -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.peer = peer
|
||||
self.canSwitch = canSwitch
|
||||
self.action = action
|
||||
}
|
||||
|
||||
static func ==(lhs: AccountSwitchComponent, rhs: AccountSwitchComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.peer != rhs.peer {
|
||||
return false
|
||||
}
|
||||
if lhs.canSwitch != rhs.canSwitch {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private let backgroundView = GlassBackgroundView()
|
||||
private let avatar = ComponentView<Empty>()
|
||||
private let arrow = ComponentView<Empty>()
|
||||
private let button = HighlightTrackingButton()
|
||||
|
||||
private var component: AccountSwitchComponent?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.backgroundView)
|
||||
|
||||
self.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@objc private func buttonPressed() {
|
||||
if let component = self.component {
|
||||
component.action()
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: AccountSwitchComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
self.component = component
|
||||
|
||||
let size = CGSize(width: component.canSwitch ? 76.0 : 44.0, height: 44.0)
|
||||
|
||||
let avatarSize = self.avatar.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
AvatarComponent(
|
||||
context: component.context,
|
||||
theme: component.theme,
|
||||
peer: component.peer,
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 36.0, height: 36.0)
|
||||
)
|
||||
if let avatarView = self.avatar.view {
|
||||
if avatarView.superview == nil {
|
||||
avatarView.isUserInteractionEnabled = false
|
||||
self.backgroundView.contentView.addSubview(avatarView)
|
||||
}
|
||||
avatarView.frame = CGRect(origin: CGPoint(x: 4.0, y: 4.0), size: avatarSize)
|
||||
}
|
||||
|
||||
let arrowSize = self.arrow.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
BundleIconComponent(name: "Navigation/Disclosure", tintColor: component.theme.rootController.navigationBar.secondaryTextColor)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
if let arrowView = self.arrow.view {
|
||||
if arrowView.superview == nil {
|
||||
arrowView.isUserInteractionEnabled = false
|
||||
self.backgroundView.contentView.addSubview(arrowView)
|
||||
self.backgroundView.contentView.addSubview(self.button)
|
||||
}
|
||||
arrowView.frame = CGRect(origin: CGPoint(x: size.width - arrowSize.width - 8.0, y: floorToScreenPixels((size.height - arrowSize.height) / 2.0)), size: arrowSize)
|
||||
transition.setAlpha(view: arrowView, alpha: component.canSwitch ? 1.0 : 0.0)
|
||||
}
|
||||
|
||||
self.backgroundView.update(size: size, cornerRadius: size.height * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: .init(kind: .panel), isInteractive: component.canSwitch, transition: transition)
|
||||
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: .zero, size: size))
|
||||
transition.setFrame(view: self.button, 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)
|
||||
}
|
||||
}
|
||||
+648
@@ -0,0 +1,648 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import AccountContext
|
||||
import TelegramPresentationData
|
||||
import ComponentFlow
|
||||
import ViewControllerComponent
|
||||
import SheetComponent
|
||||
import MultilineTextComponent
|
||||
import GlassBarButtonComponent
|
||||
import ButtonComponent
|
||||
import PresentationDataUtils
|
||||
import BundleIconComponent
|
||||
import ListSectionComponent
|
||||
import ListActionItemComponent
|
||||
import AvatarComponent
|
||||
import Markdown
|
||||
import PhoneNumberFormat
|
||||
import ContextUI
|
||||
import AccountUtils
|
||||
import GlassBackgroundComponent
|
||||
import AccountPeerContextItem
|
||||
|
||||
private final class AuthConfirmationSheetContent: CombinedComponent {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
||||
let context: AccountContext
|
||||
let subject: MessageActionUrlAuthResult
|
||||
let completion: (AccountContext, EnginePeer, Bool, Bool) -> Void
|
||||
let cancel: (Bool) -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
subject: MessageActionUrlAuthResult,
|
||||
completion: @escaping (AccountContext, EnginePeer, Bool, Bool) -> Void,
|
||||
cancel: @escaping (Bool) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.subject = subject
|
||||
self.completion = completion
|
||||
self.cancel = cancel
|
||||
}
|
||||
|
||||
static func ==(lhs: AuthConfirmationSheetContent, rhs: AuthConfirmationSheetContent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class State: ComponentState {
|
||||
private let context: AccountContext
|
||||
private let subject: MessageActionUrlAuthResult
|
||||
|
||||
var peer: EnginePeer?
|
||||
var forcedAccount: (AccountContext, EnginePeer)?
|
||||
|
||||
fileprivate var inProgress = false
|
||||
var allowWrite = true
|
||||
weak var controller: ViewController?
|
||||
|
||||
init(context: AccountContext, subject: MessageActionUrlAuthResult) {
|
||||
self.context = context
|
||||
self.subject = subject
|
||||
|
||||
super.init()
|
||||
|
||||
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peer in
|
||||
guard let self, let peer else {
|
||||
return
|
||||
}
|
||||
self.peer = peer
|
||||
self.updated()
|
||||
})
|
||||
}
|
||||
|
||||
func displayPhoneNumberConfirmation(commit: @escaping (Bool) -> Void) {
|
||||
guard case let .request(domain, _, _, _) = self.subject else {
|
||||
return
|
||||
}
|
||||
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId))
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peer in
|
||||
guard let self, case let .user(user) = peer, let phone = user.phone else {
|
||||
return
|
||||
}
|
||||
let phoneNumber = formatPhoneNumber(context: self.context, number: phone).replacingOccurrences(of: " ", with: "\u{00A0}")
|
||||
|
||||
let alertController = textAlertController(
|
||||
context: self.context,
|
||||
title: nil,
|
||||
text: "\(domain)\n\(phoneNumber)",
|
||||
actions: [
|
||||
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
|
||||
commit(false)
|
||||
}),
|
||||
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {
|
||||
commit(true)
|
||||
})
|
||||
]
|
||||
)
|
||||
self.controller?.present(alertController, in: .window(.root))
|
||||
})
|
||||
}
|
||||
|
||||
func presentAccountSwitchMenu() {
|
||||
// Account switch menu is disabled in this compatibility build.
|
||||
}
|
||||
}
|
||||
|
||||
func makeState() -> State {
|
||||
return State(context: self.context, subject: self.subject)
|
||||
}
|
||||
|
||||
static var body: Body {
|
||||
let closeButton = Child(GlassBarButtonComponent.self)
|
||||
let accountButton = Child(AccountSwitchComponent.self)
|
||||
let avatar = Child(AvatarComponent.self)
|
||||
let title = Child(MultilineTextComponent.self)
|
||||
let description = Child(MultilineTextComponent.self)
|
||||
let clientSection = Child(ListSectionComponent.self)
|
||||
let optionsSection = Child(ListSectionComponent.self)
|
||||
let cancelButton = Child(ButtonComponent.self)
|
||||
let doneButton = Child(ButtonComponent.self)
|
||||
|
||||
return { context in
|
||||
let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
|
||||
let component = context.component
|
||||
let theme = environment.theme
|
||||
let strings = environment.strings
|
||||
let state = context.state
|
||||
if state.controller == nil {
|
||||
state.controller = environment.controller()
|
||||
}
|
||||
|
||||
let presentationData = context.component.context.sharedContext.currentPresentationData.with { $0 }
|
||||
let _ = strings
|
||||
|
||||
guard case let .request(domain, bot, clientData, flags) = component.subject else {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
|
||||
|
||||
let closeButton = closeButton.update(
|
||||
component: GlassBarButtonComponent(
|
||||
size: CGSize(width: 44.0, height: 44.0),
|
||||
backgroundColor: nil,
|
||||
isDark: theme.overallDarkAppearance,
|
||||
state: .glass,
|
||||
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
|
||||
BundleIconComponent(
|
||||
name: "Navigation/Close",
|
||||
tintColor: theme.chat.inputPanel.panelControlColor
|
||||
)
|
||||
)),
|
||||
action: { _ in
|
||||
component.cancel(true)
|
||||
}
|
||||
),
|
||||
availableSize: CGSize(width: 44.0, height: 44.0),
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(closeButton
|
||||
.position(CGPoint(x: 16.0 + closeButton.size.width / 2.0, y: 16.0 + closeButton.size.height / 2.0))
|
||||
)
|
||||
|
||||
if let peer = state.peer {
|
||||
let accountButton = accountButton.update(
|
||||
component: AccountSwitchComponent(
|
||||
context: state.forcedAccount?.0 ?? component.context,
|
||||
theme: environment.theme,
|
||||
peer: state.forcedAccount?.1 ?? peer,
|
||||
canSwitch: false,
|
||||
action: { [weak state] in
|
||||
state?.presentAccountSwitchMenu()
|
||||
}
|
||||
),
|
||||
availableSize: context.availableSize,
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(accountButton
|
||||
.position(CGPoint(x: context.availableSize.width - 16.0 - accountButton.size.width / 2.0, y: 16.0 + accountButton.size.height / 2.0))
|
||||
)
|
||||
}
|
||||
|
||||
var contentHeight: CGFloat = 32.0
|
||||
let avatar = avatar.update(
|
||||
component: AvatarComponent(
|
||||
context: component.context,
|
||||
theme: environment.theme,
|
||||
peer: EnginePeer(bot)
|
||||
),
|
||||
availableSize: CGSize(width: 92.0, height: 92.0),
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(avatar
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + avatar.size.height / 2.0))
|
||||
)
|
||||
contentHeight += avatar.size.height
|
||||
contentHeight += 18.0
|
||||
|
||||
let titleFont = Font.bold(24.0)
|
||||
let titleText = "Sign in to **\(domain)**"
|
||||
let title = title.update(
|
||||
component: MultilineTextComponent(
|
||||
text: .markdown(text: titleText, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: theme.actionSheet.primaryTextColor), bold: MarkdownAttributeSet(font: titleFont, textColor: theme.actionSheet.controlAccentColor), link: MarkdownAttributeSet(font: titleFont, textColor: theme.actionSheet.primaryTextColor), linkAttribute: { _ in return nil })),
|
||||
horizontalAlignment: .center,
|
||||
maximumNumberOfLines: 2
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude),
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(title
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + title.size.height / 2.0))
|
||||
)
|
||||
contentHeight += title.size.height
|
||||
contentHeight += 16.0
|
||||
|
||||
let textFont = Font.regular(15.0)
|
||||
let boldTextFont = Font.semibold(15.0)
|
||||
let descriptionText = "Review requested permissions and tap **Continue** to proceed."
|
||||
let description = description.update(
|
||||
component: MultilineTextComponent(
|
||||
text: .markdown(text: descriptionText, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: theme.actionSheet.primaryTextColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: theme.actionSheet.primaryTextColor), link: MarkdownAttributeSet(font: textFont, textColor: theme.actionSheet.primaryTextColor), linkAttribute: { _ in return nil })),
|
||||
horizontalAlignment: .center,
|
||||
maximumNumberOfLines: 3,
|
||||
lineSpacing: 0.2
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude),
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(description
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + description.size.height / 2.0))
|
||||
)
|
||||
contentHeight += description.size.height
|
||||
contentHeight += 16.0
|
||||
|
||||
var clientSectionItems: [AnyComponentWithIdentity<Empty>] = []
|
||||
clientSectionItems.append(
|
||||
AnyComponentWithIdentity(id: "device", component: AnyComponent(
|
||||
ListActionItemComponent(
|
||||
theme: theme,
|
||||
style: .glass,
|
||||
title: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: "Device",
|
||||
font: Font.regular(17.0),
|
||||
textColor: theme.list.itemPrimaryTextColor
|
||||
)),
|
||||
maximumNumberOfLines: 1
|
||||
)),
|
||||
contentInsets: UIEdgeInsets(top: 19.0, left: 0.0, bottom: 19.0, right: 0.0),
|
||||
accessory: .custom(ListActionItemComponent.CustomAccessory(
|
||||
component: AnyComponentWithIdentity(
|
||||
id: "info",
|
||||
component: AnyComponent(
|
||||
VStack([
|
||||
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: clientData?.platform ?? "",
|
||||
font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize),
|
||||
textColor: theme.list.itemPrimaryTextColor
|
||||
)),
|
||||
maximumNumberOfLines: 1
|
||||
))),
|
||||
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: clientData?.browser ?? "",
|
||||
font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize / 17.0 * 15.0),
|
||||
textColor: theme.list.itemSecondaryTextColor
|
||||
)),
|
||||
horizontalAlignment: .left,
|
||||
truncationType: .middle,
|
||||
maximumNumberOfLines: 1
|
||||
)))
|
||||
], alignment: .right, spacing: 3.0)
|
||||
)
|
||||
),
|
||||
insets: UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 14.0),
|
||||
isInteractive: true
|
||||
)),
|
||||
action: nil
|
||||
)
|
||||
))
|
||||
)
|
||||
|
||||
clientSectionItems.append(
|
||||
AnyComponentWithIdentity(id: "region", component: AnyComponent(
|
||||
ListActionItemComponent(
|
||||
theme: theme,
|
||||
style: .glass,
|
||||
title: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: "IP Address",
|
||||
font: Font.regular(17.0),
|
||||
textColor: theme.list.itemPrimaryTextColor
|
||||
)),
|
||||
maximumNumberOfLines: 1
|
||||
)),
|
||||
contentInsets: UIEdgeInsets(top: 19.0, left: 0.0, bottom: 19.0, right: 0.0),
|
||||
accessory: .custom(ListActionItemComponent.CustomAccessory(
|
||||
component: AnyComponentWithIdentity(
|
||||
id: "info",
|
||||
component: AnyComponent(
|
||||
VStack([
|
||||
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: clientData?.ip ?? "",
|
||||
font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize),
|
||||
textColor: theme.list.itemPrimaryTextColor
|
||||
)),
|
||||
maximumNumberOfLines: 1
|
||||
))),
|
||||
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: clientData?.region ?? "",
|
||||
font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize / 17.0 * 15.0),
|
||||
textColor: theme.list.itemSecondaryTextColor
|
||||
)),
|
||||
horizontalAlignment: .left,
|
||||
truncationType: .middle,
|
||||
maximumNumberOfLines: 1
|
||||
)))
|
||||
], alignment: .right, spacing: 3.0)
|
||||
)
|
||||
),
|
||||
insets: UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 14.0),
|
||||
isInteractive: true
|
||||
)),
|
||||
action: nil
|
||||
)
|
||||
))
|
||||
)
|
||||
|
||||
let clientSection = clientSection.update(
|
||||
component: ListSectionComponent(
|
||||
theme: theme,
|
||||
style: .glass,
|
||||
header: nil,
|
||||
footer: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: "Session details are shown for security verification.",
|
||||
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
|
||||
textColor: environment.theme.list.freeTextColor
|
||||
)),
|
||||
maximumNumberOfLines: 0
|
||||
)),
|
||||
items: clientSectionItems
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height),
|
||||
transition: context.transition
|
||||
)
|
||||
context.add(clientSection
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + clientSection.size.height / 2.0))
|
||||
)
|
||||
contentHeight += clientSection.size.height
|
||||
|
||||
if flags.contains(.requestWriteAccess) {
|
||||
contentHeight += 38.0
|
||||
|
||||
var optionsSectionItems: [AnyComponentWithIdentity<Empty>] = []
|
||||
optionsSectionItems.append(AnyComponentWithIdentity(id: "allowWrite", component: AnyComponent(ListActionItemComponent(
|
||||
theme: theme,
|
||||
style: .glass,
|
||||
title: AnyComponent(VStack([
|
||||
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: "Allow Messages",
|
||||
font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize),
|
||||
textColor: theme.list.itemPrimaryTextColor
|
||||
)),
|
||||
maximumNumberOfLines: 1
|
||||
))),
|
||||
], alignment: .left, spacing: 2.0)),
|
||||
accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: state.allowWrite, action: { [weak state] _ in
|
||||
guard let state else {
|
||||
return
|
||||
}
|
||||
state.allowWrite = !state.allowWrite
|
||||
state.updated()
|
||||
})),
|
||||
action: nil
|
||||
))))
|
||||
let optionsSection = optionsSection.update(
|
||||
component: ListSectionComponent(
|
||||
theme: theme,
|
||||
style: .glass,
|
||||
header: nil,
|
||||
footer: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: "Allow \(EnginePeer(bot).compactDisplayTitle) to send you messages.",
|
||||
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
|
||||
textColor: environment.theme.list.freeTextColor
|
||||
)),
|
||||
maximumNumberOfLines: 0
|
||||
)),
|
||||
items: optionsSectionItems
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height),
|
||||
transition: context.transition
|
||||
)
|
||||
context.add(optionsSection
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + optionsSection.size.height / 2.0))
|
||||
)
|
||||
contentHeight += optionsSection.size.height
|
||||
}
|
||||
contentHeight += 32.0
|
||||
|
||||
let buttonSpacing: CGFloat = 10.0
|
||||
let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0)
|
||||
let buttonWidth = (context.availableSize.width - buttonInsets.left - buttonInsets.right - buttonSpacing) / 2.0
|
||||
|
||||
let cancelButton = cancelButton.update(
|
||||
component: ButtonComponent(
|
||||
background: ButtonComponent.Background(
|
||||
style: .glass,
|
||||
color: theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.1),
|
||||
foreground: theme.list.itemPrimaryTextColor,
|
||||
pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
|
||||
),
|
||||
content: AnyComponentWithIdentity(
|
||||
id: AnyHashable(0),
|
||||
component: AnyComponent(MultilineTextComponent(text: .plain(NSMutableAttributedString(string: strings.Common_Cancel, font: Font.semibold(17.0), textColor: theme.list.itemPrimaryTextColor, paragraphAlignment: .center))))
|
||||
),
|
||||
action: {
|
||||
component.cancel(true)
|
||||
}
|
||||
),
|
||||
availableSize: CGSize(width: buttonWidth, height: 52.0),
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(cancelButton
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0 - buttonSpacing / 2.0 - cancelButton.size.width / 2.0, y: contentHeight + cancelButton.size.height / 2.0))
|
||||
)
|
||||
|
||||
let doneButton = doneButton.update(
|
||||
component: ButtonComponent(
|
||||
background: ButtonComponent.Background(
|
||||
style: .glass,
|
||||
color: theme.list.itemCheckColors.fillColor,
|
||||
foreground: theme.list.itemCheckColors.foregroundColor,
|
||||
pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9),
|
||||
cornerRadius: 10.0,
|
||||
),
|
||||
content: AnyComponentWithIdentity(
|
||||
id: AnyHashable(0),
|
||||
component: AnyComponent(MultilineTextComponent(text: .plain(NSMutableAttributedString(string: "Continue", font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center))))
|
||||
),
|
||||
displaysProgress: state.inProgress,
|
||||
action: { [weak state] in
|
||||
guard let state else {
|
||||
return
|
||||
}
|
||||
var allowWrite = false
|
||||
if flags.contains(.requestWriteAccess) && state.allowWrite {
|
||||
allowWrite = true
|
||||
}
|
||||
|
||||
let accountContext = state.forcedAccount?.0 ?? component.context
|
||||
guard let accountPeer = state.forcedAccount?.1 ?? state.peer else {
|
||||
return
|
||||
}
|
||||
|
||||
if flags.contains(.requestPhoneNumber) {
|
||||
state.displayPhoneNumberConfirmation(commit: { sharePhoneNumber in
|
||||
component.completion(accountContext, accountPeer, allowWrite, sharePhoneNumber)
|
||||
state.inProgress = true
|
||||
state.updated()
|
||||
})
|
||||
} else {
|
||||
component.completion(accountContext, accountPeer, allowWrite, false)
|
||||
state.inProgress = true
|
||||
state.updated()
|
||||
}
|
||||
}
|
||||
),
|
||||
availableSize: CGSize(width: buttonWidth, height: 52.0),
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(doneButton
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0 + buttonSpacing / 2.0 + doneButton.size.width / 2.0, y: contentHeight + doneButton.size.height / 2.0))
|
||||
)
|
||||
contentHeight += doneButton.size.height
|
||||
contentHeight += buttonInsets.bottom
|
||||
|
||||
return CGSize(width: context.availableSize.width, height: contentHeight)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class AuthConfirmationSheetComponent: CombinedComponent {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
||||
let context: AccountContext
|
||||
let subject: MessageActionUrlAuthResult
|
||||
let completion: (AccountContext, EnginePeer, Bool, Bool) -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
subject: MessageActionUrlAuthResult,
|
||||
completion: @escaping (AccountContext, EnginePeer, Bool, Bool) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.subject = subject
|
||||
self.completion = completion
|
||||
}
|
||||
|
||||
static func ==(lhs: AuthConfirmationSheetComponent, rhs: AuthConfirmationSheetComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
static var body: Body {
|
||||
let sheet = Child(SheetComponent<EnvironmentType>.self)
|
||||
let animateOut = StoredActionSlot(Action<Void>.self)
|
||||
|
||||
return { context in
|
||||
let environment = context.environment[EnvironmentType.self]
|
||||
let controller = environment.controller
|
||||
|
||||
let sheet = sheet.update(
|
||||
component: SheetComponent<EnvironmentType>(
|
||||
content: AnyComponent<EnvironmentType>(AuthConfirmationSheetContent(
|
||||
context: context.component.context,
|
||||
subject: context.component.subject,
|
||||
completion: context.component.completion,
|
||||
cancel: { animate in
|
||||
if animate {
|
||||
animateOut.invoke(Action { _ in
|
||||
if let controller = controller() {
|
||||
controller.dismiss(completion: nil)
|
||||
}
|
||||
})
|
||||
} else if let controller = controller() {
|
||||
controller.dismiss(animated: false, completion: nil)
|
||||
}
|
||||
}
|
||||
)),
|
||||
style: .glass,
|
||||
backgroundColor: .color(environment.theme.list.modalBlocksBackgroundColor),
|
||||
followContentSizeChanges: true,
|
||||
clipsContent: true,
|
||||
animateOut: animateOut
|
||||
),
|
||||
environment: {
|
||||
environment
|
||||
SheetComponentEnvironment(
|
||||
isDisplaying: environment.value.isVisible,
|
||||
isCentered: environment.metrics.widthClass == .regular,
|
||||
hasInputHeight: !environment.inputHeight.isZero,
|
||||
regularMetricsSize: CGSize(width: 430.0, height: 900.0),
|
||||
dismiss: { animated in
|
||||
if animated {
|
||||
animateOut.invoke(Action { _ in
|
||||
if let controller = controller() {
|
||||
controller.dismiss(completion: nil)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
if let controller = controller() {
|
||||
controller.dismiss(completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
availableSize: context.availableSize,
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
context.add(sheet
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
|
||||
)
|
||||
|
||||
return context.availableSize
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class AuthConfirmationScreen: ViewControllerComponentContainer {
|
||||
private let context: AccountContext
|
||||
private let subject: MessageActionUrlAuthResult
|
||||
fileprivate let completion: (AccountContext, EnginePeer, Bool, Bool) -> Void
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
subject: MessageActionUrlAuthResult,
|
||||
completion: @escaping (AccountContext, EnginePeer, Bool, Bool) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.subject = subject
|
||||
self.completion = completion
|
||||
|
||||
super.init(
|
||||
context: context,
|
||||
component: AuthConfirmationSheetComponent(
|
||||
context: context,
|
||||
subject: subject,
|
||||
completion: completion
|
||||
),
|
||||
navigationBarAppearance: .none,
|
||||
statusBarStyle: .ignore,
|
||||
theme: .default
|
||||
)
|
||||
|
||||
self.navigationPresentation = .flatModal
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
self.view.disablesInteractiveModalDismiss = true
|
||||
}
|
||||
|
||||
public func dismissAnimated() {
|
||||
if let view = self.node.hostView.findTaggedView(tag: SheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? SheetComponent<ViewControllerComponentContainer.Environment>.View {
|
||||
view.dismissAnimated()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class AuthConfirmationReferenceContentSource: ContextReferenceContentSource {
|
||||
private let controller: ViewController
|
||||
private let sourceView: UIView
|
||||
|
||||
let forceDisplayBelowKeyboard = true
|
||||
|
||||
init(controller: ViewController, sourceView: UIView) {
|
||||
self.controller = controller
|
||||
self.sourceView = sourceView
|
||||
}
|
||||
|
||||
func transitionInfo() -> ContextControllerReferenceViewInfo? {
|
||||
return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds)
|
||||
}
|
||||
}
|
||||
+6
-4
@@ -83,6 +83,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
public let contextSourceNode: ContextExtractedContentContainingNode
|
||||
private let containerNode: ContextControllerSourceNode
|
||||
public let imageNode: TransformImageNode
|
||||
public var sizeCoefficient: Float = 1.0
|
||||
private var enableSynchronousImageApply: Bool = false
|
||||
private var backgroundNode: WallpaperBubbleBackgroundNode?
|
||||
public private(set) var placeholderNode: StickerShimmerEffectNode
|
||||
@@ -117,7 +118,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
private let disposables = DisposableSet()
|
||||
|
||||
private var viaBotNode: TextNode?
|
||||
private let dateAndStatusNode: ChatMessageDateAndStatusNode
|
||||
public let dateAndStatusNode: ChatMessageDateAndStatusNode
|
||||
private var threadInfoNode: ChatMessageThreadInfoNode?
|
||||
private var replyInfoNode: ChatMessageReplyInfoNode?
|
||||
private var replyBackgroundContent: WallpaperBubbleBackgroundNode?
|
||||
@@ -1165,6 +1166,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
context: item.context,
|
||||
presentationData: item.presentationData,
|
||||
edited: edited,
|
||||
isDeleted: item.message.ghostgramIsDeleted,
|
||||
impressionCount: viewCount,
|
||||
dateText: dateText,
|
||||
type: statusType,
|
||||
@@ -1410,11 +1412,11 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
ReplyMarkupMessageAttribute(
|
||||
rows: [
|
||||
ReplyMarkupRow(buttons: [
|
||||
ReplyMarkupButton(title: item.presentationData.strings.Chat_PostApproval_Message_ActionReject, titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonDecline)),
|
||||
ReplyMarkupButton(title: item.presentationData.strings.Chat_PostApproval_Message_ActionApprove, titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonApprove))
|
||||
ReplyMarkupButton(title: item.presentationData.strings.Chat_PostApproval_Message_ActionReject, titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonDecline), style: nil),
|
||||
ReplyMarkupButton(title: item.presentationData.strings.Chat_PostApproval_Message_ActionApprove, titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonApprove), style: nil)
|
||||
]),
|
||||
ReplyMarkupRow(buttons: [
|
||||
ReplyMarkupButton(title: item.presentationData.strings.Chat_PostApproval_Message_ActionSuggestChanges, titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonSuggestChanges))
|
||||
ReplyMarkupButton(title: item.presentationData.strings.Chat_PostApproval_Message_ActionSuggestChanges, titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonSuggestChanges), style: nil)
|
||||
])
|
||||
],
|
||||
flags: [],
|
||||
|
||||
+1
@@ -749,6 +749,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
|
||||
context: context,
|
||||
presentationData: presentationData,
|
||||
edited: edited && !isPreview,
|
||||
isDeleted: message.ghostgramIsDeleted,
|
||||
impressionCount: !isPreview ? viewCount : nil,
|
||||
dateText: dateText,
|
||||
type: statusType,
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
sgdeps = [
|
||||
"//Swiftgram/SGStrings:SGStrings",
|
||||
"//Swiftgram/SGSimpleSettings:SGSimpleSettings",
|
||||
"//submodules/TranslateUI:TranslateUI"
|
||||
]
|
||||
|
||||
sgsrc = [
|
||||
"//Swiftgram/SGDoubleTapMessageAction:SGDoubleTapMessageAction"
|
||||
]
|
||||
|
||||
swift_library(
|
||||
name = "ChatMessageBubbleItemNode",
|
||||
module_name = "ChatMessageBubbleItemNode",
|
||||
srcs = glob([
|
||||
srcs = sgsrc + glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
deps = sgdeps + [
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Display",
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
|
||||
+93
-21
@@ -1,3 +1,6 @@
|
||||
import SGStrings
|
||||
import SGSimpleSettings
|
||||
import TranslateUI
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
@@ -132,7 +135,7 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
|
||||
|
||||
outer: for (message, itemAttributes) in item.content {
|
||||
for attribute in message.attributes {
|
||||
if let attribute = attribute as? RestrictedContentMessageAttribute, attribute.platformText(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) != nil {
|
||||
if let attribute = attribute as? RestrictedContentMessageAttribute, attribute.platformText(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }, chatId: message.author?.id.id._internalGetInt64Value()) != nil {
|
||||
result.append((message, ChatMessageRestrictedBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
|
||||
needReactions = false
|
||||
break outer
|
||||
@@ -316,6 +319,35 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
|
||||
isMediaInverted = true
|
||||
}
|
||||
|
||||
|
||||
// MARK: Swiftgram
|
||||
var message = message
|
||||
if message.canRevealContent(contentSettings: item.context.currentContentSettings.with { $0 }) {
|
||||
let originalTextLength = message.text.count
|
||||
let noticeString = i18n("Message.HoldToShowOrReport", item.presentationData.strings.baseLanguageCode)
|
||||
|
||||
message = message.withUpdatedText(message.text + "\n" + noticeString)
|
||||
let noticeStringLength = noticeString.count
|
||||
let startIndex = originalTextLength + 1 // +1 for the newline character
|
||||
// Calculate the end index, which is the start index plus the length of noticeString
|
||||
let endIndex = startIndex + noticeStringLength
|
||||
|
||||
var newAttributes = message.attributes
|
||||
newAttributes.append(
|
||||
TextEntitiesMessageAttribute(
|
||||
entities: [
|
||||
MessageTextEntity(
|
||||
range: startIndex..<endIndex,
|
||||
// TODO(swiftgram): Add more instructions to collapsed block?
|
||||
type: .BlockQuote(isCollapsed: false) //.Custom(type: ApplicationSpecificEntityType.Button)
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
message = message.withUpdatedAttributes(newAttributes)
|
||||
}
|
||||
|
||||
|
||||
if isMediaInverted {
|
||||
result.insert((message, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: isFile ? .condensed : .default)), at: addedPriceInfo ? 1 : 0)
|
||||
} else {
|
||||
@@ -482,7 +514,7 @@ private func mapVisibility(_ visibility: ListViewItemNodeVisibility, boundsSize:
|
||||
}
|
||||
|
||||
private func isDeletedBubbleMessage(_ message: Message) -> Bool {
|
||||
return AntiDeleteManager.shared.isMessageDeleted(peerId: message.id.peerId.toInt64(), messageId: message.id.id) || AntiDeleteManager.shared.isMessageDeleted(text: message.text)
|
||||
return message.ghostgramIsDeleted
|
||||
}
|
||||
|
||||
public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode {
|
||||
@@ -1235,7 +1267,6 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
if let shareButtonNode = strongSelf.shareButtonNode, shareButtonNode.frame.contains(point) {
|
||||
return .fail
|
||||
}
|
||||
|
||||
if let actionButtonsNode = strongSelf.actionButtonsNode {
|
||||
if let _ = actionButtonsNode.hitTest(strongSelf.view.convert(point, to: actionButtonsNode.view), with: nil) {
|
||||
return .fail
|
||||
@@ -1734,6 +1765,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
let isFailed = item.content.firstMessage.effectivelyFailed(timestamp: item.context.account.network.getApproximateRemoteTimestamp())
|
||||
|
||||
var needsShareButton = false
|
||||
var mayHaveSeparateCommentsButton = false // MARK: Swiftgram
|
||||
var needsSummarizeButton = false
|
||||
|
||||
if incoming, case let .customChatContents(contents) = item.associatedData.subject, case .hashTagSearch = contents.kind {
|
||||
@@ -1789,7 +1821,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
needsShareButton = true
|
||||
}
|
||||
}
|
||||
var mayHaveSeparateCommentsButton = false
|
||||
// var mayHaveSeparateCommentsButton = false // MARK: Swiftgram
|
||||
if !needsShareButton {
|
||||
loop: for media in item.message.media {
|
||||
if media is TelegramMediaGame || media is TelegramMediaInvoice {
|
||||
@@ -1831,7 +1863,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
needsShareButton = true
|
||||
}
|
||||
for attribute in item.content.firstMessage.attributes {
|
||||
if let attribute = attribute as? RestrictedContentMessageAttribute, attribute.platformText(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) != nil {
|
||||
if let attribute = attribute as? RestrictedContentMessageAttribute, attribute.platformText(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }, chatId: item.content.firstMessage.author?.id.id._internalGetInt64Value()) != nil {
|
||||
needsShareButton = false
|
||||
needsSummarizeButton = false
|
||||
}
|
||||
@@ -1862,11 +1894,21 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
}
|
||||
|
||||
tmpWidth -= deliveryFailedInset
|
||||
// MARK: Swifgram
|
||||
let renderWideChannelPosts: Bool
|
||||
if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = channel.info, SGSimpleSettings.shared.wideChannelPosts, !(mayHaveSeparateCommentsButton && hasCommentButton(item: item)) {
|
||||
renderWideChannelPosts = true
|
||||
|
||||
tmpWidth = baseWidth
|
||||
needsShareButton = false
|
||||
} else {
|
||||
renderWideChannelPosts = false
|
||||
}
|
||||
|
||||
let (contentNodeMessagesAndClasses, needSeparateContainers, needReactions) = contentNodeMessagesAndClassesForItem(item)
|
||||
|
||||
var maximumContentWidth = floor(tmpWidth - layoutConstants.bubble.edgeInset * 3.0 - layoutConstants.bubble.contentInsets.left - layoutConstants.bubble.contentInsets.right - avatarInset)
|
||||
if (needsShareButton && !isSidePanelOpen) {
|
||||
if needsShareButton && !isSidePanelOpen {
|
||||
maximumContentWidth -= 10.0
|
||||
}
|
||||
|
||||
@@ -2386,13 +2428,29 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
var mosaicStatusSizeAndApply: (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageDateAndStatusNode)?
|
||||
|
||||
if let mosaicRange = mosaicRange {
|
||||
let maxSize = layoutConstants.image.maxDimensions.fittedToWidthOrSmaller(maximumContentWidth - layoutConstants.image.bubbleInsets.left - layoutConstants.image.bubbleInsets.right)
|
||||
let (innerFramesAndPositions, innerSize) = chatMessageBubbleMosaicLayout(maxSize: maxSize, itemSizes: contentPropertiesAndLayouts[mosaicRange].map { item in
|
||||
// MARK: Swiftgram
|
||||
var maxDimensions = layoutConstants.image.maxDimensions
|
||||
if renderWideChannelPosts {
|
||||
maxDimensions.width = maximumContentWidth
|
||||
}
|
||||
var maxSize = maxDimensions.fittedToWidthOrSmaller(maximumContentWidth - layoutConstants.image.bubbleInsets.left - layoutConstants.image.bubbleInsets.right)
|
||||
var (innerFramesAndPositions, innerSize) = chatMessageBubbleMosaicLayout(maxSize: maxSize, itemSizes: contentPropertiesAndLayouts[mosaicRange].map { item in
|
||||
guard let size = item.0, size.width > 0.0, size.height > 0 else {
|
||||
return CGSize(width: 256.0, height: 256.0)
|
||||
}
|
||||
return size
|
||||
})
|
||||
}/*, TODO(swiftgram): fillWidth: SGSimpleSettings.shared.wideChannelPosts */)
|
||||
// MARK: Swiftgram
|
||||
if innerSize.height > maxSize.height, maxDimensions.width != layoutConstants.image.maxDimensions.width {
|
||||
maxDimensions.width = max(round(maxDimensions.width * maxSize.height / innerSize.height), layoutConstants.image.maxDimensions.width)
|
||||
maxSize = maxDimensions.fittedToWidthOrSmaller(maximumContentWidth - layoutConstants.image.bubbleInsets.left - layoutConstants.image.bubbleInsets.right)
|
||||
(innerFramesAndPositions, innerSize) = chatMessageBubbleMosaicLayout(maxSize: maxSize, itemSizes: contentPropertiesAndLayouts[mosaicRange].map { item in
|
||||
guard let size = item.0, size.width > 0.0, size.height > 0 else {
|
||||
return CGSize(width: 256.0, height: 256.0)
|
||||
}
|
||||
return size
|
||||
}/*, TODO(swiftgram): fillWidth: SGSimpleSettings.shared.wideChannelPosts */)
|
||||
}
|
||||
|
||||
let framesAndPositions = innerFramesAndPositions.map { ($0.0.offsetBy(dx: layoutConstants.image.bubbleInsets.left, dy: layoutConstants.image.bubbleInsets.top), $0.1) }
|
||||
|
||||
@@ -2474,6 +2532,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
context: item.context,
|
||||
presentationData: item.presentationData,
|
||||
edited: edited && !item.presentationData.isPreview,
|
||||
isDeleted: message.ghostgramIsDeleted,
|
||||
impressionCount: !item.presentationData.isPreview ? viewCount : nil,
|
||||
dateText: dateText,
|
||||
type: statusType,
|
||||
@@ -2852,7 +2911,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
ReplyMarkupMessageAttribute(
|
||||
rows: [
|
||||
ReplyMarkupRow(
|
||||
buttons: [ReplyMarkupButton(title: item.presentationData.strings.Channel_AdminLog_ShowMoreMessages(Int32(messages.count - 1)), titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: MemoryBuffer(data: Data())))]
|
||||
buttons: [ReplyMarkupButton(title: item.presentationData.strings.Channel_AdminLog_ShowMoreMessages(Int32(messages.count - 1)), titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: MemoryBuffer(data: Data())), style: nil)]
|
||||
)
|
||||
],
|
||||
flags: [],
|
||||
@@ -2891,8 +2950,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
ReplyMarkupMessageAttribute(
|
||||
rows: [
|
||||
ReplyMarkupRow(buttons: [
|
||||
ReplyMarkupButton(title: item.presentationData.strings.Chat_GiftPurchaseOffer_Reject, titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonDecline)),
|
||||
ReplyMarkupButton(title: item.presentationData.strings.Chat_GiftPurchaseOffer_Accept, titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonApprove))
|
||||
ReplyMarkupButton(title: item.presentationData.strings.Chat_GiftPurchaseOffer_Reject, titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonDecline), style: nil),
|
||||
ReplyMarkupButton(title: item.presentationData.strings.Chat_GiftPurchaseOffer_Accept, titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonApprove), style: nil)
|
||||
])
|
||||
],
|
||||
flags: [],
|
||||
@@ -2940,11 +2999,11 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
ReplyMarkupMessageAttribute(
|
||||
rows: [
|
||||
ReplyMarkupRow(buttons: [
|
||||
ReplyMarkupButton(title: item.presentationData.strings.Chat_PostApproval_Message_ActionReject, titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonDecline)),
|
||||
ReplyMarkupButton(title: item.presentationData.strings.Chat_PostApproval_Message_ActionApprove, titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonApprove))
|
||||
ReplyMarkupButton(title: item.presentationData.strings.Chat_PostApproval_Message_ActionReject, titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonDecline), style: nil),
|
||||
ReplyMarkupButton(title: item.presentationData.strings.Chat_PostApproval_Message_ActionApprove, titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonApprove), style: nil)
|
||||
]),
|
||||
ReplyMarkupRow(buttons: [
|
||||
ReplyMarkupButton(title: item.presentationData.strings.Chat_PostApproval_Message_ActionSuggestChanges, titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonSuggestChanges))
|
||||
ReplyMarkupButton(title: item.presentationData.strings.Chat_PostApproval_Message_ActionSuggestChanges, titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonSuggestChanges), style: nil)
|
||||
])
|
||||
],
|
||||
flags: [],
|
||||
@@ -2978,7 +3037,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
ReplyMarkupMessageAttribute(
|
||||
rows: [
|
||||
ReplyMarkupRow(buttons: [
|
||||
ReplyMarkupButton(title: item.presentationData.strings.Chat_MessageContinueLastThread, titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: button))
|
||||
ReplyMarkupButton(title: item.presentationData.strings.Chat_MessageContinueLastThread, titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: button), style: nil)
|
||||
])
|
||||
],
|
||||
flags: [],
|
||||
@@ -4371,7 +4430,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
strongSelf.replyInfoNode = nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let deletedMessageAlpha = CGFloat(AntiDeleteManager.shared.deletedMessageDisplayAlpha)
|
||||
var deletedMessageStableIds = Set<UInt32>()
|
||||
for (message, _) in item.content {
|
||||
@@ -4524,7 +4583,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
}
|
||||
|
||||
contentContainer?.update(size: relativeFrame.size, contentOrigin: contentOrigin, selectionInsets: selectionInsets, index: index, presentationData: item.presentationData, graphics: graphics, backgroundType: backgroundType, presentationContext: item.controllerInteraction.presentationContext, mediaBox: item.context.account.postbox.mediaBox, messageSelection: itemSelection)
|
||||
|
||||
|
||||
if let contentContainer = contentContainer {
|
||||
let containerAlpha: CGFloat = deletedMessageStableIds.contains(stableId) ? deletedMessageAlpha : 1.0
|
||||
if case .System = animation {
|
||||
@@ -4536,7 +4595,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
|
||||
index += 1
|
||||
}
|
||||
|
||||
|
||||
let mainContainerAlpha: CGFloat
|
||||
if contentContainerNodeFrames.isEmpty, !deletedMessageStableIds.isEmpty {
|
||||
mainContainerAlpha = deletedMessageAlpha
|
||||
@@ -5153,7 +5212,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
animation.animator.updateAlpha(layer: shareButtonNode.layer, alpha: (isCurrentlyPlayingMedia || isSidePanelOpen) ? 0.0 : 1.0, completion: nil)
|
||||
animation.animator.updateScale(layer: shareButtonNode.layer, scale: (isCurrentlyPlayingMedia || isSidePanelOpen) ? 0.001 : 1.0, completion: nil)
|
||||
}
|
||||
|
||||
|
||||
if case .System = animation, strongSelf.mainContextSourceNode.isExtractedToContextPreview {
|
||||
legacyTransition.updateFrame(node: strongSelf.backgroundNode, frame: backgroundFrame)
|
||||
if let backgroundHighlightNode = strongSelf.backgroundHighlightNode {
|
||||
@@ -5291,18 +5350,32 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
case let .optionalAction(f):
|
||||
f()
|
||||
case let .openContextMenu(openContextMenu):
|
||||
switch (sgDoubleTapMessageAction(incoming: openContextMenu.tapMessage.effectivelyIncoming(item.context.account.peerId), message: openContextMenu.tapMessage)) {
|
||||
case SGSimpleSettings.MessageDoubleTapAction.none.rawValue:
|
||||
break
|
||||
case SGSimpleSettings.MessageDoubleTapAction.edit.rawValue:
|
||||
item.controllerInteraction.sgStartMessageEdit(openContextMenu.tapMessage)
|
||||
default:
|
||||
if canAddMessageReactions(message: openContextMenu.tapMessage) {
|
||||
item.controllerInteraction.updateMessageReaction(openContextMenu.tapMessage, .default, false, nil)
|
||||
} else {
|
||||
item.controllerInteraction.openMessageContextMenu(openContextMenu.tapMessage, openContextMenu.selectAll, self, openContextMenu.subFrame, nil, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if case .tap = gesture {
|
||||
item.controllerInteraction.clickThroughMessage(self.view, location)
|
||||
} else if case .doubleTap = gesture {
|
||||
switch (sgDoubleTapMessageAction(incoming: item.message.effectivelyIncoming(item.context.account.peerId), message: item.message)) {
|
||||
case SGSimpleSettings.MessageDoubleTapAction.none.rawValue:
|
||||
break
|
||||
case SGSimpleSettings.MessageDoubleTapAction.edit.rawValue:
|
||||
item.controllerInteraction.sgStartMessageEdit(item.message)
|
||||
default:
|
||||
if canAddMessageReactions(message: item.message) {
|
||||
item.controllerInteraction.updateMessageReaction(item.message, .default, false, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
@@ -5971,7 +6044,6 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
if let shareButtonNode = self.shareButtonNode, shareButtonNode.frame.contains(point) {
|
||||
return shareButtonNode.view.hitTest(self.view.convert(point, to: shareButtonNode.view), with: event)
|
||||
}
|
||||
|
||||
if let selectionNode = self.selectionNode {
|
||||
if let result = self.traceSelectionNodes(parent: self, point: point.offsetBy(dx: -42.0, dy: 0.0)) {
|
||||
return result.view
|
||||
|
||||
+1
@@ -298,6 +298,7 @@ public class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
context: item.context,
|
||||
presentationData: item.presentationData,
|
||||
edited: edited,
|
||||
isDeleted: item.topMessage.ghostgramIsDeleted,
|
||||
impressionCount: viewCount,
|
||||
dateText: dateText,
|
||||
type: statusType,
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
sgdeps = [
|
||||
"//Swiftgram/SGSimpleSettings:SGSimpleSettings"
|
||||
]
|
||||
|
||||
swift_library(
|
||||
name = "ChatMessageDateAndStatusNode",
|
||||
module_name = "ChatMessageDateAndStatusNode",
|
||||
@@ -9,7 +13,7 @@ swift_library(
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
deps = sgdeps + [
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Postbox",
|
||||
"//submodules/TelegramCore",
|
||||
|
||||
+57
-62
@@ -1,3 +1,4 @@
|
||||
import SGSimpleSettings
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
@@ -30,6 +31,10 @@ private func maybeAddRotationAnimation(_ layer: CALayer, duration: Double) {
|
||||
layer.add(basicAnimation, forKey: "clockFrameAnimation")
|
||||
}
|
||||
|
||||
private func generateDeletedStatusIcon(color: UIColor) -> UIImage? {
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: color)?.precomposed()
|
||||
}
|
||||
|
||||
public enum ChatMessageDateAndStatusOutgoingType: Equatable {
|
||||
case Sent(read: Bool)
|
||||
case Sending
|
||||
@@ -183,6 +188,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
var context: AccountContext
|
||||
var presentationData: ChatPresentationData
|
||||
var edited: Bool
|
||||
var isDeleted: Bool
|
||||
var impressionCount: Int?
|
||||
var dateText: String
|
||||
var type: ChatMessageDateAndStatusType
|
||||
@@ -201,7 +207,6 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
var tonAmount: Int64?
|
||||
var isPinned: Bool
|
||||
var hasAutoremove: Bool
|
||||
var isDeleted: Bool
|
||||
var canViewReactionList: Bool
|
||||
var animationCache: AnimationCache
|
||||
var animationRenderer: MultiAnimationRenderer
|
||||
@@ -210,6 +215,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
context: AccountContext,
|
||||
presentationData: ChatPresentationData,
|
||||
edited: Bool,
|
||||
isDeleted: Bool,
|
||||
impressionCount: Int?,
|
||||
dateText: String,
|
||||
type: ChatMessageDateAndStatusType,
|
||||
@@ -228,7 +234,6 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
tonAmount: Int64? = nil,
|
||||
isPinned: Bool,
|
||||
hasAutoremove: Bool,
|
||||
isDeleted: Bool = false,
|
||||
canViewReactionList: Bool,
|
||||
animationCache: AnimationCache,
|
||||
animationRenderer: MultiAnimationRenderer
|
||||
@@ -236,6 +241,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
self.context = context
|
||||
self.presentationData = presentationData
|
||||
self.edited = edited
|
||||
self.isDeleted = isDeleted
|
||||
self.impressionCount = impressionCount == 0 ? nil : impressionCount
|
||||
self.dateText = dateText
|
||||
self.type = type
|
||||
@@ -254,7 +260,6 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
self.tonAmount = tonAmount
|
||||
self.isPinned = isPinned
|
||||
self.hasAutoremove = hasAutoremove
|
||||
self.isDeleted = isDeleted
|
||||
self.canViewReactionList = canViewReactionList
|
||||
self.animationCache = animationCache
|
||||
self.animationRenderer = animationRenderer
|
||||
@@ -268,6 +273,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
private var clockFrameNode: ASImageNode?
|
||||
private var clockMinNode: ASImageNode?
|
||||
private let dateNode: TextNode
|
||||
private var deletedIcon: ASImageNode?
|
||||
private var impressionIcon: ASImageNode?
|
||||
private var reactionNodes: [MessageReaction.Reaction: StatusReactionNode] = [:]
|
||||
private let reactionButtonsContainer = ReactionButtonsAsyncLayoutContainer()
|
||||
@@ -277,7 +283,6 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
private var replyCountNode: TextNode?
|
||||
private var starsIcon: ASImageNode?
|
||||
private var starsCountNode: TextNode?
|
||||
private var deletedIcon: ASImageNode?
|
||||
|
||||
private var type: ChatMessageDateAndStatusType?
|
||||
private var theme: ChatPresentationThemeData?
|
||||
@@ -330,6 +335,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
var clockMinNode = self.clockMinNode
|
||||
|
||||
var currentBackgroundNode = self.backgroundNode
|
||||
var currentDeletedIcon = self.deletedIcon
|
||||
var currentImpressionIcon = self.impressionIcon
|
||||
var currentRepliesIcon = self.repliesIcon
|
||||
var currentStarsIcon = self.starsIcon
|
||||
@@ -353,6 +359,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
let loadedCheckPartialImage: UIImage?
|
||||
let clockFrameImage: UIImage?
|
||||
let clockMinImage: UIImage?
|
||||
let deletedImage: UIImage?
|
||||
var impressionImage: UIImage?
|
||||
var repliesImage: UIImage?
|
||||
var starsImage: UIImage?
|
||||
@@ -541,6 +548,8 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
starsImage = graphics.freeTonIcon
|
||||
}
|
||||
}
|
||||
|
||||
deletedImage = arguments.isDeleted ? generateDeletedStatusIcon(color: dateColor) : nil
|
||||
|
||||
var updatedDateText = arguments.dateText
|
||||
if arguments.edited {
|
||||
@@ -561,6 +570,23 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
var checkReadFrame: CGRect?
|
||||
|
||||
var clockPosition = CGPoint()
|
||||
|
||||
var deletedSize = CGSize()
|
||||
var deletedWidth: CGFloat = 0.0
|
||||
if deletedImage != nil {
|
||||
if currentDeletedIcon == nil {
|
||||
let iconNode = ASImageNode()
|
||||
iconNode.isLayerBacked = true
|
||||
iconNode.displayWithoutProcessing = true
|
||||
iconNode.displaysAsynchronously = false
|
||||
iconNode.contentMode = .scaleAspectFit
|
||||
currentDeletedIcon = iconNode
|
||||
}
|
||||
deletedSize = CGSize(width: 11.0, height: 11.0)
|
||||
deletedWidth = deletedSize.width + 4.0
|
||||
} else {
|
||||
currentDeletedIcon = nil
|
||||
}
|
||||
|
||||
var impressionSize = CGSize()
|
||||
var impressionWidth: CGFloat = 0.0
|
||||
@@ -606,36 +632,6 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
currentStarsIcon = nil
|
||||
}
|
||||
|
||||
// ANTIDELETE: Deleted message icon
|
||||
var currentDeletedIcon = self?.deletedIcon
|
||||
var deletedIconSize = CGSize()
|
||||
if arguments.isDeleted {
|
||||
if currentDeletedIcon == nil {
|
||||
let iconNode = ASImageNode()
|
||||
iconNode.isLayerBacked = true
|
||||
iconNode.displayWithoutProcessing = true
|
||||
iconNode.displaysAsynchronously = false
|
||||
currentDeletedIcon = iconNode
|
||||
}
|
||||
// Use trash icon from bundle or create simple one
|
||||
let deletedImage = UIImage(bundleImageName: "Chat/Message/DeletedIcon") ?? generateTintedImage(image: UIImage(systemName: "trash"), color: dateColor)
|
||||
deletedIconSize = deletedImage?.size ?? CGSize(width: 10, height: 10)
|
||||
// Scale down the image
|
||||
if let img = deletedImage {
|
||||
let scaledSize = CGSize(width: 10, height: 10)
|
||||
UIGraphicsBeginImageContextWithOptions(scaledSize, false, 0.0)
|
||||
img.draw(in: CGRect(origin: .zero, size: scaledSize))
|
||||
let scaledImage = UIGraphicsGetImageFromCurrentImageContext()
|
||||
UIGraphicsEndImageContext()
|
||||
currentDeletedIcon?.image = scaledImage
|
||||
deletedIconSize = scaledSize
|
||||
} else {
|
||||
currentDeletedIcon?.image = deletedImage
|
||||
}
|
||||
} else {
|
||||
currentDeletedIcon = nil
|
||||
}
|
||||
|
||||
if let outgoingStatus = outgoingStatus {
|
||||
switch outgoingStatus {
|
||||
case .Sending:
|
||||
@@ -670,7 +666,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
clockMinNode?.displayWithoutProcessing = true
|
||||
clockMinNode?.frame = CGRect(origin: CGPoint(), size: clockMinImage?.size ?? CGSize())
|
||||
}
|
||||
clockPosition = CGPoint(x: leftInset + date.size.width + 8.5, y: 7.5 + offset)
|
||||
clockPosition = CGPoint(x: leftInset + deletedWidth + impressionWidth + date.size.width + 8.5, y: 7.5 + offset)
|
||||
case let .Sent(read):
|
||||
let hideStatus: Bool
|
||||
switch arguments.type {
|
||||
@@ -710,9 +706,9 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
let checkSize = loadedCheckFullImage!.size
|
||||
|
||||
if read {
|
||||
checkReadFrame = CGRect(origin: CGPoint(x: leftInset + impressionWidth + date.size.width + 5.0 + statusWidth - checkSize.width, y: 3.0 + offset), size: checkSize)
|
||||
checkReadFrame = CGRect(origin: CGPoint(x: leftInset + deletedWidth + impressionWidth + date.size.width + 5.0 + statusWidth - checkSize.width, y: 3.0 + offset), size: checkSize)
|
||||
}
|
||||
checkSentFrame = CGRect(origin: CGPoint(x: leftInset + impressionWidth + date.size.width + 5.0 + statusWidth - checkSize.width - checkOffset, y: 3.0 + offset), size: checkSize)
|
||||
checkSentFrame = CGRect(origin: CGPoint(x: leftInset + deletedWidth + impressionWidth + date.size.width + 5.0 + statusWidth - checkSize.width - checkOffset, y: 3.0 + offset), size: checkSize)
|
||||
}
|
||||
case .Failed:
|
||||
statusWidth = 0.0
|
||||
@@ -799,15 +795,9 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
reactionInset += 13.0
|
||||
}
|
||||
|
||||
// ANTIDELETE: Add deleted icon space
|
||||
var deletedIconWidth: CGFloat = 0.0
|
||||
if arguments.isDeleted {
|
||||
deletedIconWidth = deletedIconSize.width + 3.0
|
||||
}
|
||||
|
||||
leftInset += reactionInset
|
||||
|
||||
let layoutSize = CGSize(width: leftInset + deletedIconWidth + impressionWidth + date.size.width + statusWidth + backgroundInsets.left + backgroundInsets.right, height: date.size.height + backgroundInsets.top + backgroundInsets.bottom)
|
||||
let layoutSize = CGSize(width: leftInset + deletedWidth + impressionWidth + date.size.width + statusWidth + backgroundInsets.left + backgroundInsets.right, height: date.size.height + backgroundInsets.top + backgroundInsets.bottom)
|
||||
|
||||
let verticalReactionsInset: CGFloat
|
||||
let verticalInset: CGFloat
|
||||
@@ -1132,8 +1122,26 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
|
||||
let _ = dateApply()
|
||||
|
||||
if let currentDeletedIcon = currentDeletedIcon {
|
||||
let deletedIconFrame = CGRect(origin: CGPoint(x: leftOffset + leftInset + backgroundInsets.left, y: backgroundInsets.top + 1.0 + offset + verticalInset + floor((date.size.height - deletedSize.height) / 2.0)), size: deletedSize)
|
||||
currentDeletedIcon.displaysAsynchronously = false
|
||||
if currentDeletedIcon.image !== deletedImage {
|
||||
currentDeletedIcon.image = deletedImage
|
||||
}
|
||||
if currentDeletedIcon.supernode == nil {
|
||||
strongSelf.deletedIcon = currentDeletedIcon
|
||||
strongSelf.addSubnode(currentDeletedIcon)
|
||||
currentDeletedIcon.frame = deletedIconFrame
|
||||
} else {
|
||||
animation.animator.updateFrame(layer: currentDeletedIcon.layer, frame: deletedIconFrame, completion: nil)
|
||||
}
|
||||
} else if let deletedIcon = strongSelf.deletedIcon {
|
||||
deletedIcon.removeFromSupernode()
|
||||
strongSelf.deletedIcon = nil
|
||||
}
|
||||
|
||||
if let currentImpressionIcon = currentImpressionIcon {
|
||||
let impressionIconFrame = CGRect(origin: CGPoint(x: leftOffset + leftInset + backgroundInsets.left, y: backgroundInsets.top + 1.0 + offset + verticalInset + floor((date.size.height - impressionSize.height) / 2.0)), size: impressionSize)
|
||||
let impressionIconFrame = CGRect(origin: CGPoint(x: leftOffset + leftInset + backgroundInsets.left + deletedWidth, y: backgroundInsets.top + 1.0 + offset + verticalInset + floor((date.size.height - impressionSize.height) / 2.0)), size: impressionSize)
|
||||
currentImpressionIcon.displaysAsynchronously = false
|
||||
if currentImpressionIcon.image !== impressionImage {
|
||||
currentImpressionIcon.image = impressionImage
|
||||
@@ -1150,22 +1158,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
strongSelf.impressionIcon = nil
|
||||
}
|
||||
|
||||
// ANTIDELETE: Position deleted icon
|
||||
if let currentDeletedIcon = currentDeletedIcon {
|
||||
let deletedIconFrame = CGRect(origin: CGPoint(x: leftOffset + leftInset + backgroundInsets.left + impressionWidth, y: backgroundInsets.top + 1.0 + offset + verticalInset + floor((date.size.height - deletedIconSize.height) / 2.0)), size: deletedIconSize)
|
||||
if currentDeletedIcon.supernode == nil {
|
||||
strongSelf.deletedIcon = currentDeletedIcon
|
||||
strongSelf.addSubnode(currentDeletedIcon)
|
||||
currentDeletedIcon.frame = deletedIconFrame
|
||||
} else {
|
||||
animation.animator.updateFrame(layer: currentDeletedIcon.layer, frame: deletedIconFrame, completion: nil)
|
||||
}
|
||||
} else if let deletedIcon = strongSelf.deletedIcon {
|
||||
deletedIcon.removeFromSupernode()
|
||||
strongSelf.deletedIcon = nil
|
||||
}
|
||||
|
||||
animation.animator.updateFrame(layer: strongSelf.dateNode.layer, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset + backgroundInsets.left + impressionWidth + deletedIconWidth, y: backgroundInsets.top + 1.0 + offset + verticalInset), size: date.size), completion: nil)
|
||||
animation.animator.updateFrame(layer: strongSelf.dateNode.layer, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset + backgroundInsets.left + deletedWidth + impressionWidth, y: backgroundInsets.top + 1.0 + offset + verticalInset), size: date.size), completion: nil)
|
||||
|
||||
if let clockFrameNode = clockFrameNode {
|
||||
let clockPosition = CGPoint(x: leftOffset + backgroundInsets.left + clockPosition.x + reactionInset, y: backgroundInsets.top + clockPosition.y + verticalInset)
|
||||
@@ -1527,5 +1520,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
public func shouldDisplayInlineDateReactions(message: Message, isPremium: Bool, forceInline: Bool) -> Bool {
|
||||
return false
|
||||
// MARK: Swiftgram
|
||||
// With 10.13 it now hides reactions in favor of message effect badge
|
||||
return SGSimpleSettings.shared.hideReactions
|
||||
}
|
||||
|
||||
+1
@@ -437,6 +437,7 @@ public class ChatMessageFactCheckBubbleContentNode: ChatMessageBubbleContentNode
|
||||
context: item.context,
|
||||
presentationData: item.presentationData,
|
||||
edited: edited && !item.presentationData.isPreview,
|
||||
isDeleted: message.ghostgramIsDeleted,
|
||||
impressionCount: !item.presentationData.isPreview ? viewCount : nil,
|
||||
dateText: dateText,
|
||||
type: statusType,
|
||||
|
||||
+16
-3
@@ -282,7 +282,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
switch action.action {
|
||||
case let .starGift(gift, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
|
||||
releasedBy = gift.releasedBy
|
||||
case let .starGiftUnique(gift, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
|
||||
case let .starGiftUnique(gift, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
|
||||
releasedBy = gift.releasedBy
|
||||
default:
|
||||
break
|
||||
@@ -378,6 +378,17 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
|
||||
override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, unboundSize: CGSize?, maxWidth: CGFloat, layout: (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) {
|
||||
return { item, _, _, _, _, _ in
|
||||
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 0.0, hidesBackground: .always, forceFullCorners: false, forceAlignment: .center)
|
||||
return (contentProperties, nil, 220.0, { _, _ in
|
||||
return (220.0, { _ in
|
||||
return (CGSize(width: 220.0, height: 120.0), { [weak self] _, _, _ in
|
||||
self?.item = item
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
#if false
|
||||
let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
|
||||
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
||||
let makeSubtitleLayout = TextNodeWithEntities.asyncLayout(self.subtitleNode)
|
||||
@@ -402,10 +413,10 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
|
||||
let cachedTonImage = self.cachedTonImage
|
||||
|
||||
return { item, layoutConstants, _, _, _, _ in
|
||||
let asyncLayout: (ChatMessageBubbleContentItem, ChatMessageItemLayoutConstants, ChatMessageBubblePreparePosition, Bool?, CGSize, CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) = { (item: ChatMessageBubbleContentItem, layoutConstants: ChatMessageItemLayoutConstants, preparePosition: ChatMessageBubblePreparePosition, messageSelection: Bool?, constrainedSize: CGSize, avatarInset: CGFloat) in
|
||||
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 0.0, hidesBackground: .always, forceFullCorners: false, forceAlignment: .center)
|
||||
|
||||
return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in
|
||||
return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { (constrainedSize: CGSize, position: ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void)) in
|
||||
var giftSize = CGSize(width: 220.0, height: 240.0)
|
||||
|
||||
let incoming: Bool
|
||||
@@ -1471,6 +1482,8 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
})
|
||||
})
|
||||
}
|
||||
return asyncLayout
|
||||
#endif
|
||||
}
|
||||
|
||||
override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
|
||||
|
||||
+1
@@ -565,6 +565,7 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode,
|
||||
context: item.context,
|
||||
presentationData: item.presentationData,
|
||||
edited: edited,
|
||||
isDeleted: item.topMessage.ghostgramIsDeleted,
|
||||
impressionCount: !item.presentationData.isPreview ? viewCount : nil,
|
||||
dateText: dateText,
|
||||
type: statusType,
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
sgdeps = [
|
||||
"//Swiftgram/SGSimpleSettings:SGSimpleSettings"
|
||||
]
|
||||
|
||||
|
||||
swift_library(
|
||||
name = "ChatMessageInteractiveFileNode",
|
||||
module_name = "ChatMessageInteractiveFileNode",
|
||||
@@ -9,7 +14,7 @@ swift_library(
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
deps = sgdeps + [
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Postbox",
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
|
||||
+68
-10
@@ -1,3 +1,4 @@
|
||||
import SGSimpleSettings
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
@@ -349,16 +350,53 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
private func transcribe() {
|
||||
guard let _ = self.arguments, let context = self.context, let message = self.message else {
|
||||
guard let arguments = self.arguments, let context = self.context, let message = self.message else {
|
||||
return
|
||||
}
|
||||
|
||||
if !context.isPremium, case .inProgress = self.audioTranscriptionState {
|
||||
if /*!context.isPremium,*/ case .inProgress = self.audioTranscriptionState {
|
||||
return
|
||||
}
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
// GHOSTGRAM: Premium check removed - local transcription is free!
|
||||
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: arguments.context.currentAppConfiguration.with { $0 })
|
||||
|
||||
let transcriptionText = self.forcedAudioTranscriptionText ?? transcribedText(message: message)
|
||||
// MARK: Swiftgram
|
||||
if transcriptionText == nil && false {
|
||||
if premiumConfiguration.audioTransciptionTrialCount > 0 {
|
||||
if !arguments.associatedData.isPremium {
|
||||
if self.presentAudioTranscriptionTooltip(finished: false) {
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
guard arguments.associatedData.isPremium else {
|
||||
if self.hapticFeedback == nil {
|
||||
self.hapticFeedback = HapticFeedback()
|
||||
}
|
||||
self.hapticFeedback?.impact(.medium)
|
||||
|
||||
let tipController = UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_voiceToText", scale: 0.065, colors: [:], title: nil, text: presentationData.strings.Message_AudioTranscription_SubscribeToPremium, customUndoText: presentationData.strings.Message_AudioTranscription_SubscribeToPremiumAction, timeout: nil), elevatedLayout: false, position: .top, animateInAsReplacement: false, action: { action in
|
||||
if case .undo = action {
|
||||
var replaceImpl: ((ViewController) -> Void)?
|
||||
let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .voiceToText, forceDark: false, action: {
|
||||
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .settings, forceDark: false, dismissed: nil)
|
||||
replaceImpl?(controller)
|
||||
}, dismissed: nil)
|
||||
replaceImpl = { [weak controller] c in
|
||||
controller?.replace(with: c)
|
||||
}
|
||||
arguments.controllerInteraction.navigationController()?.pushViewController(controller, animated: true)
|
||||
|
||||
let _ = ApplicationSpecificNotice.incrementAudioTranscriptionSuggestion(accountManager: context.sharedContext.accountManager).startStandalone()
|
||||
}
|
||||
return false })
|
||||
arguments.controllerInteraction.presentControllerInCurrent(tipController, nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var shouldBeginTranscription = false
|
||||
var shouldExpandNow = false
|
||||
@@ -384,8 +422,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
self.audioTranscriptionState = .inProgress
|
||||
self.requestUpdateLayout(true)
|
||||
|
||||
// GHOSTGRAM: Always use local transcription (free, private, on-device!)
|
||||
if true {
|
||||
if context.sharedContext.immediateExperimentalUISettings.localTranscription || !arguments.associatedData.isPremium || SGSimpleSettings.shared.transcriptionBackend == SGSimpleSettings.TranscriptionBackend.apple.rawValue {
|
||||
let appLocale = presentationData.strings.baseLanguageCode
|
||||
|
||||
let signal: Signal<LocallyTranscribedAudio?, NoError> = context.engine.data.get(TelegramEngine.EngineData.Item.Messages.Message(id: message.id))
|
||||
@@ -417,7 +454,8 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
guard let result = result else {
|
||||
return .single(nil)
|
||||
}
|
||||
return transcribeAudio(path: result, appLocale: appLocale)
|
||||
|
||||
return transcribeAudio(path: result, appLocale: arguments.controllerInteraction.sgGetChatPredictedLang() ?? appLocale)
|
||||
}
|
||||
|
||||
self.transcribeDisposable = (signal
|
||||
@@ -605,6 +643,8 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
var audioWaveform: AudioWaveform?
|
||||
var isVoice = false
|
||||
var audioDuration: Int32 = 0
|
||||
var isConsumed: Bool?
|
||||
|
||||
var consumableContentIcon: UIImage?
|
||||
for attribute in arguments.message.attributes {
|
||||
if let attribute = attribute as? ConsumableContentMessageAttribute {
|
||||
@@ -615,6 +655,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
consumableContentIcon = PresentationResourcesChat.chatBubbleConsumableContentOutgoingIcon(arguments.presentationData.theme.theme)
|
||||
}
|
||||
}
|
||||
isConsumed = attribute.consumed
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -733,8 +774,25 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
if Namespaces.Message.allNonRegular.contains(arguments.message.id.namespace) {
|
||||
displayTranscribe = false
|
||||
} else if arguments.message.id.peerId.namespace != Namespaces.Peer.SecretChat && !isViewOnceMessage && !arguments.presentationData.isPreview {
|
||||
// GHOSTGRAM: Always show transcribe button for voice messages
|
||||
displayTranscribe = true
|
||||
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: arguments.context.currentAppConfiguration.with { $0 })
|
||||
// MARK: Swiftgram
|
||||
if arguments.associatedData.isPremium || true {
|
||||
displayTranscribe = true
|
||||
} else if premiumConfiguration.audioTransciptionTrialCount > 0 {
|
||||
if arguments.incoming {
|
||||
if audioDuration < premiumConfiguration.audioTransciptionTrialMaxDuration {
|
||||
displayTranscribe = true
|
||||
}
|
||||
}
|
||||
} else if arguments.associatedData.alwaysDisplayTranscribeButton.canBeDisplayed {
|
||||
if audioDuration >= 60 {
|
||||
displayTranscribe = true
|
||||
} else if arguments.incoming && isConsumed == false && arguments.associatedData.alwaysDisplayTranscribeButton.displayForNotConsumed {
|
||||
displayTranscribe = true
|
||||
}
|
||||
} else if arguments.associatedData.alwaysDisplayTranscribeButton.providedByGroupBoost {
|
||||
displayTranscribe = true
|
||||
}
|
||||
}
|
||||
|
||||
let transcribedText = forcedAudioTranscriptionText ?? transcribedText(message: arguments.message)
|
||||
@@ -749,7 +807,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
let currentTime = Int32(Date().timeIntervalSince1970)
|
||||
if transcribedText == nil, let cooldownUntilTime = arguments.associatedData.audioTranscriptionTrial.cooldownUntilTime, cooldownUntilTime > currentTime {
|
||||
if transcribedText == nil, let cooldownUntilTime = arguments.associatedData.audioTranscriptionTrial.cooldownUntilTime, cooldownUntilTime > currentTime, { return false }() /* MARK: Swiftgram */ {
|
||||
updatedAudioTranscriptionState = .locked
|
||||
}
|
||||
|
||||
@@ -899,6 +957,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
context: arguments.context,
|
||||
presentationData: arguments.presentationData,
|
||||
edited: edited && !arguments.presentationData.isPreview,
|
||||
isDeleted: arguments.topMessage.ghostgramIsDeleted,
|
||||
impressionCount: !arguments.presentationData.isPreview ? viewCount : nil,
|
||||
dateText: dateText,
|
||||
type: statusType,
|
||||
@@ -2173,4 +2232,3 @@ public final class FileMessageSelectionNode: ASDisplayNode {
|
||||
self.checkNode.frame = CGRect(origin: checkOrigin, size: checkSize)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -577,6 +577,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
||||
context: item.context,
|
||||
presentationData: item.presentationData,
|
||||
edited: edited && !sentViaBot && !item.presentationData.isPreview,
|
||||
isDeleted: item.topMessage.ghostgramIsDeleted,
|
||||
impressionCount: !item.presentationData.isPreview ? viewCount : nil,
|
||||
dateText: dateText,
|
||||
type: statusType,
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
sgdeps = [
|
||||
"//Swiftgram/SGSimpleSettings:SGSimpleSettings"
|
||||
]
|
||||
|
||||
swift_library(
|
||||
name = "ChatMessageInteractiveMediaNode",
|
||||
module_name = "ChatMessageInteractiveMediaNode",
|
||||
@@ -9,7 +13,7 @@ swift_library(
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
deps = sgdeps + [
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Postbox",
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
|
||||
+25
@@ -1,3 +1,4 @@
|
||||
import SGSimpleSettings
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
@@ -947,6 +948,8 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
||||
var isSticker = false
|
||||
var maxDimensions = layoutConstants.image.maxDimensions
|
||||
var maxHeight = layoutConstants.image.maxDimensions.height
|
||||
// MARK: Swiftgram
|
||||
var imageOriginalMaxDimensions: CGSize?
|
||||
var isStory = false
|
||||
var isGift = false
|
||||
|
||||
@@ -969,6 +972,19 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
||||
}
|
||||
} else if let image = media as? TelegramMediaImage, let dimensions = largestImageRepresentation(image.representations)?.dimensions {
|
||||
unboundSize = CGSize(width: max(10.0, floor(dimensions.cgSize.width * 0.5)), height: max(10.0, floor(dimensions.cgSize.height * 0.5)))
|
||||
// MARK: Swiftgram
|
||||
if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = channel.info, SGSimpleSettings.shared.wideChannelPosts {
|
||||
imageOriginalMaxDimensions = maxDimensions
|
||||
switch sizeCalculation {
|
||||
case let .constrained(constrainedSize):
|
||||
maxDimensions.width = constrainedSize.width
|
||||
case .unconstrained:
|
||||
maxDimensions.width = unboundSize.width
|
||||
}
|
||||
if message.text.isEmpty {
|
||||
maxDimensions.width = max(layoutConstants.image.maxDimensions.width, unboundSize.aspectFitted(CGSize(width: maxDimensions.width, height: layoutConstants.image.minDimensions.height)).width)
|
||||
}
|
||||
}
|
||||
} else if let file = media as? TelegramMediaFile, var dimensions = file.dimensions {
|
||||
if let thumbnail = file.previewRepresentations.first {
|
||||
let dimensionsVertical = dimensions.width < dimensions.height
|
||||
@@ -1112,6 +1128,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
||||
context: context,
|
||||
presentationData: presentationData,
|
||||
edited: dateAndStatus.edited && !presentationData.isPreview,
|
||||
isDeleted: message.ghostgramIsDeleted,
|
||||
impressionCount: !presentationData.isPreview ? dateAndStatus.viewCount : nil,
|
||||
dateText: dateAndStatus.dateText,
|
||||
type: dateAndStatus.type,
|
||||
@@ -1196,6 +1213,9 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
||||
}
|
||||
|
||||
boundingSize = CGSize(width: boundingWidth, height: filledSize.height).cropped(CGSize(width: CGFloat.greatestFiniteMagnitude, height: maxHeight))
|
||||
if let imageOriginalMaxDimensions = imageOriginalMaxDimensions {
|
||||
boundingSize.height = min(boundingSize.height, nativeSize.aspectFitted(imageOriginalMaxDimensions).height)
|
||||
}
|
||||
boundingSize.height = max(boundingSize.height, layoutConstants.image.minDimensions.height)
|
||||
boundingSize.width = max(boundingSize.width, layoutConstants.image.minDimensions.width)
|
||||
switch contentMode {
|
||||
@@ -2992,6 +3012,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
||||
icon = .eye
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if displaySpoiler, let context = self.context {
|
||||
let extendedMediaOverlayNode: ExtendedMediaOverlayNode
|
||||
@@ -3272,6 +3293,10 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
||||
}
|
||||
|
||||
public func scrubberTransition() -> GalleryItemScrubberTransition? {
|
||||
if "".isEmpty {
|
||||
return nil
|
||||
}
|
||||
|
||||
final class TimestampContainerTransitionView: UIView {
|
||||
let containerView: UIView
|
||||
let containerMaskView: UIImageView
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
sgdeps = [
|
||||
"//Swiftgram/SGSimpleSettings:SGSimpleSettings",
|
||||
"//submodules/TranslateUI:TranslateUI"
|
||||
]
|
||||
|
||||
swift_library(
|
||||
name = "ChatMessageItemImpl",
|
||||
module_name = "ChatMessageItemImpl",
|
||||
@@ -9,7 +14,7 @@ swift_library(
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
deps = sgdeps + [
|
||||
"//submodules/Postbox",
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Display",
|
||||
|
||||
+17
-4
@@ -1,3 +1,5 @@
|
||||
import SGSimpleSettings
|
||||
import TranslateUI
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Postbox
|
||||
@@ -327,14 +329,14 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible
|
||||
}
|
||||
displayAuthorInfo = incoming && peerId.isGroupOrChannel && effectiveAuthor != nil
|
||||
|
||||
if let channel = content.firstMessage.peers[content.firstMessage.id.peerId] as? TelegramChannel, channel.isForumOrMonoForum {
|
||||
if let chatPeer = content.firstMessage.peers[content.firstMessage.id.peerId], chatPeer.isForumOrMonoForum {
|
||||
if case .replyThread = chatLocation {
|
||||
if channel.isMonoForum && chatLocation.threadId != context.account.peerId.toInt64() {
|
||||
if chatPeer.isMonoForum && chatLocation.threadId != context.account.peerId.toInt64() {
|
||||
displayAuthorInfo = false
|
||||
}
|
||||
} else {
|
||||
if channel.isMonoForum {
|
||||
if let linkedMonoforumId = channel.linkedMonoforumId, let mainChannel = content.firstMessage.peers[linkedMonoforumId] as? TelegramChannel, mainChannel.hasPermission(.manageDirect) {
|
||||
if chatPeer.isMonoForum {
|
||||
if let chatPeer = chatPeer as? TelegramChannel, let linkedMonoforumId = chatPeer.linkedMonoforumId, let mainChannel = content.firstMessage.peers[linkedMonoforumId] as? TelegramChannel, mainChannel.hasPermission(.manageDirect) {
|
||||
headerSeparableThreadId = content.firstMessage.threadId
|
||||
|
||||
if let threadId = content.firstMessage.threadId, let peer = content.firstMessage.peers[EnginePeer.Id(threadId)] {
|
||||
@@ -533,6 +535,17 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible
|
||||
|
||||
let configure = {
|
||||
let node = (viewClassName as! ChatMessageItemView.Type).init(rotated: self.controllerInteraction.chatIsRotated)
|
||||
if let node = node as? ChatMessageStickerItemNode {
|
||||
node.sizeCoefficient = Float(SGSimpleSettings.shared.stickerSize) / 100.0
|
||||
if !SGSimpleSettings.shared.stickerTimestamp {
|
||||
node.dateAndStatusNode.isHidden = true
|
||||
}
|
||||
} else if let node = node as? ChatMessageAnimatedStickerItemNode {
|
||||
node.sizeCoefficient = Float(SGSimpleSettings.shared.stickerSize) / 100.0
|
||||
if !SGSimpleSettings.shared.stickerTimestamp {
|
||||
node.dateAndStatusNode.isHidden = true
|
||||
}
|
||||
}
|
||||
node.setupItem(self, synchronousLoad: synchronousLoads)
|
||||
|
||||
let nodeLayout = node.asyncLayout()
|
||||
|
||||
+17
@@ -1,3 +1,4 @@
|
||||
import SGSimpleSettings
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
@@ -663,6 +664,9 @@ open class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol {
|
||||
public var playedEffectAnimation: Bool = false
|
||||
public var effectAnimationNodes: [ChatMessageTransitionNode.DecorationItemNode] = []
|
||||
|
||||
private var wasFilteredKeywordTested: Bool = false
|
||||
private var matchedFilterKeyword: String? = nil
|
||||
|
||||
public required init(rotated: Bool) {
|
||||
super.init(layerBacked: false, rotated: rotated)
|
||||
if rotated {
|
||||
@@ -683,10 +687,23 @@ open class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol {
|
||||
|
||||
self.item = nil
|
||||
self.frame = CGRect()
|
||||
self.wasFilteredKeywordTested = false
|
||||
self.matchedFilterKeyword = nil
|
||||
}
|
||||
|
||||
open func setupItem(_ item: ChatMessageItem, synchronousLoad: Bool) {
|
||||
self.item = item
|
||||
|
||||
if !self.wasFilteredKeywordTested && !SGSimpleSettings.shared.messageFilterKeywords.isEmpty && SGSimpleSettings.shared.ephemeralStatus > 1 {
|
||||
let incomingMessage = item.message.effectivelyIncoming(item.context.account.peerId)
|
||||
if incomingMessage {
|
||||
if let matchedKeyword = SGSimpleSettings.shared.messageFilterKeywords.first(where: { item.message.text.contains($0) }) {
|
||||
self.matchedFilterKeyword = matchedKeyword
|
||||
self.alpha = item.presentationData.theme.theme.overallDarkAppearance ? 0.2 : 0.3
|
||||
}
|
||||
}
|
||||
}
|
||||
self.wasFilteredKeywordTested = true
|
||||
}
|
||||
|
||||
open func updateAccessibilityData(_ accessibilityData: ChatMessageAccessibilityData) {
|
||||
|
||||
+1
@@ -274,6 +274,7 @@ public class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
context: item.context,
|
||||
presentationData: item.presentationData,
|
||||
edited: edited,
|
||||
isDeleted: item.topMessage.ghostgramIsDeleted,
|
||||
impressionCount: !item.presentationData.isPreview ? viewCount : nil,
|
||||
dateText: dateText,
|
||||
type: statusType,
|
||||
|
||||
+1
@@ -1117,6 +1117,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
context: item.context,
|
||||
presentationData: item.presentationData,
|
||||
edited: edited,
|
||||
isDeleted: item.topMessage.ghostgramIsDeleted,
|
||||
impressionCount: viewCount,
|
||||
dateText: dateText,
|
||||
type: statusType,
|
||||
|
||||
+1
@@ -130,6 +130,7 @@ public class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNod
|
||||
context: item.context,
|
||||
presentationData: item.presentationData,
|
||||
edited: edited,
|
||||
isDeleted: message.ghostgramIsDeleted,
|
||||
impressionCount: viewCount,
|
||||
dateText: dateText,
|
||||
type: statusType,
|
||||
|
||||
+1
-1
@@ -321,7 +321,7 @@ public final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode {
|
||||
if let actions = self.actions, actions.isCopyProtected {
|
||||
self.interfaceInteraction?.displayCopyProtectionTip(self.forwardButton, false)
|
||||
} else if !self.forwardButton.isImplicitlyDisabled {
|
||||
self.interfaceInteraction?.forwardSelectedMessages()
|
||||
self.interfaceInteraction?.forwardSelectedMessages(nil)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+6
-4
@@ -38,6 +38,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView {
|
||||
public let contextSourceNode: ContextExtractedContentContainingNode
|
||||
private let containerNode: ContextControllerSourceNode
|
||||
public let imageNode: TransformImageNode
|
||||
public var sizeCoefficient: Float = 1.0
|
||||
private var backgroundNode: WallpaperBubbleBackgroundNode?
|
||||
private var placeholderNode: StickerShimmerEffectNode
|
||||
public var textNode: TextNode?
|
||||
@@ -55,7 +56,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView {
|
||||
private var suggestedPostInfoNode: ChatMessageSuggestedPostInfoNode?
|
||||
|
||||
private var viaBotNode: TextNode?
|
||||
private let dateAndStatusNode: ChatMessageDateAndStatusNode
|
||||
public let dateAndStatusNode: ChatMessageDateAndStatusNode
|
||||
private var threadInfoNode: ChatMessageThreadInfoNode?
|
||||
private var replyInfoNode: ChatMessageReplyInfoNode?
|
||||
private var replyBackgroundContent: WallpaperBubbleBackgroundNode?
|
||||
@@ -654,6 +655,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView {
|
||||
context: item.context,
|
||||
presentationData: item.presentationData,
|
||||
edited: edited,
|
||||
isDeleted: item.message.ghostgramIsDeleted,
|
||||
impressionCount: viewCount,
|
||||
dateText: dateText,
|
||||
type: statusType,
|
||||
@@ -897,11 +899,11 @@ public class ChatMessageStickerItemNode: ChatMessageItemView {
|
||||
ReplyMarkupMessageAttribute(
|
||||
rows: [
|
||||
ReplyMarkupRow(buttons: [
|
||||
ReplyMarkupButton(title: item.presentationData.strings.Chat_PostApproval_Message_ActionReject, titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonDecline)),
|
||||
ReplyMarkupButton(title: item.presentationData.strings.Chat_PostApproval_Message_ActionApprove, titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonApprove))
|
||||
ReplyMarkupButton(title: item.presentationData.strings.Chat_PostApproval_Message_ActionReject, titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonDecline), style: nil),
|
||||
ReplyMarkupButton(title: item.presentationData.strings.Chat_PostApproval_Message_ActionApprove, titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonApprove), style: nil)
|
||||
]),
|
||||
ReplyMarkupRow(buttons: [
|
||||
ReplyMarkupButton(title: item.presentationData.strings.Chat_PostApproval_Message_ActionSuggestChanges, titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonSuggestChanges))
|
||||
ReplyMarkupButton(title: item.presentationData.strings.Chat_PostApproval_Message_ActionSuggestChanges, titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonSuggestChanges), style: nil)
|
||||
])
|
||||
],
|
||||
flags: [],
|
||||
|
||||
+2
-2
@@ -725,6 +725,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
context: item.context,
|
||||
presentationData: item.presentationData,
|
||||
edited: edited && !item.presentationData.isPreview,
|
||||
isDeleted: item.topMessage.ghostgramIsDeleted,
|
||||
impressionCount: !item.presentationData.isPreview ? viewCount : nil,
|
||||
dateText: dateText,
|
||||
type: statusType,
|
||||
@@ -742,7 +743,6 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
starsCount: starsCount,
|
||||
isPinned: item.message.tags.contains(.pinned) && (!item.associatedData.isInPinnedListMode || isReplyThread),
|
||||
hasAutoremove: item.message.isSelfExpiring,
|
||||
isDeleted: AntiDeleteManager.shared.isMessageDeleted(peerId: item.message.id.peerId.toInt64(), messageId: item.message.id.id) || AntiDeleteManager.shared.isMessageDeleted(text: item.message.text),
|
||||
canViewReactionList: canViewMessageReactionList(message: item.topMessage),
|
||||
animationCache: item.controllerInteraction.presentationContext.animationCache,
|
||||
animationRenderer: item.controllerInteraction.presentationContext.animationRenderer
|
||||
@@ -1256,7 +1256,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
return super.hitTest(point, with: event)
|
||||
}
|
||||
|
||||
private func updateIsTranslating(_ isTranslating: Bool) {
|
||||
public func updateIsTranslating(_ isTranslating: Bool) {
|
||||
guard let item = self.item else {
|
||||
return
|
||||
}
|
||||
|
||||
+1
@@ -1173,6 +1173,7 @@ public class ChatMessageTodoBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
context: item.context,
|
||||
presentationData: item.presentationData,
|
||||
edited: edited,
|
||||
isDeleted: item.topMessage.ghostgramIsDeleted,
|
||||
impressionCount: viewCount,
|
||||
dateText: dateText,
|
||||
type: statusType,
|
||||
|
||||
+2
-2
@@ -65,9 +65,9 @@ public final class ChatRecentActionsController: TelegramBaseController {
|
||||
}, blockMessageAuthor: { _, _ in
|
||||
}, deleteMessages: { _, _, f in
|
||||
f(.default)
|
||||
}, forwardSelectedMessages: {
|
||||
}, forwardSelectedMessages: { _ in
|
||||
}, forwardCurrentForwardMessages: {
|
||||
}, forwardMessages: { _ in
|
||||
}, forwardMessages: { _, _ in
|
||||
}, updateForwardOptionsState: { _ in
|
||||
}, presentForwardOptions: { _ in
|
||||
}, presentReplyOptions: { _ in
|
||||
|
||||
+12
@@ -1465,6 +1465,18 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
||||
break
|
||||
case .sendGift:
|
||||
break
|
||||
case .unknownDeepLink:
|
||||
break
|
||||
case .oauth:
|
||||
break
|
||||
case .chats:
|
||||
break
|
||||
case .compose:
|
||||
break
|
||||
case .postStory:
|
||||
break
|
||||
case .contacts:
|
||||
break
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
+10
-2
@@ -19,6 +19,14 @@ import GlassBackgroundComponent
|
||||
import ComponentDisplayAdapters
|
||||
import StarsParticleEffect
|
||||
|
||||
private func neutralActionButtonGlassTint(theme: PresentationTheme) -> GlassBackgroundView.TintColor {
|
||||
if theme.overallDarkAppearance {
|
||||
return .init(kind: .custom, color: UIColor(white: 0.0, alpha: 0.38))
|
||||
} else {
|
||||
return .init(kind: .custom, color: UIColor(white: 1.0, alpha: 0.68))
|
||||
}
|
||||
}
|
||||
|
||||
private final class EffectBadgeView: UIView {
|
||||
private let context: AccountContext
|
||||
private var currentEffectId: Int64?
|
||||
@@ -345,7 +353,7 @@ public final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessag
|
||||
}
|
||||
|
||||
transition.updateFrame(view: self.micButtonBackgroundView, frame: CGRect(origin: CGPoint(), size: size))
|
||||
self.micButtonBackgroundView.update(size: size, cornerRadius: size.height * 0.5, isDark: interfaceState.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: interfaceState.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7)), isInteractive: true, transition: ComponentTransition(transition))
|
||||
self.micButtonBackgroundView.update(size: size, cornerRadius: size.height * 0.5, isDark: interfaceState.theme.overallDarkAppearance, tintColor: neutralActionButtonGlassTint(theme: interfaceState.theme), isInteractive: true, transition: ComponentTransition(transition))
|
||||
|
||||
transition.updateFrame(layer: self.micButton.layer, frame: CGRect(origin: CGPoint(), size: size))
|
||||
self.micButton.layoutItems()
|
||||
@@ -401,7 +409,7 @@ public final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessag
|
||||
|
||||
transition.updateFrame(view: self.expandMediaInputButton, frame: CGRect(origin: CGPoint(), size: size))
|
||||
transition.updateFrame(view: self.expandMediaInputButtonBackgroundView, frame: CGRect(origin: CGPoint(), size: size))
|
||||
self.expandMediaInputButtonBackgroundView.update(size: size, cornerRadius: size.height * 0.5, isDark: interfaceState.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: interfaceState.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7)), isInteractive: true, transition: ComponentTransition(transition))
|
||||
self.expandMediaInputButtonBackgroundView.update(size: size, cornerRadius: size.height * 0.5, isDark: interfaceState.theme.overallDarkAppearance, tintColor: neutralActionButtonGlassTint(theme: interfaceState.theme), isInteractive: true, transition: ComponentTransition(transition))
|
||||
if let image = self.expandMediaInputButtonIcon.image {
|
||||
let expandIconFrame = CGRect(origin: CGPoint(x: floor((size.width - image.size.width) * 0.5), y: floor((size.height - image.size.height) * 0.5)), size: image.size)
|
||||
self.expandMediaInputButtonIcon.center = expandIconFrame.center
|
||||
|
||||
+4
-2
@@ -418,11 +418,11 @@ public final class ChatTextInputPanelComponent: Component {
|
||||
deleteMessages: { _, _, f in
|
||||
f(.default)
|
||||
},
|
||||
forwardSelectedMessages: {
|
||||
forwardSelectedMessages: { _ in
|
||||
},
|
||||
forwardCurrentForwardMessages: {
|
||||
},
|
||||
forwardMessages: { _ in
|
||||
forwardMessages: { _, _ in
|
||||
},
|
||||
updateForwardOptionsState: { _ in
|
||||
},
|
||||
@@ -472,6 +472,7 @@ public final class ChatTextInputPanelComponent: Component {
|
||||
var presentationInterfaceState = ChatPresentationInterfaceState(
|
||||
chatWallpaper: .color(0),
|
||||
theme: component.theme,
|
||||
preferredGlassType: .default,
|
||||
strings: component.strings,
|
||||
dateTimeFormat: PresentationDateTimeFormat(),
|
||||
nameDisplayOrder: .firstLast,
|
||||
@@ -773,6 +774,7 @@ public final class ChatTextInputPanelComponent: Component {
|
||||
var presentationInterfaceState = ChatPresentationInterfaceState(
|
||||
chatWallpaper: .color(0),
|
||||
theme: component.theme,
|
||||
preferredGlassType: .default,
|
||||
strings: component.strings,
|
||||
dateTimeFormat: PresentationDateTimeFormat(),
|
||||
nameDisplayOrder: .firstLast,
|
||||
|
||||
+209
-14
@@ -1,3 +1,9 @@
|
||||
// MARK: Swiftgram
|
||||
import TelegramUIPreferences
|
||||
import SGSimpleSettings
|
||||
import SwiftUI
|
||||
import SGInputToolbar
|
||||
|
||||
import Foundation
|
||||
import UniformTypeIdentifiers
|
||||
import UIKit
|
||||
@@ -65,6 +71,14 @@ public let chatTextInputMinFontSize: CGFloat = 5.0
|
||||
|
||||
private let minInputFontSize = chatTextInputMinFontSize
|
||||
|
||||
private func neutralChatInputGlassTint(theme: PresentationTheme, innerColor: UIColor? = nil) -> GlassBackgroundView.TintColor {
|
||||
if theme.overallDarkAppearance {
|
||||
return .init(kind: .custom, color: UIColor(white: 0.0, alpha: 0.38), innerColor: innerColor)
|
||||
} else {
|
||||
return .init(kind: .custom, color: UIColor(white: 1.0, alpha: 0.68), innerColor: innerColor)
|
||||
}
|
||||
}
|
||||
|
||||
private func calclulateTextFieldMinHeight(_ presentationInterfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat {
|
||||
var baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize)
|
||||
if "".isEmpty {
|
||||
@@ -333,6 +347,12 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
|
||||
private let hapticFeedback = HapticFeedback()
|
||||
|
||||
// MARK: Swiftgram
|
||||
private var sendWithReturnKey: Bool
|
||||
private var sendWithReturnKeyDisposable: Disposable?
|
||||
// private var toolbarHostingController: UIViewController? //Any? // UIHostingController<ChatToolbarView>?
|
||||
private var toolbarNode: ASDisplayNode?
|
||||
|
||||
public var inputTextState: ChatTextInputState {
|
||||
if let textInputNode = self.textInputNode {
|
||||
let selectionRange: Range<Int> = textInputNode.selectedRange.location ..< (textInputNode.selectedRange.location + textInputNode.selectedRange.length)
|
||||
@@ -619,6 +639,11 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
|
||||
self.textInputViewInternalInsets = UIEdgeInsets(top: 5.0, left: 12.0, bottom: 4.0, right: 11.0)
|
||||
|
||||
|
||||
// MARK: Swiftgram
|
||||
self.sendWithReturnKey = SGSimpleSettings.shared.sendWithReturnKey // MARK: Swiftgram
|
||||
//
|
||||
|
||||
var hasSpoilers = true
|
||||
var hasQuotes = true
|
||||
if presentationInterfaceState.chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat {
|
||||
@@ -755,7 +780,11 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
if let data = context.currentAppConfiguration.with({ $0 }).data, data["ios_killswitch_input_bounce"] != nil {
|
||||
self.enableBounceAnimations = false
|
||||
}*/
|
||||
|
||||
|
||||
// MARK: Swiftgram
|
||||
self.initToolbarIfNeeded(context: context)
|
||||
//
|
||||
|
||||
self.sendAsAvatarContainerNode.activated = { [weak self] gesture, _ in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
@@ -818,6 +847,10 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
}
|
||||
}
|
||||
self.attachmentButtonDisabledNode.addTarget(self, action: #selector(self.attachmentButtonPressed), forControlEvents: .touchUpInside)
|
||||
// MARK: Swiftgram
|
||||
let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(self.attachmentButtonLongPressed(_:)))
|
||||
longPressGesture.minimumPressDuration = 1.0
|
||||
self.attachmentButton.addGestureRecognizer(longPressGesture)
|
||||
|
||||
self.sendActionButtons.sendButtonLongPressed = { [weak self] node, gesture in
|
||||
self?.interfaceInteraction?.displaySendMessageOptions(node, gesture)
|
||||
@@ -1013,6 +1046,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
|
||||
deinit {
|
||||
self.statusDisposable.dispose()
|
||||
self.sendWithReturnKeyDisposable?.dispose()
|
||||
self.tooltipController?.dismiss()
|
||||
self.currentEmojiSuggestion?.disposable.dispose()
|
||||
}
|
||||
@@ -1060,6 +1094,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
self.textInputNodeClippingContainer.addSubnode(textInputNode)
|
||||
textInputNode.view.disablesInteractiveTransitionGestureRecognizer = true
|
||||
textInputNode.isUserInteractionEnabled = !self.sendingTextDisabled
|
||||
textInputNode.textView.returnKeyType = self.sendWithReturnKey ? .send : .default
|
||||
self.textInputNode = textInputNode
|
||||
|
||||
if let textInputBackgroundTapRecognizer = self.textInputBackgroundTapRecognizer {
|
||||
@@ -1421,6 +1456,16 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
let previousAdditionalSideInsets = self.validLayout?.4
|
||||
self.validLayout = (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, maxOverlayHeight, metrics, isSecondary, isMediaInputExpanded)
|
||||
|
||||
let defaultGlassTintColor: GlassBackgroundView.TintColor
|
||||
let defaultGlassTintWithInnerColor: GlassBackgroundView.TintColor
|
||||
if case .clear = interfaceState.preferredGlassType {
|
||||
defaultGlassTintColor = .init(kind: .custom, color: UIColor.clear)
|
||||
defaultGlassTintWithInnerColor = .init(kind: .custom, color: UIColor.clear, innerColor: interfaceState.theme.list.itemCheckColors.fillColor)
|
||||
} else {
|
||||
defaultGlassTintColor = neutralChatInputGlassTint(theme: interfaceState.theme)
|
||||
defaultGlassTintWithInnerColor = neutralChatInputGlassTint(theme: interfaceState.theme, innerColor: interfaceState.theme.list.itemCheckColors.fillColor)
|
||||
}
|
||||
|
||||
var leftInset = leftInset
|
||||
var rightInset = rightInset
|
||||
|
||||
@@ -2205,6 +2250,9 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
if buttonTitleUpdated && !transition.isAnimated {
|
||||
transition = .animated(duration: 0.3, curve: .easeInOut)
|
||||
}
|
||||
// MARK: Swiftgram
|
||||
let originalLeftInset = leftInset
|
||||
//
|
||||
|
||||
let textInputBackgroundWidthOffset: CGFloat = 0.0
|
||||
var attachmentButtonX: CGFloat = hideOffset.x + leftInset + leftMenuInset + 8.0
|
||||
@@ -2365,7 +2413,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
let menuButtonFrame = CGRect(x: leftInset + 8.0, y: menuButtonOriginY, width: menuButtonExpanded ? menuButtonWidth : menuCollapsedButtonWidth, height: menuButtonHeight)
|
||||
transition.updateFrameAsPositionAndBounds(node: self.menuButton, frame: menuButtonFrame)
|
||||
transition.updateFrame(view: self.menuButtonBackgroundView, frame: CGRect(origin: CGPoint(), size: menuButtonFrame.size))
|
||||
self.menuButtonBackgroundView.update(size: menuButtonFrame.size, cornerRadius: menuButtonFrame.height * 0.5, isDark: interfaceState.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: interfaceState.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7), innerColor: interfaceState.theme.chat.inputPanel.actionControlFillColor), transition: ComponentTransition(transition))
|
||||
self.menuButtonBackgroundView.update(size: menuButtonFrame.size, cornerRadius: menuButtonFrame.height * 0.5, isDark: interfaceState.theme.overallDarkAppearance, tintColor: defaultGlassTintWithInnerColor, transition: ComponentTransition(transition))
|
||||
transition.updateFrame(node: self.menuButtonClippingNode, frame: CGRect(origin: CGPoint(x: 19.0, y: 0.0), size: CGSize(width: menuButtonWidth - 19.0, height: menuButtonFrame.height)))
|
||||
var menuButtonTitleTransition = transition
|
||||
if buttonTitleUpdated {
|
||||
@@ -2385,7 +2433,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
if additionalSideInsets.right > 0.0 {
|
||||
textFieldInsets.right += additionalSideInsets.right / 3.0
|
||||
}
|
||||
if inputHasText || self.extendedSearchLayout || hasMediaDraft || hasForward {
|
||||
if SGSimpleSettings.shared.hideRecordingButton || inputHasText || self.extendedSearchLayout || hasMediaDraft || hasForward {
|
||||
} else {
|
||||
if let customRightAction = self.customRightAction, case .empty = customRightAction {
|
||||
textFieldInsets.right = 8.0
|
||||
@@ -2513,7 +2561,9 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
case let .audio(recorder, isLocked):
|
||||
let hadAudioRecorder = self.mediaActionButtons.micButton.audioRecorder != nil
|
||||
if !hadAudioRecorder, isLocked {
|
||||
self.mediaActionButtons.micButton.lock()
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.mediaActionButtons.micButton.lock()
|
||||
}
|
||||
}
|
||||
self.mediaActionButtons.micButton.audioRecorder = recorder
|
||||
audioRecordingTimeNode.audioRecorder = recorder
|
||||
@@ -2887,7 +2937,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
self.updateCounterTextNode(backgroundSize: textInputContainerBackgroundFrame.size, transition: transition)
|
||||
|
||||
let textInputContainerBackgroundTransition = ComponentTransition(transition)
|
||||
self.textInputContainerBackgroundView.update(size: textInputContainerBackgroundFrame.size, cornerRadius: floor(minimalInputHeight * 0.5), isDark: interfaceState.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: interfaceState.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7)), isInteractive: true, transition: textInputContainerBackgroundTransition)
|
||||
self.textInputContainerBackgroundView.update(size: textInputContainerBackgroundFrame.size, cornerRadius: floor(minimalInputHeight * 0.5), isDark: interfaceState.theme.overallDarkAppearance, tintColor: defaultGlassTintColor, isInteractive: true, transition: textInputContainerBackgroundTransition)
|
||||
|
||||
transition.updateFrame(layer: self.textInputBackgroundNode.layer, frame: textInputContainerBackgroundFrame)
|
||||
transition.updateAlpha(node: self.textInputBackgroundNode, alpha: audioRecordingItemsAlpha)
|
||||
@@ -3277,7 +3327,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
|
||||
let attachmentButtonFrame = CGRect(origin: CGPoint(x: attachmentButtonX, y: textInputFrame.maxY - 40.0), size: CGSize(width: 40.0, height: 40.0))
|
||||
attachmentButtonX += 40.0 + 6.0
|
||||
self.attachmentButtonBackground.update(size: attachmentButtonFrame.size, cornerRadius: attachmentButtonFrame.height * 0.5, isDark: interfaceState.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: interfaceState.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7)), isInteractive: true, transition: ComponentTransition(transition))
|
||||
self.attachmentButtonBackground.update(size: attachmentButtonFrame.size, cornerRadius: attachmentButtonFrame.height * 0.5, isDark: interfaceState.theme.overallDarkAppearance, tintColor: defaultGlassTintColor, isInteractive: true, transition: ComponentTransition(transition))
|
||||
|
||||
transition.updateFrame(layer: self.attachmentButtonBackground.layer, frame: attachmentButtonFrame)
|
||||
transition.updateFrame(layer: self.attachmentButton.layer, frame: CGRect(origin: CGPoint(), size: attachmentButtonFrame.size))
|
||||
@@ -3505,7 +3555,11 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
transition.updateFrame(view: self.glassBackgroundContainer, frame: containerFrame)
|
||||
self.glassBackgroundContainer.update(size: containerFrame.size, isDark: interfaceState.theme.overallDarkAppearance, transition: ComponentTransition(transition))
|
||||
|
||||
return contentHeight
|
||||
// MARK: Swiftgram
|
||||
var toolbarOffset: CGFloat = 0.0
|
||||
toolbarOffset = layoutToolbar(transition: transition, panelHeight: contentHeight, width: width, leftInset: originalLeftInset, rightInset: rightInset, displayBotStartButton: displayBotStartButton)
|
||||
|
||||
return contentHeight + toolbarOffset
|
||||
}
|
||||
|
||||
@objc private func slowModeButtonPressed() {
|
||||
@@ -4046,7 +4100,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
}
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let controller = PeekController(presentationData: presentationData, content: content, sourceView: {
|
||||
let controller = makePeekController(presentationData: presentationData, content: content, sourceView: {
|
||||
return (sourceView, sourceRect)
|
||||
})
|
||||
//strongSelf.peekController = controller
|
||||
@@ -4318,8 +4372,8 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
let blurTransitionOut: ComponentTransition = transition.isAnimated ? .easeInOut(duration: 0.18) : .immediate
|
||||
let sendButtonBlurOut: CGFloat = 4.0
|
||||
|
||||
var hideMicButton = false
|
||||
var hideMicButtonBackground = false
|
||||
var hideMicButton = SGSimpleSettings.shared.hideRecordingButton
|
||||
var hideMicButtonBackground = SGSimpleSettings.shared.hideRecordingButton
|
||||
|
||||
if self.customRightAction != nil {
|
||||
self.mediaActionButtons.isHidden = true
|
||||
@@ -4397,7 +4451,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
}
|
||||
}
|
||||
|
||||
if (hasText || keepSendButtonEnabled && !mediaInputIsActive && !hasSlowModeButton) {
|
||||
if (SGSimpleSettings.shared.hideRecordingButton || hasText || keepSendButtonEnabled && !mediaInputIsActive && !hasSlowModeButton) {
|
||||
if self.sendActionButtons.sendContainerNode.alpha.isZero && self.rightSlowModeInset.isZero {
|
||||
alphaTransition.updateAlpha(node: self.sendActionButtons.sendContainerNode, alpha: 1.0)
|
||||
blurTransitionIn.animateBlur(layer: self.sendActionButtons.sendContainerNode.layer, fromRadius: sendButtonBlurOut, toRadius: 0.0)
|
||||
@@ -4522,14 +4576,32 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
}
|
||||
|
||||
public func chatInputTextNodeShouldReturn() -> Bool {
|
||||
return self.chatInputTextNodeShouldReturn(modifierFlags: [])
|
||||
}
|
||||
|
||||
public func chatInputTextNodeShouldReturn(modifierFlags: UIKeyModifierFlags) -> Bool {
|
||||
var shouldSendMessage = false
|
||||
if self.sendActionButtons.sendButton.supernode != nil && !self.sendActionButtons.sendButton.isHidden && !self.sendActionButtons.sendContainerNode.alpha.isZero {
|
||||
self.sendButtonPressed()
|
||||
if let context = self.context, context.sharedContext.currentChatSettings.with({ $0 }).sendWithCmdEnter {
|
||||
if modifierFlags.contains(.command) {
|
||||
shouldSendMessage = true
|
||||
}
|
||||
} else {
|
||||
if modifierFlags.isEmpty {
|
||||
shouldSendMessage = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
if shouldSendMessage {
|
||||
self.sendButtonPressed()
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@objc public func editableTextNodeShouldReturn(_ editableTextNode: ASEditableTextNode) -> Bool {
|
||||
return self.chatInputTextNodeShouldReturn()
|
||||
return self.chatInputTextNodeShouldReturn(modifierFlags: [])
|
||||
}
|
||||
|
||||
private func applyUpdateSendButtonIcon() {
|
||||
@@ -4943,6 +5015,13 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
}
|
||||
|
||||
self.updateActivity()
|
||||
|
||||
// MARK: Swiftgram
|
||||
if self.sendWithReturnKey && text == "\n" {
|
||||
self.sendButtonPressed()
|
||||
return false
|
||||
}
|
||||
|
||||
var cleanText = text
|
||||
let removeSequences: [String] = ["\u{202d}", "\u{202c}"]
|
||||
for sequence in removeSequences {
|
||||
@@ -5169,6 +5248,11 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Swiftgram
|
||||
@objc func attachmentButtonLongPressed(_ gesture: UILongPressGestureRecognizer) {
|
||||
guard gesture.state == .began else { return }
|
||||
}
|
||||
|
||||
@objc func searchLayoutClearButtonPressed() {
|
||||
if let interfaceInteraction = self.interfaceInteraction {
|
||||
interfaceInteraction.updateTextInputStateAndMode { textInputState, inputMode in
|
||||
@@ -5477,3 +5561,114 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
|
||||
return AttachmentInputPanelTransition(inputNode: self, accessoryPanelNode: accessoryPanelNode, menuButtonNode: self.menuButton, menuButtonBackgroundView: self.menuButtonBackgroundView, menuIconNode: self.menuButtonIconNode, menuTextNode: self.menuButtonTextNode, prepareForDismiss: { self.menuButtonIconNode.enqueueState(.app, animated: false) })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: Swiftgram
|
||||
extension ChatTextInputPanelNode {
|
||||
|
||||
func initToolbarIfNeeded(context: AccountContext) {
|
||||
guard #available(iOS 13.0, *) else { return }
|
||||
guard SGSimpleSettings.shared.inputToolbar else { return }
|
||||
guard context.sharedContext.immediateSGStatus.status > 1 else { return }
|
||||
guard self.toolbarNode == nil else { return }
|
||||
let toolbarView = ChatToolbarView(
|
||||
onQuote: { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.interfaceInteraction?.sgSelectLastWordIfIdle()
|
||||
strongSelf.formatAttributesQuote(strongSelf)
|
||||
},
|
||||
onSpoiler: { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.interfaceInteraction?.sgSelectLastWordIfIdle()
|
||||
strongSelf.formatAttributesSpoiler(strongSelf)
|
||||
},
|
||||
onBold: { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.interfaceInteraction?.sgSelectLastWordIfIdle()
|
||||
strongSelf.formatAttributesBold(strongSelf)
|
||||
},
|
||||
onItalic: { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.interfaceInteraction?.sgSelectLastWordIfIdle()
|
||||
strongSelf.formatAttributesItalic(strongSelf)
|
||||
},
|
||||
onMonospace: { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.interfaceInteraction?.sgSelectLastWordIfIdle()
|
||||
strongSelf.formatAttributesMonospace(strongSelf)
|
||||
},
|
||||
onLink: { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.interfaceInteraction?.sgSelectLastWordIfIdle()
|
||||
strongSelf.formatAttributesLink(strongSelf)
|
||||
},
|
||||
onStrikethrough: { [weak self]
|
||||
in guard let strongSelf = self else { return }
|
||||
strongSelf.interfaceInteraction?.sgSelectLastWordIfIdle()
|
||||
strongSelf.formatAttributesStrikethrough(strongSelf)
|
||||
},
|
||||
onUnderline: { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.interfaceInteraction?.sgSelectLastWordIfIdle()
|
||||
strongSelf.formatAttributesUnderline(strongSelf)
|
||||
},
|
||||
onCode: { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.interfaceInteraction?.sgSelectLastWordIfIdle()
|
||||
strongSelf.formatAttributesCodeBlock(strongSelf)
|
||||
},
|
||||
onNewLine: { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.interfaceInteraction?.sgSetNewLine()
|
||||
},
|
||||
// TODO(swiftgram): Binding
|
||||
showNewLine: .constant(true), //.constant(self.sendWithReturnKey)
|
||||
onClearFormatting: { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
|
||||
return (chatTextInputClearFormattingAttributes(current), inputMode)
|
||||
}
|
||||
}
|
||||
)
|
||||
let toolbarHostingController = UIHostingController(rootView: toolbarView)
|
||||
toolbarHostingController.view.backgroundColor = UIColor.clear
|
||||
let toolbarNode = ASDisplayNode { toolbarHostingController.view }
|
||||
self.toolbarNode = toolbarNode
|
||||
// assigning toolbarHostingController bugs responsivness and overrides layout
|
||||
// self.toolbarHostingController = toolbarHostingController
|
||||
|
||||
// Disable "Swipe to go back" gesture when touching scrollview
|
||||
self.view.interactiveTransitionGestureRecognizerTest = { [weak self] point in
|
||||
if let self, let _ = self.toolbarNode?.view.hitTest(point, with: nil) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
self.addSubnode(toolbarNode)
|
||||
}
|
||||
|
||||
func layoutToolbar(transition: ContainedViewLayoutTransition, panelHeight: CGFloat, width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, displayBotStartButton: Bool) -> CGFloat {
|
||||
var toolbarHeight: CGFloat = 0.0
|
||||
var toolbarSpacing: CGFloat = 0.0
|
||||
if let toolbarNode = self.toolbarNode {
|
||||
if displayBotStartButton {
|
||||
toolbarNode.view.alpha = 0.0
|
||||
// transition.updateAlpha(node: toolbarNode, alpha: 0.0)
|
||||
/*} else if !self.isFocused {
|
||||
transition.updateAlpha(node: toolbarNode, alpha: 0.0, completion: { _ in
|
||||
toolbarNode.isHidden = true
|
||||
})*/
|
||||
} else {
|
||||
if !self.isFocused {
|
||||
transition.updateAlpha(node: toolbarNode, alpha: 0.0)
|
||||
} else {
|
||||
toolbarHeight = 44.0
|
||||
toolbarSpacing = 6.0
|
||||
transition.updateFrame(node: toolbarNode, frame: CGRect(origin: CGPoint(x: leftInset, y: panelHeight + toolbarSpacing), size: CGSize(width: width - rightInset - leftInset, height: toolbarHeight)))
|
||||
transition.updateAlpha(node: toolbarNode, alpha: 1.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
return toolbarHeight + toolbarSpacing
|
||||
}
|
||||
}
|
||||
|
||||
+9
-1
@@ -6,6 +6,14 @@ import ComponentFlow
|
||||
import GlassBackgroundComponent
|
||||
import AppBundle
|
||||
|
||||
private func neutralInputButtonGlassTint(theme: PresentationTheme) -> GlassBackgroundView.TintColor {
|
||||
if theme.overallDarkAppearance {
|
||||
return .init(kind: .custom, color: UIColor(white: 0.0, alpha: 0.38))
|
||||
} else {
|
||||
return .init(kind: .custom, color: UIColor(white: 1.0, alpha: 0.68))
|
||||
}
|
||||
}
|
||||
|
||||
final class InputIconButtonComponent: Component {
|
||||
let theme: PresentationTheme
|
||||
let name: String
|
||||
@@ -87,7 +95,7 @@ final class InputIconButtonComponent: Component {
|
||||
}
|
||||
|
||||
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size))
|
||||
self.backgroundView.update(size: size, cornerRadius: size.height * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: component.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7)), isInteractive: true, transition: transition)
|
||||
self.backgroundView.update(size: size, cornerRadius: size.height * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: neutralInputButtonGlassTint(theme: component.theme), isInteractive: true, transition: transition)
|
||||
|
||||
self.button.frame = CGRect(origin: CGPoint(), size: size)
|
||||
|
||||
|
||||
+10
-2
@@ -7,6 +7,14 @@ import GlassBackgroundComponent
|
||||
import AnimatedTextComponent
|
||||
import StarsParticleEffect
|
||||
|
||||
private func neutralStarReactionGlassTint(theme: PresentationTheme) -> GlassBackgroundView.TintColor {
|
||||
if theme.overallDarkAppearance {
|
||||
return .init(kind: .custom, color: UIColor(white: 0.0, alpha: 0.38))
|
||||
} else {
|
||||
return .init(kind: .custom, color: UIColor(white: 1.0, alpha: 0.68))
|
||||
}
|
||||
}
|
||||
|
||||
final class StarReactionButtonBadgeComponent: Component {
|
||||
let theme: PresentationTheme
|
||||
let count: Int
|
||||
@@ -80,7 +88,7 @@ final class StarReactionButtonBadgeComponent: Component {
|
||||
if component.isFilled {
|
||||
backgroundTintColor = .init(kind: .custom, color: UIColor(rgb: 0xFFB10D))
|
||||
} else {
|
||||
backgroundTintColor = .init(kind: .panel, color: component.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7))
|
||||
backgroundTintColor = neutralStarReactionGlassTint(theme: component.theme)
|
||||
}
|
||||
|
||||
self.backgroundView.update(size: backgroundFrame.size, cornerRadius: backgroundFrame.height * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: backgroundTintColor, isInteractive: true, transition: transition)
|
||||
@@ -338,7 +346,7 @@ final class StarReactionButtonComponent: Component {
|
||||
if component.isFilled {
|
||||
backgroundTintColor = .init(kind: .custom, color: UIColor(rgb: 0xFFB10D))
|
||||
} else {
|
||||
backgroundTintColor = .init(kind: .panel, color: component.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7))
|
||||
backgroundTintColor = neutralStarReactionGlassTint(theme: component.theme)
|
||||
}
|
||||
|
||||
self.backgroundView.update(size: backgroundFrame.size, cornerRadius: backgroundFrame.height * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: backgroundTintColor, isInteractive: false, transition: transition)
|
||||
|
||||
+7
@@ -174,6 +174,9 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
|
||||
}
|
||||
|
||||
public let openMessage: (Message, OpenMessageParams) -> Bool
|
||||
// MARK: Swiftgram
|
||||
public let sgStartMessageEdit: (Message) -> Void
|
||||
public let sgGetChatPredictedLang: () -> String?
|
||||
public let openPeer: (EnginePeer, ChatControllerInteractionNavigateToPeer, MessageReference?, OpenPeerSource) -> Void
|
||||
public let openPeerMention: (String, Promise<Bool>?) -> Void
|
||||
public let openMessageContextMenu: (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?, CGPoint?) -> Void
|
||||
@@ -343,6 +346,8 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
|
||||
|
||||
public init(
|
||||
openMessage: @escaping (Message, OpenMessageParams) -> Bool,
|
||||
sgGetChatPredictedLang: @escaping () -> String? = { return nil },
|
||||
sgStartMessageEdit: @escaping (Message) -> Void = { _ in },
|
||||
openPeer: @escaping (EnginePeer, ChatControllerInteractionNavigateToPeer, MessageReference?, OpenPeerSource) -> Void,
|
||||
openPeerMention: @escaping (String, Promise<Bool>?) -> Void,
|
||||
openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?, CGPoint?) -> Void,
|
||||
@@ -465,6 +470,8 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
|
||||
presentationContext: ChatPresentationContext
|
||||
) {
|
||||
self.openMessage = openMessage
|
||||
self.sgGetChatPredictedLang = sgGetChatPredictedLang
|
||||
self.sgStartMessageEdit = sgStartMessageEdit
|
||||
self.openPeer = openPeer
|
||||
self.openPeerMention = openPeerMention
|
||||
self.openMessageContextMenu = openMessageContextMenu
|
||||
|
||||
+3
-1
@@ -1,3 +1,4 @@
|
||||
import SGSimpleSettings
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
@@ -483,7 +484,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
public init(context: AccountContext, currentInputData: InputData, updatedInputData: Signal<InputData, NoError>, defaultToEmojiTab: Bool, opaqueTopPanelBackground: Bool = false, useOpaqueTheme: Bool = false, interaction: ChatEntityKeyboardInputNode.Interaction?, chatPeerId: PeerId?, stateContext: StateContext?, forceHasPremium: Bool = false) {
|
||||
self.context = context
|
||||
self.currentInputData = currentInputData
|
||||
self.defaultToEmojiTab = defaultToEmojiTab
|
||||
self.defaultToEmojiTab = SGSimpleSettings.shared.forceEmojiTab ? true : defaultToEmojiTab
|
||||
self.opaqueTopPanelBackground = opaqueTopPanelBackground
|
||||
self.useOpaqueTheme = useOpaqueTheme
|
||||
self.stateContext = stateContext
|
||||
@@ -2653,6 +2654,7 @@ public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputVi
|
||||
let presentationInterfaceState = ChatPresentationInterfaceState(
|
||||
chatWallpaper: .builtin(WallpaperSettings()),
|
||||
theme: self.presentationData.theme,
|
||||
preferredGlassType: .default,
|
||||
strings: self.presentationData.strings,
|
||||
dateTimeFormat: self.presentationData.dateTimeFormat,
|
||||
nameDisplayOrder: self.presentationData.nameDisplayOrder,
|
||||
|
||||
+27
-5
@@ -1,4 +1,5 @@
|
||||
import Foundation
|
||||
import SGSimpleSettings
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
@@ -334,6 +335,11 @@ private final class ItemNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
func updateLayout(height: CGFloat, transition: ContainedViewLayoutTransition) -> (width: CGFloat, shortWidth: CGFloat) {
|
||||
// MARK: Swiftgram
|
||||
var height = height
|
||||
if SGSimpleSettings.shared.hideTabBar {
|
||||
height = 46.0
|
||||
}
|
||||
let titleSize = self.titleNode.updateLayout(CGSize(width: 160.0, height: .greatestFiniteMagnitude))
|
||||
let _ = self.titleActiveNode.updateLayout(CGSize(width: 160.0, height: .greatestFiniteMagnitude))
|
||||
let titleFrame = CGRect(origin: CGPoint(x: -self.titleNode.insets.left, y: floor((height - titleSize.height) / 2.0)), size: titleSize)
|
||||
@@ -380,6 +386,11 @@ private final class ItemNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
func updateArea(size: CGSize, sideInset: CGFloat, useShortTitle: Bool, transition: ContainedViewLayoutTransition) {
|
||||
// MARK: Swiftgram
|
||||
var size = size
|
||||
if SGSimpleSettings.shared.hideTabBar {
|
||||
size.height = 46.0
|
||||
}
|
||||
transition.updateAlpha(node: self.titleContainer, alpha: useShortTitle ? 0.0 : 1.0)
|
||||
transition.updateAlpha(node: self.shortTitleContainer, alpha: useShortTitle ? 1.0 : 0.0)
|
||||
|
||||
@@ -534,7 +545,11 @@ public final class ChatListFilterTabContainerNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
public init(context: AccountContext) {
|
||||
// MARK: Swiftgram
|
||||
public let inline: Bool
|
||||
private var backgroundNode: NavigationBackgroundNode? = nil
|
||||
|
||||
public init(inline: Bool = false, context: AccountContext) {
|
||||
self.context = context
|
||||
|
||||
self.backgroundContainerView = GlassBackgroundContainerView()
|
||||
@@ -547,6 +562,13 @@ public final class ChatListFilterTabContainerNode: ASDisplayNode {
|
||||
self.selectedBackgroundNode.displaysAsynchronously = false
|
||||
self.selectedBackgroundNode.displayWithoutProcessing = true
|
||||
|
||||
// MARK: Swiftgram
|
||||
self.inline = inline
|
||||
if self.inline {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
self.backgroundNode = NavigationBackgroundNode(color: presentationData.theme.rootController.navigationBar.blurredBackgroundColor)
|
||||
}
|
||||
|
||||
super.init()
|
||||
|
||||
self.view.addSubview(self.backgroundContainerView)
|
||||
@@ -703,7 +725,7 @@ public final class ChatListFilterTabContainerNode: ASDisplayNode {
|
||||
self.backgroundContainerView.update(size: backgroundSize, isDark: presentationData.theme.overallDarkAppearance, transition: ComponentTransition(transition))
|
||||
|
||||
transition.updateFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: backgroundSize))
|
||||
self.backgroundView.update(size: backgroundSize, cornerRadius: backgroundSize.height * 0.5, isDark: presentationData.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: UIColor(white: presentationData.theme.overallDarkAppearance ? 0.0 : 1.0, alpha: 0.6)), isInteractive: true, transition: ComponentTransition(transition))
|
||||
self.backgroundView.update(size: backgroundSize, cornerRadius: backgroundSize.height * 0.5, isDark: presentationData.theme.overallDarkAppearance, tintColor: .init(kind: .panel), isInteractive: true, transition: ComponentTransition(transition))
|
||||
|
||||
var isEditing = isEditing
|
||||
if isReordering {
|
||||
@@ -828,7 +850,7 @@ public final class ChatListFilterTabContainerNode: ASDisplayNode {
|
||||
selectionFraction = 0.0
|
||||
}
|
||||
|
||||
itemNode.updateText(strings: presentationData.strings, title: filter.title(strings: presentationData.strings), shortTitle: i == 0 ? filter.shortTitle(strings: presentationData.strings) : filter.title(strings: presentationData.strings), unreadCount: unreadCount, unreadHasUnmuted: unreadHasUnmuted, isNoFilter: isNoFilter, selectionFraction: selectionFraction, isEditing: isEditing, isReordering: isReordering, canReorderAllChats: canReorderAllChats, isDisabled: isDisabled, presentationData: presentationData, transition: itemNodeTransition)
|
||||
itemNode.updateText(strings: presentationData.strings, title: filter.title(strings: presentationData.strings), shortTitle: filter.shortTitle(strings: presentationData.strings), unreadCount: unreadCount, unreadHasUnmuted: unreadHasUnmuted, isNoFilter: isNoFilter, selectionFraction: selectionFraction, isEditing: isEditing, isReordering: isReordering, canReorderAllChats: canReorderAllChats, isDisabled: isDisabled, presentationData: presentationData, transition: itemNodeTransition)
|
||||
}
|
||||
var removeKeys: [ChatListFilterTabEntryId] = []
|
||||
for (id, _) in self.itemNodes {
|
||||
@@ -875,7 +897,7 @@ public final class ChatListFilterTabContainerNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
let minSpacing: CGFloat = 26.0
|
||||
let minSpacing: CGFloat = 26.0 / (SGSimpleSettings.shared.compactFolderNames ? 2.5 : 1.0)
|
||||
|
||||
let resolvedSideInset: CGFloat = 14.0
|
||||
var leftOffset: CGFloat = resolvedSideInset
|
||||
@@ -898,7 +920,7 @@ public final class ChatListFilterTabContainerNode: ASDisplayNode {
|
||||
itemNodeTransition = .immediate
|
||||
}
|
||||
|
||||
let useShortTitle = itemId == .all && useShortTitles
|
||||
let useShortTitle = itemId == .all && sgUseShortAllChatsTitle(useShortTitles)
|
||||
let paneNodeSize = useShortTitle ? paneNodeShortSize : paneNodeLongSize
|
||||
|
||||
let paneFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((backgroundSize.height - paneNodeSize.height) / 2.0)), size: paneNodeSize)
|
||||
|
||||
+6
@@ -186,6 +186,12 @@ final class ChatListNoticeItemNode: ItemListRevealOptionsItemNode {
|
||||
var alignment: NSTextAlignment = .left
|
||||
|
||||
switch item.notice {
|
||||
// MARK: Swiftgram
|
||||
case let .sgUrl(_, title, text, _, _, _):
|
||||
let titleStringValue = NSMutableAttributedString(attributedString: NSAttributedString(string: title, font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor))
|
||||
titleString = titleStringValue
|
||||
|
||||
textString = NSAttributedString(string: text ?? "", font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor)
|
||||
case let .clearStorage(sizeFraction):
|
||||
let sizeString = dataSizeString(Int64(sizeFraction), formatting: DataSizeStringFormatting(strings: item.strings, decimalSeparator: "."))
|
||||
let rawTitleString = item.strings.ChatList_StorageHintTitle(sizeString)
|
||||
|
||||
@@ -976,35 +976,40 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega
|
||||
}
|
||||
self.otherButton.addTarget(self, action: #selector(self.otherButtonPressed), forControlEvents: .touchUpInside)
|
||||
|
||||
self.disposable.set(combineLatest(
|
||||
queue: Queue.mainQueue(),
|
||||
self.context.engine.themes.getChatThemes(accountManager: self.context.sharedContext.accountManager),
|
||||
self.uniqueGiftChatThemesContext.state
|
||||
|> mapToSignal { state -> Signal<(UniqueGiftChatThemesContext.State, [EnginePeer.Id: EnginePeer]), NoError> in
|
||||
var peerIds: [EnginePeer.Id] = []
|
||||
for theme in state.themes {
|
||||
if case let .gift(gift, _) = theme, case let .unique(uniqueGift) = gift, let themePeerId = uniqueGift.themePeerId {
|
||||
peerIds.append(themePeerId)
|
||||
}
|
||||
let chatThemesSignal: Signal<[TelegramTheme], NoError> = self.context.engine.themes.getChatThemes(accountManager: self.context.sharedContext.accountManager)
|
||||
let uniqueGiftThemesSignal: Signal<(UniqueGiftChatThemesContext.State, [EnginePeer.Id: EnginePeer]), NoError> = self.uniqueGiftChatThemesContext.state
|
||||
|> mapToSignal { [context] state -> Signal<(UniqueGiftChatThemesContext.State, [EnginePeer.Id: EnginePeer]), NoError> in
|
||||
var peerIds: [EnginePeer.Id] = []
|
||||
for theme in state.themes {
|
||||
if case let .gift(gift, _) = theme, case let .unique(uniqueGift) = gift, let themePeerId = uniqueGift.themePeerId {
|
||||
peerIds.append(themePeerId)
|
||||
}
|
||||
return combineLatest(
|
||||
.single(state),
|
||||
context.engine.data.get(
|
||||
EngineDataMap(peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init))
|
||||
) |> map { peers in
|
||||
var result: [EnginePeer.Id: EnginePeer] = [:]
|
||||
for peerId in peerIds {
|
||||
if let maybePeer = peers[peerId], let peer = maybePeer {
|
||||
result[peerId] = peer
|
||||
}
|
||||
}
|
||||
return combineLatest(
|
||||
.single(state),
|
||||
context.engine.data.get(
|
||||
EngineDataMap(peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init))
|
||||
) |> map { peers in
|
||||
var result: [EnginePeer.Id: EnginePeer] = [:]
|
||||
for peerId in peerIds {
|
||||
if let maybePeer = peers[peerId], let peer = maybePeer {
|
||||
result[peerId] = peer
|
||||
}
|
||||
return result
|
||||
}
|
||||
)
|
||||
},
|
||||
return result
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
let combinedSignal: Signal<([TelegramTheme], (UniqueGiftChatThemesContext.State, [EnginePeer.Id: EnginePeer]), ChatTheme?, Bool), NoError> = combineLatest(
|
||||
chatThemesSignal,
|
||||
uniqueGiftThemesSignal,
|
||||
self.selectedThemePromise.get(),
|
||||
self.isDarkAppearancePromise.get()
|
||||
).startStrict(next: { [weak self] themes, uniqueGiftChatThemesStateAndPeers, selectedTheme, isDarkAppearance in
|
||||
)
|
||||
|> deliverOnMainQueue
|
||||
|
||||
let combinedDisposable = combinedSignal.startStrict(next: { [weak self] themes, uniqueGiftChatThemesStateAndPeers, selectedTheme, isDarkAppearance in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
@@ -1045,7 +1050,7 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega
|
||||
var peer: EnginePeer?
|
||||
if case let .unique(uniqueGift) = gift {
|
||||
for attribute in uniqueGift.attributes {
|
||||
if case let .model(_, file, _) = attribute {
|
||||
if case let .model(_, file, _, _) = attribute {
|
||||
emojiFile = file
|
||||
}
|
||||
}
|
||||
@@ -1139,7 +1144,8 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
})
|
||||
self.disposable.set(combinedDisposable)
|
||||
|
||||
self.switchThemeButton.highligthedChanged = { [weak self] highlighted in
|
||||
if let strongSelf = self {
|
||||
|
||||
@@ -450,6 +450,7 @@ final class ComposeTodoScreenComponent: Component {
|
||||
let presentationInterfaceState = ChatPresentationInterfaceState(
|
||||
chatWallpaper: .builtin(WallpaperSettings()),
|
||||
theme: presentationData.theme,
|
||||
preferredGlassType: .default,
|
||||
strings: presentationData.strings,
|
||||
dateTimeFormat: presentationData.dateTimeFormat,
|
||||
nameDisplayOrder: presentationData.nameDisplayOrder,
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "ContextControllerImpl",
|
||||
module_name = "ContextControllerImpl",
|
||||
srcs = [
|
||||
"Sources/ContextControllerCompatibility.swift",
|
||||
],
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/ContextUI",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
+445
@@ -0,0 +1,445 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import TelegramPresentationData
|
||||
import SwiftSignalKit
|
||||
import Markdown
|
||||
import ContextUI
|
||||
|
||||
public final class ContextActionNode: ASDisplayNode, ContextActionNodeProtocol {
|
||||
private var presentationData: PresentationData
|
||||
private(set) var action: ContextMenuActionItem
|
||||
private let getController: () -> ContextControllerProtocol?
|
||||
private let actionSelected: (ContextMenuActionResult) -> Void
|
||||
private let requestLayout: () -> Void
|
||||
private let requestUpdateAction: (AnyHashable, ContextMenuActionItem) -> Void
|
||||
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let highlightedBackgroundNode: ASDisplayNode
|
||||
private let textNode: ImmediateTextNode
|
||||
private let statusNode: ImmediateTextNode?
|
||||
private let iconNode: ASImageNode
|
||||
private let titleIconNode: ASImageNode
|
||||
private let badgeBackgroundNode: ASImageNode
|
||||
private let badgeTextNode: ImmediateTextNode
|
||||
private let buttonNode: HighlightTrackingButtonNode
|
||||
|
||||
private var iconDisposable: Disposable?
|
||||
|
||||
private var pointerInteraction: PointerInteraction?
|
||||
|
||||
public var isActionEnabled: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
public init(presentationData: PresentationData, action: ContextMenuActionItem, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void, requestLayout: @escaping () -> Void, requestUpdateAction: @escaping (AnyHashable, ContextMenuActionItem) -> Void) {
|
||||
self.presentationData = presentationData
|
||||
self.action = action
|
||||
self.getController = getController
|
||||
self.actionSelected = actionSelected
|
||||
self.requestLayout = requestLayout
|
||||
self.requestUpdateAction = requestUpdateAction
|
||||
|
||||
let textFont = Font.regular(presentationData.listsFontSize.baseDisplaySize)
|
||||
let smallTextFont = Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 14.0 / 17.0))
|
||||
|
||||
let boldTextFont = Font.semibold(presentationData.listsFontSize.baseDisplaySize)
|
||||
let smallBoldTextFont = Font.semibold(floor(presentationData.listsFontSize.baseDisplaySize * 14.0 / 17.0))
|
||||
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.isAccessibilityElement = false
|
||||
self.backgroundNode.backgroundColor = presentationData.theme.contextMenu.itemBackgroundColor
|
||||
self.highlightedBackgroundNode = ASDisplayNode()
|
||||
self.highlightedBackgroundNode.isAccessibilityElement = false
|
||||
self.highlightedBackgroundNode.backgroundColor = presentationData.theme.contextMenu.itemHighlightedBackgroundColor
|
||||
self.highlightedBackgroundNode.alpha = 0.0
|
||||
|
||||
self.textNode = ImmediateTextNode()
|
||||
self.textNode.isAccessibilityElement = false
|
||||
self.textNode.isUserInteractionEnabled = false
|
||||
self.textNode.displaysAsynchronously = false
|
||||
let textColor: UIColor
|
||||
switch action.textColor {
|
||||
case .primary:
|
||||
textColor = presentationData.theme.contextMenu.primaryColor
|
||||
case .destructive:
|
||||
textColor = presentationData.theme.contextMenu.destructiveColor
|
||||
case .disabled:
|
||||
textColor = presentationData.theme.contextMenu.primaryColor.withMultipliedAlpha(0.4)
|
||||
}
|
||||
|
||||
let titleFont: UIFont
|
||||
let titleBoldFont: UIFont
|
||||
switch action.textFont {
|
||||
case .regular:
|
||||
titleFont = textFont
|
||||
titleBoldFont = boldTextFont
|
||||
case .small:
|
||||
titleFont = smallTextFont
|
||||
titleBoldFont = smallBoldTextFont
|
||||
case let .custom(customFont, _, _):
|
||||
titleFont = customFont
|
||||
titleBoldFont = customFont
|
||||
}
|
||||
|
||||
let subtitleFont = Font.regular(presentationData.listsFontSize.baseDisplaySize * 14.0 / 17.0)
|
||||
|
||||
if action.parseMarkdown {
|
||||
let attributedText = parseMarkdownIntoAttributedString(action.text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: textColor), bold: MarkdownAttributeSet(font: titleBoldFont, textColor: textColor), link: MarkdownAttributeSet(font: titleFont, textColor: textColor), linkAttribute: { _ in
|
||||
return nil
|
||||
}))
|
||||
self.textNode.attributedText = attributedText
|
||||
} else {
|
||||
self.textNode.attributedText = NSAttributedString(string: action.text, font: titleFont, textColor: textColor)
|
||||
}
|
||||
|
||||
switch action.textLayout {
|
||||
case .singleLine:
|
||||
self.textNode.maximumNumberOfLines = 1
|
||||
self.statusNode = nil
|
||||
case .twoLinesMax:
|
||||
self.textNode.maximumNumberOfLines = 2
|
||||
self.statusNode = nil
|
||||
case let .secondLineWithValue(value):
|
||||
self.textNode.maximumNumberOfLines = 1
|
||||
let statusNode = ImmediateTextNode()
|
||||
statusNode.isAccessibilityElement = false
|
||||
statusNode.isUserInteractionEnabled = false
|
||||
statusNode.displaysAsynchronously = false
|
||||
statusNode.attributedText = NSAttributedString(string: value, font: subtitleFont, textColor: presentationData.theme.contextMenu.secondaryColor)
|
||||
statusNode.maximumNumberOfLines = 1
|
||||
self.statusNode = statusNode
|
||||
case let .secondLineWithAttributedValue(value):
|
||||
self.textNode.maximumNumberOfLines = 1
|
||||
let statusNode = ImmediateTextNode()
|
||||
statusNode.isAccessibilityElement = false
|
||||
statusNode.isUserInteractionEnabled = false
|
||||
statusNode.displaysAsynchronously = false
|
||||
|
||||
let mutableString = value.mutableCopy() as! NSMutableAttributedString
|
||||
mutableString.addAttribute(.foregroundColor, value: presentationData.theme.contextMenu.secondaryColor, range: NSRange(location: 0, length: mutableString.length))
|
||||
mutableString.addAttribute(.font, value: subtitleFont, range: NSRange(location: 0, length: mutableString.length))
|
||||
statusNode.attributedText = mutableString
|
||||
statusNode.maximumNumberOfLines = 1
|
||||
self.statusNode = statusNode
|
||||
case .multiline:
|
||||
self.textNode.maximumNumberOfLines = 0
|
||||
self.statusNode = nil
|
||||
}
|
||||
|
||||
self.iconNode = ASImageNode()
|
||||
self.iconNode.isAccessibilityElement = false
|
||||
self.iconNode.displaysAsynchronously = false
|
||||
self.iconNode.displayWithoutProcessing = true
|
||||
self.iconNode.isUserInteractionEnabled = false
|
||||
if let iconSource = action.iconSource {
|
||||
self.iconNode.clipsToBounds = true
|
||||
self.iconNode.contentMode = iconSource.contentMode
|
||||
self.iconNode.cornerRadius = iconSource.cornerRadius
|
||||
} else {
|
||||
self.iconNode.image = action.icon(presentationData.theme)
|
||||
}
|
||||
|
||||
self.titleIconNode = ASImageNode()
|
||||
self.titleIconNode.isAccessibilityElement = false
|
||||
self.titleIconNode.displaysAsynchronously = false
|
||||
self.titleIconNode.displayWithoutProcessing = true
|
||||
self.titleIconNode.isUserInteractionEnabled = false
|
||||
self.titleIconNode.image = action.textIcon(presentationData.theme)
|
||||
|
||||
self.badgeBackgroundNode = ASImageNode()
|
||||
self.badgeBackgroundNode.isAccessibilityElement = false
|
||||
self.badgeBackgroundNode.displaysAsynchronously = false
|
||||
self.badgeBackgroundNode.displayWithoutProcessing = true
|
||||
self.badgeBackgroundNode.isUserInteractionEnabled = false
|
||||
|
||||
self.badgeTextNode = ImmediateTextNode()
|
||||
if let badge = action.badge {
|
||||
let badgeFillColor: UIColor
|
||||
let badgeForegroundColor: UIColor
|
||||
switch badge.color {
|
||||
case .accent:
|
||||
badgeForegroundColor = presentationData.theme.contextMenu.badgeForegroundColor
|
||||
badgeFillColor = presentationData.theme.contextMenu.badgeFillColor
|
||||
case .inactive:
|
||||
badgeForegroundColor = presentationData.theme.contextMenu.badgeInactiveForegroundColor
|
||||
badgeFillColor = presentationData.theme.contextMenu.badgeInactiveFillColor
|
||||
}
|
||||
self.badgeBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 18.0, color: badgeFillColor)
|
||||
self.badgeTextNode.attributedText = NSAttributedString(string: badge.value, font: Font.regular(14.0), textColor: badgeForegroundColor)
|
||||
}
|
||||
self.badgeTextNode.isAccessibilityElement = false
|
||||
self.badgeTextNode.isUserInteractionEnabled = false
|
||||
self.badgeTextNode.displaysAsynchronously = false
|
||||
|
||||
self.buttonNode = HighlightTrackingButtonNode()
|
||||
self.buttonNode.isAccessibilityElement = true
|
||||
self.buttonNode.accessibilityLabel = action.text
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
self.addSubnode(self.highlightedBackgroundNode)
|
||||
self.addSubnode(self.textNode)
|
||||
self.statusNode.flatMap(self.addSubnode)
|
||||
self.addSubnode(self.iconNode)
|
||||
self.addSubnode(self.badgeBackgroundNode)
|
||||
self.addSubnode(self.badgeTextNode)
|
||||
self.addSubnode(self.buttonNode)
|
||||
if let _ = self.titleIconNode.image {
|
||||
self.addSubnode(self.titleIconNode)
|
||||
}
|
||||
|
||||
self.buttonNode.highligthedChanged = { [weak self] highligted in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if highligted {
|
||||
strongSelf.highlightedBackgroundNode.alpha = 1.0
|
||||
} else {
|
||||
strongSelf.highlightedBackgroundNode.alpha = 0.0
|
||||
strongSelf.highlightedBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
|
||||
}
|
||||
}
|
||||
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
|
||||
self.buttonNode.isUserInteractionEnabled = self.action.action != nil
|
||||
|
||||
if let iconSource = action.iconSource {
|
||||
self.iconDisposable = (iconSource.signal
|
||||
|> deliverOnMainQueue).start(next: { [weak self] image in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.iconNode.image = image
|
||||
}).strict()
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.iconDisposable?.dispose()
|
||||
}
|
||||
|
||||
public override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.pointerInteraction = PointerInteraction(node: self.buttonNode, style: .hover, willEnter: { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.highlightedBackgroundNode.alpha = 0.75
|
||||
}
|
||||
}, willExit: { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.highlightedBackgroundNode.alpha = 0.0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public func updateLayout(constrainedWidth: CGFloat, previous: ContextActionSibling, next: ContextActionSibling) -> (CGSize, (CGSize, ContainedViewLayoutTransition) -> Void) {
|
||||
let sideInset: CGFloat = 16.0
|
||||
let iconSideInset: CGFloat = 12.0
|
||||
let verticalInset: CGFloat = 12.0
|
||||
|
||||
let iconSize: CGSize
|
||||
if let iconSource = self.action.iconSource {
|
||||
iconSize = iconSource.size
|
||||
} else {
|
||||
iconSize = self.iconNode.image.flatMap({ $0.size }) ?? CGSize()
|
||||
}
|
||||
|
||||
let standardIconWidth: CGFloat = 32.0
|
||||
var rightTextInset: CGFloat = sideInset
|
||||
if !iconSize.width.isZero {
|
||||
rightTextInset = max(iconSize.width, standardIconWidth) + iconSideInset + sideInset
|
||||
}
|
||||
if let iconSize = self.titleIconNode.image?.size {
|
||||
rightTextInset += iconSize.width + 10.0
|
||||
}
|
||||
|
||||
let badgeTextSize = self.badgeTextNode.updateLayout(CGSize(width: constrainedWidth, height: .greatestFiniteMagnitude))
|
||||
let badgeInset: CGFloat = 4.0
|
||||
|
||||
let badgeSize: CGSize
|
||||
let badgeWidthSpace: CGFloat
|
||||
let badgeSpacing: CGFloat = 10.0
|
||||
if badgeTextSize.width.isZero {
|
||||
badgeSize = CGSize()
|
||||
badgeWidthSpace = 0.0
|
||||
} else {
|
||||
badgeSize = CGSize(width: max(18.0, badgeTextSize.width + badgeInset * 2.0), height: 18.0)
|
||||
badgeWidthSpace = badgeSize.width + badgeSpacing
|
||||
}
|
||||
|
||||
let textSize = self.textNode.updateLayout(CGSize(width: constrainedWidth - sideInset - rightTextInset - badgeWidthSpace, height: .greatestFiniteMagnitude))
|
||||
let statusSize = self.statusNode?.updateLayout(CGSize(width: constrainedWidth - sideInset - rightTextInset - badgeWidthSpace, height: .greatestFiniteMagnitude)) ?? CGSize()
|
||||
|
||||
if !statusSize.width.isZero, let statusNode = self.statusNode {
|
||||
let verticalSpacing: CGFloat = 2.0
|
||||
let combinedTextHeight = textSize.height + verticalSpacing + statusSize.height
|
||||
return (CGSize(width: max(textSize.width, statusSize.width) + sideInset + rightTextInset + badgeWidthSpace, height: verticalInset * 2.0 + combinedTextHeight), { size, transition in
|
||||
let verticalOrigin = floor((size.height - combinedTextHeight) / 2.0)
|
||||
let textFrame = CGRect(origin: CGPoint(x: sideInset, y: verticalOrigin), size: textSize)
|
||||
transition.updateFrameAdditive(node: self.textNode, frame: textFrame)
|
||||
transition.updateFrameAdditive(node: statusNode, frame: CGRect(origin: CGPoint(x: sideInset, y: verticalOrigin + verticalSpacing + textSize.height), size: statusSize))
|
||||
|
||||
let badgeFrame = CGRect(origin: CGPoint(x: textFrame.maxX + badgeSpacing, y: floor((size.height - badgeSize.height) / 2.0)), size: badgeSize)
|
||||
transition.updateFrame(node: self.badgeBackgroundNode, frame: badgeFrame)
|
||||
transition.updateFrame(node: self.badgeTextNode, frame: CGRect(origin: CGPoint(x: badgeFrame.minX + floorToScreenPixels((badgeFrame.width - badgeTextSize.width) / 2.0), y: badgeFrame.minY + floor((badgeFrame.height - badgeTextSize.height) / 2.0)), size: badgeTextSize))
|
||||
|
||||
if !iconSize.width.isZero {
|
||||
transition.updateFrameAdditive(node: self.iconNode, frame: CGRect(origin: CGPoint(x: size.width - standardIconWidth - iconSideInset + floor((standardIconWidth - iconSize.width) / 2.0), y: floor((size.height - iconSize.height) / 2.0)), size: iconSize))
|
||||
}
|
||||
|
||||
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)))
|
||||
transition.updateFrame(node: self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)))
|
||||
transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)))
|
||||
|
||||
if let iconSize = self.titleIconNode.image?.size {
|
||||
transition.updateFrame(node: self.titleIconNode, frame: CGRect(origin: CGPoint(x: self.textNode.frame.maxX + 7.0, y: floorToScreenPixels(self.textNode.frame.midY - iconSize.height / 2.0)), size: iconSize))
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return (CGSize(width: textSize.width + sideInset + rightTextInset + badgeWidthSpace, height: verticalInset * 2.0 + textSize.height), { size, transition in
|
||||
let verticalOrigin = floor((size.height - textSize.height) / 2.0)
|
||||
let textFrame = CGRect(origin: CGPoint(x: sideInset, y: verticalOrigin), size: textSize)
|
||||
transition.updateFrameAdditive(node: self.textNode, frame: textFrame)
|
||||
|
||||
if !iconSize.width.isZero {
|
||||
transition.updateFrameAdditive(node: self.iconNode, frame: CGRect(origin: CGPoint(x: size.width - standardIconWidth - iconSideInset + floor((standardIconWidth - iconSize.width) / 2.0), y: floor((size.height - iconSize.height) / 2.0)), size: iconSize))
|
||||
}
|
||||
|
||||
let badgeFrame = CGRect(origin: CGPoint(x: textFrame.maxX + badgeSpacing, y: floor((size.height - badgeSize.height) / 2.0)), size: badgeSize)
|
||||
transition.updateFrame(node: self.badgeBackgroundNode, frame: badgeFrame)
|
||||
transition.updateFrame(node: self.badgeTextNode, frame: CGRect(origin: CGPoint(x: badgeFrame.minX + floorToScreenPixels((badgeFrame.width - badgeTextSize.width) / 2.0), y: badgeFrame.minY + floor((badgeFrame.height - badgeTextSize.height) / 2.0)), size: badgeTextSize))
|
||||
|
||||
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)))
|
||||
transition.updateFrame(node: self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)))
|
||||
transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)))
|
||||
|
||||
if let iconSize = self.titleIconNode.image?.size {
|
||||
transition.updateFrame(node: self.titleIconNode, frame: CGRect(origin: CGPoint(x: self.textNode.frame.maxX + 7.0, y: floorToScreenPixels(self.textNode.frame.midY - iconSize.height / 2.0)), size: iconSize))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public func updateTheme(presentationData: PresentationData) {
|
||||
self.presentationData = presentationData
|
||||
|
||||
self.backgroundNode.backgroundColor = presentationData.theme.contextMenu.itemBackgroundColor
|
||||
self.highlightedBackgroundNode.backgroundColor = presentationData.theme.contextMenu.itemHighlightedBackgroundColor
|
||||
|
||||
let textColor: UIColor
|
||||
switch action.textColor {
|
||||
case .primary:
|
||||
textColor = presentationData.theme.contextMenu.primaryColor
|
||||
case .destructive:
|
||||
textColor = presentationData.theme.contextMenu.destructiveColor
|
||||
case .disabled:
|
||||
textColor = presentationData.theme.contextMenu.primaryColor.withMultipliedAlpha(0.4)
|
||||
}
|
||||
|
||||
let textFont = Font.regular(presentationData.listsFontSize.baseDisplaySize)
|
||||
let smallTextFont = Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 14.0 / 17.0))
|
||||
let titleFont: UIFont
|
||||
switch self.action.textFont {
|
||||
case .regular:
|
||||
titleFont = textFont
|
||||
case .small:
|
||||
titleFont = smallTextFont
|
||||
case let .custom(customFont, _, _):
|
||||
titleFont = customFont
|
||||
}
|
||||
|
||||
self.textNode.attributedText = NSAttributedString(string: self.action.text, font: titleFont, textColor: textColor)
|
||||
|
||||
let subtitleFont = Font.regular(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0)
|
||||
switch self.action.textLayout {
|
||||
case let .secondLineWithValue(value):
|
||||
self.statusNode?.attributedText = NSAttributedString(string: value, font: subtitleFont, textColor: presentationData.theme.contextMenu.secondaryColor)
|
||||
case let .secondLineWithAttributedValue(value):
|
||||
let mutableString = value.mutableCopy() as! NSMutableAttributedString
|
||||
mutableString.addAttribute(.foregroundColor, value: presentationData.theme.contextMenu.secondaryColor, range: NSRange(location: 0, length: mutableString.length))
|
||||
mutableString.addAttribute(.font, value: subtitleFont, range: NSRange(location: 0, length: mutableString.length))
|
||||
self.statusNode?.attributedText = mutableString
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if self.action.iconSource == nil {
|
||||
self.iconNode.image = self.action.icon(presentationData.theme)
|
||||
}
|
||||
|
||||
self.badgeBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 18.0, color: presentationData.theme.contextMenu.badgeFillColor)
|
||||
self.badgeTextNode.attributedText = NSAttributedString(string: self.badgeTextNode.attributedText?.string ?? "", font: Font.regular(14.0), textColor: presentationData.theme.contextMenu.badgeForegroundColor)
|
||||
}
|
||||
|
||||
@objc private func buttonPressed() {
|
||||
self.performAction()
|
||||
}
|
||||
|
||||
func updateAction(item: ContextMenuActionItem) {
|
||||
self.action = item
|
||||
|
||||
let textColor: UIColor
|
||||
switch self.action.textColor {
|
||||
case .primary:
|
||||
textColor = self.presentationData.theme.contextMenu.primaryColor
|
||||
case .destructive:
|
||||
textColor = self.presentationData.theme.contextMenu.destructiveColor
|
||||
case .disabled:
|
||||
textColor = self.presentationData.theme.contextMenu.primaryColor.withMultipliedAlpha(0.4)
|
||||
}
|
||||
|
||||
let textFont = Font.regular(self.presentationData.listsFontSize.baseDisplaySize)
|
||||
let smallTextFont = Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 14.0 / 17.0))
|
||||
let titleFont: UIFont
|
||||
switch self.action.textFont {
|
||||
case .regular:
|
||||
titleFont = textFont
|
||||
case .small:
|
||||
titleFont = smallTextFont
|
||||
case let .custom(customFont, _, _):
|
||||
titleFont = customFont
|
||||
}
|
||||
|
||||
self.textNode.attributedText = NSAttributedString(string: self.action.text, font: titleFont, textColor: textColor)
|
||||
|
||||
if self.action.iconSource == nil {
|
||||
self.iconNode.image = self.action.icon(self.presentationData.theme)
|
||||
}
|
||||
|
||||
self.requestLayout()
|
||||
}
|
||||
|
||||
private var performedAction = false
|
||||
public func performAction() {
|
||||
guard let controller = self.getController(), !self.performedAction else {
|
||||
return
|
||||
}
|
||||
self.action.action?(ContextMenuActionItem.Action(
|
||||
controller: controller,
|
||||
dismissWithResult: { [weak self] result in
|
||||
self?.performedAction = true
|
||||
self?.actionSelected(result)
|
||||
},
|
||||
updateAction: { [weak self] id, updatedAction in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.requestUpdateAction(id, updatedAction)
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
public func setIsHighlighted(_ value: Bool) {
|
||||
if value && self.buttonNode.isUserInteractionEnabled {
|
||||
self.highlightedBackgroundNode.alpha = 1.0
|
||||
} else {
|
||||
self.highlightedBackgroundNode.alpha = 0.0
|
||||
}
|
||||
}
|
||||
|
||||
public func actionNode(at point: CGPoint) -> ContextActionNodeProtocol {
|
||||
return self
|
||||
}
|
||||
}
|
||||
+853
@@ -0,0 +1,853 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import TextSelectionNode
|
||||
import Markdown
|
||||
import AppBundle
|
||||
import TextFormat
|
||||
import TextNodeWithEntities
|
||||
import SwiftSignalKit
|
||||
import ContextUI
|
||||
import GlassBackgroundComponent
|
||||
import ComponentFlow
|
||||
import ComponentDisplayAdapters
|
||||
|
||||
private final class ContextActionsSelectionGestureRecognizer: UIPanGestureRecognizer {
|
||||
var updateLocation: ((CGPoint, Bool) -> Void)?
|
||||
var completed: ((Bool) -> Void)?
|
||||
|
||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesBegan(touches, with: event)
|
||||
|
||||
self.updateLocation?(touches.first!.location(in: self.view), false)
|
||||
}
|
||||
|
||||
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesMoved(touches, with: event)
|
||||
|
||||
self.updateLocation?(touches.first!.location(in: self.view), true)
|
||||
}
|
||||
|
||||
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesEnded(touches, with: event)
|
||||
|
||||
self.completed?(true)
|
||||
}
|
||||
|
||||
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesCancelled(touches, with: event)
|
||||
|
||||
self.completed?(false)
|
||||
}
|
||||
}
|
||||
|
||||
private enum ContextItemNode {
|
||||
case action(ContextActionNode)
|
||||
case custom(ContextMenuCustomNode)
|
||||
case itemSeparator(ASDisplayNode)
|
||||
case separator(ASDisplayNode)
|
||||
}
|
||||
|
||||
private final class InnerActionsContainerNode: ASDisplayNode {
|
||||
private let blurBackground: Bool
|
||||
private let presentationData: PresentationData
|
||||
private let containerNode: ASDisplayNode
|
||||
private var effectView: UIVisualEffectView?
|
||||
private var itemNodes: [ContextItemNode]
|
||||
private let feedbackTap: () -> Void
|
||||
|
||||
private(set) var gesture: UIGestureRecognizer?
|
||||
private var currentHighlightedActionNode: ContextActionNodeProtocol?
|
||||
|
||||
var panSelectionGestureEnabled: Bool = true {
|
||||
didSet {
|
||||
if self.panSelectionGestureEnabled != oldValue, let gesture = self.gesture {
|
||||
gesture.isEnabled = self.panSelectionGestureEnabled
|
||||
|
||||
self.itemNodes.forEach({ itemNode in
|
||||
switch itemNode {
|
||||
case let .action(actionNode):
|
||||
actionNode.isUserInteractionEnabled = !self.panSelectionGestureEnabled
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(presentationData: PresentationData, items: [ContextMenuItem], getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void, requestLayout: @escaping () -> Void, feedbackTap: @escaping () -> Void, blurBackground: Bool) {
|
||||
self.presentationData = presentationData
|
||||
self.feedbackTap = feedbackTap
|
||||
self.blurBackground = blurBackground
|
||||
|
||||
self.containerNode = ASDisplayNode()
|
||||
self.containerNode.clipsToBounds = true
|
||||
self.containerNode.cornerRadius = 14.0
|
||||
self.containerNode.backgroundColor = presentationData.theme.contextMenu.backgroundColor
|
||||
|
||||
var requestUpdateAction: ((AnyHashable, ContextMenuActionItem) -> Void)?
|
||||
|
||||
var itemNodes: [ContextItemNode] = []
|
||||
for i in 0 ..< items.count {
|
||||
switch items[i] {
|
||||
case let .action(action):
|
||||
itemNodes.append(.action(ContextActionNode(presentationData: presentationData, action: action, getController: getController, actionSelected: actionSelected, requestLayout: requestLayout, requestUpdateAction: { id, action in
|
||||
requestUpdateAction?(id, action)
|
||||
})))
|
||||
case let .custom(item, _):
|
||||
let itemNode = item.node(presentationData: presentationData, getController: getController, actionSelected: actionSelected)
|
||||
itemNodes.append(.custom(itemNode))
|
||||
case .separator:
|
||||
let separatorNode = ASDisplayNode()
|
||||
separatorNode.backgroundColor = presentationData.theme.contextMenu.itemSeparatorColor
|
||||
itemNodes.append(.separator(separatorNode))
|
||||
}
|
||||
}
|
||||
|
||||
self.itemNodes = itemNodes
|
||||
|
||||
super.init()
|
||||
|
||||
requestUpdateAction = { [weak self] id, action in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
loop: for itemNode in strongSelf.itemNodes {
|
||||
switch itemNode {
|
||||
case let .action(contextActionNode):
|
||||
if contextActionNode.action.id == id {
|
||||
contextActionNode.updateAction(item: action)
|
||||
break loop
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.addSubnode(self.containerNode)
|
||||
|
||||
self.itemNodes.forEach({ itemNode in
|
||||
switch itemNode {
|
||||
case let .action(actionNode):
|
||||
actionNode.isUserInteractionEnabled = false
|
||||
self.containerNode.addSubnode(actionNode)
|
||||
case let .custom(itemNode):
|
||||
self.containerNode.addSubnode(itemNode)
|
||||
case let .itemSeparator(separatorNode):
|
||||
self.containerNode.addSubnode(separatorNode)
|
||||
case let .separator(separatorNode):
|
||||
self.containerNode.addSubnode(separatorNode)
|
||||
}
|
||||
})
|
||||
|
||||
let gesture = ContextActionsSelectionGestureRecognizer(target: nil, action: nil)
|
||||
self.gesture = gesture
|
||||
gesture.updateLocation = { [weak self] point, moved in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
var actionNode = strongSelf.actionNode(at: point)
|
||||
if let actionNodeValue = actionNode, !actionNodeValue.isActionEnabled {
|
||||
actionNode = nil
|
||||
}
|
||||
if actionNode !== strongSelf.currentHighlightedActionNode {
|
||||
if actionNode != nil, moved {
|
||||
strongSelf.feedbackTap()
|
||||
}
|
||||
strongSelf.currentHighlightedActionNode?.setIsHighlighted(false)
|
||||
}
|
||||
strongSelf.currentHighlightedActionNode = actionNode
|
||||
actionNode?.setIsHighlighted(true)
|
||||
}
|
||||
gesture.completed = { [weak self] performAction in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if let currentHighlightedActionNode = strongSelf.currentHighlightedActionNode {
|
||||
strongSelf.currentHighlightedActionNode = nil
|
||||
currentHighlightedActionNode.setIsHighlighted(false)
|
||||
if performAction {
|
||||
currentHighlightedActionNode.performAction()
|
||||
}
|
||||
}
|
||||
}
|
||||
self.view.addGestureRecognizer(gesture)
|
||||
gesture.isEnabled = self.panSelectionGestureEnabled
|
||||
}
|
||||
|
||||
func updateLayout(widthClass: ContainerViewLayoutSizeClass, constrainedWidth: CGFloat, constrainedHeight: CGFloat, minimalWidth: CGFloat?, transition: ContainedViewLayoutTransition) -> CGSize {
|
||||
var minActionsWidth: CGFloat = 250.0
|
||||
if let minimalWidth = minimalWidth, minimalWidth > minActionsWidth {
|
||||
minActionsWidth = minimalWidth
|
||||
}
|
||||
|
||||
switch widthClass {
|
||||
case .compact:
|
||||
minActionsWidth = max(minActionsWidth, floor(constrainedWidth / 3.0))
|
||||
if let effectView = self.effectView {
|
||||
self.effectView = nil
|
||||
effectView.removeFromSuperview()
|
||||
}
|
||||
case .regular:
|
||||
if self.effectView == nil {
|
||||
let effectView: UIVisualEffectView
|
||||
if #available(iOS 13.0, *) {
|
||||
if self.presentationData.theme.rootController.keyboardColor == .dark {
|
||||
effectView = UIVisualEffectView(effect: UIBlurEffect(style: .systemMaterialDark))
|
||||
} else {
|
||||
effectView = UIVisualEffectView(effect: UIBlurEffect(style: .systemMaterialLight))
|
||||
}
|
||||
} else {
|
||||
effectView = UIVisualEffectView(effect: UIBlurEffect(style: .light))
|
||||
}
|
||||
self.effectView = effectView
|
||||
self.containerNode.view.insertSubview(effectView, at: 0)
|
||||
}
|
||||
}
|
||||
minActionsWidth = min(minActionsWidth, constrainedWidth)
|
||||
let separatorHeight: CGFloat = 8.0
|
||||
|
||||
var maxWidth: CGFloat = 0.0
|
||||
var contentHeight: CGFloat = 0.0
|
||||
var heightsAndCompletions: [(CGFloat, (CGSize, ContainedViewLayoutTransition) -> Void)?] = []
|
||||
for i in 0 ..< self.itemNodes.count {
|
||||
switch self.itemNodes[i] {
|
||||
case let .action(itemNode):
|
||||
let previous: ContextActionSibling
|
||||
let next: ContextActionSibling
|
||||
if i == 0 {
|
||||
previous = .none
|
||||
} else if case .separator = self.itemNodes[i - 1] {
|
||||
previous = .separator
|
||||
} else {
|
||||
previous = .item
|
||||
}
|
||||
if i == self.itemNodes.count - 1 {
|
||||
next = .none
|
||||
} else if case .separator = self.itemNodes[i + 1] {
|
||||
next = .separator
|
||||
} else {
|
||||
next = .item
|
||||
}
|
||||
let (minSize, complete) = itemNode.updateLayout(constrainedWidth: constrainedWidth, previous: previous, next: next)
|
||||
maxWidth = max(maxWidth, minSize.width)
|
||||
heightsAndCompletions.append((minSize.height, complete))
|
||||
contentHeight += minSize.height
|
||||
case let .custom(itemNode):
|
||||
let (minSize, complete) = itemNode.updateLayout(constrainedWidth: constrainedWidth, constrainedHeight: constrainedHeight)
|
||||
maxWidth = max(maxWidth, minSize.width)
|
||||
heightsAndCompletions.append((minSize.height, complete))
|
||||
contentHeight += minSize.height
|
||||
case .itemSeparator:
|
||||
heightsAndCompletions.append(nil)
|
||||
contentHeight += UIScreenPixel
|
||||
case .separator:
|
||||
heightsAndCompletions.append(nil)
|
||||
contentHeight += separatorHeight
|
||||
}
|
||||
}
|
||||
|
||||
maxWidth = max(maxWidth, minActionsWidth)
|
||||
|
||||
var verticalOffset: CGFloat = 0.0
|
||||
for i in 0 ..< heightsAndCompletions.count {
|
||||
switch self.itemNodes[i] {
|
||||
case let .action(itemNode):
|
||||
if let (itemHeight, itemCompletion) = heightsAndCompletions[i] {
|
||||
let itemSize = CGSize(width: maxWidth, height: itemHeight)
|
||||
transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: itemSize))
|
||||
itemCompletion(itemSize, transition)
|
||||
verticalOffset += itemHeight
|
||||
}
|
||||
case let .custom(itemNode):
|
||||
if let (itemHeight, itemCompletion) = heightsAndCompletions[i] {
|
||||
let itemSize = CGSize(width: maxWidth, height: itemHeight)
|
||||
transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: itemSize))
|
||||
itemCompletion(itemSize, transition)
|
||||
verticalOffset += itemHeight
|
||||
}
|
||||
case let .itemSeparator(separatorNode):
|
||||
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: CGSize(width: maxWidth, height: UIScreenPixel)))
|
||||
verticalOffset += UIScreenPixel
|
||||
case let .separator(separatorNode):
|
||||
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: verticalOffset + floorToScreenPixels((separatorHeight - UIScreenPixel) * 0.5)), size: CGSize(width: maxWidth, height: UIScreenPixel)))
|
||||
verticalOffset += separatorHeight
|
||||
}
|
||||
}
|
||||
|
||||
let size = CGSize(width: maxWidth, height: verticalOffset)
|
||||
let bounds = CGRect(origin: CGPoint(), size: size)
|
||||
|
||||
transition.updateFrame(node: self.containerNode, frame: bounds)
|
||||
if let effectView = self.effectView {
|
||||
transition.updateFrame(view: effectView, frame: bounds)
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
func updateTheme(presentationData: PresentationData) {
|
||||
for itemNode in self.itemNodes {
|
||||
switch itemNode {
|
||||
case let .action(action):
|
||||
action.updateTheme(presentationData: presentationData)
|
||||
case let .custom(item):
|
||||
item.updateTheme(presentationData: presentationData)
|
||||
case let .separator(separator):
|
||||
separator.backgroundColor = presentationData.theme.contextMenu.sectionSeparatorColor
|
||||
case let .itemSeparator(itemSeparator):
|
||||
itemSeparator.backgroundColor = presentationData.theme.contextMenu.itemSeparatorColor
|
||||
}
|
||||
}
|
||||
|
||||
self.containerNode.backgroundColor = presentationData.theme.contextMenu.backgroundColor
|
||||
}
|
||||
|
||||
func actionNode(at point: CGPoint) -> ContextActionNodeProtocol? {
|
||||
for itemNode in self.itemNodes {
|
||||
switch itemNode {
|
||||
case let .action(actionNode):
|
||||
if actionNode.frame.contains(point) {
|
||||
return actionNode
|
||||
}
|
||||
case let .custom(node):
|
||||
if let node = node as? ContextActionNodeProtocol, node.frame.contains(point) {
|
||||
return node.actionNode(at: self.convert(point, to: node))
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
final class InnerTextSelectionTipContainerNode: ASDisplayNode {
|
||||
private let presentationData: PresentationData
|
||||
private var background: (container: GlassBackgroundContainerView, background: GlassBackgroundView)?
|
||||
private let textNode: TextNodeWithEntities
|
||||
private var textSelectionNode: TextSelectionNode?
|
||||
private let iconNode: ASImageNode
|
||||
private let placeholderNode: ASDisplayNode
|
||||
|
||||
var tip: ContextController.Tip
|
||||
|
||||
private let text: String
|
||||
private var arguments: TextNodeWithEntities.Arguments?
|
||||
private var file: TelegramMediaFile?
|
||||
private let targetSelectionIndex: Int?
|
||||
|
||||
private var hapticFeedback: HapticFeedback?
|
||||
|
||||
private var action: (() -> Void)?
|
||||
var requestDismiss: (@escaping () -> Void) -> Void = { _ in }
|
||||
|
||||
init(presentationData: PresentationData, tip: ContextController.Tip, isInline: Bool) {
|
||||
self.tip = tip
|
||||
self.presentationData = presentationData
|
||||
|
||||
if !isInline {
|
||||
self.background = (GlassBackgroundContainerView(), GlassBackgroundView())
|
||||
} else {
|
||||
self.background = nil
|
||||
}
|
||||
|
||||
self.textNode = TextNodeWithEntities()
|
||||
self.textNode.textNode.displaysAsynchronously = false
|
||||
self.textNode.textNode.isUserInteractionEnabled = false
|
||||
|
||||
var isUserInteractionEnabled = false
|
||||
var icon: UIImage?
|
||||
switch tip {
|
||||
case .textSelection:
|
||||
var rawText = self.presentationData.strings.ChatContextMenu_TextSelectionTip2
|
||||
if let range = rawText.range(of: "|") {
|
||||
rawText.removeSubrange(range)
|
||||
self.text = rawText
|
||||
self.targetSelectionIndex = NSRange(range, in: rawText).lowerBound
|
||||
} else {
|
||||
self.text = rawText
|
||||
self.targetSelectionIndex = 1
|
||||
}
|
||||
icon = UIImage(bundleImageName: "Chat/Context Menu/Tip")
|
||||
case .quoteSelection:
|
||||
var rawText = presentationData.strings.ChatContextMenu_QuoteSelectionTip
|
||||
if let range = rawText.range(of: "|") {
|
||||
rawText.removeSubrange(range)
|
||||
self.text = rawText
|
||||
self.targetSelectionIndex = NSRange(range, in: rawText).lowerBound
|
||||
} else {
|
||||
self.text = rawText
|
||||
self.targetSelectionIndex = 1
|
||||
}
|
||||
icon = UIImage(bundleImageName: "Chat/Context Menu/Tip")
|
||||
case .messageViewsPrivacy:
|
||||
self.text = self.presentationData.strings.ChatContextMenu_MessageViewsPrivacyTip
|
||||
self.targetSelectionIndex = nil
|
||||
icon = UIImage(bundleImageName: "Chat/Context Menu/Tip")
|
||||
case let .messageCopyProtection(isChannel):
|
||||
self.text = isChannel ? self.presentationData.strings.Conversation_CopyProtectionInfoChannel : self.presentationData.strings.Conversation_CopyProtectionInfoGroup
|
||||
self.targetSelectionIndex = nil
|
||||
icon = UIImage(bundleImageName: "Chat/Context Menu/ReportCopyright")
|
||||
case let .animatedEmoji(text, arguments, file, action):
|
||||
self.action = action
|
||||
self.text = text ?? ""
|
||||
self.arguments = arguments
|
||||
self.file = file
|
||||
self.targetSelectionIndex = nil
|
||||
icon = nil
|
||||
isUserInteractionEnabled = text != nil
|
||||
case let .notificationTopicExceptions(text, action):
|
||||
self.action = action
|
||||
self.text = text
|
||||
self.targetSelectionIndex = nil
|
||||
icon = nil
|
||||
isUserInteractionEnabled = action != nil
|
||||
case let .starsReactions(topCount):
|
||||
self.action = nil
|
||||
self.text = self.presentationData.strings.Chat_SendStarsToBecomeTopInfo("\(topCount)").string
|
||||
self.targetSelectionIndex = nil
|
||||
icon = nil
|
||||
isUserInteractionEnabled = action != nil
|
||||
case .videoProcessing:
|
||||
self.action = nil
|
||||
self.text = self.presentationData.strings.Chat_VideoProcessingInfo
|
||||
self.targetSelectionIndex = nil
|
||||
icon = nil
|
||||
isUserInteractionEnabled = action != nil
|
||||
case .collageReordering:
|
||||
self.action = nil
|
||||
self.text = self.presentationData.strings.Camera_CollageReorderingInfo
|
||||
self.targetSelectionIndex = nil
|
||||
icon = UIImage(bundleImageName: "Chat/Context Menu/Tip")
|
||||
}
|
||||
|
||||
self.iconNode = ASImageNode()
|
||||
self.iconNode.displaysAsynchronously = false
|
||||
self.iconNode.displayWithoutProcessing = true
|
||||
self.iconNode.image = generateTintedImage(image: icon, color: presentationData.theme.contextMenu.primaryColor)
|
||||
|
||||
self.placeholderNode = ASDisplayNode()
|
||||
self.placeholderNode.clipsToBounds = true
|
||||
self.placeholderNode.cornerRadius = 4.0
|
||||
self.placeholderNode.isUserInteractionEnabled = false
|
||||
|
||||
super.init()
|
||||
|
||||
let textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: presentationData.theme.contextMenu.primaryColor.withAlphaComponent(0.15), knob: presentationData.theme.contextMenu.primaryColor, knobDiameter: 8.0, isDark: presentationData.theme.overallDarkAppearance), strings: presentationData.strings, textNode: self.textNode.textNode, updateIsActive: { _ in
|
||||
}, present: { _, _ in
|
||||
}, rootNode: { [weak self] in
|
||||
return self
|
||||
}, performAction: { _, _ in
|
||||
})
|
||||
self.textSelectionNode = textSelectionNode
|
||||
|
||||
let parentView: UIView
|
||||
if let background = self.background {
|
||||
self.view.addSubview(background.container)
|
||||
background.container.contentView.addSubview(background.background)
|
||||
parentView = background.background.contentView
|
||||
} else {
|
||||
parentView = self.view
|
||||
}
|
||||
|
||||
parentView.addSubview(self.textNode.textNode.view)
|
||||
parentView.addSubview(self.iconNode.view)
|
||||
parentView.addSubview(self.placeholderNode.view)
|
||||
|
||||
self.textSelectionNode.flatMap { parentView.addSubview($0.view) }
|
||||
|
||||
parentView.addSubview(textSelectionNode.highlightAreaNode.view)
|
||||
|
||||
parentView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.onTapGesture(_:))))
|
||||
|
||||
let shimmeringForegroundColor: UIColor
|
||||
if presentationData.theme.overallDarkAppearance {
|
||||
let backgroundColor = presentationData.theme.contextMenu.backgroundColor.blitOver(presentationData.theme.list.plainBackgroundColor, alpha: 1.0)
|
||||
shimmeringForegroundColor = presentationData.theme.contextMenu.primaryColor.blitOver(backgroundColor, alpha: 0.1)
|
||||
} else {
|
||||
shimmeringForegroundColor = presentationData.theme.contextMenu.primaryColor.withMultipliedAlpha(0.07)
|
||||
}
|
||||
|
||||
self.placeholderNode.backgroundColor = shimmeringForegroundColor
|
||||
|
||||
self.isUserInteractionEnabled = isUserInteractionEnabled
|
||||
}
|
||||
|
||||
@objc func onTapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
self.requestDismiss({
|
||||
self.action?()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func animateTransitionInside(other: InnerTextSelectionTipContainerNode) {
|
||||
let nodes: [ASDisplayNode] = [
|
||||
self.textNode.textNode,
|
||||
self.iconNode,
|
||||
self.placeholderNode
|
||||
]
|
||||
|
||||
for node in nodes {
|
||||
if let background = other.background {
|
||||
background.background.contentView.addSubview(node.view)
|
||||
} else {
|
||||
other.view.addSubview(node.view)
|
||||
}
|
||||
node.layer.animateAlpha(from: node.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak node] _ in
|
||||
node?.removeFromSupernode()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func animateContentIn() {
|
||||
let nodes: [ASDisplayNode] = [
|
||||
self.textNode.textNode,
|
||||
self.iconNode
|
||||
]
|
||||
|
||||
for node in nodes {
|
||||
node.layer.animateAlpha(from: 0.0, to: node.alpha, duration: 0.25)
|
||||
}
|
||||
}
|
||||
|
||||
func updateLayout(widthClass: ContainerViewLayoutSizeClass, presentation: ContextControllerActionsStackNode.Presentation, width: CGFloat, transition: ContainedViewLayoutTransition) -> CGSize {
|
||||
let topInset: CGFloat = self.background != nil ? 16.0 : 9.0
|
||||
let bottomInset: CGFloat = self.background != nil ? 16.0 : 9.0
|
||||
let horizontalInset: CGFloat = 18.0
|
||||
let standardIconWidth: CGFloat = 32.0
|
||||
let iconSideInset: CGFloat = 20.0
|
||||
|
||||
let textFont = Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0))
|
||||
let boldTextFont = Font.bold(floor(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0))
|
||||
let textColor = self.presentationData.theme.contextMenu.primaryColor
|
||||
let linkColor = self.presentationData.theme.overallDarkAppearance ? UIColor(rgb: 0x64d2ff) : self.presentationData.theme.contextMenu.badgeFillColor
|
||||
|
||||
let iconSize = self.iconNode.image?.size ?? CGSize(width: 16.0, height: 16.0)
|
||||
|
||||
let text = self.text.replacingOccurrences(of: "#", with: "# ")
|
||||
let attributedText = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: boldTextFont, textColor: linkColor), linkAttribute: { _ in
|
||||
return nil
|
||||
})))
|
||||
if let file = self.file {
|
||||
let range = (attributedText.string as NSString).range(of: "#")
|
||||
if range.location != NSNotFound {
|
||||
attributedText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: file.fileId.id, file: file), range: range)
|
||||
}
|
||||
}
|
||||
|
||||
let shimmeringForegroundColor: UIColor
|
||||
if presentationData.theme.overallDarkAppearance {
|
||||
let backgroundColor = presentationData.theme.contextMenu.backgroundColor.blitOver(presentationData.theme.list.plainBackgroundColor, alpha: 1.0)
|
||||
shimmeringForegroundColor = presentationData.theme.contextMenu.primaryColor.blitOver(backgroundColor, alpha: 0.1)
|
||||
} else {
|
||||
shimmeringForegroundColor = presentationData.theme.contextMenu.primaryColor.withMultipliedAlpha(0.07)
|
||||
}
|
||||
|
||||
let textRightInset: CGFloat
|
||||
if let _ = self.iconNode.image {
|
||||
textRightInset = iconSize.width - 2.0
|
||||
} else {
|
||||
textRightInset = 0.0
|
||||
}
|
||||
|
||||
let makeTextLayout = TextNodeWithEntities.asyncLayout(self.textNode)
|
||||
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, minimumNumberOfLines: 0, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: width - horizontalInset * 2.0 - textRightInset, height: .greatestFiniteMagnitude), alignment: .left, lineSpacing: 0.12, cutout: nil, insets: UIEdgeInsets(), lineColor: nil, textShadowColor: nil, textStroke: nil))
|
||||
let _ = textApply(self.arguments?.withUpdatedPlaceholderColor(shimmeringForegroundColor))
|
||||
|
||||
let textFrame = CGRect(origin: CGPoint(x: horizontalInset, y: topInset), size: textLayout.size)
|
||||
transition.updateFrame(node: self.textNode.textNode, frame: textFrame)
|
||||
if textFrame.size.height.isZero {
|
||||
self.textNode.textNode.alpha = 0.0
|
||||
} else if self.textNode.textNode.alpha.isZero {
|
||||
self.textNode.textNode.alpha = 1.0
|
||||
self.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
self.placeholderNode.layer.animateAlpha(from: self.placeholderNode.alpha, to: 1.0, duration: 0.2)
|
||||
}
|
||||
self.textNode.visibilityRect = CGRect.infinite
|
||||
|
||||
var contentHeight = textLayout.size.height
|
||||
if contentHeight.isZero {
|
||||
contentHeight = 32.0
|
||||
}
|
||||
|
||||
let size = CGSize(width: width, height: contentHeight + topInset + bottomInset)
|
||||
|
||||
let lineHeight: CGFloat = 8.0
|
||||
transition.updateFrame(node: self.placeholderNode, frame: CGRect(origin: CGPoint(x: horizontalInset, y: floorToScreenPixels((size.height - lineHeight) / 2.0)), size: CGSize(width: width - horizontalInset * 2.0, height: lineHeight)))
|
||||
transition.updateAlpha(node: self.placeholderNode, alpha: textFrame.height.isZero ? 1.0 : 0.0)
|
||||
|
||||
let iconFrame = CGRect(origin: CGPoint(x: iconSideInset + floor((standardIconWidth - iconSize.width) / 2.0), y: floor((size.height - iconSize.height) / 2.0)), size: iconSize)
|
||||
transition.updateFrame(node: self.iconNode, frame: iconFrame)
|
||||
|
||||
if let textSelectionNode = self.textSelectionNode {
|
||||
transition.updateFrame(node: textSelectionNode, frame: textFrame)
|
||||
textSelectionNode.highlightAreaNode.frame = textFrame
|
||||
}
|
||||
|
||||
return size
|
||||
}
|
||||
|
||||
func setActualSize(size: CGSize, transition: ContainedViewLayoutTransition) {
|
||||
let transition = ComponentTransition(transition)
|
||||
|
||||
if let background = self.background {
|
||||
background.container.update(size: size, isDark: self.presentationData.theme.overallDarkAppearance, transition: transition)
|
||||
transition.setFrame(view: background.container, frame: CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
background.background.update(size: size, cornerRadius: min(30.0, size.height * 0.5), isDark: self.presentationData.theme.overallDarkAppearance, tintColor: .init(kind: .panel), transition: transition)
|
||||
transition.setFrame(view: background.background, frame: CGRect(origin: CGPoint(), size: size))
|
||||
}
|
||||
}
|
||||
|
||||
func updateTheme(presentationData: PresentationData) {
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
if let textSelectionNode = self.textSelectionNode, let targetSelectionIndex = self.targetSelectionIndex {
|
||||
textSelectionNode.pretendInitiateSelection()
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5, execute: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.textSelectionNode?.pretendExtendSelection(to: targetSelectionIndex)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func updateHighlight(animated: Bool) {
|
||||
}
|
||||
|
||||
private var isButtonHighlighted = false
|
||||
private var isHighlighted = false
|
||||
func setHighlighted(_ highlighted: Bool) {
|
||||
guard self.isHighlighted != highlighted else {
|
||||
return
|
||||
}
|
||||
self.isHighlighted = highlighted
|
||||
|
||||
if highlighted {
|
||||
if self.hapticFeedback == nil {
|
||||
self.hapticFeedback = HapticFeedback()
|
||||
}
|
||||
self.hapticFeedback?.tap()
|
||||
}
|
||||
|
||||
self.updateHighlight(animated: false)
|
||||
}
|
||||
|
||||
func highlightGestureMoved(location: CGPoint) {
|
||||
if self.bounds.contains(location) && self.isUserInteractionEnabled {
|
||||
self.setHighlighted(true)
|
||||
} else {
|
||||
self.setHighlighted(false)
|
||||
}
|
||||
}
|
||||
|
||||
func highlightGestureFinished(performAction: Bool) {
|
||||
if self.isHighlighted {
|
||||
self.setHighlighted(false)
|
||||
if performAction {
|
||||
self.requestDismiss({
|
||||
self.action?()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class ContextActionsContainerNode: ASDisplayNode {
|
||||
private let presentationData: PresentationData
|
||||
private let getController: () -> ContextControllerProtocol?
|
||||
private let blurBackground: Bool
|
||||
private let shadowNode: ASImageNode
|
||||
private let additionalShadowNode: ASImageNode?
|
||||
private let additionalActionsNode: InnerActionsContainerNode?
|
||||
private let actionsNode: InnerActionsContainerNode
|
||||
private let scrollNode: ASScrollNode
|
||||
|
||||
private var tip: ContextController.Tip?
|
||||
private var textSelectionTipNode: InnerTextSelectionTipContainerNode?
|
||||
private var textSelectionTipNodeDisposable: Disposable?
|
||||
|
||||
var panSelectionGestureEnabled: Bool = true {
|
||||
didSet {
|
||||
if self.panSelectionGestureEnabled != oldValue {
|
||||
self.actionsNode.panSelectionGestureEnabled = self.panSelectionGestureEnabled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var hasAdditionalActions: Bool {
|
||||
return self.additionalActionsNode != nil
|
||||
}
|
||||
|
||||
init(presentationData: PresentationData, items: ContextController.Items, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void, requestLayout: @escaping () -> Void, feedbackTap: @escaping () -> Void, blurBackground: Bool) {
|
||||
self.presentationData = presentationData
|
||||
self.getController = getController
|
||||
self.blurBackground = blurBackground
|
||||
self.shadowNode = ASImageNode()
|
||||
self.shadowNode.displaysAsynchronously = false
|
||||
self.shadowNode.displayWithoutProcessing = true
|
||||
self.shadowNode.image = UIImage(bundleImageName: "Components/Context Menu/Shadow")?.stretchableImage(withLeftCapWidth: 60, topCapHeight: 60)
|
||||
self.shadowNode.contentMode = .scaleToFill
|
||||
self.shadowNode.isHidden = true
|
||||
|
||||
var items = items
|
||||
if case var .list(itemList) = items.content, let firstItem = itemList.first, case let .custom(_, additional) = firstItem, additional {
|
||||
let additionalShadowNode = ASImageNode()
|
||||
additionalShadowNode.displaysAsynchronously = false
|
||||
additionalShadowNode.displayWithoutProcessing = true
|
||||
additionalShadowNode.image = self.shadowNode.image
|
||||
additionalShadowNode.contentMode = .scaleToFill
|
||||
additionalShadowNode.isHidden = true
|
||||
self.additionalShadowNode = additionalShadowNode
|
||||
|
||||
self.additionalActionsNode = InnerActionsContainerNode(presentationData: presentationData, items: [firstItem], getController: getController, actionSelected: actionSelected, requestLayout: requestLayout, feedbackTap: feedbackTap, blurBackground: blurBackground)
|
||||
itemList.removeFirst()
|
||||
items.content = .list(itemList)
|
||||
} else {
|
||||
self.additionalShadowNode = nil
|
||||
self.additionalActionsNode = nil
|
||||
}
|
||||
|
||||
var itemList: [ContextMenuItem] = []
|
||||
if case let .list(list) = items.content {
|
||||
itemList = list
|
||||
}
|
||||
|
||||
self.actionsNode = InnerActionsContainerNode(presentationData: presentationData, items: itemList, getController: getController, actionSelected: actionSelected, requestLayout: requestLayout, feedbackTap: feedbackTap, blurBackground: blurBackground)
|
||||
|
||||
self.tip = items.tip
|
||||
|
||||
self.scrollNode = ASScrollNode()
|
||||
self.scrollNode.canCancelAllTouchesInViews = true
|
||||
self.scrollNode.view.delaysContentTouches = false
|
||||
self.scrollNode.view.showsVerticalScrollIndicator = false
|
||||
if #available(iOS 11.0, *) {
|
||||
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
|
||||
super.init()
|
||||
|
||||
self.additionalActionsNode.flatMap(self.scrollNode.addSubnode)
|
||||
self.scrollNode.addSubnode(self.actionsNode)
|
||||
self.addSubnode(self.scrollNode)
|
||||
|
||||
if let tipSignal = items.tipSignal {
|
||||
self.textSelectionTipNodeDisposable = (tipSignal
|
||||
|> deliverOnMainQueue).start(next: { [weak self] tip in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.tip = tip
|
||||
requestLayout()
|
||||
}).strict()
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.textSelectionTipNodeDisposable?.dispose()
|
||||
}
|
||||
|
||||
func updateLayout(widthClass: ContainerViewLayoutSizeClass, presentation: ContextControllerActionsStackNode.Presentation, constrainedWidth: CGFloat, constrainedHeight: CGFloat, transition: ContainedViewLayoutTransition) -> CGSize {
|
||||
var widthClass = widthClass
|
||||
if !self.blurBackground {
|
||||
widthClass = .regular
|
||||
}
|
||||
|
||||
var contentSize = CGSize()
|
||||
let actionsSize = self.actionsNode.updateLayout(widthClass: widthClass, constrainedWidth: constrainedWidth, constrainedHeight: constrainedHeight, minimalWidth: nil, transition: transition)
|
||||
|
||||
if let additionalActionsNode = self.additionalActionsNode, let additionalShadowNode = self.additionalShadowNode {
|
||||
let additionalActionsSize = additionalActionsNode.updateLayout(widthClass: widthClass, constrainedWidth: actionsSize.width, constrainedHeight: constrainedHeight, minimalWidth: actionsSize.width, transition: transition)
|
||||
contentSize = additionalActionsSize
|
||||
|
||||
let bounds = CGRect(origin: CGPoint(), size: additionalActionsSize)
|
||||
transition.updateFrame(node: additionalShadowNode, frame: bounds.insetBy(dx: -30.0, dy: -30.0))
|
||||
additionalShadowNode.isHidden = widthClass == .compact
|
||||
|
||||
transition.updateFrame(node: additionalActionsNode, frame: CGRect(origin: CGPoint(), size: additionalActionsSize))
|
||||
contentSize.height += 8.0
|
||||
}
|
||||
|
||||
let bounds = CGRect(origin: CGPoint(x: 0.0, y: contentSize.height), size: actionsSize)
|
||||
transition.updateFrame(node: self.shadowNode, frame: bounds.insetBy(dx: -30.0, dy: -30.0))
|
||||
self.shadowNode.isHidden = widthClass == .compact
|
||||
|
||||
contentSize.width = max(contentSize.width, actionsSize.width)
|
||||
contentSize.height += actionsSize.height
|
||||
|
||||
transition.updateFrame(node: self.actionsNode, frame: bounds)
|
||||
|
||||
if let tip = self.tip {
|
||||
if let textSelectionTipNode = self.textSelectionTipNode, textSelectionTipNode.tip == tip {
|
||||
} else {
|
||||
if let textSelectionTipNode = self.textSelectionTipNode {
|
||||
self.textSelectionTipNode = nil
|
||||
textSelectionTipNode.removeFromSupernode()
|
||||
}
|
||||
|
||||
let textSelectionTipNode = InnerTextSelectionTipContainerNode(presentationData: self.presentationData, tip: tip, isInline: false)
|
||||
let getController = self.getController
|
||||
textSelectionTipNode.requestDismiss = { completion in
|
||||
getController()?.dismiss(completion: completion)
|
||||
}
|
||||
self.textSelectionTipNode = textSelectionTipNode
|
||||
self.scrollNode.addSubnode(textSelectionTipNode)
|
||||
}
|
||||
} else {
|
||||
if let textSelectionTipNode = self.textSelectionTipNode {
|
||||
self.textSelectionTipNode = nil
|
||||
textSelectionTipNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
|
||||
if let textSelectionTipNode = self.textSelectionTipNode {
|
||||
contentSize.height += 8.0
|
||||
let textSelectionTipSize = textSelectionTipNode.updateLayout(widthClass: widthClass, presentation: presentation, width: actionsSize.width, transition: transition)
|
||||
transition.updateFrame(node: textSelectionTipNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentSize.height), size: textSelectionTipSize))
|
||||
textSelectionTipNode.setActualSize(size: textSelectionTipSize, transition: transition)
|
||||
contentSize.height += textSelectionTipSize.height
|
||||
}
|
||||
|
||||
return contentSize
|
||||
}
|
||||
|
||||
func updateSize(containerSize: CGSize, contentSize: CGSize) {
|
||||
self.scrollNode.view.contentSize = contentSize
|
||||
self.scrollNode.frame = CGRect(origin: CGPoint(), size: containerSize)
|
||||
}
|
||||
|
||||
func actionNode(at point: CGPoint) -> ContextActionNodeProtocol? {
|
||||
return self.actionsNode.actionNode(at: self.view.convert(point, to: self.actionsNode.view))
|
||||
}
|
||||
|
||||
func updateTheme(presentationData: PresentationData) {
|
||||
self.actionsNode.updateTheme(presentationData: presentationData)
|
||||
self.textSelectionTipNode?.updateTheme(presentationData: presentationData)
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
self.textSelectionTipNode?.animateIn()
|
||||
}
|
||||
|
||||
func animateOut(offset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
guard let additionalActionsNode = self.additionalActionsNode, let additionalShadowNode = self.additionalShadowNode else {
|
||||
return
|
||||
}
|
||||
|
||||
transition.animatePosition(node: additionalActionsNode, to: CGPoint(x: 0.0, y: offset / 2.0), additive: true)
|
||||
transition.animatePosition(node: additionalShadowNode, to: CGPoint(x: 0.0, y: offset / 2.0), additive: true)
|
||||
additionalActionsNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||
additionalShadowNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||
additionalActionsNode.layer.animateScale(from: 1.0, to: 0.75, duration: 0.15, removeOnCompletion: false)
|
||||
additionalShadowNode.layer.animateScale(from: 1.0, to: 0.75, duration: 0.15, removeOnCompletion: false)
|
||||
}
|
||||
}
|
||||
+2206
File diff suppressed because it is too large
Load Diff
+6
@@ -0,0 +1,6 @@
|
||||
import ContextUI
|
||||
|
||||
public typealias ContextControllerImpl = ContextController
|
||||
public typealias ContextControllerActionsStackNodeImpl = ContextControllerActionsStackNode
|
||||
public typealias PeekControllerImpl = PeekController
|
||||
public typealias PinchControllerImpl = PinchController
|
||||
+1835
File diff suppressed because it is too large
Load Diff
+2203
File diff suppressed because it is too large
Load Diff
+42
@@ -0,0 +1,42 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import TelegramPresentationData
|
||||
import TextSelectionNode
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import ReactionSelectionNode
|
||||
import ContextUI
|
||||
|
||||
enum ContextControllerPresentationNodeStateTransition {
|
||||
case animateIn
|
||||
case animateOut(result: ContextMenuActionResult, completion: () -> Void)
|
||||
}
|
||||
|
||||
protocol ContextControllerPresentationNode: ASDisplayNode {
|
||||
var ready: Signal<Bool, NoError> { get }
|
||||
|
||||
func replaceItems(items: ContextController.Items, animated: Bool?)
|
||||
func pushItems(items: ContextController.Items)
|
||||
func popItems()
|
||||
func wantsDisplayBelowKeyboard() -> Bool
|
||||
|
||||
func update(
|
||||
presentationData: PresentationData,
|
||||
layout: ContainerViewLayout,
|
||||
transition: ContainedViewLayoutTransition,
|
||||
stateTransition: ContextControllerPresentationNodeStateTransition?
|
||||
)
|
||||
|
||||
func animateOutToReaction(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, reducedCurve: Bool, onHit: (() -> Void)?, completion: @escaping () -> Void)
|
||||
func cancelReactionAnimation()
|
||||
|
||||
func highlightGestureMoved(location: CGPoint, hover: Bool)
|
||||
func highlightGestureFinished(performAction: Bool)
|
||||
|
||||
func decreaseHighlightedIndex()
|
||||
func increaseHighlightedIndex()
|
||||
|
||||
func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition)
|
||||
}
|
||||
+886
@@ -0,0 +1,886 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import TelegramPresentationData
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import ReactionSelectionNode
|
||||
import ComponentFlow
|
||||
import TabSelectorComponent
|
||||
import PlainButtonComponent
|
||||
import MultilineTextComponent
|
||||
import ComponentDisplayAdapters
|
||||
import AccountContext
|
||||
import ContextUI
|
||||
|
||||
final class ContextSourceContainer: ASDisplayNode {
|
||||
final class Source {
|
||||
weak var controller: ContextControllerImpl?
|
||||
|
||||
let id: AnyHashable
|
||||
let title: String
|
||||
let footer: String?
|
||||
let context: AccountContext?
|
||||
let source: ContextContentSource
|
||||
let closeActionTitle: String?
|
||||
let closeAction: (() -> Void)?
|
||||
|
||||
private var _presentationNode: ContextControllerPresentationNode?
|
||||
var presentationNode: ContextControllerPresentationNode {
|
||||
return self._presentationNode!
|
||||
}
|
||||
|
||||
var currentPresentationStateTransition: ContextControllerPresentationNodeStateTransition?
|
||||
|
||||
var validLayout: ContainerViewLayout?
|
||||
var presentationData: PresentationData?
|
||||
var delayLayoutUpdate: Bool = false
|
||||
var isAnimatingOut: Bool = false
|
||||
|
||||
var itemsDisposables = DisposableSet()
|
||||
|
||||
let ready = Promise<Bool>()
|
||||
private let contentReady = Promise<Bool>()
|
||||
private let actionsReady = Promise<Bool>()
|
||||
|
||||
init(
|
||||
controller: ContextControllerImpl,
|
||||
id: AnyHashable,
|
||||
title: String,
|
||||
footer: String?,
|
||||
context: AccountContext?,
|
||||
source: ContextContentSource,
|
||||
items: Signal<ContextController.Items, NoError>,
|
||||
closeActionTitle: String? = nil,
|
||||
closeAction: (() -> Void)? = nil
|
||||
) {
|
||||
self.controller = controller
|
||||
self.id = id
|
||||
self.title = title
|
||||
self.footer = footer
|
||||
self.context = context
|
||||
self.source = source
|
||||
self.closeActionTitle = closeActionTitle
|
||||
self.closeAction = closeAction
|
||||
|
||||
self.ready.set(combineLatest(queue: .mainQueue(), self.contentReady.get(), self.actionsReady.get())
|
||||
|> map { a, b -> Bool in
|
||||
return a && b
|
||||
}
|
||||
|> distinctUntilChanged)
|
||||
|
||||
switch source {
|
||||
case let .location(source):
|
||||
self.contentReady.set(.single(true))
|
||||
|
||||
let presentationNode = ContextControllerExtractedPresentationNode(
|
||||
context: self.context,
|
||||
getController: { [weak self] in
|
||||
guard let self else {
|
||||
return nil
|
||||
}
|
||||
return self.controller
|
||||
},
|
||||
requestUpdate: { [weak self] transition in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.update(transition: transition)
|
||||
},
|
||||
requestUpdateOverlayWantsToBeBelowKeyboard: { [weak self] transition in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if let controller = self.controller {
|
||||
controller.overlayWantsToBeBelowKeyboardUpdated(transition: transition)
|
||||
}
|
||||
},
|
||||
requestDismiss: { [weak self] result in
|
||||
guard let self, let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
controller.controllerNode.dismissedForCancel?()
|
||||
controller.controllerNode.beginDismiss(result)
|
||||
},
|
||||
requestAnimateOut: { [weak self] result, completion in
|
||||
guard let self, let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
controller.controllerNode.animateOut(result: result, completion: completion)
|
||||
},
|
||||
source: .location(source)
|
||||
)
|
||||
self._presentationNode = presentationNode
|
||||
case let .reference(source):
|
||||
self.contentReady.set(.single(true))
|
||||
|
||||
let presentationNode = ContextControllerExtractedPresentationNode(
|
||||
context: self.context,
|
||||
getController: { [weak self] in
|
||||
guard let self else {
|
||||
return nil
|
||||
}
|
||||
return self.controller
|
||||
},
|
||||
requestUpdate: { [weak self] transition in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.update(transition: transition)
|
||||
},
|
||||
requestUpdateOverlayWantsToBeBelowKeyboard: { [weak self] transition in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if let controller = self.controller {
|
||||
controller.overlayWantsToBeBelowKeyboardUpdated(transition: transition)
|
||||
}
|
||||
},
|
||||
requestDismiss: { [weak self] result in
|
||||
guard let self, let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
controller.controllerNode.dismissedForCancel?()
|
||||
controller.controllerNode.beginDismiss(result)
|
||||
},
|
||||
requestAnimateOut: { [weak self] result, completion in
|
||||
guard let self, let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
controller.controllerNode.animateOut(result: result, completion: completion)
|
||||
},
|
||||
source: .reference(source)
|
||||
)
|
||||
self._presentationNode = presentationNode
|
||||
case let .extracted(source):
|
||||
self.contentReady.set(.single(true))
|
||||
|
||||
let presentationNode = ContextControllerExtractedPresentationNode(
|
||||
context: self.context,
|
||||
getController: { [weak self] in
|
||||
guard let self else {
|
||||
return nil
|
||||
}
|
||||
return self.controller
|
||||
},
|
||||
requestUpdate: { [weak self] transition in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.update(transition: transition)
|
||||
},
|
||||
requestUpdateOverlayWantsToBeBelowKeyboard: { [weak self] transition in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if let controller = self.controller {
|
||||
controller.overlayWantsToBeBelowKeyboardUpdated(transition: transition)
|
||||
}
|
||||
},
|
||||
requestDismiss: { [weak self] result in
|
||||
guard let self, let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
if let _ = self.closeActionTitle {
|
||||
} else {
|
||||
controller.controllerNode.dismissedForCancel?()
|
||||
controller.controllerNode.beginDismiss(result)
|
||||
}
|
||||
},
|
||||
requestAnimateOut: { [weak self] result, completion in
|
||||
guard let self, let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
controller.controllerNode.animateOut(result: result, completion: completion)
|
||||
},
|
||||
source: .extracted(source)
|
||||
)
|
||||
self._presentationNode = presentationNode
|
||||
case let .controller(source):
|
||||
self.contentReady.set(source.controller.ready.get())
|
||||
|
||||
let presentationNode = ContextControllerExtractedPresentationNode(
|
||||
context: self.context,
|
||||
getController: { [weak self] in
|
||||
guard let self else {
|
||||
return nil
|
||||
}
|
||||
return self.controller
|
||||
},
|
||||
requestUpdate: { [weak self] transition in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.update(transition: transition)
|
||||
},
|
||||
requestUpdateOverlayWantsToBeBelowKeyboard: { [weak self] transition in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if let controller = self.controller {
|
||||
controller.overlayWantsToBeBelowKeyboardUpdated(transition: transition)
|
||||
}
|
||||
},
|
||||
requestDismiss: { [weak self] result in
|
||||
guard let self, let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
controller.controllerNode.dismissedForCancel?()
|
||||
controller.controllerNode.beginDismiss(result)
|
||||
},
|
||||
requestAnimateOut: { [weak self] result, completion in
|
||||
guard let self, let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
controller.controllerNode.animateOut(result: result, completion: completion)
|
||||
},
|
||||
source: .controller(source)
|
||||
)
|
||||
self._presentationNode = presentationNode
|
||||
}
|
||||
|
||||
self.itemsDisposables.add((items |> deliverOnMainQueue).start(next: { [weak self] items in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
self.setItems(items: items, animated: nil)
|
||||
self.actionsReady.set(.single(true))
|
||||
}))
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.itemsDisposables.dispose()
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
self.currentPresentationStateTransition = .animateIn
|
||||
self.update(transition: .animated(duration: 0.5, curve: .spring))
|
||||
}
|
||||
|
||||
func animateOut(result: ContextMenuActionResult, completion: @escaping () -> Void) {
|
||||
self.currentPresentationStateTransition = .animateOut(result: result, completion: completion)
|
||||
if let _ = self.validLayout {
|
||||
if case let .custom(transition) = result {
|
||||
self.delayLayoutUpdate = true
|
||||
Queue.mainQueue().after(0.1) {
|
||||
self.delayLayoutUpdate = false
|
||||
self.update(transition: transition)
|
||||
self.isAnimatingOut = true
|
||||
}
|
||||
} else {
|
||||
self.update(transition: .animated(duration: 0.35, curve: .easeInOut))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) {
|
||||
self.presentationNode.addRelativeContentOffset(offset, transition: transition)
|
||||
}
|
||||
|
||||
func cancelReactionAnimation() {
|
||||
self.presentationNode.cancelReactionAnimation()
|
||||
}
|
||||
|
||||
func animateOutToReaction(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, reducedCurve: Bool, onHit: (() -> Void)?, completion: @escaping () -> Void) {
|
||||
self.presentationNode.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, animateTargetContainer: animateTargetContainer, addStandaloneReactionAnimation: addStandaloneReactionAnimation, reducedCurve: reducedCurve, onHit: onHit, completion: completion)
|
||||
}
|
||||
|
||||
func setItems(items: Signal<ContextController.Items, NoError>, animated: Bool) {
|
||||
self.itemsDisposables.dispose()
|
||||
self.itemsDisposables = DisposableSet()
|
||||
self.itemsDisposables.add((items
|
||||
|> deliverOnMainQueue).start(next: { [weak self] items in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.setItems(items: items, animated: animated)
|
||||
}))
|
||||
}
|
||||
|
||||
func setItems(items: ContextController.Items, animated: Bool?) {
|
||||
self.presentationNode.replaceItems(items: items, animated: animated)
|
||||
}
|
||||
|
||||
func pushItems(items: Signal<ContextController.Items, NoError>) {
|
||||
self.itemsDisposables.add((items
|
||||
|> deliverOnMainQueue).start(next: { [weak self] items in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.presentationNode.pushItems(items: items)
|
||||
}))
|
||||
}
|
||||
|
||||
func popItems() {
|
||||
self.itemsDisposables.removeLast()
|
||||
self.presentationNode.popItems()
|
||||
}
|
||||
|
||||
func update(transition: ContainedViewLayoutTransition) {
|
||||
guard let validLayout = self.validLayout else {
|
||||
return
|
||||
}
|
||||
guard let presentationData = self.presentationData else {
|
||||
return
|
||||
}
|
||||
self.update(presentationData: presentationData, layout: validLayout, transition: transition)
|
||||
}
|
||||
|
||||
func update(
|
||||
presentationData: PresentationData,
|
||||
layout: ContainerViewLayout,
|
||||
transition: ContainedViewLayoutTransition
|
||||
) {
|
||||
if self.isAnimatingOut || self.delayLayoutUpdate {
|
||||
return
|
||||
}
|
||||
|
||||
self.validLayout = layout
|
||||
self.presentationData = presentationData
|
||||
|
||||
let presentationStateTransition = self.currentPresentationStateTransition
|
||||
self.currentPresentationStateTransition = .none
|
||||
|
||||
self.presentationNode.update(
|
||||
presentationData: presentationData,
|
||||
layout: layout,
|
||||
transition: transition,
|
||||
stateTransition: presentationStateTransition
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private struct PanState {
|
||||
var fraction: CGFloat
|
||||
|
||||
init(fraction: CGFloat) {
|
||||
self.fraction = fraction
|
||||
}
|
||||
}
|
||||
|
||||
private weak var controller: ContextControllerImpl?
|
||||
|
||||
private let backgroundNode: NavigationBackgroundNode
|
||||
|
||||
var sources: [Source] = []
|
||||
var activeIndex: Int = 0
|
||||
|
||||
private var tabSelector: ComponentView<Empty>?
|
||||
private var footer: ComponentView<Empty>?
|
||||
private var closeButton: ComponentView<Empty>?
|
||||
|
||||
private var presentationData: PresentationData?
|
||||
private var validLayout: ContainerViewLayout?
|
||||
private var panState: PanState?
|
||||
|
||||
let ready = Promise<Bool>()
|
||||
|
||||
var activeSource: Source? {
|
||||
if self.activeIndex >= self.sources.count {
|
||||
return nil
|
||||
}
|
||||
return self.sources[self.activeIndex]
|
||||
}
|
||||
|
||||
var overlayWantsToBeBelowKeyboard: Bool {
|
||||
return self.activeSource?.presentationNode.wantsDisplayBelowKeyboard() ?? false
|
||||
}
|
||||
|
||||
init(controller: ContextControllerImpl, configuration: ContextController.Configuration, context: AccountContext?) {
|
||||
self.controller = controller
|
||||
|
||||
self.backgroundNode = NavigationBackgroundNode(color: .clear, enableBlur: false)
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
|
||||
for i in 0 ..< configuration.sources.count {
|
||||
let source = configuration.sources[i]
|
||||
|
||||
let mappedSource = Source(
|
||||
controller: controller,
|
||||
id: source.id,
|
||||
title: source.title,
|
||||
footer: source.footer,
|
||||
context: context,
|
||||
source: source.source,
|
||||
items: source.items,
|
||||
closeActionTitle: source.closeActionTitle,
|
||||
closeAction: source.closeAction
|
||||
)
|
||||
self.sources.append(mappedSource)
|
||||
self.addSubnode(mappedSource.presentationNode)
|
||||
|
||||
if source.id == configuration.initialId {
|
||||
self.activeIndex = i
|
||||
}
|
||||
}
|
||||
|
||||
self.ready.set(self.sources[self.activeIndex].ready.get())
|
||||
|
||||
self.view.addGestureRecognizer(InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] _ in
|
||||
guard let self else {
|
||||
return []
|
||||
}
|
||||
if self.sources.count <= 1 {
|
||||
return []
|
||||
}
|
||||
return [.left, .right]
|
||||
}))
|
||||
}
|
||||
|
||||
@objc private func panGesture(_ recognizer: InteractiveTransitionGestureRecognizer) {
|
||||
switch recognizer.state {
|
||||
case .began, .changed:
|
||||
if let validLayout = self.validLayout {
|
||||
var translationX = recognizer.translation(in: self.view).x
|
||||
if self.activeIndex == 0 && translationX > 0.0 {
|
||||
translationX = scrollingRubberBandingOffset(offset: abs(translationX), bandingStart: 0.0, range: 20.0)
|
||||
} else if self.activeIndex == self.sources.count - 1 && translationX < 0.0 {
|
||||
translationX = -scrollingRubberBandingOffset(offset: abs(translationX), bandingStart: 0.0, range: 20.0)
|
||||
}
|
||||
|
||||
self.panState = PanState(fraction: translationX / validLayout.size.width)
|
||||
self.update(transition: .immediate)
|
||||
}
|
||||
case .cancelled, .ended:
|
||||
if let panState = self.panState {
|
||||
self.panState = nil
|
||||
|
||||
let velocity = recognizer.velocity(in: self.view)
|
||||
|
||||
var nextIndex = self.activeIndex
|
||||
if panState.fraction < -0.4 {
|
||||
nextIndex += 1
|
||||
} else if panState.fraction > 0.4 {
|
||||
nextIndex -= 1
|
||||
} else if abs(velocity.x) >= 200.0 {
|
||||
if velocity.x < 0.0 {
|
||||
nextIndex += 1
|
||||
} else {
|
||||
nextIndex -= 1
|
||||
}
|
||||
}
|
||||
if nextIndex < 0 {
|
||||
nextIndex = 0
|
||||
}
|
||||
if nextIndex > self.sources.count - 1 {
|
||||
nextIndex = self.sources.count - 1
|
||||
}
|
||||
if nextIndex != self.activeIndex {
|
||||
self.activeIndex = nextIndex
|
||||
}
|
||||
|
||||
self.update(transition: .animated(duration: 0.4, curve: .spring))
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
|
||||
// if let activeSource = self.activeSource {
|
||||
// activeSource.animateIn()
|
||||
// }
|
||||
for source in self.sources {
|
||||
source.animateIn()
|
||||
}
|
||||
if let footerView = self.footer?.view {
|
||||
footerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
if let tabSelectorView = self.tabSelector?.view {
|
||||
tabSelectorView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
if let closeButtonView = self.closeButton?.view {
|
||||
closeButtonView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
|
||||
func animateOut(result: ContextMenuActionResult, completion: @escaping () -> Void) {
|
||||
let delayDismissal = self.activeSource?.closeAction != nil
|
||||
let delay: Double = delayDismissal ? 0.2 : 0.0
|
||||
let duration: Double = delayDismissal ? 0.35 : 0.2
|
||||
|
||||
self.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, delay: delay, removeOnCompletion: false, completion: { _ in
|
||||
if delayDismissal {
|
||||
Queue.mainQueue().after(0.55) {
|
||||
completion()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if let footerView = self.footer?.view {
|
||||
footerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, delay: delay, removeOnCompletion: false)
|
||||
}
|
||||
if let tabSelectorView = self.tabSelector?.view {
|
||||
tabSelectorView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, delay: delay, removeOnCompletion: false)
|
||||
}
|
||||
if let closeButtonView = self.closeButton?.view {
|
||||
closeButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, delay: delay, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
for source in self.sources {
|
||||
if source !== self.activeSource {
|
||||
source.animateOut(result: result, completion: {})
|
||||
}
|
||||
}
|
||||
|
||||
if let activeSource = self.activeSource {
|
||||
activeSource.animateOut(result: result, completion: delayDismissal ? {} : completion)
|
||||
} else {
|
||||
completion()
|
||||
}
|
||||
}
|
||||
|
||||
func highlightGestureMoved(location: CGPoint, hover: Bool) {
|
||||
if self.activeIndex >= self.sources.count {
|
||||
return
|
||||
}
|
||||
self.sources[self.activeIndex].presentationNode.highlightGestureMoved(location: location, hover: hover)
|
||||
}
|
||||
|
||||
func highlightGestureFinished(performAction: Bool) {
|
||||
if self.activeIndex >= self.sources.count {
|
||||
return
|
||||
}
|
||||
self.sources[self.activeIndex].presentationNode.highlightGestureFinished(performAction: performAction)
|
||||
}
|
||||
|
||||
func performHighlightedAction() {
|
||||
self.activeSource?.presentationNode.highlightGestureFinished(performAction: true)
|
||||
}
|
||||
|
||||
func decreaseHighlightedIndex() {
|
||||
self.activeSource?.presentationNode.decreaseHighlightedIndex()
|
||||
}
|
||||
|
||||
func increaseHighlightedIndex() {
|
||||
self.activeSource?.presentationNode.increaseHighlightedIndex()
|
||||
}
|
||||
|
||||
func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) {
|
||||
if let activeSource = self.activeSource {
|
||||
activeSource.addRelativeContentOffset(offset, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
func cancelReactionAnimation() {
|
||||
if let activeSource = self.activeSource {
|
||||
activeSource.cancelReactionAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
func animateOutToReaction(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, reducedCurve: Bool, onHit: (() -> Void)?, completion: @escaping () -> Void) {
|
||||
if let activeSource = self.activeSource {
|
||||
activeSource.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, animateTargetContainer: animateTargetContainer, addStandaloneReactionAnimation: addStandaloneReactionAnimation, reducedCurve: reducedCurve, onHit: onHit, completion: completion)
|
||||
} else {
|
||||
completion()
|
||||
}
|
||||
}
|
||||
|
||||
func setItems(items: Signal<ContextController.Items, NoError>, animated: Bool) {
|
||||
if let activeSource = self.activeSource {
|
||||
activeSource.setItems(items: items, animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
func pushItems(items: Signal<ContextController.Items, NoError>) {
|
||||
if let activeSource = self.activeSource {
|
||||
activeSource.pushItems(items: items)
|
||||
}
|
||||
}
|
||||
|
||||
func popItems() {
|
||||
if let activeSource = self.activeSource {
|
||||
activeSource.popItems()
|
||||
}
|
||||
}
|
||||
|
||||
private func update(transition: ContainedViewLayoutTransition) {
|
||||
if let presentationData = self.presentationData, let validLayout = self.validLayout {
|
||||
self.update(presentationData: presentationData, layout: validLayout, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
func update(
|
||||
presentationData: PresentationData,
|
||||
layout: ContainerViewLayout,
|
||||
transition: ContainedViewLayoutTransition
|
||||
) {
|
||||
self.presentationData = presentationData
|
||||
self.validLayout = layout
|
||||
|
||||
var childLayout = layout
|
||||
|
||||
if let activeSource = self.activeSource {
|
||||
switch activeSource.source {
|
||||
case .location, .reference:
|
||||
self.backgroundNode.updateColor(
|
||||
color: .clear,
|
||||
enableBlur: false,
|
||||
forceKeepBlur: false,
|
||||
transition: .immediate
|
||||
)
|
||||
case let .extracted(extracted):
|
||||
self.backgroundNode.updateColor(
|
||||
color: extracted.blurBackground ? presentationData.theme.contextMenu.dimColor : .clear,
|
||||
enableBlur: extracted.blurBackground,
|
||||
forceKeepBlur: extracted.blurBackground,
|
||||
transition: .immediate
|
||||
)
|
||||
case .controller:
|
||||
if case .regular = layout.metrics.widthClass {
|
||||
self.backgroundNode.updateColor(
|
||||
color: UIColor(white: 0.0, alpha: 0.4),
|
||||
enableBlur: false,
|
||||
forceKeepBlur: false,
|
||||
transition: .immediate
|
||||
)
|
||||
} else {
|
||||
self.backgroundNode.updateColor(
|
||||
color: presentationData.theme.contextMenu.dimColor,
|
||||
enableBlur: true,
|
||||
forceKeepBlur: true,
|
||||
transition: .immediate
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: layout.size), beginWithCurrentState: true)
|
||||
self.backgroundNode.update(size: layout.size, transition: transition)
|
||||
|
||||
if self.sources.count > 1 {
|
||||
let tabSelector: ComponentView<Empty>
|
||||
if let current = self.tabSelector {
|
||||
tabSelector = current
|
||||
} else {
|
||||
tabSelector = ComponentView()
|
||||
self.tabSelector = tabSelector
|
||||
}
|
||||
let mappedItems = self.sources.map { source -> TabSelectorComponent.Item in
|
||||
return TabSelectorComponent.Item(id: source.id, title: source.title)
|
||||
}
|
||||
let tabSelectorSize = tabSelector.update(
|
||||
transition: ComponentTransition(transition),
|
||||
component: AnyComponent(TabSelectorComponent(
|
||||
colors: TabSelectorComponent.Colors(
|
||||
foreground: presentationData.theme.contextMenu.primaryColor.withMultipliedAlpha(0.8),
|
||||
selection: presentationData.theme.contextMenu.primaryColor.withMultipliedAlpha(0.1)
|
||||
),
|
||||
theme: presentationData.theme,
|
||||
customLayout: TabSelectorComponent.CustomLayout(
|
||||
font: Font.medium(14.0),
|
||||
spacing: 9.0
|
||||
),
|
||||
items: mappedItems,
|
||||
selectedId: self.activeSource?.id,
|
||||
setSelectedId: { [weak self] id in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if let index = self.sources.firstIndex(where: { $0.id == id }) {
|
||||
self.activeIndex = index
|
||||
self.update(transition: .animated(duration: 0.4, curve: .spring))
|
||||
}
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: layout.size.width, height: 44.0)
|
||||
)
|
||||
childLayout.intrinsicInsets.bottom += 30.0
|
||||
|
||||
if let footerText = self.activeSource?.footer {
|
||||
var footerTransition = transition
|
||||
let footer: ComponentView<Empty>
|
||||
if let current = self.footer {
|
||||
footer = current
|
||||
} else {
|
||||
footerTransition = .immediate
|
||||
footer = ComponentView()
|
||||
self.footer = footer
|
||||
}
|
||||
|
||||
let footerSize = footer.update(
|
||||
transition: ComponentTransition(footerTransition),
|
||||
component: AnyComponent(
|
||||
MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: footerText, font: Font.regular(13.0), textColor: presentationData.theme.contextMenu.primaryColor.withMultipliedAlpha(0.4))),
|
||||
horizontalAlignment: .center,
|
||||
maximumNumberOfLines: 0,
|
||||
lineSpacing: 0.1
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: layout.size.width, height: 144.0)
|
||||
)
|
||||
|
||||
let spacing: CGFloat = 20.0
|
||||
childLayout.intrinsicInsets.bottom += footerSize.height + spacing
|
||||
|
||||
if let footerView = footer.view {
|
||||
if footerView.superview == nil {
|
||||
self.view.addSubview(footerView)
|
||||
|
||||
footerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||
}
|
||||
footerTransition.updateFrame(view: footerView, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - footerSize.width) * 0.5), y: layout.size.height - layout.intrinsicInsets.bottom - tabSelectorSize.height - footerSize.height - spacing), size: footerSize))
|
||||
}
|
||||
} else if let footer = self.footer {
|
||||
self.footer = nil
|
||||
footer.view?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
|
||||
footer.view?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
|
||||
if let tabSelectorView = tabSelector.view {
|
||||
if tabSelectorView.superview == nil {
|
||||
self.view.addSubview(tabSelectorView)
|
||||
}
|
||||
transition.updateFrame(view: tabSelectorView, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - tabSelectorSize.width) * 0.5), y: layout.size.height - layout.intrinsicInsets.bottom - tabSelectorSize.height), size: tabSelectorSize))
|
||||
}
|
||||
} else if let source = self.sources.first, let closeActionTitle = source.closeActionTitle {
|
||||
let closeButton: ComponentView<Empty>
|
||||
if let current = self.closeButton {
|
||||
closeButton = current
|
||||
} else {
|
||||
closeButton = ComponentView()
|
||||
self.closeButton = closeButton
|
||||
}
|
||||
|
||||
let closeButtonSize = closeButton.update(
|
||||
transition: ComponentTransition(transition),
|
||||
component: AnyComponent(PlainButtonComponent(
|
||||
content: AnyComponent(
|
||||
CloseButtonComponent(
|
||||
backgroundColor: presentationData.theme.contextMenu.primaryColor.withMultipliedAlpha(0.1),
|
||||
text: closeActionTitle
|
||||
)
|
||||
),
|
||||
effectAlignment: .center,
|
||||
action: { [weak self, weak source] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if let source, let closeAction = source.closeAction {
|
||||
closeAction()
|
||||
} else {
|
||||
self.controller?.dismiss(result: .dismissWithoutContent, completion: nil)
|
||||
}
|
||||
},
|
||||
animateAlpha: false
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: layout.size.width, height: 44.0)
|
||||
)
|
||||
childLayout.intrinsicInsets.bottom += 30.0
|
||||
|
||||
if let closeButtonView = closeButton.view {
|
||||
if closeButtonView.superview == nil {
|
||||
self.view.addSubview(closeButtonView)
|
||||
}
|
||||
transition.updateFrame(view: closeButtonView, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - closeButtonSize.width) * 0.5), y: layout.size.height - layout.intrinsicInsets.bottom - closeButtonSize.height - 10.0), size: closeButtonSize))
|
||||
}
|
||||
} else if let tabSelector = self.tabSelector {
|
||||
self.tabSelector = nil
|
||||
tabSelector.view?.removeFromSuperview()
|
||||
}
|
||||
|
||||
for i in 0 ..< self.sources.count {
|
||||
var itemFrame = CGRect(origin: CGPoint(), size: childLayout.size)
|
||||
itemFrame.origin.x += CGFloat(i - self.activeIndex) * childLayout.size.width
|
||||
if let panState = self.panState {
|
||||
itemFrame.origin.x += panState.fraction * childLayout.size.width
|
||||
}
|
||||
|
||||
let itemTransition = transition
|
||||
itemTransition.updateFrame(node: self.sources[i].presentationNode, frame: itemFrame)
|
||||
self.sources[i].update(
|
||||
presentationData: presentationData,
|
||||
layout: childLayout,
|
||||
transition: itemTransition
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if let tabSelectorView = self.tabSelector?.view {
|
||||
if let result = tabSelectorView.hitTest(self.view.convert(point, to: tabSelectorView), with: event) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
if let closeButtonView = self.closeButton?.view {
|
||||
if let result = closeButtonView.hitTest(self.view.convert(point, to: closeButtonView), with: event) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
guard let activeSource = self.activeSource else {
|
||||
return nil
|
||||
}
|
||||
return activeSource.presentationNode.view.hitTest(point, with: event)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private final class CloseButtonComponent: CombinedComponent {
|
||||
let backgroundColor: UIColor
|
||||
let text: String
|
||||
|
||||
init(
|
||||
backgroundColor: UIColor,
|
||||
text: String
|
||||
) {
|
||||
self.backgroundColor = backgroundColor
|
||||
self.text = text
|
||||
}
|
||||
|
||||
static func ==(lhs: CloseButtonComponent, rhs: CloseButtonComponent) -> Bool {
|
||||
if lhs.backgroundColor != rhs.backgroundColor {
|
||||
return false
|
||||
}
|
||||
if lhs.text != rhs.text {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
static var body: Body {
|
||||
let background = Child(RoundedRectangle.self)
|
||||
let text = Child(Text.self)
|
||||
|
||||
return { context in
|
||||
let text = text.update(
|
||||
component: Text(
|
||||
text: "\(context.component.text)",
|
||||
font: Font.regular(17.0),
|
||||
color: .white
|
||||
),
|
||||
availableSize: CGSize(width: 200.0, height: 100.0),
|
||||
transition: .immediate
|
||||
)
|
||||
|
||||
let backgroundSize = CGSize(width: text.size.width + 34.0, height: 36.0)
|
||||
let background = background.update(
|
||||
component: RoundedRectangle(color: context.component.backgroundColor, cornerRadius: 18.0),
|
||||
availableSize: backgroundSize,
|
||||
transition: .immediate
|
||||
)
|
||||
|
||||
context.add(background
|
||||
.position(CGPoint(x: backgroundSize.width / 2.0, y: backgroundSize.height / 2.0))
|
||||
)
|
||||
|
||||
context.add(text
|
||||
.position(CGPoint(x: backgroundSize.width / 2.0, y: backgroundSize.height / 2.0))
|
||||
)
|
||||
|
||||
return backgroundSize
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import ContextUI
|
||||
|
||||
public final class PeekControllerImpl: ViewController, PeekController, ContextControllerProtocol {
|
||||
public var useComplexItemsTransitionAnimation: Bool = false
|
||||
public var immediateItemsTransitionAnimation = false
|
||||
|
||||
public func getActionsMinHeight() -> ContextController.ActionsHeight? {
|
||||
return nil
|
||||
}
|
||||
|
||||
public func setItems(_ items: Signal<ContextController.Items, NoError>, minHeight: ContextController.ActionsHeight?, animated: Bool) {
|
||||
}
|
||||
|
||||
public func setItems(_ items: Signal<ContextController.Items, NoError>, minHeight: ContextController.ActionsHeight?, previousActionsTransition: ContextController.PreviousActionsTransition) {
|
||||
}
|
||||
|
||||
public func pushItems(items: Signal<ContextController.Items, NoError>) {
|
||||
self.controllerNode.pushItems(items: items)
|
||||
}
|
||||
|
||||
public func popItems() {
|
||||
self.controllerNode.popItems()
|
||||
}
|
||||
|
||||
private var controllerNode: PeekControllerNode {
|
||||
return self.displayNode as! PeekControllerNode
|
||||
}
|
||||
|
||||
public var contentNode: PeekControllerContentNode & ASDisplayNode {
|
||||
return self.controllerNode.contentNode
|
||||
}
|
||||
|
||||
private let presentationData: PresentationData
|
||||
private let content: PeekControllerContent
|
||||
public var sourceView: () -> (UIView, CGRect)?
|
||||
private let activateImmediately: Bool
|
||||
|
||||
public var visibilityUpdated: ((Bool) -> Void)?
|
||||
|
||||
public var getOverlayViews: (() -> [UIView])?
|
||||
|
||||
public var appeared: (() -> Void)?
|
||||
public var disappeared: (() -> Void)?
|
||||
|
||||
private var animatedIn = false
|
||||
|
||||
private let _ready = Promise<Bool>()
|
||||
override public var ready: Promise<Bool> {
|
||||
return self._ready
|
||||
}
|
||||
|
||||
public init(presentationData: PresentationData, content: PeekControllerContent, sourceView: @escaping () -> (UIView, CGRect)?, activateImmediately: Bool = false) {
|
||||
self.presentationData = presentationData
|
||||
self.content = content
|
||||
self.sourceView = sourceView
|
||||
self.activateImmediately = activateImmediately
|
||||
|
||||
super.init(navigationBarPresentationData: nil)
|
||||
|
||||
self.statusBar.statusBarStyle = .Ignore
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override public func loadDisplayNode() {
|
||||
self.displayNode = PeekControllerNode(presentationData: self.presentationData, controller: self, content: self.content, requestDismiss: { [weak self] in
|
||||
self?.dismiss()
|
||||
})
|
||||
self.displayNodeDidLoad()
|
||||
}
|
||||
|
||||
private func getSourceRect() -> CGRect {
|
||||
if let (sourceView, sourceRect) = self.sourceView() {
|
||||
return sourceView.convert(sourceRect, to: self.view)
|
||||
} else {
|
||||
let size = self.displayNode.bounds.size
|
||||
return CGRect(origin: CGPoint(x: floor((size.width - 10.0) / 2.0), y: floor((size.height - 10.0) / 2.0)), size: CGSize(width: 10.0, height: 10.0))
|
||||
}
|
||||
}
|
||||
|
||||
override public func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
if !self.animatedIn {
|
||||
self.animatedIn = true
|
||||
self.controllerNode.animateIn(from: self.getSourceRect())
|
||||
|
||||
self.visibilityUpdated?(true)
|
||||
|
||||
if self.activateImmediately {
|
||||
self.controllerNode.activateMenu(immediately: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
|
||||
self.controllerNode.containerLayoutUpdated(layout, transition: transition)
|
||||
}
|
||||
|
||||
override public func dismiss(completion: (() -> Void)? = nil) {
|
||||
self.visibilityUpdated?(false)
|
||||
self.controllerNode.animateOut(to: self.getSourceRect(), completion: { [weak self] in
|
||||
self?.presentingViewController?.dismiss(animated: false, completion: nil)
|
||||
})
|
||||
}
|
||||
|
||||
public func dismiss(result: ContextMenuActionResult, completion: (() -> Void)?) {
|
||||
self.dismiss(completion: completion)
|
||||
}
|
||||
}
|
||||
+483
@@ -0,0 +1,483 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import ContextUI
|
||||
|
||||
private let animationDurationFactor: Double = 1.0
|
||||
|
||||
final class PeekControllerNode: ViewControllerTracingNode, PeekControllerNodeProtocol {
|
||||
private let requestDismiss: () -> Void
|
||||
|
||||
private let presentationData: PresentationData
|
||||
private let theme: PeekControllerTheme
|
||||
|
||||
private weak var controller: PeekController?
|
||||
|
||||
private let blurView: UIView
|
||||
private let dimNode: ASDisplayNode
|
||||
private let containerBackgroundNode: ASImageNode
|
||||
private let containerNode: ASDisplayNode
|
||||
private let darkDimNode: ASDisplayNode
|
||||
|
||||
private var validLayout: ContainerViewLayout?
|
||||
|
||||
private var content: PeekControllerContent
|
||||
var contentNode: PeekControllerContentNode & ASDisplayNode
|
||||
private var contentNodeHasValidLayout = false
|
||||
|
||||
private var topAccessoryNode: ASDisplayNode?
|
||||
private var fullScreenAccessoryNode: (PeekControllerAccessoryNode & ASDisplayNode)?
|
||||
|
||||
private var actionsStackNode: ContextControllerActionsStackNode
|
||||
|
||||
private var hapticFeedback = HapticFeedback()
|
||||
|
||||
private var initialContinueGesturePoint: CGPoint?
|
||||
private var didMoveFromInitialGesturePoint = false
|
||||
private var highlightedActionNode: ContextActionNodeProtocol?
|
||||
|
||||
init(presentationData: PresentationData, controller: PeekController, content: PeekControllerContent, requestDismiss: @escaping () -> Void) {
|
||||
self.presentationData = presentationData
|
||||
self.requestDismiss = requestDismiss
|
||||
self.theme = PeekControllerTheme(presentationTheme: presentationData.theme)
|
||||
self.controller = controller
|
||||
|
||||
self.dimNode = ASDisplayNode()
|
||||
|
||||
let blurView = UIVisualEffectView(effect: UIBlurEffect(style: self.theme.isDark ? .dark : .light))
|
||||
blurView.isUserInteractionEnabled = false
|
||||
self.blurView = blurView
|
||||
|
||||
self.darkDimNode = ASDisplayNode()
|
||||
self.darkDimNode.alpha = 0.0
|
||||
self.darkDimNode.backgroundColor = presentationData.theme.contextMenu.dimColor
|
||||
self.darkDimNode.isUserInteractionEnabled = false
|
||||
|
||||
switch content.menuActivation() {
|
||||
case .drag:
|
||||
self.dimNode.backgroundColor = nil
|
||||
self.blurView.alpha = 1.0
|
||||
case .press:
|
||||
self.dimNode.backgroundColor = UIColor(white: self.theme.isDark ? 0.0 : 1.0, alpha: 0.5)
|
||||
self.blurView.alpha = 0.0
|
||||
}
|
||||
|
||||
self.containerBackgroundNode = ASImageNode()
|
||||
self.containerBackgroundNode.isLayerBacked = true
|
||||
self.containerBackgroundNode.displaysAsynchronously = false
|
||||
|
||||
self.containerNode = ASDisplayNode()
|
||||
|
||||
self.content = content
|
||||
self.contentNode = content.node()
|
||||
self.topAccessoryNode = content.topAccessoryNode()
|
||||
self.fullScreenAccessoryNode = content.fullScreenAccessoryNode(blurView: blurView)
|
||||
self.fullScreenAccessoryNode?.alpha = 0.0
|
||||
|
||||
var activatedActionImpl: (() -> Void)?
|
||||
var requestLayoutImpl: ((ContainedViewLayoutTransition) -> Void)?
|
||||
|
||||
self.actionsStackNode = ContextControllerActionsStackNodeImpl(
|
||||
context: nil,
|
||||
getController: { [weak controller] in
|
||||
return controller
|
||||
},
|
||||
requestDismiss: { result in
|
||||
activatedActionImpl?()
|
||||
},
|
||||
requestUpdate: { transition in
|
||||
requestLayoutImpl?(transition)
|
||||
}
|
||||
)
|
||||
self.actionsStackNode.alpha = 0.0
|
||||
|
||||
let items = ContextController.Items(
|
||||
id: 0,
|
||||
content: .list(content.menuItems()),
|
||||
context: nil,
|
||||
reactionItems: [],
|
||||
selectedReactionItems: Set(),
|
||||
reactionsTitle: nil,
|
||||
reactionsLocked: false,
|
||||
animationCache: nil,
|
||||
alwaysAllowPremiumReactions: false,
|
||||
allPresetReactionsAreAvailable: false,
|
||||
getEmojiContent: nil,
|
||||
disablePositionLock: false,
|
||||
tip: nil,
|
||||
tipSignal: nil,
|
||||
dismissed: nil
|
||||
)
|
||||
if let item = makeContextControllerActionsStackItem(items: items).first {
|
||||
self.actionsStackNode.replace(
|
||||
item: item,
|
||||
animated: false
|
||||
)
|
||||
}
|
||||
|
||||
super.init()
|
||||
|
||||
requestLayoutImpl = { [weak self] transition in
|
||||
self?.updateLayout(transition: transition)
|
||||
}
|
||||
|
||||
if content.presentation() == .freeform {
|
||||
self.containerNode.isUserInteractionEnabled = false
|
||||
} else {
|
||||
self.containerNode.clipsToBounds = true
|
||||
self.containerNode.cornerRadius = 16.0
|
||||
}
|
||||
|
||||
self.addSubnode(self.dimNode)
|
||||
self.view.addSubview(self.blurView)
|
||||
self.addSubnode(self.darkDimNode)
|
||||
self.containerNode.addSubnode(self.contentNode)
|
||||
|
||||
self.addSubnode(self.actionsStackNode)
|
||||
self.addSubnode(self.containerNode)
|
||||
|
||||
|
||||
if let fullScreenAccessoryNode = self.fullScreenAccessoryNode {
|
||||
self.fullScreenAccessoryNode?.dismiss = { [weak self] in
|
||||
self?.requestDismiss()
|
||||
}
|
||||
self.addSubnode(fullScreenAccessoryNode)
|
||||
}
|
||||
|
||||
activatedActionImpl = { [weak self] in
|
||||
self?.requestDismiss()
|
||||
}
|
||||
|
||||
self.hapticFeedback.prepareTap()
|
||||
|
||||
controller.ready.set(self.contentNode.ready())
|
||||
}
|
||||
|
||||
deinit {
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimNodeTap(_:))))
|
||||
self.view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))))
|
||||
}
|
||||
|
||||
func updateLayout(transition: ContainedViewLayoutTransition = .immediate) {
|
||||
if let layout = self.validLayout {
|
||||
self.containerLayoutUpdated(layout, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
func replaceItem(items: Signal<ContextController.Items, NoError>) {
|
||||
let _ = (items
|
||||
|> deliverOnMainQueue).start(next: { [weak self] items in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if let item = makeContextControllerActionsStackItem(items: items).first {
|
||||
self.actionsStackNode.replace(item: item, animated: false)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func pushItems(items: Signal<ContextController.Items, NoError>) {
|
||||
let _ = (items
|
||||
|> deliverOnMainQueue).start(next: { [weak self] items in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if let item = makeContextControllerActionsStackItem(items: items).first {
|
||||
self.actionsStackNode.push(item: item, currentScrollingState: nil, positionLock: nil, animated: true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func popItems() {
|
||||
self.actionsStackNode.pop()
|
||||
}
|
||||
|
||||
func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
self.validLayout = layout
|
||||
|
||||
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
transition.updateFrame(node: self.darkDimNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
transition.updateFrame(view: self.blurView, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
|
||||
var layoutInsets = layout.insets(options: [])
|
||||
let containerWidth = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: layout.safeInsets.left)
|
||||
|
||||
layoutInsets.left = floor((layout.size.width - containerWidth) / 2.0)
|
||||
layoutInsets.right = layoutInsets.left
|
||||
if !layoutInsets.bottom.isZero {
|
||||
layoutInsets.bottom -= 12.0
|
||||
}
|
||||
|
||||
let maxContainerSize = CGSize(width: layout.size.width - 14.0 * 2.0, height: layout.size.height - layoutInsets.top - layoutInsets.bottom - 90.0)
|
||||
|
||||
let contentSize = self.contentNode.updateLayout(size: maxContainerSize, transition: self.contentNodeHasValidLayout ? transition : .immediate)
|
||||
if self.contentNodeHasValidLayout {
|
||||
transition.updateFrame(node: self.contentNode, frame: CGRect(origin: CGPoint(), size: contentSize))
|
||||
} else {
|
||||
self.contentNode.frame = CGRect(origin: CGPoint(), size: contentSize)
|
||||
}
|
||||
|
||||
let actionsSideInset: CGFloat = layout.safeInsets.left + 11.0
|
||||
|
||||
let actionsSize = self.actionsStackNode.update(
|
||||
presentationData: self.presentationData,
|
||||
constrainedSize: CGSize(width: layout.size.width - actionsSideInset * 2.0, height: layout.size.height),
|
||||
presentation: .inline,
|
||||
transition: transition
|
||||
)
|
||||
|
||||
let containerFrame: CGRect
|
||||
let actionsFrame: CGRect
|
||||
if layout.size.width > layout.size.height {
|
||||
if self.actionsStackNode.alpha.isZero {
|
||||
containerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - contentSize.width) / 2.0), y: floor((layout.size.height - contentSize.height) / 2.0)), size: contentSize)
|
||||
} else {
|
||||
containerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - contentSize.width) / 3.0), y: floor((layout.size.height - contentSize.height) / 2.0)), size: contentSize)
|
||||
}
|
||||
actionsFrame = CGRect(origin: CGPoint(x: containerFrame.maxX + 32.0, y: floor((layout.size.height - actionsSize.height) / 2.0)), size: actionsSize)
|
||||
} else {
|
||||
switch self.content.presentation() {
|
||||
case .contained:
|
||||
containerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - contentSize.width) / 2.0), y: floor((layout.size.height - contentSize.height) / 2.0)), size: contentSize)
|
||||
case .freeform:
|
||||
var fraction: CGFloat = 1.0 / 3.0
|
||||
if let _ = self.controller?.appeared {
|
||||
fraction *= 1.33
|
||||
}
|
||||
containerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - contentSize.width) / 2.0), y: floor((layout.size.height - contentSize.height) * fraction)), size: contentSize)
|
||||
}
|
||||
actionsFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - actionsSize.width) / 2.0), y: containerFrame.maxY + 64.0), size: actionsSize)
|
||||
}
|
||||
transition.updateFrame(node: self.containerNode, frame: containerFrame)
|
||||
|
||||
transition.updateFrame(node: self.actionsStackNode, frame: actionsFrame)
|
||||
|
||||
if let fullScreenAccessoryNode = self.fullScreenAccessoryNode {
|
||||
transition.updateFrame(node: fullScreenAccessoryNode, frame: CGRect(origin: .zero, size: layout.size))
|
||||
fullScreenAccessoryNode.updateLayout(size: layout.size, transition: transition)
|
||||
}
|
||||
|
||||
self.contentNodeHasValidLayout = true
|
||||
}
|
||||
|
||||
func animateIn(from rect: CGRect) {
|
||||
self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
self.blurView.layer.animateAlpha(from: 0.0, to: self.blurView.alpha, duration: 0.3)
|
||||
|
||||
let offset = CGPoint(x: rect.midX - self.containerNode.position.x, y: rect.midY - self.containerNode.position.y)
|
||||
self.containerNode.layer.animateSpring(from: NSValue(cgPoint: offset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.4, initialVelocity: 0.0, damping: 110.0, additive: true)
|
||||
|
||||
if let appeared = self.controller?.appeared {
|
||||
appeared()
|
||||
let scale = rect.width / self.contentNode.frame.width
|
||||
self.containerNode.layer.animateSpring(from: scale as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4, initialVelocity: 0.0, damping: 110.0)
|
||||
} else {
|
||||
self.containerNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4, initialVelocity: 0.0, damping: 110.0)
|
||||
self.containerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
||||
}
|
||||
|
||||
if let topAccessoryNode = self.topAccessoryNode {
|
||||
topAccessoryNode.layer.animateSpring(from: NSValue(cgPoint: offset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.4, initialVelocity: 0.0, damping: 110.0, additive: true)
|
||||
topAccessoryNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4, initialVelocity: 0.0, damping: 110.0)
|
||||
topAccessoryNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
||||
}
|
||||
|
||||
if case .press = self.content.menuActivation() {
|
||||
self.hapticFeedback.tap()
|
||||
} else {
|
||||
self.hapticFeedback.impact()
|
||||
}
|
||||
}
|
||||
|
||||
func animateOut(to rect: CGRect, completion: @escaping () -> Void) {
|
||||
self.isUserInteractionEnabled = false
|
||||
|
||||
self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
||||
self.blurView.layer.animateAlpha(from: self.blurView.alpha, to: 0.0, duration: 0.25, removeOnCompletion: false)
|
||||
self.darkDimNode.layer.animateAlpha(from: self.darkDimNode.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
||||
|
||||
let springDuration: Double = 0.42 * animationDurationFactor
|
||||
let springDamping: CGFloat = 104.0
|
||||
|
||||
var scaleCompleted = false
|
||||
var positionCompleted = false
|
||||
let outCompletion = {
|
||||
if scaleCompleted && positionCompleted {
|
||||
}
|
||||
}
|
||||
|
||||
let offset = CGPoint(x: rect.midX - self.containerNode.position.x, y: rect.midY - self.containerNode.position.y)
|
||||
self.containerNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint()), to: NSValue(cgPoint: offset), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, removeOnCompletion: false, additive: true, completion: { _ in
|
||||
positionCompleted = true
|
||||
outCompletion()
|
||||
completion()
|
||||
})
|
||||
|
||||
if let _ = self.controller?.disappeared {
|
||||
self.controller?.disappeared?()
|
||||
let scale = rect.width / self.contentNode.frame.width
|
||||
self.containerNode.layer.animateScale(from: 1.0, to: scale, duration: 0.25, removeOnCompletion: false, completion: { _ in
|
||||
scaleCompleted = true
|
||||
outCompletion()
|
||||
})
|
||||
} else {
|
||||
self.containerNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.25, removeOnCompletion: false, completion: { _ in
|
||||
scaleCompleted = true
|
||||
outCompletion()
|
||||
})
|
||||
self.containerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
if !self.actionsStackNode.alpha.isZero {
|
||||
let actionsOffset = CGPoint(x: rect.midX - self.actionsStackNode.position.x, y: rect.midY - self.actionsStackNode.position.y)
|
||||
self.actionsStackNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2 * animationDurationFactor, removeOnCompletion: false)
|
||||
self.actionsStackNode.layer.animateSpring(from: 1.0 as NSNumber, to: 0.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping, removeOnCompletion: false)
|
||||
self.actionsStackNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint()), to: NSValue(cgPoint: actionsOffset), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, additive: true)
|
||||
}
|
||||
|
||||
if let fullScreenAccessoryNode = self.fullScreenAccessoryNode, !fullScreenAccessoryNode.alpha.isZero {
|
||||
fullScreenAccessoryNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2 * animationDurationFactor, removeOnCompletion: false)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func dimNodeTap(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
self.requestDismiss()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func panGesture(_ recognizer: UIPanGestureRecognizer) {
|
||||
guard case .drag = self.content.menuActivation() else {
|
||||
return
|
||||
}
|
||||
|
||||
let location = recognizer.location(in: self.view)
|
||||
switch recognizer.state {
|
||||
case .began:
|
||||
break
|
||||
case .changed:
|
||||
self.applyDraggingOffset(location)
|
||||
case .cancelled, .ended:
|
||||
self.endDragging(location)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func applyDraggingOffset(_ offset: CGPoint) {
|
||||
let localPoint = offset
|
||||
let initialPoint: CGPoint
|
||||
if let current = self.initialContinueGesturePoint {
|
||||
initialPoint = current
|
||||
} else {
|
||||
initialPoint = localPoint
|
||||
self.initialContinueGesturePoint = localPoint
|
||||
}
|
||||
if !self.actionsStackNode.alpha.isZero {
|
||||
if !self.didMoveFromInitialGesturePoint {
|
||||
let distance = abs(localPoint.y - initialPoint.y)
|
||||
if distance > 12.0 {
|
||||
self.didMoveFromInitialGesturePoint = true
|
||||
}
|
||||
}
|
||||
if self.didMoveFromInitialGesturePoint {
|
||||
let actionPoint = self.view.convert(localPoint, to: self.actionsStackNode.view)
|
||||
self.actionsStackNode.highlightGestureMoved(location: actionPoint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func endDragging(_ location: CGPoint) {
|
||||
if self.didMoveFromInitialGesturePoint {
|
||||
self.actionsStackNode.highlightGestureFinished(performAction: true)
|
||||
} else if self.actionsStackNode.alpha.isZero {
|
||||
if let fullScreenAccessoryNode = self.fullScreenAccessoryNode, !fullScreenAccessoryNode.alpha.isZero {
|
||||
} else {
|
||||
self.requestDismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func activateMenu(immediately: Bool) {
|
||||
if self.content.menuItems().isEmpty {
|
||||
if let fullScreenAccessoryNode = self.fullScreenAccessoryNode {
|
||||
fullScreenAccessoryNode.alpha = 1.0
|
||||
fullScreenAccessoryNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
||||
|
||||
let previousBlurAlpha = self.blurView.alpha
|
||||
self.blurView.alpha = 1.0
|
||||
self.blurView.layer.animateAlpha(from: previousBlurAlpha, to: self.blurView.alpha, duration: 0.3)
|
||||
}
|
||||
return
|
||||
} else {
|
||||
if let fullScreenAccessoryNode = self.fullScreenAccessoryNode {
|
||||
fullScreenAccessoryNode.alpha = 1.0
|
||||
fullScreenAccessoryNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
||||
}
|
||||
}
|
||||
if case .press = self.content.menuActivation() {
|
||||
self.hapticFeedback.impact()
|
||||
}
|
||||
|
||||
let springDuration: Double = 0.42 * animationDurationFactor
|
||||
let springDamping: CGFloat = 104.0
|
||||
|
||||
let previousBlurAlpha = self.blurView.alpha
|
||||
self.blurView.alpha = 1.0
|
||||
self.blurView.layer.animateAlpha(from: previousBlurAlpha, to: self.blurView.alpha, duration: 0.25)
|
||||
|
||||
let previousDarkDimAlpha = self.darkDimNode.alpha
|
||||
self.darkDimNode.alpha = 1.0
|
||||
self.darkDimNode.layer.animateAlpha(from: previousDarkDimAlpha, to: 1.0, duration: 0.3)
|
||||
|
||||
if let layout = self.validLayout {
|
||||
self.containerLayoutUpdated(layout, transition: .animated(duration: springDuration, curve: .spring))
|
||||
}
|
||||
|
||||
let animateIn = {
|
||||
self.actionsStackNode.alpha = 1.0
|
||||
self.actionsStackNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2 * animationDurationFactor)
|
||||
self.actionsStackNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping)
|
||||
|
||||
let localContentSourceFrame = self.containerNode.frame
|
||||
self.actionsStackNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: localContentSourceFrame.center.x - self.actionsStackNode.position.x, y: localContentSourceFrame.center.y - self.actionsStackNode.position.y)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, additive: true)
|
||||
}
|
||||
if immediately {
|
||||
animateIn()
|
||||
} else {
|
||||
Queue.mainQueue().after(0.02, animateIn)
|
||||
}
|
||||
}
|
||||
|
||||
func updateContent(content: PeekControllerContent) {
|
||||
let contentNode = self.contentNode
|
||||
contentNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak contentNode] _ in
|
||||
contentNode?.removeFromSupernode()
|
||||
})
|
||||
contentNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.15, removeOnCompletion: false)
|
||||
|
||||
self.content = content
|
||||
self.contentNode = content.node()
|
||||
self.containerNode.addSubnode(self.contentNode)
|
||||
self.contentNodeHasValidLayout = false
|
||||
|
||||
self.replaceItem(items: .single(ContextController.Items(content: .list(content.menuItems()))))
|
||||
|
||||
self.contentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
|
||||
self.contentNode.layer.animateSpring(from: 0.35 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5)
|
||||
|
||||
if let layout = self.validLayout {
|
||||
self.containerLayoutUpdated(layout, transition: .animated(duration: 0.15, curve: .easeInOut))
|
||||
}
|
||||
|
||||
self.hapticFeedback.tap()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import TelegramPresentationData
|
||||
import TextSelectionNode
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import UIKitRuntimeUtils
|
||||
import ContextUI
|
||||
|
||||
private final class PinchControllerNode: ViewControllerTracingNode {
|
||||
private weak var controller: PinchController?
|
||||
|
||||
private var initialSourceFrame: CGRect?
|
||||
|
||||
private let clippingNode: ASDisplayNode
|
||||
private let scrollingContainer: ASDisplayNode
|
||||
|
||||
private let sourceNode: PinchSourceContainerNode
|
||||
private let disableScreenshots: Bool
|
||||
private let getContentAreaInScreenSpace: () -> CGRect
|
||||
|
||||
private let dimNode: ASDisplayNode
|
||||
|
||||
private var validLayout: ContainerViewLayout?
|
||||
private var isAnimatingOut: Bool = false
|
||||
|
||||
private var hapticFeedback: HapticFeedback?
|
||||
|
||||
init(controller: PinchController, sourceNode: PinchSourceContainerNode, disableScreenshots: Bool, getContentAreaInScreenSpace: @escaping () -> CGRect) {
|
||||
self.controller = controller
|
||||
self.sourceNode = sourceNode
|
||||
self.disableScreenshots = disableScreenshots
|
||||
self.getContentAreaInScreenSpace = getContentAreaInScreenSpace
|
||||
|
||||
self.dimNode = ASDisplayNode()
|
||||
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
|
||||
self.dimNode.alpha = 0.0
|
||||
|
||||
self.clippingNode = ASDisplayNode()
|
||||
self.clippingNode.clipsToBounds = true
|
||||
|
||||
self.scrollingContainer = ASDisplayNode()
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.dimNode)
|
||||
self.addSubnode(self.clippingNode)
|
||||
self.clippingNode.addSubnode(self.scrollingContainer)
|
||||
|
||||
self.sourceNode.deactivate = { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.controller?.dismiss()
|
||||
}
|
||||
|
||||
self.sourceNode.updated = { [weak self] scale, pinchLocation, offset in
|
||||
guard let strongSelf = self, let initialSourceFrame = strongSelf.initialSourceFrame else {
|
||||
return
|
||||
}
|
||||
strongSelf.dimNode.alpha = max(0.0, min(1.0, scale - 1.0))
|
||||
|
||||
let pinchOffset = CGPoint(
|
||||
x: pinchLocation.x - initialSourceFrame.width / 2.0,
|
||||
y: pinchLocation.y - initialSourceFrame.height / 2.0
|
||||
)
|
||||
|
||||
var transform = CATransform3DIdentity
|
||||
transform = CATransform3DTranslate(transform, offset.x - pinchOffset.x * (scale - 1.0), offset.y - pinchOffset.y * (scale - 1.0), 0.0)
|
||||
transform = CATransform3DScale(transform, scale, scale, 0.0)
|
||||
|
||||
strongSelf.sourceNode.contentNode.transform = transform
|
||||
}
|
||||
|
||||
if self.disableScreenshots {
|
||||
setLayerDisableScreenshots(self.layer, true)
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
}
|
||||
|
||||
func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition, previousActionsContainerNode: ContextActionsContainerNode?) {
|
||||
if self.isAnimatingOut {
|
||||
return
|
||||
}
|
||||
|
||||
self.validLayout = layout
|
||||
|
||||
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
transition.updateFrame(node: self.clippingNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
let convertedFrame = convertFrame(self.sourceNode.bounds, from: self.sourceNode.view, to: self.view)
|
||||
self.sourceNode.contentNode.frame = convertedFrame
|
||||
self.initialSourceFrame = convertedFrame
|
||||
self.scrollingContainer.addSubnode(self.sourceNode.contentNode)
|
||||
|
||||
var updatedContentAreaInScreenSpace = self.getContentAreaInScreenSpace()
|
||||
updatedContentAreaInScreenSpace.origin.x = 0.0
|
||||
updatedContentAreaInScreenSpace.size.width = self.bounds.width
|
||||
|
||||
self.clippingNode.layer.animateFrame(from: updatedContentAreaInScreenSpace, to: self.clippingNode.frame, duration: 0.18 * 1.0, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
|
||||
self.clippingNode.layer.animateBoundsOriginYAdditive(from: updatedContentAreaInScreenSpace.minY, to: 0.0, duration: 0.18 * 1.0, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
|
||||
}
|
||||
|
||||
func animateOut(completion: @escaping () -> Void) {
|
||||
self.isAnimatingOut = true
|
||||
|
||||
let performCompletion: () -> Void = { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.isAnimatingOut = false
|
||||
|
||||
strongSelf.sourceNode.restoreToNaturalSize()
|
||||
strongSelf.sourceNode.addSubnode(strongSelf.sourceNode.contentNode)
|
||||
|
||||
strongSelf.sourceNode.animatedOut?()
|
||||
|
||||
completion()
|
||||
}
|
||||
|
||||
let convertedFrame = convertFrame(self.sourceNode.bounds, from: self.sourceNode.view, to: self.view)
|
||||
self.sourceNode.contentNode.frame = convertedFrame
|
||||
self.initialSourceFrame = convertedFrame
|
||||
|
||||
if let (scale, pinchLocation, offset) = self.sourceNode.gesture.currentTransform, let initialSourceFrame = self.initialSourceFrame {
|
||||
let duration = 0.3
|
||||
let transitionCurve: ContainedViewLayoutTransitionCurve = .easeInOut
|
||||
|
||||
var updatedContentAreaInScreenSpace = self.getContentAreaInScreenSpace()
|
||||
updatedContentAreaInScreenSpace.origin.x = 0.0
|
||||
updatedContentAreaInScreenSpace.size.width = self.bounds.width
|
||||
|
||||
self.clippingNode.layer.animateFrame(from: self.clippingNode.frame, to: updatedContentAreaInScreenSpace, duration: duration * 1.0, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false)
|
||||
self.clippingNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: updatedContentAreaInScreenSpace.minY, duration: duration * 1.0, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false)
|
||||
|
||||
let transition: ContainedViewLayoutTransition = .animated(duration: duration, curve: .spring)
|
||||
if self.hapticFeedback == nil {
|
||||
self.hapticFeedback = HapticFeedback()
|
||||
}
|
||||
self.hapticFeedback?.prepareImpact(.light)
|
||||
self.hapticFeedback?.impact(.light)
|
||||
|
||||
self.sourceNode.scaleUpdated?(1.0, transition)
|
||||
|
||||
let pinchOffset = CGPoint(
|
||||
x: pinchLocation.x - initialSourceFrame.width / 2.0,
|
||||
y: pinchLocation.y - initialSourceFrame.height / 2.0
|
||||
)
|
||||
|
||||
var transform = CATransform3DIdentity
|
||||
transform = CATransform3DScale(transform, scale, scale, 0.0)
|
||||
|
||||
self.sourceNode.contentNode.transform = CATransform3DIdentity
|
||||
self.sourceNode.contentNode.position = CGPoint(x: initialSourceFrame.midX, y: initialSourceFrame.midY)
|
||||
self.sourceNode.contentNode.layer.animateSpring(from: scale as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: duration * 1.2, damping: 110.0)
|
||||
self.sourceNode.contentNode.layer.animatePosition(from: CGPoint(x: offset.x - pinchOffset.x * (scale - 1.0), y: offset.y - pinchOffset.y * (scale - 1.0)), to: CGPoint(), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, additive: true, force: true, completion: { _ in
|
||||
performCompletion()
|
||||
})
|
||||
|
||||
let dimNodeTransition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: transitionCurve)
|
||||
dimNodeTransition.updateAlpha(node: self.dimNode, alpha: 0.0)
|
||||
} else {
|
||||
performCompletion()
|
||||
}
|
||||
}
|
||||
|
||||
func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) {
|
||||
if self.isAnimatingOut {
|
||||
self.scrollingContainer.bounds = self.scrollingContainer.bounds.offsetBy(dx: 0.0, dy: offset.y)
|
||||
transition.animateOffsetAdditive(node: self.scrollingContainer, offset: -offset.y)
|
||||
}
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public final class PinchControllerImpl: ViewController, PinchController, StandalonePresentableController {
|
||||
private let _ready = Promise<Bool>()
|
||||
override public var ready: Promise<Bool> {
|
||||
return self._ready
|
||||
}
|
||||
|
||||
private let sourceNode: PinchSourceContainerNode
|
||||
private let disableScreenshots: Bool
|
||||
private let getContentAreaInScreenSpace: () -> CGRect
|
||||
|
||||
private var wasDismissed = false
|
||||
|
||||
private var controllerNode: PinchControllerNode {
|
||||
return self.displayNode as! PinchControllerNode
|
||||
}
|
||||
|
||||
public init(sourceNode: PinchSourceContainerNode, disableScreenshots: Bool = false, getContentAreaInScreenSpace: @escaping () -> CGRect) {
|
||||
self.sourceNode = sourceNode
|
||||
self.disableScreenshots = disableScreenshots
|
||||
self.getContentAreaInScreenSpace = getContentAreaInScreenSpace
|
||||
|
||||
super.init(navigationBarPresentationData: nil)
|
||||
|
||||
self.statusBar.statusBarStyle = .Ignore
|
||||
|
||||
self.lockOrientation = true
|
||||
self.blocksBackgroundWhenInOverlay = true
|
||||
}
|
||||
|
||||
required init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
}
|
||||
|
||||
override public func loadDisplayNode() {
|
||||
self.displayNode = PinchControllerNode(controller: self, sourceNode: self.sourceNode, disableScreenshots: self.disableScreenshots, getContentAreaInScreenSpace: self.getContentAreaInScreenSpace)
|
||||
|
||||
self.displayNodeDidLoad()
|
||||
|
||||
self._ready.set(.single(true))
|
||||
}
|
||||
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
|
||||
self.controllerNode.updateLayout(layout: layout, transition: transition, previousActionsContainerNode: nil)
|
||||
}
|
||||
|
||||
override public func viewDidAppear(_ animated: Bool) {
|
||||
if self.ignoreAppearanceMethodInvocations() {
|
||||
return
|
||||
}
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
self.controllerNode.animateIn()
|
||||
}
|
||||
|
||||
override public func dismiss(completion: (() -> Void)? = nil) {
|
||||
if !self.wasDismissed {
|
||||
self.wasDismissed = true
|
||||
self.controllerNode.animateOut(completion: { [weak self] in
|
||||
self?.presentingViewController?.dismiss(animated: false, completion: nil)
|
||||
completion?()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) {
|
||||
self.controllerNode.addRelativeContentOffset(offset, transition: transition)
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import SwiftSignalKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import TelegramCore
|
||||
import AccountContext
|
||||
import EmojiStatusComponent
|
||||
|
||||
final class ReactionPreviewView: UIView {
|
||||
private let context: AccountContext
|
||||
private let file: TelegramMediaFile
|
||||
|
||||
private let icon = ComponentView<Empty>()
|
||||
|
||||
init(context: AccountContext, file: TelegramMediaFile) {
|
||||
self.context = context
|
||||
self.file = file
|
||||
|
||||
super.init(frame: CGRect())
|
||||
}
|
||||
|
||||
required init(coder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func update(size: CGSize) {
|
||||
let iconSize = self.icon.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(EmojiStatusComponent(
|
||||
context: self.context,
|
||||
animationCache: self.context.animationCache,
|
||||
animationRenderer: self.context.animationRenderer,
|
||||
content: .animation(
|
||||
content: .file(file: self.file),
|
||||
size: size,
|
||||
placeholderColor: .clear,
|
||||
themeColor: .white,
|
||||
loopMode: .forever
|
||||
),
|
||||
isVisibleForAnimations: true,
|
||||
action: nil
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: size
|
||||
)
|
||||
let iconFrame = CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) * 0.5), y: floor((size.height - iconSize.height) * 0.5)), size: iconSize)
|
||||
if let iconView = self.icon.view {
|
||||
if iconView.superview == nil {
|
||||
self.addSubview(iconView)
|
||||
}
|
||||
iconView.frame = iconFrame
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,23 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
sgdeps = [
|
||||
"//Swiftgram/SGSimpleSettings:SGSimpleSettings"
|
||||
]
|
||||
|
||||
sgsrc = [
|
||||
"//Swiftgram/SGEmojiKeyboardDefaultFirst:SGEmojiKeyboardDefaultFirst"
|
||||
]
|
||||
|
||||
swift_library(
|
||||
name = "EntityKeyboard",
|
||||
module_name = "EntityKeyboard",
|
||||
srcs = glob([
|
||||
srcs = sgsrc + glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
deps = sgdeps + [
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/Display:Display",
|
||||
"//submodules/ComponentFlow:ComponentFlow",
|
||||
|
||||
+2
-2
@@ -126,9 +126,9 @@ public final class EntityKeyboardAnimationData: Equatable {
|
||||
var file: TelegramMediaFile?
|
||||
var color: UIColor?
|
||||
for attribute in gift.attributes {
|
||||
if case let .model(_, fileValue, _) = attribute {
|
||||
if case let StarGift.UniqueGift.Attribute.model(_, fileValue, _, _) = attribute {
|
||||
file = fileValue
|
||||
} else if case let .backdrop(_, _, innerColor, outerColor, _, _, _) = attribute {
|
||||
} else if case let StarGift.UniqueGift.Attribute.backdrop(_, _, innerColor, outerColor, _, _, _) = attribute {
|
||||
color = UIColor(rgb: UInt32(bitPattern: innerColor))
|
||||
let _ = outerColor
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import SGSimpleSettings
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
@@ -558,9 +559,22 @@ public final class EntityKeyboardComponent: Component {
|
||||
|
||||
let emojiContentItemIdUpdated = ActionSlot<(AnyHashable, AnyHashable?, ComponentTransition)>()
|
||||
if let emojiContent = component.emojiContent {
|
||||
contents.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(emojiContent)))
|
||||
let effectiveEmojiContent: EmojiPagerContentComponent
|
||||
// MARK: Swiftgram
|
||||
if SGSimpleSettings.shared.defaultEmojisFirst {
|
||||
effectiveEmojiContent = emojiContent.withUpdatedItemGroups(
|
||||
panelItemGroups: sgPatchEmojiKeyboardItems(emojiContent.panelItemGroups),
|
||||
contentItemGroups: sgPatchEmojiKeyboardItems(emojiContent.contentItemGroups),
|
||||
itemContentUniqueId: emojiContent.itemContentUniqueId,
|
||||
emptySearchResults: emojiContent.emptySearchResults,
|
||||
searchState: emojiContent.searchState
|
||||
)
|
||||
} else {
|
||||
effectiveEmojiContent = emojiContent
|
||||
}
|
||||
contents.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(effectiveEmojiContent)))
|
||||
var topEmojiItems: [EntityKeyboardTopPanelComponent.Item] = []
|
||||
for itemGroup in emojiContent.panelItemGroups {
|
||||
for itemGroup in effectiveEmojiContent.panelItemGroups {
|
||||
if !itemGroup.items.isEmpty {
|
||||
if let id = itemGroup.groupId.base as? String, id != "peerSpecific" {
|
||||
if id == "recent" || id == "liked" || id == "collectible" {
|
||||
@@ -612,12 +626,12 @@ public final class EntityKeyboardComponent: Component {
|
||||
id: itemGroup.supergroupId,
|
||||
isReorderable: !itemGroup.isFeatured,
|
||||
content: AnyComponent(EntityKeyboardAnimationTopPanelComponent(
|
||||
context: emojiContent.context,
|
||||
context: effectiveEmojiContent.context,
|
||||
item: itemGroup.headerItem ?? animationData,
|
||||
isFeatured: itemGroup.isFeatured,
|
||||
isPremiumLocked: itemGroup.isPremiumLocked,
|
||||
animationCache: emojiContent.animationCache,
|
||||
animationRenderer: emojiContent.animationRenderer,
|
||||
animationCache: effectiveEmojiContent.animationCache,
|
||||
animationRenderer: effectiveEmojiContent.animationRenderer,
|
||||
theme: component.theme,
|
||||
title: itemGroup.title ?? "",
|
||||
customTintColor: component.customTintColor ?? itemGroup.customTintColor,
|
||||
|
||||
+9
-9
@@ -219,7 +219,7 @@ public final class GiftCompositionComponent: Component {
|
||||
_ attribute: StarGift.UniqueGift.Attribute,
|
||||
animDuration: Double
|
||||
) {
|
||||
guard let geom = self.spinGeom, case let .model(_, file, _) = attribute else {
|
||||
guard let geom = self.spinGeom, case let StarGift.UniqueGift.Attribute.model(_, file, _, _) = attribute else {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -340,7 +340,7 @@ public final class GiftCompositionComponent: Component {
|
||||
self.decelItemHosts.removeAll()
|
||||
|
||||
for (i, attribute) in tail.reversed().enumerated() {
|
||||
guard case let .model(_, file, _) = attribute else { continue }
|
||||
guard case let StarGift.UniqueGift.Attribute.model(_, file, _, _) = attribute else { continue }
|
||||
|
||||
let node = DefaultAnimatedStickerNodeImpl()
|
||||
node.isUserInteractionEnabled = false
|
||||
@@ -648,7 +648,7 @@ public final class GiftCompositionComponent: Component {
|
||||
|
||||
for attribute in gift.attributes {
|
||||
switch attribute {
|
||||
case let .model(_, file, _):
|
||||
case let .model(_, file, _, _):
|
||||
animationFile = file
|
||||
if !self.fetchedFiles.contains(file.fileId.id) {
|
||||
self.disposables.add(freeMediaFileResourceInteractiveFetched(account: component.context.account, userLocation: .other, fileReference: .standalone(media: file), resource: file.resource).start())
|
||||
@@ -705,7 +705,7 @@ public final class GiftCompositionComponent: Component {
|
||||
self.previewBackdrops = backdrops
|
||||
}
|
||||
|
||||
for case let .model(_, file, _) in self.previewModels where !self.fetchedFiles.contains(file.fileId.id) {
|
||||
for case let StarGift.UniqueGift.Attribute.model(_, file, _, _) in self.previewModels where !self.fetchedFiles.contains(file.fileId.id) {
|
||||
self.disposables.add(freeMediaFileResourceInteractiveFetched(account: component.context.account, userLocation: .other, fileReference: .standalone(media: file), resource: file.resource).start())
|
||||
self.fetchedFiles.insert(file.fileId.id)
|
||||
}
|
||||
@@ -722,7 +722,7 @@ public final class GiftCompositionComponent: Component {
|
||||
if self.previewBackdropIndex < 0 {
|
||||
self.previewBackdropIndex = 0
|
||||
}
|
||||
if case let .model(_, file, _) = self.previewModels[Int(self.previewModelIndex)] {
|
||||
if case let StarGift.UniqueGift.Attribute.model(_, file, _, _) = self.previewModels[Int(self.previewModelIndex)] {
|
||||
animationFile = file
|
||||
component.externalState?.previewModel = self.previewModels[Int(self.previewModelIndex)]
|
||||
}
|
||||
@@ -851,12 +851,12 @@ public final class GiftCompositionComponent: Component {
|
||||
if let (previewAttributes, mainGift) = uniqueSpinContext {
|
||||
var mainModelFile: TelegramMediaFile?
|
||||
for attribute in mainGift.attributes {
|
||||
if case let .model(_, file, _) = attribute { mainModelFile = file; break }
|
||||
if case let StarGift.UniqueGift.Attribute.model(_, file, _, _) = attribute { mainModelFile = file; break }
|
||||
}
|
||||
|
||||
var models: [StarGift.UniqueGift.Attribute] = []
|
||||
for attribute in previewAttributes {
|
||||
if case let .model(_, file, _) = attribute,
|
||||
if case let StarGift.UniqueGift.Attribute.model(_, file, _, _) = attribute,
|
||||
file.fileId.id != mainModelFile?.fileId.id {
|
||||
models.append(attribute)
|
||||
}
|
||||
@@ -866,7 +866,7 @@ public final class GiftCompositionComponent: Component {
|
||||
return availableSize
|
||||
}
|
||||
|
||||
for case let .model(_, file, _) in models where !self.fetchedFiles.contains(file.fileId.id) {
|
||||
for case let StarGift.UniqueGift.Attribute.model(_, file, _, _) in models where !self.fetchedFiles.contains(file.fileId.id) {
|
||||
self.disposables.add(freeMediaFileResourceInteractiveFetched(
|
||||
account: component.context.account,
|
||||
userLocation: .other,
|
||||
@@ -912,7 +912,7 @@ public final class GiftCompositionComponent: Component {
|
||||
} else if !nowAnimating && wasAnimating {
|
||||
var tail = Array(models.shuffled().prefix(6))
|
||||
if let mainModelFile {
|
||||
tail.append(.model(name: "", file: mainModelFile, rarity: 0))
|
||||
tail.append(.model(name: "", file: mainModelFile, rarity: .permille(0), crafted: false))
|
||||
}
|
||||
self.beginDecelerationWithQueue(
|
||||
tail: tail,
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "GiftCraftScreen",
|
||||
module_name = "GiftCraftScreen",
|
||||
srcs = [
|
||||
"Sources/GiftCraftScreen.swift",
|
||||
],
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Display",
|
||||
"//submodules/TelegramCore",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/Components/ViewControllerComponent",
|
||||
"//submodules/Components/SheetComponent",
|
||||
"//submodules/Components/BundleIconComponent",
|
||||
"//submodules/Components/BalancedTextComponent",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/TelegramUI/Components/PlainButtonComponent",
|
||||
"//submodules/TelegramUI/Components/ButtonComponent",
|
||||
"//submodules/TelegramUI/Components/Gifts/GiftItemComponent",
|
||||
"//submodules/TelegramUI/Components/GlassBarButtonComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
+833
@@ -0,0 +1,833 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import AccountContext
|
||||
import GiftItemComponent
|
||||
import GlassBackgroundComponent
|
||||
import GlassBarButtonComponent
|
||||
import BundleIconComponent
|
||||
import LottieComponent
|
||||
|
||||
private let cubeSide: CGFloat = 110.0
|
||||
|
||||
struct GiftItem: Equatable {
|
||||
let gift: StarGift.UniqueGift
|
||||
let reference: StarGiftReference
|
||||
}
|
||||
|
||||
final class CraftTableComponent: Component {
|
||||
enum Result {
|
||||
case gift(ProfileGiftsContext.State.StarGift)
|
||||
case fail
|
||||
}
|
||||
|
||||
let context: AccountContext
|
||||
let gifts: [Int32: GiftItem]
|
||||
let buttonColor: UIColor
|
||||
let isCrafting: Bool
|
||||
let result: Result?
|
||||
let select: (Int32) -> Void
|
||||
let remove: (Int32) -> Void
|
||||
let willFinish: (Bool) -> Void
|
||||
let finished: (UIView?) -> Void
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
gifts: [Int32: GiftItem],
|
||||
buttonColor: UIColor,
|
||||
isCrafting: Bool,
|
||||
result: Result?,
|
||||
select: @escaping (Int32) -> Void,
|
||||
remove: @escaping (Int32) -> Void,
|
||||
willFinish: @escaping (Bool) -> Void,
|
||||
finished: @escaping (UIView?) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.gifts = gifts
|
||||
self.buttonColor = buttonColor
|
||||
self.isCrafting = isCrafting
|
||||
self.result = result
|
||||
self.select = select
|
||||
self.remove = remove
|
||||
self.willFinish = willFinish
|
||||
self.finished = finished
|
||||
}
|
||||
|
||||
public static func ==(lhs: CraftTableComponent, rhs: CraftTableComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.gifts != rhs.gifts {
|
||||
return false
|
||||
}
|
||||
if lhs.buttonColor != rhs.buttonColor {
|
||||
return false
|
||||
}
|
||||
if lhs.isCrafting != rhs.isCrafting {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private var selectedGifts: [AnyHashable: ComponentView<Empty>] = [:]
|
||||
private var faces: [AnyHashable: ComponentView<Empty>] = [:]
|
||||
private let successFace = ComponentView<Empty>()
|
||||
|
||||
private let anvilPlayOnce = ActionSlot<Void>()
|
||||
private let animationView = CubeAnimationView()
|
||||
private let craftFailPlayOnce = ActionSlot<Void>()
|
||||
|
||||
private var didSetupFinishAnimation = false
|
||||
private var flipFaces = false
|
||||
|
||||
private var isSuccess = false
|
||||
private var isFailed = false
|
||||
private var failDidStartCrossAnimation = false
|
||||
private var failDidBringToFront = false
|
||||
private var failWillFinish = false
|
||||
private var failDidFinish = false
|
||||
|
||||
private var component: CraftTableComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.animationView)
|
||||
|
||||
self.animationView.onStickerLaunch = {
|
||||
HapticFeedback().impact(.soft)
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func setupFailureAnimation() {
|
||||
guard !self.didSetupFinishAnimation else {
|
||||
return
|
||||
}
|
||||
self.didSetupFinishAnimation = true
|
||||
|
||||
self.animationView.onFinishApproach = { [weak self] isUpsideDown, isClockwise in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
self.isFailed = true
|
||||
self.animationView.setSticker(nil, face: 0, mirror: false)
|
||||
|
||||
var availableStickers: [ComponentView<Empty>] = []
|
||||
for (id, gift) in self.selectedGifts {
|
||||
if let id = id.base as? Int, component.gifts[Int32(id)] != nil {
|
||||
availableStickers.append(gift)
|
||||
}
|
||||
}
|
||||
let wrappingCount = min(2, availableStickers.count)
|
||||
for i in 0 ..< wrappingCount {
|
||||
if let sticker = availableStickers[i].view {
|
||||
let face: Int
|
||||
if isClockwise {
|
||||
face = i + 1
|
||||
} else {
|
||||
face = 3 - i
|
||||
}
|
||||
self.animationView.setSticker(sticker, face: face, mirror: isUpsideDown, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
self.flipFaces = isUpsideDown
|
||||
|
||||
Queue.mainQueue().after(0.3, {
|
||||
self.failWillFinish = true
|
||||
self.component?.willFinish(false)
|
||||
|
||||
self.craftFailPlayOnce.invoke(Void())
|
||||
})
|
||||
|
||||
Queue.mainQueue().after(0.5, {
|
||||
self.failDidFinish = true
|
||||
self.component?.finished(nil)
|
||||
})
|
||||
|
||||
self.state?.updated(transition: .easeInOut(duration: 0.4))
|
||||
}
|
||||
}
|
||||
|
||||
func setupSuccessAnimation(_ gift: StarGift.UniqueGift) {
|
||||
guard !self.didSetupFinishAnimation, let component = self.component else {
|
||||
return
|
||||
}
|
||||
self.didSetupFinishAnimation = true
|
||||
|
||||
self.animationView.isSuccess = true
|
||||
|
||||
self.animationView.onFinishApproach = { [weak self] isUpsideDown, isClockwise in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.isSuccess = true
|
||||
|
||||
var availableStickers: [ComponentView<Empty>] = []
|
||||
for (id, gift) in self.selectedGifts {
|
||||
if let id = id.base as? Int, component.gifts[Int32(id)] != nil {
|
||||
availableStickers.append(gift)
|
||||
}
|
||||
}
|
||||
let wrappingCount = min(2, availableStickers.count)
|
||||
for i in 0 ..< wrappingCount {
|
||||
if let sticker = availableStickers[i].view {
|
||||
let face: Int
|
||||
if isClockwise {
|
||||
face = i + 1
|
||||
} else {
|
||||
face = 3 - i
|
||||
}
|
||||
self.animationView.setSticker(sticker, face: face, mirror: isUpsideDown, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
self.flipFaces = isUpsideDown
|
||||
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||
let _ = self.successFace.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
GiftItemComponent(
|
||||
context: component.context,
|
||||
style: .glass,
|
||||
theme: presentationData.theme,
|
||||
strings: presentationData.strings,
|
||||
peer: nil,
|
||||
subject: .uniqueGift(gift: gift, price: nil),
|
||||
ribbon: nil,
|
||||
resellPrice: nil,
|
||||
isHidden: false,
|
||||
isSelected: false,
|
||||
isPinned: false,
|
||||
isEditing: false,
|
||||
mode: .grid,
|
||||
cornerRadius: 28.0,
|
||||
action: nil,
|
||||
contextAction: nil
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: cubeSide, height: cubeSide)
|
||||
)
|
||||
if let successView = self.successFace.view as? GiftItemComponent.View {
|
||||
let backgroundLayer = successView.backgroundLayer
|
||||
if let patternView = successView.pattern {
|
||||
backgroundLayer.opacity = 0.0
|
||||
patternView.alpha = 0.0
|
||||
Queue.mainQueue().after(1.0, {
|
||||
let transition = ComponentTransition.easeInOut(duration: 0.3)
|
||||
|
||||
transition.animateBlur(layer: backgroundLayer, fromRadius: 10.0, toRadius: 0.0)
|
||||
transition.setAlpha(layer: backgroundLayer, alpha: 1.0)
|
||||
|
||||
transition.setAlpha(view: patternView, alpha: 1.0)
|
||||
transition.animateBlur(layer: patternView.layer, fromRadius: 10.0, toRadius: 0.0)
|
||||
|
||||
Queue.mainQueue().after(1.0, {
|
||||
self.component?.finished(successView)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
self.animationView.setSticker(successView, face: 0, mirror: isUpsideDown)
|
||||
}
|
||||
self.state?.updated()
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: CraftTableComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
let previousComponent = self.component
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
self.animationView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: availableSize)
|
||||
|
||||
let permilleValue = component.gifts.reduce(0, { $0 + Int($1.value.gift.craftChancePermille ?? 0) })
|
||||
|
||||
for index in 0 ..< 6 {
|
||||
let face: ComponentView<Empty>
|
||||
if let current = self.faces[index] {
|
||||
face = current
|
||||
} else {
|
||||
face = ComponentView<Empty>()
|
||||
self.faces[index] = face
|
||||
}
|
||||
|
||||
let faceComponent: AnyComponent<Empty>
|
||||
var faceItems: [AnyComponentWithIdentity<Empty>] = []
|
||||
if index == 0 {
|
||||
faceItems.append(
|
||||
AnyComponentWithIdentity(id: "background", component: AnyComponent(
|
||||
CubeFaceComponent(color: component.buttonColor, cornerRadius: 28.0)
|
||||
))
|
||||
)
|
||||
if !component.isCrafting || self.isFailed {
|
||||
faceItems.append(
|
||||
AnyComponentWithIdentity(id: "glass", component: AnyComponent(
|
||||
GlassBackgroundComponent(size: CGSize(width: cubeSide, height: cubeSide), cornerRadius: 28.0, isDark: true, tintColor: .init(kind: .custom, color: component.buttonColor))
|
||||
))
|
||||
)
|
||||
}
|
||||
if self.isFailed {
|
||||
faceItems.append(
|
||||
AnyComponentWithIdentity(id: "faildial", component: AnyComponent(
|
||||
DialIndicatorComponent(
|
||||
content: AnyComponentWithIdentity(id: "gift", component: AnyComponent(
|
||||
LottieComponent(
|
||||
content: LottieComponent.AppBundleContent(name: "CraftFail"),
|
||||
color: .white,
|
||||
size: CGSize(width: 52.0, height: 52.0),
|
||||
playOnce: self.craftFailPlayOnce
|
||||
)
|
||||
)),
|
||||
backgroundColor: .white.withAlphaComponent(0.1),
|
||||
foregroundColor: .white,
|
||||
diameter: 84.0,
|
||||
contentSize: CGSize(width: 44.0, height: 44.0),
|
||||
lineWidth: 5.0,
|
||||
fontSize: 18.0,
|
||||
progress: 0.0,
|
||||
value: component.gifts.count,
|
||||
suffix: "",
|
||||
isVisible: true,
|
||||
isFlipped: self.flipFaces
|
||||
)
|
||||
))
|
||||
)
|
||||
} else if !self.isSuccess {
|
||||
faceItems.append(
|
||||
AnyComponentWithIdentity(id: "dial", component: AnyComponent(
|
||||
DialIndicatorComponent(
|
||||
content: AnyComponentWithIdentity(id: "empty", component: AnyComponent(Rectangle(color: .clear))),
|
||||
backgroundColor: .white.withAlphaComponent(0.1),
|
||||
foregroundColor: .white,
|
||||
diameter: 84.0,
|
||||
lineWidth: 5.0,
|
||||
fontSize: 18.0,
|
||||
progress: CGFloat(permilleValue) / 10.0 / 100.0,
|
||||
value: permilleValue / 10,
|
||||
suffix: "%",
|
||||
isVisible: !component.isCrafting
|
||||
)
|
||||
))
|
||||
)
|
||||
faceItems.append(
|
||||
AnyComponentWithIdentity(id: "icon", component: AnyComponent(
|
||||
LottieComponent(
|
||||
content: LottieComponent.AppBundleContent(name: "Anvil"),
|
||||
size: CGSize(width: 52.0, height: 52.0),
|
||||
playOnce: self.anvilPlayOnce
|
||||
)
|
||||
))
|
||||
)
|
||||
}
|
||||
} else {
|
||||
faceItems.append(
|
||||
AnyComponentWithIdentity(id: "background", component: AnyComponent(
|
||||
CubeFaceComponent(color: component.buttonColor, cornerRadius: 28.0)
|
||||
))
|
||||
)
|
||||
faceItems.append(
|
||||
AnyComponentWithIdentity(id: "icon", component: AnyComponent(
|
||||
BundleIconComponent(name: "Components/CubeSide", tintColor: nil, flipVertically: index < 4 ? self.flipFaces : false)
|
||||
))
|
||||
)
|
||||
}
|
||||
faceComponent = AnyComponent(
|
||||
ZStack(faceItems)
|
||||
)
|
||||
|
||||
let _ = face.update(
|
||||
transition: transition,
|
||||
component: faceComponent,
|
||||
environment: {},
|
||||
containerSize: CGSize(width: cubeSide, height: cubeSide)
|
||||
)
|
||||
}
|
||||
|
||||
if previousComponent == nil {
|
||||
var faceViews: [UIView] = []
|
||||
for index in 0 ..< 6 {
|
||||
if let faceView = self.faces[index]?.view {
|
||||
faceView.bounds = CGRect(origin: .zero, size: CGSize(width: cubeSide, height: cubeSide))
|
||||
faceView.clipsToBounds = true
|
||||
faceView.layer.rasterizationScale = UIScreenScale
|
||||
faceView.layer.cornerRadius = 28.0
|
||||
faceViews.append(faceView)
|
||||
}
|
||||
}
|
||||
self.animationView.setFaces(faceViews)
|
||||
}
|
||||
|
||||
var stickerViews: [UIView] = []
|
||||
for index in 0 ..< 4 {
|
||||
let itemId = AnyHashable(index)
|
||||
|
||||
var itemTransition = transition
|
||||
let visibleItem: ComponentView<Empty>
|
||||
if let current = self.selectedGifts[itemId] {
|
||||
visibleItem = current
|
||||
} else {
|
||||
visibleItem = ComponentView()
|
||||
self.selectedGifts[itemId] = visibleItem
|
||||
itemTransition = .immediate
|
||||
}
|
||||
|
||||
let gift = component.gifts[Int32(index)]
|
||||
|
||||
let _ = visibleItem.update(
|
||||
transition: itemTransition,
|
||||
component: AnyComponent(
|
||||
GiftSlotComponent(
|
||||
context: component.context,
|
||||
gift: gift,
|
||||
buttonColor: component.buttonColor,
|
||||
isCrafting: component.isCrafting,
|
||||
action: {
|
||||
component.select(Int32(index))
|
||||
},
|
||||
removeAction: index > 0 ? {
|
||||
component.remove(Int32(index))
|
||||
} : nil
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: cubeSide, height: cubeSide)
|
||||
)
|
||||
if let itemView = visibleItem.view {
|
||||
stickerViews.append(itemView)
|
||||
}
|
||||
}
|
||||
|
||||
if previousComponent == nil {
|
||||
self.animationView.setStickers(stickerViews)
|
||||
}
|
||||
|
||||
if let previousComponent, previousComponent.isCrafting != component.isCrafting {
|
||||
var indices: [Int] = []
|
||||
for index in component.gifts.keys.sorted() {
|
||||
indices.append(Int(index))
|
||||
}
|
||||
|
||||
Queue.mainQueue().after(0.55) {
|
||||
HapticFeedback().impact(.light)
|
||||
}
|
||||
|
||||
self.anvilPlayOnce.invoke(Void())
|
||||
Queue.mainQueue().after(0.75, {
|
||||
self.animationView.startStickerSequence(indices: indices)
|
||||
|
||||
switch component.result {
|
||||
case let .gift(gift):
|
||||
if case let .unique(uniqueGift) = gift.gift {
|
||||
self.setupSuccessAnimation(uniqueGift)
|
||||
}
|
||||
case .fail:
|
||||
self.setupFailureAnimation()
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
final class GiftSlotComponent: Component {
|
||||
let context: AccountContext
|
||||
let gift: GiftItem?
|
||||
let buttonColor: UIColor
|
||||
let isCrafting: Bool
|
||||
let action: () -> Void
|
||||
let removeAction: (() -> Void)?
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
gift: GiftItem?,
|
||||
buttonColor: UIColor,
|
||||
isCrafting: Bool,
|
||||
action: @escaping () -> Void,
|
||||
removeAction: (() -> Void)?
|
||||
) {
|
||||
self.context = context
|
||||
self.gift = gift
|
||||
self.buttonColor = buttonColor
|
||||
self.isCrafting = isCrafting
|
||||
self.action = action
|
||||
self.removeAction = removeAction
|
||||
}
|
||||
|
||||
public static func ==(lhs: GiftSlotComponent, rhs: GiftSlotComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.gift != rhs.gift {
|
||||
return false
|
||||
}
|
||||
if lhs.buttonColor != rhs.buttonColor {
|
||||
return false
|
||||
}
|
||||
if lhs.isCrafting != rhs.isCrafting {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private let backgroundView = GlassBackgroundView()
|
||||
private let addIcon = UIImageView()
|
||||
private var icon: ComponentView<Empty>?
|
||||
private let button = HighlightTrackingButton()
|
||||
|
||||
private var badge: ComponentView<Empty>?
|
||||
private var removeIcon: ComponentView<Empty>?
|
||||
private let removeButton = HighlightTrackingButton()
|
||||
|
||||
private var component: GiftSlotComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addIcon.image = generateAddIcon(backgroundColor: .white)
|
||||
|
||||
self.addSubview(self.backgroundView)
|
||||
self.backgroundView.contentView.addSubview(self.addIcon)
|
||||
self.backgroundView.contentView.addSubview(self.button)
|
||||
self.addSubview(self.removeButton)
|
||||
|
||||
self.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside)
|
||||
self.removeButton.addTarget(self, action: #selector(self.removeButtonPressed), for: .touchUpInside)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@objc private func buttonPressed() {
|
||||
guard let _ = self.component?.removeAction else {
|
||||
return
|
||||
}
|
||||
self.component?.action()
|
||||
}
|
||||
|
||||
@objc private func removeButtonPressed() {
|
||||
self.component?.removeAction?()
|
||||
}
|
||||
|
||||
func update(component: GiftSlotComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
let previousComponent = self.component
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let backgroundFrame = CGRect(origin: .zero, size: availableSize).insetBy(dx: 1.0, dy: 1.0)
|
||||
self.backgroundView.update(size: backgroundFrame.size, cornerRadius: 28.0, isDark: true, tintColor: .init(kind: .custom, color: component.buttonColor), isInteractive: true, transition: .immediate)
|
||||
transition.setFrame(view: self.backgroundView, frame: backgroundFrame)
|
||||
if component.gift == nil && component.isCrafting && previousComponent?.isCrafting == false {
|
||||
transition.setBlur(layer: self.backgroundView.layer, radius: 10.0)
|
||||
self.backgroundView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.35, removeOnCompletion: false)
|
||||
transition.setBlur(layer: self.addIcon.layer, radius: 10.0)
|
||||
}
|
||||
transition.setAlpha(view: self.addIcon, alpha: component.isCrafting ? 0.0 : 1.0)
|
||||
|
||||
if let icon = self.addIcon.image {
|
||||
transition.setFrame(view: self.addIcon, frame: CGRect(origin: CGPoint(x: floor((backgroundFrame.width - icon.size.width) / 2.0), y: floor((backgroundFrame.height - icon.size.height) / 2.0)), size: icon.size))
|
||||
}
|
||||
|
||||
if previousComponent?.gift?.gift.id != component.gift?.gift.id {
|
||||
if let iconView = self.icon?.view {
|
||||
if transition.animation.isImmediate {
|
||||
iconView.removeFromSuperview()
|
||||
} else {
|
||||
transition.setScale(view: iconView, scale: 0.01)
|
||||
transition.setAlpha(view: iconView, alpha: 0.0, completion: { _ in
|
||||
iconView.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
self.icon = nil
|
||||
}
|
||||
|
||||
if (previousComponent?.gift?.gift.id == nil) != (component.gift?.gift.id == nil) || ((previousComponent?.isCrafting ?? false) != component.isCrafting && component.isCrafting) {
|
||||
if let badgeView = self.badge?.view {
|
||||
if transition.animation.isImmediate {
|
||||
badgeView.removeFromSuperview()
|
||||
} else {
|
||||
transition.setBlur(layer: badgeView.layer, radius: 10.0)
|
||||
transition.setAlpha(view: badgeView, alpha: 0.0, completion: { _ in
|
||||
badgeView.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
self.badge = nil
|
||||
|
||||
if let removeButtonView = self.removeIcon?.view {
|
||||
if transition.animation.isImmediate {
|
||||
removeButtonView.removeFromSuperview()
|
||||
} else {
|
||||
transition.setBlur(layer: removeButtonView.layer, radius: 10.0)
|
||||
transition.setAlpha(view: removeButtonView, alpha: 0.0, completion: { _ in
|
||||
removeButtonView.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
self.removeIcon = nil
|
||||
}
|
||||
|
||||
if let gift = component.gift {
|
||||
let icon: ComponentView<Empty>
|
||||
var iconTransition = transition
|
||||
if let current = self.icon {
|
||||
icon = current
|
||||
} else {
|
||||
iconTransition = .immediate
|
||||
icon = ComponentView()
|
||||
self.icon = icon
|
||||
}
|
||||
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||
let iconSize = icon.update(
|
||||
transition: iconTransition,
|
||||
component: AnyComponent(
|
||||
GiftItemComponent(
|
||||
context: component.context,
|
||||
style: .glass,
|
||||
theme: presentationData.theme,
|
||||
strings: presentationData.strings,
|
||||
peer: nil,
|
||||
subject: .uniqueGift(gift: gift.gift, price: nil),
|
||||
ribbon: nil,
|
||||
resellPrice: nil,
|
||||
isHidden: false,
|
||||
isSelected: false,
|
||||
isPinned: false,
|
||||
isEditing: false,
|
||||
mode: .grid,
|
||||
cornerRadius: 28.0,
|
||||
action: nil,
|
||||
contextAction: nil
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: availableSize.height)
|
||||
)
|
||||
let iconFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: iconSize)
|
||||
if let iconView = icon.view {
|
||||
if iconView.superview == nil {
|
||||
iconView.isUserInteractionEnabled = false
|
||||
if let badgeView = self.badge?.view {
|
||||
self.backgroundView.contentView.insertSubview(iconView, belowSubview: badgeView)
|
||||
} else {
|
||||
self.backgroundView.contentView.addSubview(iconView)
|
||||
}
|
||||
|
||||
if !transition.animation.isImmediate {
|
||||
transition.animateAlpha(view: iconView, from: 0.0, to: 1.0)
|
||||
transition.animateScale(view: iconView, from: 0.01, to: 1.0)
|
||||
}
|
||||
}
|
||||
iconTransition.setFrame(view: iconView, frame: iconFrame)
|
||||
}
|
||||
|
||||
if !component.isCrafting {
|
||||
var buttonColor: UIColor = component.buttonColor
|
||||
if let backdropAttribute = gift.gift.attributes.first(where: { attribute in
|
||||
if case .backdrop = attribute {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}), case let .backdrop(_, _, innerColor, _, _, _, _) = backdropAttribute {
|
||||
buttonColor = UIColor(rgb: UInt32(bitPattern: innerColor)).withMultipliedBrightnessBy(0.65)
|
||||
}
|
||||
|
||||
let badge: ComponentView<Empty>
|
||||
var badgeTransition = transition
|
||||
if let current = self.badge {
|
||||
badge = current
|
||||
} else {
|
||||
badgeTransition = .immediate
|
||||
badge = ComponentView()
|
||||
self.badge = badge
|
||||
}
|
||||
|
||||
let badgeSize = badge.update(
|
||||
transition: badgeTransition,
|
||||
component: AnyComponent(
|
||||
ZStack([
|
||||
AnyComponentWithIdentity(id: "background", component: AnyComponent(
|
||||
RoundedRectangle(color: buttonColor, cornerRadius: 13.5, size: CGSize(width: 54.0, height: 27.0))
|
||||
)),
|
||||
AnyComponentWithIdentity(id: "icon", component: AnyComponent(
|
||||
Text(text: "\((gift.gift.craftChancePermille ?? 0) / 10)%", font: Font.semibold(17.0), color: .white)
|
||||
))
|
||||
])
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 54.0, height: 27.0)
|
||||
)
|
||||
let badgeFrame = CGRect(origin: CGPoint(x: -6.0, y: -6.0 - UIScreenPixel), size: badgeSize)
|
||||
if let badgeView = badge.view {
|
||||
if badgeView.superview == nil {
|
||||
badgeView.isUserInteractionEnabled = false
|
||||
self.backgroundView.contentView.addSubview(badgeView)
|
||||
|
||||
if !transition.animation.isImmediate {
|
||||
transition.animateAlpha(view: badgeView, from: 0.0, to: 1.0)
|
||||
transition.animateScale(view: badgeView, from: 0.01, to: 1.0)
|
||||
}
|
||||
}
|
||||
badgeTransition.setFrame(view: badgeView, frame: badgeFrame)
|
||||
}
|
||||
|
||||
|
||||
if let _ = component.removeAction {
|
||||
let removeButton: ComponentView<Empty>
|
||||
var removeButtonTransition = transition
|
||||
if let current = self.removeIcon {
|
||||
removeButton = current
|
||||
} else {
|
||||
removeButtonTransition = .immediate
|
||||
removeButton = ComponentView()
|
||||
self.removeIcon = removeButton
|
||||
}
|
||||
|
||||
let removeButtonSize = removeButton.update(
|
||||
transition: removeButtonTransition,
|
||||
component: AnyComponent(
|
||||
ZStack([
|
||||
AnyComponentWithIdentity(id: "background", component: AnyComponent(
|
||||
RoundedRectangle(color: buttonColor, cornerRadius: 13.5, size: CGSize(width: 27.0, height: 27.0))
|
||||
)),
|
||||
AnyComponentWithIdentity(id: "icon", component: AnyComponent(
|
||||
BundleIconComponent(name: "Media Gallery/PictureInPictureClose", tintColor: .white)
|
||||
))
|
||||
])
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 27.0, height: 27.0)
|
||||
)
|
||||
let removeButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - 21.0, y: -6.0 - UIScreenPixel), size: removeButtonSize)
|
||||
if let removeButtonView = removeButton.view {
|
||||
if removeButtonView.superview == nil {
|
||||
removeButtonView.isUserInteractionEnabled = false
|
||||
self.backgroundView.contentView.addSubview(removeButtonView)
|
||||
|
||||
if !transition.animation.isImmediate {
|
||||
transition.animateAlpha(view: removeButtonView, from: 0.0, to: 1.0)
|
||||
transition.animateScale(view: removeButtonView, from: 0.01, to: 1.0)
|
||||
}
|
||||
}
|
||||
removeButtonTransition.setFrame(view: removeButtonView, frame: removeButtonFrame)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.isUserInteractionEnabled = !component.isCrafting
|
||||
self.button.frame = CGRect(origin: .zero, size: availableSize)
|
||||
|
||||
self.removeButton.isUserInteractionEnabled = component.removeAction != nil
|
||||
if let removeIcon = self.removeIcon?.view {
|
||||
self.removeButton.frame = removeIcon.frame.insetBy(dx: -8.0, dy: -8.0)
|
||||
}
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
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 generateAddIcon(backgroundColor: UIColor) -> UIImage? {
|
||||
return generateImage(CGSize(width: 46.0, height: 46.0), contextGenerator: { size, context in
|
||||
context.clear(CGRect(origin: .zero, size: size))
|
||||
|
||||
context.setFillColor(backgroundColor.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: .zero, size: size))
|
||||
|
||||
context.setBlendMode(.clear)
|
||||
context.setStrokeColor(UIColor.clear.cgColor)
|
||||
context.setLineWidth(4.0)
|
||||
context.setLineCap(.round)
|
||||
|
||||
context.move(to: CGPoint(x: 23.0, y: 13.0))
|
||||
context.addLine(to: CGPoint(x: 23.0, y: 33.0))
|
||||
context.strokePath()
|
||||
|
||||
context.move(to: CGPoint(x: 13.0, y: 23.0))
|
||||
context.addLine(to: CGPoint(x: 33.0, y: 23.0))
|
||||
context.strokePath()
|
||||
})
|
||||
}
|
||||
|
||||
private final class CubeFaceComponent: Component {
|
||||
private let color: UIColor
|
||||
private let cornerRadius: CGFloat
|
||||
|
||||
public init(color: UIColor, cornerRadius: CGFloat) {
|
||||
self.color = color
|
||||
self.cornerRadius = cornerRadius
|
||||
}
|
||||
|
||||
public static func ==(lhs: CubeFaceComponent, rhs: CubeFaceComponent) -> Bool {
|
||||
if !lhs.color.isEqual(rhs.color) {
|
||||
return false
|
||||
}
|
||||
if lhs.cornerRadius != rhs.cornerRadius {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
override public init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
self.clipsToBounds = true
|
||||
self.layer.cornerCurve = .continuous
|
||||
}
|
||||
|
||||
required public init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
transition.setBackgroundColor(view: view, color: self.color)
|
||||
transition.setCornerRadius(layer: view.layer, cornerRadius: self.cornerRadius)
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
+792
@@ -0,0 +1,792 @@
|
||||
import UIKit
|
||||
import simd
|
||||
import Display
|
||||
|
||||
final class Transform3DView: UIView {
|
||||
override class var layerClass: AnyClass { CATransformLayer.self }
|
||||
}
|
||||
|
||||
final class PassthroughView: UIView {
|
||||
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
||||
for subview in self.subviews where !subview.isHidden && subview.alpha > 0 && subview.isUserInteractionEnabled {
|
||||
let converted = self.convert(point, to: subview)
|
||||
if subview.point(inside: converted, with: event) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
final class CubeAnimationView: UIView {
|
||||
private let cubeSize: CGFloat
|
||||
private var perspective: CGFloat = 400.0
|
||||
private let stickerSize: CGFloat
|
||||
private let stickerGap: CGFloat
|
||||
|
||||
private let camera = UIView()
|
||||
private let cubeContainer = Transform3DView()
|
||||
private var faces: [UIView] = []
|
||||
private var faceOccupants: [Int: UIView] = [:]
|
||||
|
||||
let stickerContainer = PassthroughView()
|
||||
|
||||
private var stickers: [UIView] = []
|
||||
private var isRunning = false
|
||||
|
||||
private var displayLink: SharedDisplayLinkDriver.Link?
|
||||
private var lastTimestamp: CFTimeInterval = 0
|
||||
private var warpDisplayLink: SharedDisplayLinkDriver.Link?
|
||||
private weak var warpView: UIView?
|
||||
private var warpStartQuad: Quad?
|
||||
private var warpEndQuad: Quad?
|
||||
private var warpDuration: TimeInterval = 0
|
||||
private var warpDynamicTarget: (() -> Quad)?
|
||||
private var warpCompletion: (() -> Void)?
|
||||
private var warpStartTimestamp: CFTimeInterval = 0
|
||||
private var warpLastProgress: CGFloat = 0
|
||||
private var warpCurrentQuad: Quad?
|
||||
private var warpHasCompleted = false
|
||||
private var warpSnapshot: UIView?
|
||||
|
||||
private var rotation = SIMD3<Float>(repeating: 0)
|
||||
private var angularVelocity = SIMD3<Float>(repeating: 0)
|
||||
|
||||
private let dampingPerSecond: Float = 0.66
|
||||
private let finishSpringX: Float = 28.0
|
||||
private let finishSpringY: Float = 18.0
|
||||
private let finishDampingX: Float = 2.0 * sqrt(28.0)
|
||||
private let finishDampingY: Float = 2.0 * sqrt(18.0)
|
||||
private let finishWobbleAmplitudeZ: Float = 10.0 * .pi / 180.0
|
||||
private let finishWobbleCycles: Float = 1.0
|
||||
private let finishWobbleDampingExponent: Float = 0.6
|
||||
private let finishSuccessScale: Float = 1.3
|
||||
private let finishSuccessScaleTriggerAngle: Float = 0.4 * .pi
|
||||
private let finishApproachTriggerAngle: Float = 1.5 * .pi
|
||||
private let baseImpulseStrength: Float = 4.0
|
||||
private let impactNudgeDistance: CGFloat = 20.0
|
||||
private let impactNudgeEmphasis: CGFloat = 28.0
|
||||
|
||||
private var isFinishingX = false
|
||||
private var isFinishingY = false
|
||||
private var finishTargetX: Float = 0.0
|
||||
private var finishTargetY: Float = 0.0
|
||||
private var finishDirectionY: Float = 1.0
|
||||
private var finishRotationY: Float = 0.0
|
||||
private var finishTargetYUnwrapped: Float = 0.0
|
||||
private var finishRemainingYStart: Float = 0.0
|
||||
private var finishDelayTimerX: Timer?
|
||||
private var finishDelayTimerY: Timer?
|
||||
private var cubeScale: Float = 1.0
|
||||
private var hasFiredFinishApproach = false
|
||||
|
||||
var isSuccess = false
|
||||
|
||||
var onStickerLaunch: (() -> Void)?
|
||||
var onFinishApproach: ((Bool, Bool) -> Void)?
|
||||
|
||||
private let defaultStickOrder: [Int] = [0, 5, 4, 3]
|
||||
private let sequenceStickOrders: [String: [Int]] = [
|
||||
"0": [0],
|
||||
"0,1": [0, 5],
|
||||
"0,2": [0, 5],
|
||||
"0,3": [0, 5],
|
||||
"0,1,2": [0, 5, 4],
|
||||
"0,1,3": [0, 5, 2],
|
||||
"0,2,3": [0, 5, 1],
|
||||
"0,1,2,3": [0, 5, 4, 3]
|
||||
]
|
||||
private var activeStickOrder: [Int] = []
|
||||
|
||||
init(cubeSize: CGFloat = 110.0, stickerSize: CGFloat = 76.0, stickerGap: CGFloat = 30.0) {
|
||||
self.cubeSize = cubeSize
|
||||
self.stickerSize = stickerSize
|
||||
self.stickerGap = stickerGap
|
||||
|
||||
super.init(frame: .zero)
|
||||
|
||||
self.activeStickOrder = self.defaultStickOrder
|
||||
|
||||
self.camera.backgroundColor = .clear
|
||||
self.camera.clipsToBounds = false
|
||||
self.addSubview(self.camera)
|
||||
|
||||
self.cubeContainer.backgroundColor = .clear
|
||||
self.cubeContainer.clipsToBounds = false
|
||||
self.camera.addSubview(self.cubeContainer)
|
||||
|
||||
var p = CATransform3DIdentity
|
||||
p.m34 = -1.0 / self.perspective
|
||||
self.camera.layer.sublayerTransform = p
|
||||
self.stickerContainer.layer.sublayerTransform = p
|
||||
|
||||
self.stickerContainer.backgroundColor = .clear
|
||||
self.stickerContainer.clipsToBounds = false
|
||||
self.addSubview(self.stickerContainer)
|
||||
|
||||
#if DEBUG
|
||||
let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
|
||||
self.camera.addGestureRecognizer(pan)
|
||||
#endif
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
self.camera.bounds = CGRect(x: 0, y: 0, width: self.cubeSize, height: self.cubeSize)
|
||||
self.camera.center = CGPoint(x: self.bounds.midX, y: self.bounds.midY)
|
||||
|
||||
self.cubeContainer.frame = self.camera.bounds
|
||||
self.stickerContainer.frame = self.bounds
|
||||
|
||||
self.layoutStickers()
|
||||
self.layoutFaces()
|
||||
self.applyCubeRotation()
|
||||
}
|
||||
|
||||
func setStickers(_ views: [UIView]) {
|
||||
self.stickers = views
|
||||
for view in views {
|
||||
view.layer.anchorPoint = .zero
|
||||
view.isUserInteractionEnabled = true
|
||||
|
||||
if view.superview !== self.stickerContainer {
|
||||
self.stickerContainer.addSubview(view)
|
||||
}
|
||||
}
|
||||
self.layoutStickers()
|
||||
}
|
||||
|
||||
func setSticker(_ sticker: UIView?, face index: Int, mirror: Bool, animated: Bool = false) {
|
||||
guard self.faces.indices.contains(index) else {
|
||||
return
|
||||
}
|
||||
|
||||
if let existing = self.faceOccupants[index] {
|
||||
existing.removeFromSuperview()
|
||||
self.faceOccupants[index] = nil
|
||||
}
|
||||
|
||||
guard let sticker else {
|
||||
return
|
||||
}
|
||||
|
||||
if let priorIndex = self.faceOccupants.first(where: { $0.value === sticker })?.key {
|
||||
self.faceOccupants[priorIndex] = nil
|
||||
}
|
||||
|
||||
if animated, let stickerSuperview = sticker.superview, let snapshotView = sticker.snapshotView(afterScreenUpdates: false) {
|
||||
stickerSuperview.addSubview(snapshotView)
|
||||
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
|
||||
snapshotView.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
sticker.removeFromSuperview()
|
||||
|
||||
let targetFace = self.faces[index]
|
||||
targetFace.addSubview(sticker)
|
||||
self.faceOccupants[index] = sticker
|
||||
|
||||
sticker.layer.removeAllAnimations()
|
||||
sticker.transform = .identity
|
||||
sticker.layer.transform = CATransform3DIdentity
|
||||
sticker.layer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
|
||||
sticker.layer.isDoubleSided = false
|
||||
sticker.clipsToBounds = false
|
||||
sticker.isUserInteractionEnabled = false
|
||||
|
||||
let faceStickerSize = self.cubeSize
|
||||
sticker.bounds = CGRect(x: 0, y: 0, width: faceStickerSize, height: faceStickerSize)
|
||||
sticker.center = CGPoint(x: self.cubeSize / 2, y: self.cubeSize / 2)
|
||||
|
||||
var snappedAngle: CGFloat = 0.0
|
||||
if mirror {
|
||||
snappedAngle += .pi
|
||||
}
|
||||
sticker.transform = CGAffineTransform(rotationAngle: snappedAngle)
|
||||
|
||||
if animated {
|
||||
sticker.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
|
||||
func startStickerSequence(indices: [Int]? = nil) {
|
||||
guard !self.isRunning else {
|
||||
return
|
||||
}
|
||||
guard self.stickers.contains(where: { $0.superview === self.stickerContainer }) else {
|
||||
return
|
||||
}
|
||||
self.isRunning = true
|
||||
|
||||
let sequence: [Int]
|
||||
if let indices, !indices.isEmpty {
|
||||
var seen = Set<Int>()
|
||||
var result: [Int] = []
|
||||
for index in indices where self.stickers.indices.contains(index) {
|
||||
if seen.insert(index).inserted {
|
||||
result.append(index)
|
||||
}
|
||||
}
|
||||
sequence = result
|
||||
} else {
|
||||
sequence = Array(self.stickers.indices)
|
||||
}
|
||||
|
||||
var stickOrder: [Int]
|
||||
let key = sequence.map(String.init).joined(separator: ",")
|
||||
if let order = self.sequenceStickOrders[key] {
|
||||
stickOrder = order
|
||||
} else {
|
||||
stickOrder = Array(self.defaultStickOrder.prefix(sequence.count))
|
||||
}
|
||||
self.activeStickOrder = stickOrder
|
||||
|
||||
self.scheduleStickerSequence(from: 0, indices: sequence)
|
||||
}
|
||||
|
||||
func resetAll() {
|
||||
self.isRunning = false
|
||||
self.resetStickers()
|
||||
self.resetCube()
|
||||
self.activeStickOrder = self.defaultStickOrder
|
||||
}
|
||||
|
||||
func setFaces(_ views: [UIView]) {
|
||||
guard views.count == 6 else {
|
||||
return
|
||||
}
|
||||
self.faces.forEach { $0.removeFromSuperview() }
|
||||
self.faces = views
|
||||
for face in views {
|
||||
face.layer.isDoubleSided = false
|
||||
self.cubeContainer.addSubview(face)
|
||||
}
|
||||
self.layoutFaces()
|
||||
}
|
||||
|
||||
private func layoutFaces() {
|
||||
guard self.faces.count == 6 else {
|
||||
return
|
||||
}
|
||||
let half = self.cubeSize / 2
|
||||
|
||||
for face in self.faces {
|
||||
face.bounds = CGRect(x: 0, y: 0, width: self.cubeSize, height: self.cubeSize)
|
||||
face.center = CGPoint(x: self.cubeSize / 2, y: self.cubeSize / 2)
|
||||
}
|
||||
|
||||
func faceTransform(rx: CGFloat, ry: CGFloat) -> CATransform3D {
|
||||
var m = CATransform3DIdentity
|
||||
m = CATransform3DRotate(m, rx, 1, 0, 0)
|
||||
m = CATransform3DRotate(m, ry, 0, 1, 0)
|
||||
m = CATransform3DTranslate(m, 0, 0, half)
|
||||
return m
|
||||
}
|
||||
|
||||
self.faces[0].layer.transform = faceTransform(rx: 0, ry: 0)
|
||||
self.faces[1].layer.transform = faceTransform(rx: 0, ry: .pi / 2)
|
||||
self.faces[2].layer.transform = faceTransform(rx: 0, ry: .pi)
|
||||
self.faces[3].layer.transform = faceTransform(rx: 0, ry: -.pi / 2)
|
||||
self.faces[4].layer.transform = faceTransform(rx: -.pi / 2, ry: 0)
|
||||
self.faces[5].layer.transform = faceTransform(rx: .pi / 2, ry: 0)
|
||||
}
|
||||
|
||||
private func animateWarp(for view: UIView, from startQuad: Quad, to targetQuad: Quad, duration: TimeInterval, dynamicTarget: (() -> Quad)? = nil, completion: @escaping () -> Void) {
|
||||
self.cancelWarp()
|
||||
self.warpView = view
|
||||
self.warpStartQuad = startQuad
|
||||
self.warpEndQuad = targetQuad
|
||||
self.warpDuration = duration
|
||||
self.warpDynamicTarget = dynamicTarget
|
||||
self.warpCompletion = completion
|
||||
self.warpStartTimestamp = 0
|
||||
self.warpLastProgress = 0
|
||||
self.warpHasCompleted = false
|
||||
self.warpCurrentQuad = startQuad
|
||||
startQuad.apply(to: view)
|
||||
|
||||
let link = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max) { [weak self] _ in
|
||||
self?.stepWarp()
|
||||
}
|
||||
link.isPaused = false
|
||||
self.warpDisplayLink = link
|
||||
}
|
||||
|
||||
private func stepWarp() {
|
||||
guard let view = self.warpView, let currentQuad = self.warpCurrentQuad, let endQuad = self.warpEndQuad else {
|
||||
self.finishWarp()
|
||||
return
|
||||
}
|
||||
|
||||
if self.warpStartTimestamp == 0 {
|
||||
self.warpStartTimestamp = CACurrentMediaTime()
|
||||
}
|
||||
|
||||
let elapsed = CACurrentMediaTime() - self.warpStartTimestamp
|
||||
let progress = self.warpDuration > 0 ? min(1.0, elapsed / self.warpDuration) : 1.0
|
||||
let t = CGFloat(progress)
|
||||
let eased = t * t * (3 - 2 * t)
|
||||
let target = self.warpDynamicTarget?() ?? endQuad
|
||||
let delta = eased - self.warpLastProgress
|
||||
let remaining = max(1 - self.warpLastProgress, 0.0001)
|
||||
let weight = max(0, min(1, delta / remaining))
|
||||
let nextQuad = currentQuad.interpolated(to: target, t: weight)
|
||||
nextQuad.apply(to: view)
|
||||
self.warpCurrentQuad = nextQuad
|
||||
self.warpLastProgress = eased
|
||||
|
||||
if progress >= 1.0 {
|
||||
self.finishWarp()
|
||||
}
|
||||
}
|
||||
|
||||
private func cancelWarp() {
|
||||
self.warpHasCompleted = true
|
||||
self.warpDisplayLink?.invalidate()
|
||||
self.warpDisplayLink = nil
|
||||
self.warpCompletion = nil
|
||||
self.clearWarpState()
|
||||
}
|
||||
|
||||
private func finishWarp() {
|
||||
guard !self.warpHasCompleted else { return }
|
||||
self.warpHasCompleted = true
|
||||
self.warpDisplayLink?.invalidate()
|
||||
self.warpDisplayLink = nil
|
||||
self.warpCompletion?()
|
||||
self.warpCompletion = nil
|
||||
self.clearWarpState()
|
||||
}
|
||||
|
||||
private func clearWarpState() {
|
||||
self.warpView = nil
|
||||
self.warpStartQuad = nil
|
||||
self.warpEndQuad = nil
|
||||
self.warpDynamicTarget = nil
|
||||
self.warpStartTimestamp = 0
|
||||
self.warpLastProgress = 0
|
||||
self.warpCurrentQuad = nil
|
||||
}
|
||||
|
||||
private func projectedQuad(for face: UIView) -> ProjectedFace {
|
||||
let bounds = face.bounds
|
||||
|
||||
func project(_ p: CGPoint) -> CGPoint {
|
||||
let inRoot = face.layer.convert(p, to: self.layer)
|
||||
return self.stickerContainer.layer.convert(inRoot, from: self.layer)
|
||||
}
|
||||
|
||||
var topLeft = project(CGPoint(x: bounds.minX, y: bounds.minY))
|
||||
var topRight = project(CGPoint(x: bounds.maxX, y: bounds.minY))
|
||||
var bottomLeft = project(CGPoint(x: bounds.minX, y: bounds.maxY))
|
||||
var bottomRight = project(CGPoint(x: bounds.maxX, y: bounds.maxY))
|
||||
|
||||
func center(_ a: CGPoint, _ b: CGPoint) -> CGPoint {
|
||||
CGPoint(x: (a.x + b.x) * 0.5, y: (a.y + b.y) * 0.5)
|
||||
}
|
||||
|
||||
func normalized(_ v: CGPoint) -> CGPoint? {
|
||||
let len = hypot(v.x, v.y)
|
||||
guard len > 1e-5 else { return nil }
|
||||
return CGPoint(x: v.x / len, y: v.y / len)
|
||||
}
|
||||
|
||||
func dot(_ a: CGPoint, _ b: CGPoint) -> CGFloat {
|
||||
a.x * b.x + a.y * b.y
|
||||
}
|
||||
|
||||
let screenUp = CGPoint(x: 0, y: -1)
|
||||
let screenRight = CGPoint(x: 1, y: 0)
|
||||
|
||||
if let up = normalized(CGPoint(
|
||||
x: center(topLeft, topRight).x - center(bottomLeft, bottomRight).x,
|
||||
y: center(topLeft, topRight).y - center(bottomLeft, bottomRight).y
|
||||
)), dot(up, screenUp) < 0 {
|
||||
swap(&topLeft, &bottomLeft)
|
||||
swap(&topRight, &bottomRight)
|
||||
}
|
||||
|
||||
let faceOrigin = project(.zero)
|
||||
let faceX = project(CGPoint(x: 1, y: 0))
|
||||
|
||||
if let right = normalized(CGPoint(
|
||||
x: center(topRight, bottomRight).x - center(topLeft, bottomLeft).x,
|
||||
y: center(topRight, bottomRight).y - center(topLeft, bottomLeft).y
|
||||
)), dot(right, screenRight) < 0 {
|
||||
swap(&topLeft, &topRight)
|
||||
swap(&bottomLeft, &bottomRight)
|
||||
}
|
||||
|
||||
let quad = Quad(topLeft: topLeft, topRight: topRight, bottomLeft: bottomLeft, bottomRight: bottomRight)
|
||||
|
||||
let desiredTopVector = CGPoint(x: quad.topRight.x - quad.topLeft.x, y: quad.topRight.y - quad.topLeft.y)
|
||||
let baseTopVector = CGPoint(x: faceX.x - faceOrigin.x, y: faceX.y - faceOrigin.y)
|
||||
|
||||
let desiredAngle = atan2(desiredTopVector.y, desiredTopVector.x)
|
||||
let baseAngle = atan2(baseTopVector.y, baseTopVector.x)
|
||||
let rotation = normalizeAngle(desiredAngle - baseAngle)
|
||||
|
||||
return ProjectedFace(quad: quad, rotation: rotation)
|
||||
}
|
||||
|
||||
private func layoutStickers() {
|
||||
guard !self.stickers.isEmpty else {
|
||||
return
|
||||
}
|
||||
|
||||
let cubeCenterInSticker = self.stickerContainer.convert(self.camera.center, from: self)
|
||||
let r = self.cubeSize / 2 + self.stickerGap + self.stickerSize / 2
|
||||
let scale = self.stickerSize / self.cubeSize
|
||||
|
||||
let positions = [
|
||||
CGPoint(x: cubeCenterInSticker.x - r, y: cubeCenterInSticker.y - r * 0.4),
|
||||
CGPoint(x: cubeCenterInSticker.x + r, y: cubeCenterInSticker.y - r * 0.4),
|
||||
CGPoint(x: cubeCenterInSticker.x - r, y: cubeCenterInSticker.y + r * 0.4),
|
||||
CGPoint(x: cubeCenterInSticker.x + r, y: cubeCenterInSticker.y + r * 0.4)
|
||||
]
|
||||
|
||||
for (i, view) in self.stickers.enumerated() {
|
||||
if view.superview !== self.stickerContainer {
|
||||
continue
|
||||
}
|
||||
view.bounds = CGRect(x: 0, y: 0, width: self.cubeSize, height: self.cubeSize)
|
||||
view.transform = CGAffineTransform(scaleX: scale, y: scale)
|
||||
view.center = CGPoint(x: positions[i].x - self.stickerSize * 0.5, y: positions[i].y - self.stickerSize * 0.5)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func handlePan(_ gesture: UIPanGestureRecognizer) {
|
||||
let translation = gesture.translation(in: self.camera)
|
||||
switch gesture.state {
|
||||
case .changed:
|
||||
let delta = CGPoint(x: translation.x, y: translation.y)
|
||||
|
||||
self.rotation.y += Float(delta.x) * 0.018
|
||||
self.rotation.x += Float(-delta.y) * 0.018
|
||||
self.rotation = normalizedRotation(self.rotation)
|
||||
self.applyCubeRotation()
|
||||
|
||||
gesture.setTranslation(.zero, in: self.camera)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func launchStickerView(_ sticker: UIView, emphasized: Bool, willFinish: Bool = false) {
|
||||
guard sticker.superview === self.stickerContainer else {
|
||||
return
|
||||
}
|
||||
var number = 0
|
||||
if self.faceOccupants.count < self.activeStickOrder.count {
|
||||
number = self.activeStickOrder[self.faceOccupants.count]
|
||||
}
|
||||
let faceIndex = number
|
||||
guard self.faces.count > faceIndex else { return }
|
||||
let targetFace = self.faces[faceIndex]
|
||||
|
||||
let startCenterInSticker = sticker.center
|
||||
let cubeCenterInSticker = self.stickerContainer.convert(self.camera.center, from: self)
|
||||
|
||||
sticker.isUserInteractionEnabled = false
|
||||
sticker.layer.isDoubleSided = false
|
||||
|
||||
let faceStickerSize = self.cubeSize
|
||||
let duration: TimeInterval = 0.2
|
||||
let startQuad = Quad(rect: sticker.frame)
|
||||
let animationView: UIView
|
||||
if let snapshot = sticker.snapshotView(afterScreenUpdates: false) {
|
||||
self.warpSnapshot?.removeFromSuperview()
|
||||
self.warpSnapshot = snapshot
|
||||
|
||||
snapshot.bounds = sticker.bounds
|
||||
snapshot.center = sticker.center
|
||||
snapshot.layer.anchorPoint = sticker.layer.anchorPoint
|
||||
snapshot.layer.transform = sticker.layer.transform
|
||||
snapshot.layer.isDoubleSided = sticker.layer.isDoubleSided
|
||||
snapshot.isUserInteractionEnabled = false
|
||||
self.stickerContainer.addSubview(snapshot)
|
||||
|
||||
sticker.isHidden = true
|
||||
animationView = snapshot
|
||||
} else {
|
||||
animationView = sticker
|
||||
}
|
||||
sticker.transform = .identity
|
||||
|
||||
let projectedFace = self.projectedQuad(for: targetFace)
|
||||
let targetQuad = projectedFace.quad
|
||||
let dynamicTarget: () -> Quad = { [weak self, weak targetFace] in
|
||||
guard let self, let face = targetFace else {
|
||||
return targetQuad
|
||||
}
|
||||
return self.projectedQuad(for: face).quad
|
||||
}
|
||||
|
||||
self.animateWarp(for: animationView, from: startQuad, to: targetQuad, duration: duration, dynamicTarget: dynamicTarget) { [weak self, weak sticker, weak targetFace, weak animationView] in
|
||||
guard let self, let sticker, let targetFace else {
|
||||
return
|
||||
}
|
||||
|
||||
self.onStickerLaunch?()
|
||||
|
||||
if let animationView, animationView !== sticker {
|
||||
animationView.removeFromSuperview()
|
||||
self.warpSnapshot = nil
|
||||
sticker.isHidden = false
|
||||
}
|
||||
|
||||
sticker.removeFromSuperview()
|
||||
targetFace.addSubview(sticker)
|
||||
self.faceOccupants[faceIndex] = sticker
|
||||
|
||||
sticker.bounds = CGRect(x: 0, y: 0, width: faceStickerSize, height: faceStickerSize)
|
||||
sticker.layer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
|
||||
sticker.center = CGPoint(x: self.cubeSize / 2, y: self.cubeSize / 2)
|
||||
sticker.layer.transform = CATransform3DIdentity
|
||||
let finalProjection = self.projectedQuad(for: targetFace)
|
||||
let snappedAngle = snappedRightAngle(finalProjection.rotation)
|
||||
sticker.transform = CGAffineTransform(rotationAngle: snappedAngle)
|
||||
|
||||
let delta = SIMD2<Float>(Float(cubeCenterInSticker.x - startCenterInSticker.x), Float(cubeCenterInSticker.y - startCenterInSticker.y))
|
||||
let direction = normalize2(delta)
|
||||
self.applyImpulse(direction: direction, emphasized: emphasized, replace: true)
|
||||
self.applyImpactSpring(direction: direction, emphasized: emphasized)
|
||||
if willFinish {
|
||||
self.startFinishingAnimation()
|
||||
}
|
||||
self.startSpinLoopIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
private func resetStickers() {
|
||||
self.cancelWarp()
|
||||
self.warpSnapshot?.removeFromSuperview()
|
||||
self.warpSnapshot = nil
|
||||
|
||||
for sticker in self.stickers {
|
||||
sticker.layer.removeAllAnimations()
|
||||
sticker.transform = .identity
|
||||
sticker.layer.transform = CATransform3DIdentity
|
||||
sticker.layer.anchorPoint = .zero
|
||||
sticker.layer.isDoubleSided = true
|
||||
sticker.clipsToBounds = false
|
||||
sticker.isUserInteractionEnabled = true
|
||||
sticker.removeFromSuperview()
|
||||
self.stickerContainer.addSubview(sticker)
|
||||
}
|
||||
|
||||
self.faceOccupants.removeAll()
|
||||
self.layoutStickers()
|
||||
}
|
||||
|
||||
private func resetCube() {
|
||||
self.displayLink?.invalidate()
|
||||
self.displayLink = nil
|
||||
self.angularVelocity = .zero
|
||||
self.lastTimestamp = 0
|
||||
self.isFinishingX = false
|
||||
self.isFinishingY = false
|
||||
self.finishDelayTimerX?.invalidate()
|
||||
self.finishDelayTimerX = nil
|
||||
self.finishDelayTimerY?.invalidate()
|
||||
self.finishDelayTimerY = nil
|
||||
self.cubeScale = 1.0
|
||||
self.hasFiredFinishApproach = false
|
||||
|
||||
self.rotation = SIMD3<Float>(repeating: 0)
|
||||
self.cubeScale = 1.0
|
||||
self.applyCubeRotation()
|
||||
}
|
||||
|
||||
private func scheduleStickerSequence(from index: Int, indices: [Int]) {
|
||||
guard self.isRunning else {
|
||||
return
|
||||
}
|
||||
guard index < indices.count else {
|
||||
self.isRunning = false
|
||||
return
|
||||
}
|
||||
|
||||
let delay: TimeInterval = index == 0 ? 0.0 : 1.0
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
guard self.isRunning else {
|
||||
return
|
||||
}
|
||||
let stickerIndex = indices[index]
|
||||
if self.stickers.indices.contains(stickerIndex) {
|
||||
let isLast = index == indices.count - 1
|
||||
self.launchStickerView(self.stickers[stickerIndex], emphasized: isLast, willFinish: isLast)
|
||||
}
|
||||
self.scheduleStickerSequence(from: index + 1, indices: indices)
|
||||
}
|
||||
}
|
||||
|
||||
private func applyImpulse(direction: SIMD2<Float>, emphasized: Bool, replace: Bool) {
|
||||
var xStrength = self.baseImpulseStrength
|
||||
var yStrength = self.baseImpulseStrength
|
||||
if emphasized {
|
||||
xStrength *= 10.0
|
||||
yStrength *= 4.0
|
||||
}
|
||||
let impulseX: Float = -direction.y * xStrength
|
||||
let impulseY: Float = direction.x * yStrength
|
||||
let impulseZ: Float = 0.0
|
||||
|
||||
if replace {
|
||||
self.angularVelocity = SIMD3<Float>(impulseX, impulseY, impulseZ)
|
||||
} else {
|
||||
self.angularVelocity += SIMD3<Float>(impulseX, impulseY, impulseZ)
|
||||
}
|
||||
}
|
||||
|
||||
private func applyImpactSpring(direction: SIMD2<Float>, emphasized: Bool) {
|
||||
guard simd_length(direction) > 0.0001 else {
|
||||
return
|
||||
}
|
||||
let distance = emphasized ? self.impactNudgeEmphasis : self.impactNudgeDistance
|
||||
let offsetX = CGFloat(direction.x) * distance
|
||||
let offsetY = CGFloat(direction.y) * distance
|
||||
|
||||
let currentTransform = self.camera.layer.presentation()?.affineTransform() ?? self.camera.transform
|
||||
self.camera.layer.removeAllAnimations()
|
||||
let impactTransform = currentTransform.translatedBy(x: offsetX, y: offsetY)
|
||||
|
||||
UIView.animate(withDuration: 0.08, delay: 0.0, options: [.curveEaseOut, .beginFromCurrentState]) {
|
||||
self.camera.transform = impactTransform
|
||||
} completion: { _ in
|
||||
UIView.animate(withDuration: 0.55, delay: 0, usingSpringWithDamping: 0.72, initialSpringVelocity: 0.2, options: .beginFromCurrentState) {
|
||||
self.camera.transform = .identity
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func startSpinLoopIfNeeded() {
|
||||
if self.displayLink == nil {
|
||||
let link = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max) { [weak self] _ in
|
||||
self?.tick()
|
||||
}
|
||||
link.isPaused = false
|
||||
self.displayLink = link
|
||||
self.lastTimestamp = 0.0
|
||||
}
|
||||
}
|
||||
|
||||
private func tick() {
|
||||
let ts = CACurrentMediaTime()
|
||||
if self.lastTimestamp == 0 { self.lastTimestamp = ts; return }
|
||||
let dt = Float(ts - self.lastTimestamp)
|
||||
self.lastTimestamp = ts
|
||||
|
||||
self.rotation += self.angularVelocity * dt
|
||||
if self.isFinishingX {
|
||||
let delta = shortestAngleDelta(from: self.rotation.x, to: self.finishTargetX)
|
||||
let accel = self.finishSpringX * delta - self.finishDampingX * self.angularVelocity.x
|
||||
self.angularVelocity.x += accel * dt
|
||||
if abs(delta) < 0.0006 && abs(self.angularVelocity.x) < 0.001 {
|
||||
self.rotation.x = self.finishTargetX
|
||||
self.angularVelocity.x = 0.0
|
||||
self.isFinishingX = false
|
||||
}
|
||||
}
|
||||
if self.isFinishingY {
|
||||
self.finishRotationY += self.angularVelocity.y * dt
|
||||
let remaining = self.finishTargetYUnwrapped - self.finishRotationY
|
||||
let accel = self.finishSpringY * remaining - self.finishDampingY * self.angularVelocity.y
|
||||
self.angularVelocity.y += accel * dt
|
||||
self.rotation.y = normalizeAngle(self.finishRotationY)
|
||||
let total = max(abs(self.finishRemainingYStart), 0.0001)
|
||||
let progress = min(max(1.0 - abs(remaining) / total, 0.0), 1.0)
|
||||
let damping = pow(1.0 - progress, self.finishWobbleDampingExponent)
|
||||
let phase = 2.0 * Float.pi * self.finishWobbleCycles * progress
|
||||
self.rotation.z = self.finishWobbleAmplitudeZ * sin(phase) * damping
|
||||
let absRemaining = abs(remaining)
|
||||
if !self.hasFiredFinishApproach && absRemaining <= self.finishApproachTriggerAngle {
|
||||
self.hasFiredFinishApproach = true
|
||||
let upsideDown = abs(shortestAngleDelta(from: self.rotation.x, to: Float.pi)) < (Float.pi / 2)
|
||||
let isClockwise = self.finishDirectionY > 0
|
||||
self.onFinishApproach?(upsideDown, isClockwise)
|
||||
}
|
||||
if self.isSuccess, absRemaining <= self.finishSuccessScaleTriggerAngle {
|
||||
let raw = (self.finishSuccessScaleTriggerAngle - absRemaining) / self.finishSuccessScaleTriggerAngle
|
||||
let eased = raw * raw * (3 - 2 * raw)
|
||||
self.cubeScale = 1.0 + (self.finishSuccessScale - 1.0) * eased
|
||||
} else if !self.isSuccess {
|
||||
self.cubeScale = 1.0
|
||||
}
|
||||
if abs(remaining) < 0.0008 && abs(self.angularVelocity.y) < 0.0015 {
|
||||
self.finishRotationY = self.finishTargetYUnwrapped
|
||||
self.rotation.y = self.finishTargetY
|
||||
self.angularVelocity.y = 0.0
|
||||
self.isFinishingY = false
|
||||
self.rotation.z = 0.0
|
||||
self.angularVelocity.z = 0.0
|
||||
}
|
||||
} else if self.rotation.z != 0 {
|
||||
self.rotation.z = 0.0
|
||||
}
|
||||
self.rotation = normalizedRotation(self.rotation)
|
||||
|
||||
let damp = pow(self.dampingPerSecond, dt)
|
||||
self.angularVelocity *= damp
|
||||
|
||||
self.applyCubeRotation()
|
||||
}
|
||||
|
||||
private func startFinishingAnimation() {
|
||||
self.finishDelayTimerX?.invalidate()
|
||||
self.finishDelayTimerX = Timer.scheduledTimer(withTimeInterval: 0.75, repeats: false) { [weak self] _ in
|
||||
self?.beginFinishingX()
|
||||
}
|
||||
}
|
||||
|
||||
private func beginFinishingX() {
|
||||
let deltaToZero = abs(shortestAngleDelta(from: self.rotation.x, to: 0))
|
||||
let deltaToPi = abs(shortestAngleDelta(from: self.rotation.x, to: Float.pi))
|
||||
self.finishTargetX = deltaToZero <= deltaToPi ? 0 : Float.pi
|
||||
self.finishTargetY = self.finishTargetX == 0 ? 0 : Float.pi
|
||||
self.isFinishingX = true
|
||||
self.finishDelayTimerY?.invalidate()
|
||||
self.finishDelayTimerY = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { [weak self] _ in
|
||||
self?.beginFinishingY()
|
||||
}
|
||||
}
|
||||
|
||||
private func beginFinishingY() {
|
||||
self.finishRotationY = self.rotation.y
|
||||
let directionY = nonZeroSign(self.angularVelocity.y, fallback: 1)
|
||||
self.finishDirectionY = directionY
|
||||
let startMod = normalizeAnglePositive(self.finishRotationY)
|
||||
let targetMod = normalizeAnglePositive(self.finishTargetY)
|
||||
let baseDelta: Float
|
||||
if directionY >= 0 {
|
||||
baseDelta = targetMod >= startMod ? targetMod - startMod : (Float.pi * 2) - (startMod - targetMod)
|
||||
} else {
|
||||
baseDelta = startMod >= targetMod ? startMod - targetMod : (Float.pi * 2) - (targetMod - startMod)
|
||||
}
|
||||
var delta = baseDelta
|
||||
if delta < Float.pi {
|
||||
delta += Float.pi * 2
|
||||
}
|
||||
self.finishTargetYUnwrapped = self.finishRotationY + directionY * delta
|
||||
self.finishRemainingYStart = self.finishTargetYUnwrapped - self.finishRotationY
|
||||
self.isFinishingY = true
|
||||
self.hasFiredFinishApproach = false
|
||||
}
|
||||
|
||||
private func applyCubeRotation() {
|
||||
var m = CATransform3DIdentity
|
||||
m = CATransform3DRotate(m, CGFloat(self.rotation.x), 1, 0, 0)
|
||||
m = CATransform3DRotate(m, CGFloat(self.rotation.y), 0, 1, 0)
|
||||
m = CATransform3DRotate(m, CGFloat(self.rotation.z), 0, 0, 1)
|
||||
m = CATransform3DScale(m, CGFloat(self.cubeScale), CGFloat(self.cubeScale), 1)
|
||||
self.cubeContainer.layer.transform = m
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
import UIKit
|
||||
import simd
|
||||
|
||||
func normalize2(_ v: SIMD2<Float>) -> SIMD2<Float> {
|
||||
let l = simd_length(v)
|
||||
return l > 1e-5 ? v / l : SIMD2<Float>(0, 0)
|
||||
}
|
||||
|
||||
func normalizedRotation(_ r: SIMD3<Float>) -> SIMD3<Float> {
|
||||
SIMD3<Float>(normalizeAngle(r.x), normalizeAngle(r.y), normalizeAngle(r.z))
|
||||
}
|
||||
|
||||
struct ProjectedFace {
|
||||
let quad: Quad
|
||||
let rotation: CGFloat
|
||||
}
|
||||
|
||||
struct Quad {
|
||||
var topLeft: CGPoint
|
||||
var topRight: CGPoint
|
||||
var bottomLeft: CGPoint
|
||||
var bottomRight: CGPoint
|
||||
|
||||
init(topLeft: CGPoint, topRight: CGPoint, bottomLeft: CGPoint, bottomRight: CGPoint) {
|
||||
self.topLeft = topLeft
|
||||
self.topRight = topRight
|
||||
self.bottomLeft = bottomLeft
|
||||
self.bottomRight = bottomRight
|
||||
}
|
||||
|
||||
init(rect: CGRect) {
|
||||
self.init(
|
||||
topLeft: rect.origin,
|
||||
topRight: CGPoint(x: rect.maxX, y: rect.minY),
|
||||
bottomLeft: CGPoint(x: rect.minX, y: rect.maxY),
|
||||
bottomRight: CGPoint(x: rect.maxX, y: rect.maxY)
|
||||
)
|
||||
}
|
||||
|
||||
func boundingBox() -> CGRect {
|
||||
let xs = [topLeft.x, topRight.x, bottomLeft.x, bottomRight.x]
|
||||
let ys = [topLeft.y, topRight.y, bottomLeft.y, bottomRight.y]
|
||||
guard let minX = xs.min(), let maxX = xs.max(), let minY = ys.min(), let maxY = ys.max() else {
|
||||
return .zero
|
||||
}
|
||||
return CGRect(x: minX, y: minY, width: maxX - minX, height: maxY - minY)
|
||||
}
|
||||
|
||||
func offsetting(dx: CGFloat, dy: CGFloat) -> Quad {
|
||||
return Quad(
|
||||
topLeft: CGPoint(x: topLeft.x + dx, y: topLeft.y + dy),
|
||||
topRight: CGPoint(x: topRight.x + dx, y: topRight.y + dy),
|
||||
bottomLeft: CGPoint(x: bottomLeft.x + dx, y: bottomLeft.y + dy),
|
||||
bottomRight: CGPoint(x: bottomRight.x + dx, y: bottomRight.y + dy)
|
||||
)
|
||||
}
|
||||
|
||||
func interpolated(to other: Quad, t: CGFloat) -> Quad {
|
||||
return Quad(
|
||||
topLeft: lerp(topLeft, other.topLeft, t),
|
||||
topRight: lerp(topRight, other.topRight, t),
|
||||
bottomLeft: lerp(bottomLeft, other.bottomLeft, t),
|
||||
bottomRight: lerp(bottomRight, other.bottomRight, t)
|
||||
)
|
||||
}
|
||||
|
||||
func apply(to view: UIView) {
|
||||
let bounds = boundingBox()
|
||||
let localQuad = offsetting(dx: -bounds.origin.x, dy: -bounds.origin.y)
|
||||
|
||||
CATransaction.begin()
|
||||
CATransaction.setDisableActions(true)
|
||||
view.frame = bounds
|
||||
let transform = rectToQuad(rect: view.bounds, quad: localQuad)
|
||||
view.layer.transform = transform
|
||||
CATransaction.commit()
|
||||
}
|
||||
}
|
||||
|
||||
func lerp(_ a: CGFloat, _ b: CGFloat, _ t: CGFloat) -> CGFloat {
|
||||
return a + (b - a) * t
|
||||
}
|
||||
|
||||
func lerp(_ a: CGPoint, _ b: CGPoint, _ t: CGFloat) -> CGPoint {
|
||||
return CGPoint(x: lerp(a.x, b.x, t), y: lerp(a.y, b.y, t))
|
||||
}
|
||||
|
||||
func normalizeAngle(_ angle: CGFloat) -> CGFloat {
|
||||
var result = angle
|
||||
let twoPi = CGFloat.pi * 2
|
||||
while result > CGFloat.pi {
|
||||
result -= twoPi
|
||||
}
|
||||
while result <= -CGFloat.pi {
|
||||
result += twoPi
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func normalizeAngle(_ angle: Float) -> Float {
|
||||
var result = angle
|
||||
let twoPi = Float.pi * 2
|
||||
while result > Float.pi {
|
||||
result -= twoPi
|
||||
}
|
||||
while result <= -Float.pi {
|
||||
result += twoPi
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func normalizeAnglePositive(_ angle: Float) -> Float {
|
||||
var result = angle
|
||||
let twoPi = Float.pi * 2
|
||||
while result < 0 { result += twoPi }
|
||||
while result >= twoPi { result -= twoPi }
|
||||
return result
|
||||
}
|
||||
|
||||
func shortestAngleDelta(from: Float, to: Float) -> Float {
|
||||
return normalizeAngle(to - from)
|
||||
}
|
||||
|
||||
func nonZeroSign(_ value: Float, fallback: Float) -> Float {
|
||||
if value > 0 { return 1 }
|
||||
if value < 0 { return -1 }
|
||||
return fallback
|
||||
}
|
||||
|
||||
func snappedRightAngle(_ angle: CGFloat) -> CGFloat {
|
||||
let quarter = CGFloat.pi / 2
|
||||
let normalized = normalizeAngle(angle)
|
||||
let step = round(normalized / quarter)
|
||||
return step * quarter
|
||||
}
|
||||
|
||||
func rectToQuad(rect: CGRect, quad: Quad) -> CATransform3D {
|
||||
let x1a = quad.topLeft.x
|
||||
let y1a = quad.topLeft.y
|
||||
let x2a = quad.topRight.x
|
||||
let y2a = quad.topRight.y
|
||||
let x3a = quad.bottomLeft.x
|
||||
let y3a = quad.bottomLeft.y
|
||||
let x4a = quad.bottomRight.x
|
||||
let y4a = quad.bottomRight.y
|
||||
|
||||
let X = rect.origin.x
|
||||
let Y = rect.origin.y
|
||||
let W = rect.size.width
|
||||
let H = rect.size.height
|
||||
|
||||
let y21 = y2a - y1a
|
||||
let y32 = y3a - y2a
|
||||
let y43 = y4a - y3a
|
||||
let y14 = y1a - y4a
|
||||
let y31 = y3a - y1a
|
||||
let y42 = y4a - y2a
|
||||
|
||||
let a = -H * (x2a * x3a * y14 + x2a * x4a * y31 - x1a * x4a * y32 + x1a * x3a * y42)
|
||||
let b = W * (x2a * x3a * y14 + x3a * x4a * y21 + x1a * x4a * y32 + x1a * x2a * y43)
|
||||
let c = H * X * (x2a * x3a * y14 + x2a * x4a * y31 - x1a * x4a * y32 + x1a * x3a * y42)
|
||||
- H * W * x1a * (x4a * y32 - x3a * y42 + x2a * y43)
|
||||
- W * Y * (x2a * x3a * y14 + x3a * x4a * y21 + x1a * x4a * y32 + x1a * x2a * y43)
|
||||
|
||||
let d = H * (-x4a * y21 * y3a + x2a * y1a * y43 - x1a * y2a * y43 - x3a * y1a * y4a + x3a * y2a * y4a)
|
||||
let e = W * (x4a * y2a * y31 - x3a * y1a * y42 - x2a * y31 * y4a + x1a * y3a * y42)
|
||||
let f = -(
|
||||
W * (x4a * (Y * y2a * y31 + H * y1a * y32)
|
||||
- x3a * (H + Y) * y1a * y42
|
||||
+ H * x2a * y1a * y43
|
||||
+ x2a * Y * (y1a - y3a) * y4a
|
||||
+ x1a * Y * y3a * (-y2a + y4a))
|
||||
- H * X * (x4a * y21 * y3a - x2a * y1a * y43 + x3a * (y1a - y2a) * y4a + x1a * y2a * (-y3a + y4a))
|
||||
)
|
||||
|
||||
let g = H * (x3a * y21 - x4a * y21 + (-x1a + x2a) * y43)
|
||||
let h = W * (-x2a * y31 + x4a * y31 + (x1a - x3a) * y42)
|
||||
var i = W * Y * (x2a * y31 - x4a * y31 - x1a * y42 + x3a * y42)
|
||||
+ H * (X * (-(x3a * y21) + x4a * y21 + x1a * y43 - x2a * y43)
|
||||
+ W * (-(x3a * y2a) + x4a * y2a + x2a * y3a - x4a * y3a - x2a * y4a + x3a * y4a))
|
||||
|
||||
let epsilon: CGFloat = 0.0001
|
||||
if abs(i) < epsilon {
|
||||
i = i >= 0 ? epsilon : -epsilon
|
||||
}
|
||||
|
||||
return CATransform3D(
|
||||
m11: a / i, m12: d / i, m13: 0, m14: g / i,
|
||||
m21: b / i, m22: e / i, m23: 0, m24: h / i,
|
||||
m31: 0, m32: 0, m33: 1, m34: 0,
|
||||
m41: c / i, m42: f / i, m43: 0, m44: 1
|
||||
)
|
||||
}
|
||||
+295
@@ -0,0 +1,295 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import AccountContext
|
||||
import MultilineTextComponent
|
||||
import AnimatedTextComponent
|
||||
|
||||
final class DialIndicatorComponent: Component {
|
||||
let content: AnyComponentWithIdentity<Empty>
|
||||
let backgroundColor: UIColor
|
||||
let foregroundColor: UIColor
|
||||
let diameter: CGFloat
|
||||
let contentSize: CGSize?
|
||||
let lineWidth: CGFloat
|
||||
let fontSize: CGFloat
|
||||
let progress: CGFloat
|
||||
let value: Int
|
||||
let suffix: String
|
||||
let isVisible: Bool
|
||||
let isFlipped: Bool
|
||||
|
||||
public init(
|
||||
content: AnyComponentWithIdentity<Empty>,
|
||||
backgroundColor: UIColor,
|
||||
foregroundColor: UIColor,
|
||||
diameter: CGFloat,
|
||||
contentSize: CGSize? = nil,
|
||||
lineWidth: CGFloat,
|
||||
fontSize: CGFloat,
|
||||
progress: CGFloat,
|
||||
value: Int,
|
||||
suffix: String,
|
||||
isVisible: Bool = true,
|
||||
isFlipped: Bool = false
|
||||
) {
|
||||
self.content = content
|
||||
self.backgroundColor = backgroundColor
|
||||
self.foregroundColor = foregroundColor
|
||||
self.diameter = diameter
|
||||
self.contentSize = contentSize
|
||||
self.lineWidth = lineWidth
|
||||
self.fontSize = fontSize
|
||||
self.progress = progress
|
||||
self.value = value
|
||||
self.suffix = suffix
|
||||
self.isVisible = isVisible
|
||||
self.isFlipped = isFlipped
|
||||
}
|
||||
|
||||
public static func ==(lhs: DialIndicatorComponent, rhs: DialIndicatorComponent) -> Bool {
|
||||
if lhs.content != rhs.content {
|
||||
return false
|
||||
}
|
||||
if lhs.backgroundColor != rhs.backgroundColor {
|
||||
return false
|
||||
}
|
||||
if lhs.foregroundColor != rhs.foregroundColor {
|
||||
return false
|
||||
}
|
||||
if lhs.diameter != rhs.diameter {
|
||||
return false
|
||||
}
|
||||
if lhs.contentSize != rhs.contentSize {
|
||||
return false
|
||||
}
|
||||
if lhs.lineWidth != rhs.lineWidth {
|
||||
return false
|
||||
}
|
||||
if lhs.fontSize != rhs.fontSize {
|
||||
return false
|
||||
}
|
||||
if lhs.progress != rhs.progress {
|
||||
return false
|
||||
}
|
||||
if lhs.value != rhs.value {
|
||||
return false
|
||||
}
|
||||
if lhs.suffix != rhs.suffix {
|
||||
return false
|
||||
}
|
||||
if lhs.isVisible != rhs.isVisible {
|
||||
return false
|
||||
}
|
||||
if lhs.isFlipped != rhs.isFlipped {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private let containerView = UIView()
|
||||
private let backgroundLayer = SimpleShapeLayer()
|
||||
private let foregroundLayer = SimpleShapeLayer()
|
||||
|
||||
private var content = ComponentView<Empty>()
|
||||
private let label = ComponentView<Empty>()
|
||||
|
||||
private var component: DialIndicatorComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
self.backgroundLayer.lineCap = .round
|
||||
self.foregroundLayer.lineCap = .round
|
||||
|
||||
self.addSubview(self.containerView)
|
||||
|
||||
self.containerView.layer.addSublayer(self.backgroundLayer)
|
||||
self.containerView.layer.addSublayer(self.foregroundLayer)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(component: DialIndicatorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
let previousComponent = self.component
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let pathSize = CGSize(width: component.diameter, height: component.diameter)
|
||||
let pathFrame = CGRect(origin: .zero, size: pathSize).insetBy(dx: component.lineWidth * 0.5, dy: component.lineWidth * 0.5)
|
||||
|
||||
let strokeStart: CGFloat = 0.125
|
||||
let strokeEnd: CGFloat = 1.0 - strokeStart
|
||||
|
||||
self.backgroundLayer.lineWidth = component.lineWidth
|
||||
self.backgroundLayer.strokeColor = component.backgroundColor.cgColor
|
||||
self.backgroundLayer.fillColor = UIColor.clear.cgColor
|
||||
self.backgroundLayer.path = CGPath(ellipseIn: pathFrame, transform: nil)
|
||||
self.backgroundLayer.transform = CATransform3DMakeRotation(.pi / 2.0, 0.0, 0.0, 1.0)
|
||||
self.backgroundLayer.strokeStart = strokeStart
|
||||
self.backgroundLayer.strokeEnd = strokeEnd
|
||||
self.backgroundLayer.frame = CGRect(origin: .zero, size: pathSize)
|
||||
|
||||
self.foregroundLayer.lineWidth = component.lineWidth
|
||||
self.foregroundLayer.strokeColor = component.foregroundColor.cgColor
|
||||
self.foregroundLayer.fillColor = UIColor.clear.cgColor
|
||||
self.foregroundLayer.path = CGPath(ellipseIn: pathFrame, transform: nil)
|
||||
self.foregroundLayer.transform = CATransform3DMakeRotation(.pi / 2.0, 0.0, 0.0, 1.0)
|
||||
self.foregroundLayer.strokeStart = strokeStart
|
||||
transition.setShapeLayerStrokeEnd(layer: self.foregroundLayer, strokeEnd: strokeStart + (strokeEnd - strokeStart) * component.progress)
|
||||
self.foregroundLayer.frame = CGRect(origin: .zero, size: pathSize)
|
||||
|
||||
if previousComponent?.content.id != component.content.id {
|
||||
if let contentView = self.content.view {
|
||||
if transition.animation.isImmediate {
|
||||
contentView.removeFromSuperview()
|
||||
} else {
|
||||
transition.setScale(view: contentView, scale: 0.01)
|
||||
transition.setAlpha(view: contentView, alpha: 0.0, completion: { _ in
|
||||
contentView.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
self.content = ComponentView()
|
||||
}
|
||||
|
||||
let contentSize = component.contentSize ?? CGSize(width: component.diameter - 16.0, height: component.diameter - 16.0)
|
||||
let contentFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((pathSize.width - contentSize.width) / 2.0), y: floorToScreenPixels((pathSize.height - contentSize.height) / 2.0)), size: contentSize)
|
||||
let _ = self.content.update(
|
||||
transition: .immediate,
|
||||
component: component.content.component,
|
||||
environment: {},
|
||||
containerSize: contentFrame.size
|
||||
)
|
||||
if let contentView = self.content.view {
|
||||
if contentView.superview == nil {
|
||||
self.containerView.addSubview(contentView)
|
||||
if !transition.animation.isImmediate {
|
||||
transition.animateScale(view: contentView, from: 0.01, to: 1.0)
|
||||
transition.animateAlpha(view: contentView, from: 0.0, to: 1.0)
|
||||
}
|
||||
}
|
||||
contentView.frame = contentFrame
|
||||
}
|
||||
|
||||
var labelItems: [AnimatedTextComponent.Item] = [
|
||||
AnimatedTextComponent.Item(id: "percent", content: .number(component.value, minDigits: 1))
|
||||
]
|
||||
if !component.suffix.isEmpty {
|
||||
labelItems.append(AnimatedTextComponent.Item(id: "suffix", content: .text(component.suffix)))
|
||||
}
|
||||
|
||||
let labelSize = self.label.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
AnimatedTextComponent(
|
||||
font: Font.semibold(component.fontSize),
|
||||
color: component.foregroundColor,
|
||||
items: labelItems
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
if let labelView = self.label.view {
|
||||
if labelView.superview == nil {
|
||||
self.containerView.addSubview(labelView)
|
||||
}
|
||||
transition.setFrame(view: labelView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((pathSize.width - labelSize.width) / 2.0) + 1.0 - UIScreenPixel, y: pathSize.height - labelSize.height + 2.0 - UIScreenPixel), size: labelSize))
|
||||
}
|
||||
|
||||
transition.setAlpha(view: self.containerView, alpha: component.isVisible ? 1.0 : 0.0)
|
||||
transition.setBlur(layer: self.containerView.layer, radius: component.isVisible ? 0.0 : 10.0)
|
||||
|
||||
self.containerView.transform = CGAffineTransform(rotationAngle: component.isFlipped ? .pi : 0.0)
|
||||
|
||||
self.containerView.frame = CGRect(origin: .zero, size: pathSize)
|
||||
|
||||
return pathSize
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
final class ColorSwatchComponent: Component {
|
||||
let innerColor: UIColor
|
||||
let outerColor: UIColor
|
||||
|
||||
public init(
|
||||
innerColor: UIColor,
|
||||
outerColor: UIColor
|
||||
) {
|
||||
self.innerColor = innerColor
|
||||
self.outerColor = outerColor
|
||||
}
|
||||
|
||||
public static func ==(lhs: ColorSwatchComponent, rhs: ColorSwatchComponent) -> Bool {
|
||||
if lhs.innerColor != rhs.innerColor {
|
||||
return false
|
||||
}
|
||||
if lhs.outerColor != rhs.outerColor {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIImageView {
|
||||
private var component: ColorSwatchComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(component: ColorSwatchComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
let previousComponent = self.component
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
if previousComponent?.innerColor != component.innerColor || previousComponent?.outerColor != component.outerColor {
|
||||
self.image = generateImage(availableSize, contextGenerator: { size, context in
|
||||
context.clear(CGRect(origin: .zero, size: size))
|
||||
if let image = UIImage(bundleImageName: "Premium/Craft/DialColorMask"), let cgImage = image.cgImage {
|
||||
context.clip(to: CGRect(origin: .zero, size: size), mask: cgImage)
|
||||
}
|
||||
var locations: [CGFloat] = [1.0, 0.95, 0.1, 0.0]
|
||||
let colors: [CGColor] = [component.innerColor.cgColor, component.innerColor.cgColor, component.outerColor.cgColor, component.outerColor.cgColor]
|
||||
|
||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
|
||||
|
||||
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
|
||||
})
|
||||
}
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import ViewControllerComponent
|
||||
import SheetComponent
|
||||
import BundleIconComponent
|
||||
import BalancedTextComponent
|
||||
import MultilineTextComponent
|
||||
import ButtonComponent
|
||||
import GiftItemComponent
|
||||
import AccountContext
|
||||
import GlassBarButtonComponent
|
||||
|
||||
private func giftCraftRibbonColor(for gift: StarGift.UniqueGift) -> GiftItemComponent.Ribbon.Color {
|
||||
for attribute in gift.attributes {
|
||||
if case let .backdrop(_, _, innerColor, outerColor, _, _, _) = attribute {
|
||||
return .custom(outerColor, innerColor)
|
||||
}
|
||||
}
|
||||
return .blue
|
||||
}
|
||||
|
||||
private final class GiftCraftSheetContent: CombinedComponent {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
||||
let context: AccountContext
|
||||
let gift: StarGift.UniqueGift
|
||||
let dismiss: () -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
gift: StarGift.UniqueGift,
|
||||
dismiss: @escaping () -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.gift = gift
|
||||
self.dismiss = dismiss
|
||||
}
|
||||
|
||||
static func ==(lhs: GiftCraftSheetContent, rhs: GiftCraftSheetContent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.gift != rhs.gift {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
static var body: Body {
|
||||
let closeButton = Child(GlassBarButtonComponent.self)
|
||||
let title = Child(BalancedTextComponent.self)
|
||||
let text = Child(MultilineTextComponent.self)
|
||||
let gift = Child(GiftItemComponent.self)
|
||||
let button = Child(ButtonComponent.self)
|
||||
|
||||
return { context in
|
||||
let environment = context.environment[EnvironmentType.self]
|
||||
let component = context.component
|
||||
let theme = environment.theme
|
||||
|
||||
var contentSize = CGSize(width: context.availableSize.width, height: 18.0)
|
||||
|
||||
let closeButton = closeButton.update(
|
||||
component: GlassBarButtonComponent(
|
||||
size: CGSize(width: 40.0, height: 40.0),
|
||||
backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor,
|
||||
isDark: theme.overallDarkAppearance,
|
||||
state: .generic,
|
||||
component: AnyComponentWithIdentity(
|
||||
id: "close",
|
||||
component: AnyComponent(
|
||||
BundleIconComponent(
|
||||
name: "Navigation/Close",
|
||||
tintColor: theme.chat.inputPanel.panelControlColor
|
||||
)
|
||||
)
|
||||
),
|
||||
action: { _ in
|
||||
component.dismiss()
|
||||
}
|
||||
),
|
||||
availableSize: CGSize(width: 40.0, height: 40.0),
|
||||
transition: context.transition
|
||||
)
|
||||
context.add(closeButton.position(CGPoint(x: environment.safeInsets.left + 16.0 + closeButton.size.width / 2.0, y: 36.0)))
|
||||
|
||||
let title = title.update(
|
||||
component: BalancedTextComponent(
|
||||
text: .plain(NSAttributedString(string: "Gift Crafting", font: Font.semibold(17.0), textColor: theme.actionSheet.primaryTextColor)),
|
||||
horizontalAlignment: .center,
|
||||
maximumNumberOfLines: 1,
|
||||
lineSpacing: 0.1
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width - 96.0, height: context.availableSize.height),
|
||||
transition: context.transition
|
||||
)
|
||||
context.add(title.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + title.size.height / 2.0)))
|
||||
contentSize.height += title.size.height + 16.0
|
||||
|
||||
let giftSize = CGSize(width: 140.0, height: 140.0)
|
||||
let gift = gift.update(
|
||||
component: GiftItemComponent(
|
||||
context: component.context,
|
||||
style: .glass,
|
||||
theme: theme,
|
||||
strings: environment.strings,
|
||||
subject: .uniqueGift(gift: component.gift, price: nil),
|
||||
ribbon: GiftItemComponent.Ribbon(
|
||||
text: "#\(component.gift.number)",
|
||||
font: .monospaced,
|
||||
color: giftCraftRibbonColor(for: component.gift)
|
||||
),
|
||||
mode: .grid
|
||||
),
|
||||
availableSize: giftSize,
|
||||
transition: context.transition
|
||||
)
|
||||
context.add(gift.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + gift.size.height / 2.0)))
|
||||
contentSize.height += gift.size.height + 16.0
|
||||
|
||||
let text = text.update(
|
||||
component: MultilineTextComponent(
|
||||
text: .plain(
|
||||
NSAttributedString(
|
||||
string: "This Swiftgram gift crafting flow is temporarily disabled in the merged build while the underlying APIs are being adapted.",
|
||||
font: Font.regular(15.0),
|
||||
textColor: theme.actionSheet.secondaryTextColor,
|
||||
paragraphAlignment: .center
|
||||
)
|
||||
),
|
||||
maximumNumberOfLines: 0
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width - 48.0, height: context.availableSize.height),
|
||||
transition: context.transition
|
||||
)
|
||||
context.add(text.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + text.size.height / 2.0)))
|
||||
contentSize.height += text.size.height + 24.0
|
||||
|
||||
let button = button.update(
|
||||
component: ButtonComponent(
|
||||
background: ButtonComponent.Background(
|
||||
style: .glass,
|
||||
color: theme.list.itemCheckColors.fillColor,
|
||||
foreground: theme.list.itemCheckColors.foregroundColor,
|
||||
pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
|
||||
),
|
||||
content: AnyComponentWithIdentity(
|
||||
id: "ok",
|
||||
component: AnyComponent(
|
||||
MultilineTextComponent(
|
||||
text: .plain(
|
||||
NSAttributedString(
|
||||
string: environment.strings.Common_OK,
|
||||
font: Font.semibold(17.0),
|
||||
textColor: theme.list.itemCheckColors.foregroundColor,
|
||||
paragraphAlignment: .center
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
action: {
|
||||
component.dismiss()
|
||||
}
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width - 60.0, height: 52.0),
|
||||
transition: context.transition
|
||||
)
|
||||
context.add(button.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + button.size.height / 2.0)).cornerRadius(10.0))
|
||||
contentSize.height += button.size.height + 16.0 + environment.safeInsets.bottom
|
||||
|
||||
return contentSize
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class GiftCraftScreenComponent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
||||
let context: AccountContext
|
||||
let gift: StarGift.UniqueGift
|
||||
|
||||
init(context: AccountContext, gift: StarGift.UniqueGift) {
|
||||
self.context = context
|
||||
self.gift = gift
|
||||
}
|
||||
|
||||
static func ==(lhs: GiftCraftScreenComponent, rhs: GiftCraftScreenComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.gift != rhs.gift {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private let sheet = ComponentView<(ViewControllerComponentContainer.Environment, SheetComponentEnvironment)>()
|
||||
private let sheetAnimateOut = ActionSlot<Action<Void>>()
|
||||
|
||||
private var component: GiftCraftScreenComponent?
|
||||
private var environment: EnvironmentType?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(
|
||||
component: GiftCraftScreenComponent,
|
||||
availableSize: CGSize,
|
||||
state: EmptyComponentState,
|
||||
environment: Environment<ViewControllerComponentContainer.Environment>,
|
||||
transition: ComponentTransition
|
||||
) -> CGSize {
|
||||
self.component = component
|
||||
|
||||
let environment = environment[ViewControllerComponentContainer.Environment.self].value
|
||||
self.environment = environment
|
||||
|
||||
let sheetEnvironment = SheetComponentEnvironment(
|
||||
isDisplaying: environment.isVisible,
|
||||
isCentered: environment.metrics.widthClass == .regular,
|
||||
hasInputHeight: !environment.inputHeight.isZero,
|
||||
regularMetricsSize: CGSize(width: 430.0, height: 900.0),
|
||||
dismiss: { [weak self] _ in
|
||||
guard let self, let environment = self.environment else {
|
||||
return
|
||||
}
|
||||
self.sheetAnimateOut.invoke(Action { _ in
|
||||
environment.controller()?.dismiss(completion: nil)
|
||||
})
|
||||
}
|
||||
)
|
||||
let _ = self.sheet.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
SheetComponent(
|
||||
content: AnyComponent(
|
||||
GiftCraftSheetContent(
|
||||
context: component.context,
|
||||
gift: component.gift,
|
||||
dismiss: { [weak self] in
|
||||
guard let self, let environment = self.environment else {
|
||||
return
|
||||
}
|
||||
self.sheetAnimateOut.invoke(Action { _ in
|
||||
environment.controller()?.dismiss(completion: nil)
|
||||
})
|
||||
}
|
||||
)
|
||||
),
|
||||
backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
|
||||
animateOut: self.sheetAnimateOut
|
||||
)
|
||||
),
|
||||
environment: {
|
||||
environment
|
||||
sheetEnvironment
|
||||
},
|
||||
containerSize: availableSize
|
||||
)
|
||||
if let sheetView = self.sheet.view {
|
||||
if sheetView.superview == nil {
|
||||
self.addSubview(sheetView)
|
||||
}
|
||||
transition.setFrame(view: sheetView, frame: CGRect(origin: CGPoint(), size: availableSize))
|
||||
}
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
func update(
|
||||
view: View,
|
||||
availableSize: CGSize,
|
||||
state: EmptyComponentState,
|
||||
environment: Environment<ViewControllerComponentContainer.Environment>,
|
||||
transition: ComponentTransition
|
||||
) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
public final class GiftCraftScreen: ViewControllerComponentContainer {
|
||||
fileprivate weak var profileGiftsContext: ProfileGiftsContext?
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
gift: StarGift.UniqueGift,
|
||||
profileGiftsContext: ProfileGiftsContext?
|
||||
) {
|
||||
self.profileGiftsContext = profileGiftsContext
|
||||
|
||||
super.init(
|
||||
context: context,
|
||||
component: GiftCraftScreenComponent(context: context, gift: gift),
|
||||
navigationBarAppearance: .none,
|
||||
statusBarStyle: .ignore,
|
||||
presentationMode: .modal,
|
||||
theme: .default
|
||||
)
|
||||
|
||||
self.navigationPresentation = .flatModal
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public func dismissAnimated() {
|
||||
if let view = self.node.hostView.findTaggedView(tag: SheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? SheetComponent<ViewControllerComponentContainer.Environment>.View {
|
||||
view.dismissAnimated()
|
||||
} else {
|
||||
self.dismiss(completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
+714
@@ -0,0 +1,714 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import TelegramStringFormatting
|
||||
import ViewControllerComponent
|
||||
import BundleIconComponent
|
||||
import MultilineTextComponent
|
||||
import GiftItemComponent
|
||||
import AccountContext
|
||||
import AnimatedTextComponent
|
||||
import Markdown
|
||||
import PresentationDataUtils
|
||||
import GiftViewScreen
|
||||
import NavigationStackComponent
|
||||
import GiftStoreScreen
|
||||
import ResizableSheetComponent
|
||||
import TooltipUI
|
||||
import GlassBarButtonComponent
|
||||
import ConfettiEffect
|
||||
import GiftLoadingShimmerView
|
||||
|
||||
final class SelectGiftPageContent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
||||
let context: AccountContext
|
||||
let craftContext: CraftGiftsContext
|
||||
let resaleContext: ResaleGiftsContext
|
||||
let gift: StarGift.UniqueGift
|
||||
let genericGift: StarGift.Gift
|
||||
let selectedGiftIds: Set<Int64>
|
||||
let starsTopUpOptions: Signal<[StarsTopUpOption]?, NoError>
|
||||
let selectGift: (GiftItem) -> Void
|
||||
let dismiss: () -> Void
|
||||
let boundsUpdated: ActionSlot<ResizableSheetComponentEnvironment.BoundsUpdate>
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
craftContext: CraftGiftsContext,
|
||||
resaleContext: ResaleGiftsContext,
|
||||
gift: StarGift.UniqueGift,
|
||||
genericGift: StarGift.Gift,
|
||||
selectedGiftIds: Set<Int64>,
|
||||
starsTopUpOptions: Signal<[StarsTopUpOption]?, NoError>,
|
||||
selectGift: @escaping (GiftItem) -> Void,
|
||||
dismiss: @escaping () -> Void,
|
||||
boundsUpdated: ActionSlot<ResizableSheetComponentEnvironment.BoundsUpdate>
|
||||
) {
|
||||
self.context = context
|
||||
self.craftContext = craftContext
|
||||
self.resaleContext = resaleContext
|
||||
self.gift = gift
|
||||
self.genericGift = genericGift
|
||||
self.selectedGiftIds = selectedGiftIds
|
||||
self.starsTopUpOptions = starsTopUpOptions
|
||||
self.selectGift = selectGift
|
||||
self.dismiss = dismiss
|
||||
self.boundsUpdated = boundsUpdated
|
||||
}
|
||||
|
||||
static func ==(lhs: SelectGiftPageContent, rhs: SelectGiftPageContent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.gift != rhs.gift {
|
||||
return false
|
||||
}
|
||||
if lhs.selectedGiftIds != rhs.selectedGiftIds {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView, UIScrollViewDelegate {
|
||||
private let myGiftsTitle = ComponentView<Empty>()
|
||||
private var gifts: [AnyHashable: ComponentView<Empty>] = [:]
|
||||
private let myGiftsPlaceholder = ComponentView<Empty>()
|
||||
private let loadingView = GiftLoadingShimmerView()
|
||||
|
||||
private let storeGiftsTitle = ComponentView<Empty>()
|
||||
private let storeGifts = ComponentView<Empty>()
|
||||
|
||||
private var craftState: CraftGiftsContext.State?
|
||||
private var craftStateDisposable: Disposable?
|
||||
|
||||
private var availableGifts: [GiftItem] = []
|
||||
private var giftMap: [Int64: ProfileGiftsContext.State.StarGift] = [:]
|
||||
|
||||
private var availableSize: CGSize?
|
||||
private var currentBounds: CGRect?
|
||||
|
||||
private var component: SelectGiftPageContent?
|
||||
private weak var state: EmptyComponentState?
|
||||
private var environment: ViewControllerComponentContainer.Environment?
|
||||
private var isUpdating: Bool = false
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
self.layer.cornerRadius = 40.0
|
||||
self.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
||||
|
||||
self.addSubview(self.loadingView)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.craftStateDisposable?.dispose()
|
||||
}
|
||||
|
||||
func updateScrolling(interactive: Bool, transition: ComponentTransition) -> CGFloat {
|
||||
guard let bounds = self.currentBounds, let availableSize = self.availableSize, let component = self.component, let environment = self.environment else {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
let visibleBounds = bounds.insetBy(dx: 0.0, dy: -10.0)
|
||||
|
||||
var contentHeight: CGFloat = 88.0 + 32.0
|
||||
|
||||
let itemSpacing: CGFloat = 10.0
|
||||
let itemSideInset = 16.0
|
||||
let itemsInRow: Int
|
||||
if availableSize.width > availableSize.height || availableSize.width > 480.0 {
|
||||
if case .tablet = environment.deviceMetrics.type {
|
||||
itemsInRow = 4
|
||||
} else {
|
||||
itemsInRow = 5
|
||||
}
|
||||
} else {
|
||||
itemsInRow = 3
|
||||
}
|
||||
let itemWidth = (availableSize.width - itemSideInset * 2.0 - itemSpacing * CGFloat(itemsInRow - 1)) / CGFloat(itemsInRow)
|
||||
let itemSize = CGSize(width: itemWidth, height: itemWidth)
|
||||
|
||||
var isLoading = false
|
||||
if self.availableGifts.isEmpty, case .loading = (self.craftState?.dataState ?? .loading) {
|
||||
isLoading = true
|
||||
}
|
||||
let loadingTransition: ComponentTransition = .easeInOut(duration: 0.25)
|
||||
let loadingSize = CGSize(width: availableSize.width, height: 180.0)
|
||||
if isLoading {
|
||||
contentHeight += 120.0
|
||||
self.loadingView.update(size: loadingSize, theme: environment.theme, itemSize: itemSize, showFilters: false, isPlain: true, transition: .immediate)
|
||||
loadingTransition.setAlpha(view: self.loadingView, alpha: 1.0)
|
||||
} else {
|
||||
loadingTransition.setAlpha(view: self.loadingView, alpha: 0.0)
|
||||
}
|
||||
transition.setFrame(view: self.loadingView, frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight - 170.0), size: loadingSize))
|
||||
|
||||
var itemFrame = CGRect(origin: CGPoint(x: itemSideInset, y: contentHeight), size: itemSize)
|
||||
var itemsHeight: CGFloat = 0.0
|
||||
var validIds: [AnyHashable] = []
|
||||
for gift in self.availableGifts {
|
||||
var isVisible = false
|
||||
if visibleBounds.intersects(itemFrame) {
|
||||
isVisible = true
|
||||
}
|
||||
if isVisible {
|
||||
let itemId = AnyHashable(gift.gift.id)
|
||||
validIds.append(itemId)
|
||||
|
||||
var itemTransition = transition
|
||||
let visibleItem: ComponentView<Empty>
|
||||
if let current = self.gifts[itemId] {
|
||||
visibleItem = current
|
||||
} else {
|
||||
visibleItem = ComponentView()
|
||||
self.gifts[itemId] = visibleItem
|
||||
itemTransition = .immediate
|
||||
}
|
||||
|
||||
var ribbonColor: GiftItemComponent.Ribbon.Color = .blue
|
||||
let ribbonText = "#\(gift.gift.number)"
|
||||
for attribute in gift.gift.attributes {
|
||||
if case let .backdrop(_, _, innerColor, outerColor, _, _, _) = attribute {
|
||||
ribbonColor = .custom(outerColor, innerColor)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
let _ = visibleItem.update(
|
||||
transition: itemTransition,
|
||||
component: AnyComponent(
|
||||
GiftItemComponent(
|
||||
context: component.context,
|
||||
style: .glass,
|
||||
theme: environment.theme,
|
||||
strings: environment.strings,
|
||||
peer: nil,
|
||||
subject: .uniqueGift(gift: gift.gift, price: nil),
|
||||
ribbon: GiftItemComponent.Ribbon(text: ribbonText, font: .monospaced, color: ribbonColor, outline: nil),
|
||||
badge: gift.gift.craftChancePermille.flatMap { "+\($0 / 10)%" },
|
||||
resellPrice: nil,
|
||||
isHidden: false,
|
||||
isSelected: false,
|
||||
isPinned: false,
|
||||
isEditing: false,
|
||||
mode: .grid,
|
||||
action: { [weak self] in
|
||||
guard let self, let component = self.component, let environment = self.environment else {
|
||||
return
|
||||
}
|
||||
HapticFeedback().impact(.light)
|
||||
|
||||
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
|
||||
if let profileGift = self.giftMap[gift.gift.id], let canCraftDate = profileGift.canCraftAt, currentTime < canCraftDate {
|
||||
let dateString = stringForFullDate(timestamp: canCraftDate, strings: environment.strings, dateTimeFormat: environment.dateTimeFormat)
|
||||
let alertController = textAlertController(
|
||||
context: component.context,
|
||||
title: environment.strings.Gift_Craft_Unavailable_Title,
|
||||
text: environment.strings.Gift_Craft_Unavailable_Text(dateString).string,
|
||||
actions: [
|
||||
TextAlertAction(type: .defaultAction, title: environment.strings.Common_OK, action: {})
|
||||
],
|
||||
parseMarkdown: true
|
||||
)
|
||||
environment.controller()?.present(alertController, in: .window(.root))
|
||||
return
|
||||
}
|
||||
|
||||
component.selectGift(gift)
|
||||
component.dismiss()
|
||||
},
|
||||
contextAction: { _, _ in }
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: itemSize
|
||||
)
|
||||
if let itemView = visibleItem.view {
|
||||
if itemView.superview == nil {
|
||||
if let _ = self.loadingView.superview {
|
||||
self.insertSubview(itemView, belowSubview: self.loadingView)
|
||||
} else {
|
||||
self.addSubview(itemView)
|
||||
}
|
||||
if !transition.animation.isImmediate {
|
||||
itemView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||
}
|
||||
}
|
||||
itemTransition.setFrame(view: itemView, frame: itemFrame)
|
||||
}
|
||||
}
|
||||
|
||||
itemsHeight = itemFrame.maxY - contentHeight
|
||||
|
||||
itemFrame.origin.x += itemFrame.width + itemSpacing
|
||||
if itemFrame.maxX > availableSize.width {
|
||||
itemFrame.origin.x = itemSideInset
|
||||
itemFrame.origin.y += itemSize.height + itemSpacing
|
||||
}
|
||||
}
|
||||
|
||||
var removeIds: [AnyHashable] = []
|
||||
for (id, item) in self.gifts {
|
||||
if !validIds.contains(id) {
|
||||
removeIds.append(id)
|
||||
if let itemView = item.view {
|
||||
if !transition.animation.isImmediate {
|
||||
itemView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.25, removeOnCompletion: false)
|
||||
itemView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
|
||||
itemView.removeFromSuperview()
|
||||
})
|
||||
} else {
|
||||
itemView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for id in removeIds {
|
||||
self.gifts.removeValue(forKey: id)
|
||||
}
|
||||
|
||||
if let state = self.craftState, case .ready = state.dataState, self.availableGifts.isEmpty {
|
||||
contentHeight += 10.0
|
||||
let myGiftsPlaceholderSize = self.myGiftsPlaceholder.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: environment.strings.Gift_Craft_Select_NoGiftsFromCollection, font: Font.regular(13.0), textColor: environment.theme.list.itemSecondaryTextColor)),
|
||||
horizontalAlignment: .center,
|
||||
maximumNumberOfLines: 3,
|
||||
lineSpacing: 0.1
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - 32.0, height: .greatestFiniteMagnitude)
|
||||
)
|
||||
let myGiftsPlaceholderFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - myGiftsPlaceholderSize.width) / 2.0), y: contentHeight), size: myGiftsPlaceholderSize)
|
||||
if let myGiftsPlaceholderView = self.myGiftsPlaceholder.view {
|
||||
if myGiftsPlaceholderView.superview == nil {
|
||||
self.addSubview(myGiftsPlaceholderView)
|
||||
}
|
||||
myGiftsPlaceholderView.frame = myGiftsPlaceholderFrame
|
||||
}
|
||||
contentHeight += myGiftsPlaceholderSize.height
|
||||
contentHeight += 32.0
|
||||
} else {
|
||||
contentHeight += itemsHeight
|
||||
contentHeight += 24.0
|
||||
}
|
||||
|
||||
if let storeGiftsView = self.storeGifts.view as? GiftStoreContentComponent.View {
|
||||
storeGiftsView.updateScrolling(bounds: bounds.offsetBy(dx: 0.0, dy: -contentHeight), interactive: interactive, transition: .immediate)
|
||||
}
|
||||
|
||||
let bottomContentOffset = max(0.0, contentHeight - bounds.origin.y - bounds.height)
|
||||
if interactive, bottomContentOffset < 800.0 {
|
||||
Queue.mainQueue().justDispatch {
|
||||
component.craftContext.loadMore()
|
||||
}
|
||||
}
|
||||
|
||||
return contentHeight
|
||||
}
|
||||
|
||||
func update(component: SelectGiftPageContent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
|
||||
self.isUpdating = true
|
||||
defer {
|
||||
self.isUpdating = false
|
||||
}
|
||||
|
||||
self.availableSize = availableSize
|
||||
if self.component == nil {
|
||||
self.currentBounds = CGRect(origin: .zero, size: availableSize)
|
||||
|
||||
component.boundsUpdated.connect { [weak self] update in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.currentBounds = update.bounds
|
||||
let _ = self.updateScrolling(interactive: update.isInteractive, transition: .immediate)
|
||||
}
|
||||
|
||||
let initialGiftItem = GiftItem(
|
||||
gift: component.gift,
|
||||
reference: .slug(slug: component.gift.slug)
|
||||
)
|
||||
self.availableGifts = [
|
||||
initialGiftItem
|
||||
]
|
||||
|
||||
self.craftStateDisposable = (component.craftContext.state
|
||||
|> deliverOnMainQueue).start(next: { [weak self] state in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.craftState = state
|
||||
|
||||
var items: [GiftItem] = []
|
||||
var giftMap: [Int64: ProfileGiftsContext.State.StarGift] = [:]
|
||||
var existingIds = Set<Int64>()
|
||||
for gift in state.gifts {
|
||||
guard let reference = gift.reference, case let .unique(uniqueGift) = gift.gift, !existingIds.contains(uniqueGift.id) else {
|
||||
continue
|
||||
}
|
||||
existingIds.insert(uniqueGift.id)
|
||||
|
||||
let giftItem = GiftItem(
|
||||
gift: uniqueGift,
|
||||
reference: reference
|
||||
)
|
||||
giftMap[uniqueGift.id] = gift
|
||||
|
||||
if component.selectedGiftIds.contains(uniqueGift.id) {
|
||||
continue
|
||||
}
|
||||
items.append(giftItem)
|
||||
}
|
||||
|
||||
self.availableGifts = items
|
||||
self.giftMap = giftMap
|
||||
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let environment = environment[ViewControllerComponentContainer.Environment.self].value
|
||||
|
||||
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
self.environment = environment
|
||||
|
||||
self.backgroundColor = environment.theme.actionSheet.opaqueItemBackgroundColor
|
||||
|
||||
var contentHeight: CGFloat = 88.0
|
||||
|
||||
let myGiftsTitleSize = self.myGiftsTitle.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Gift_Craft_Select_YourGifts.uppercased(), font: Font.semibold(14.0), textColor: environment.theme.actionSheet.secondaryTextColor)))
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0)
|
||||
)
|
||||
let myGiftsTitleFrame = CGRect(origin: CGPoint(x: 26.0, y: contentHeight), size: myGiftsTitleSize)
|
||||
if let myGiftsTitleView = self.myGiftsTitle.view {
|
||||
if myGiftsTitleView.superview == nil {
|
||||
self.addSubview(myGiftsTitleView)
|
||||
}
|
||||
transition.setFrame(view: myGiftsTitleView, frame: myGiftsTitleFrame)
|
||||
}
|
||||
|
||||
contentHeight += 32.0
|
||||
|
||||
contentHeight = self.updateScrolling(interactive: false, transition: transition)
|
||||
|
||||
let resaleCount = component.genericGift.availability?.resale ?? 0
|
||||
let saleTitle = environment.strings.Gift_Craft_Select_SaleGiftsCount(Int32(clamping: resaleCount)).uppercased()
|
||||
|
||||
let storeGiftsTitleSize = self.storeGiftsTitle.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
MultilineTextComponent(text: .plain(NSAttributedString(string: saleTitle, font: Font.semibold(14.0), textColor: environment.theme.actionSheet.secondaryTextColor)))
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0)
|
||||
)
|
||||
let storeGiftsTitleFrame = CGRect(origin: CGPoint(x: 26.0, y: contentHeight), size: storeGiftsTitleSize)
|
||||
if let storeGiftsTitleView = self.storeGiftsTitle.view {
|
||||
if storeGiftsTitleView.superview == nil {
|
||||
self.addSubview(storeGiftsTitleView)
|
||||
}
|
||||
transition.setFrame(view: storeGiftsTitleView, frame: storeGiftsTitleFrame)
|
||||
}
|
||||
contentHeight += 28.0
|
||||
|
||||
self.storeGifts.parentState = state
|
||||
let storeGiftsSize = self.storeGifts.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
GiftStoreContentComponent(
|
||||
context: component.context,
|
||||
resaleGiftsContext: component.resaleContext,
|
||||
theme: environment.theme,
|
||||
strings: environment.strings,
|
||||
dateTimeFormat: environment.dateTimeFormat,
|
||||
safeInsets: UIEdgeInsets(),
|
||||
statusBarHeight: contentHeight - 62.0,
|
||||
navigationHeight: 0.0,
|
||||
overNavigationContainer: self,
|
||||
starsContext: component.context.starsContext!,
|
||||
peerId: component.context.account.peerId,
|
||||
gift: component.genericGift,
|
||||
isPlain: true,
|
||||
confirmPurchaseImmediately: true,
|
||||
starsTopUpOptions: component.starsTopUpOptions,
|
||||
scrollToTop: {},
|
||||
controller: environment.controller,
|
||||
completion: { [weak self] uniqueGift in
|
||||
guard let self, let component = self.component, let controller = self.environment?.controller() as? SelectCraftGiftScreen, let navigationController = controller.navigationController else {
|
||||
return
|
||||
}
|
||||
let giftItem = GiftItem(gift: uniqueGift, reference: .slug(slug: uniqueGift.slug))
|
||||
component.selectGift(giftItem)
|
||||
component.dismiss()
|
||||
|
||||
navigationController.view.addSubview(ConfettiView(frame: navigationController.view.bounds))
|
||||
|
||||
Queue.mainQueue().after(1.0) {
|
||||
component.craftContext.reload()
|
||||
}
|
||||
}
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: .greatestFiniteMagnitude)
|
||||
)
|
||||
let storeGiftsFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: storeGiftsSize)
|
||||
if let storeGiftsView = self.storeGifts.view as? GiftStoreContentComponent.View {
|
||||
if storeGiftsView.superview == nil {
|
||||
self.insertSubview(storeGiftsView, at: 0)
|
||||
}
|
||||
transition.setFrame(view: storeGiftsView, frame: storeGiftsFrame)
|
||||
|
||||
storeGiftsView.updateScrolling(bounds: CGRect(origin: .zero, size: availableSize), transition: .immediate)
|
||||
}
|
||||
contentHeight += storeGiftsSize.height
|
||||
contentHeight += 90.0
|
||||
|
||||
return CGSize(width: availableSize.width, height: contentHeight)
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
private final class SheetContainerComponent: CombinedComponent {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
||||
let context: AccountContext
|
||||
let craftContext: CraftGiftsContext
|
||||
let resaleContext: ResaleGiftsContext
|
||||
let gift: StarGift.UniqueGift
|
||||
let genericGift: StarGift.Gift
|
||||
let selectedGiftIds: Set<Int64>
|
||||
let starsTopUpOptions: Signal<[StarsTopUpOption]?, NoError>
|
||||
let selectGift: (GiftItem) -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
craftContext: CraftGiftsContext,
|
||||
resaleContext: ResaleGiftsContext,
|
||||
gift: StarGift.UniqueGift,
|
||||
genericGift: StarGift.Gift,
|
||||
selectedGiftIds: Set<Int64>,
|
||||
starsTopUpOptions: Signal<[StarsTopUpOption]?, NoError>,
|
||||
selectGift: @escaping (GiftItem) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.craftContext = craftContext
|
||||
self.resaleContext = resaleContext
|
||||
self.gift = gift
|
||||
self.genericGift = genericGift
|
||||
self.selectedGiftIds = selectedGiftIds
|
||||
self.starsTopUpOptions = starsTopUpOptions
|
||||
self.selectGift = selectGift
|
||||
}
|
||||
|
||||
static func ==(lhs: SheetContainerComponent, rhs: SheetContainerComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.gift != rhs.gift {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class State: ComponentState {
|
||||
}
|
||||
|
||||
func makeState() -> State {
|
||||
return State()
|
||||
}
|
||||
|
||||
static var body: Body {
|
||||
let sheet = Child(ResizableSheetComponent<EnvironmentType>.self)
|
||||
let animateOut = StoredActionSlot(Action<Void>.self)
|
||||
|
||||
let boundsUpdated = ActionSlot<ResizableSheetComponentEnvironment.BoundsUpdate>()
|
||||
|
||||
return { context in
|
||||
let component = context.component
|
||||
let environment = context.environment[EnvironmentType.self]
|
||||
|
||||
let controller = environment.controller
|
||||
|
||||
let dismiss: (Bool) -> Void = { animated in
|
||||
if animated {
|
||||
animateOut.invoke(Action { _ in
|
||||
if let controller = controller() {
|
||||
controller.dismiss(completion: nil)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
if let controller = controller() {
|
||||
controller.dismiss(completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let theme = environment.theme
|
||||
|
||||
let backgroundColor = environment.theme.list.modalPlainBackgroundColor
|
||||
|
||||
let sheet = sheet.update(
|
||||
component: ResizableSheetComponent<EnvironmentType>(
|
||||
content: AnyComponent<EnvironmentType>(
|
||||
SelectGiftPageContent(
|
||||
context: component.context,
|
||||
craftContext: component.craftContext,
|
||||
resaleContext: component.resaleContext,
|
||||
gift: component.gift,
|
||||
genericGift: component.genericGift,
|
||||
selectedGiftIds: component.selectedGiftIds,
|
||||
starsTopUpOptions: component.starsTopUpOptions,
|
||||
selectGift: component.selectGift,
|
||||
dismiss: {
|
||||
dismiss(true)
|
||||
},
|
||||
boundsUpdated: boundsUpdated
|
||||
)
|
||||
),
|
||||
titleItem: AnyComponent(
|
||||
MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Gift_Craft_Select_Title, font: Font.semibold(17.0), textColor: environment.theme.actionSheet.primaryTextColor)))
|
||||
),
|
||||
leftItem: AnyComponent(
|
||||
GlassBarButtonComponent(
|
||||
size: CGSize(width: 44.0, height: 44.0),
|
||||
backgroundColor: nil,
|
||||
isDark: theme.overallDarkAppearance,
|
||||
state: .glass,
|
||||
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
|
||||
BundleIconComponent(
|
||||
name: "Navigation/Close",
|
||||
tintColor: theme.chat.inputPanel.panelControlColor
|
||||
)
|
||||
)),
|
||||
action: { _ in
|
||||
dismiss(true)
|
||||
}
|
||||
)
|
||||
),
|
||||
rightItem: nil,
|
||||
bottomItem: nil,
|
||||
backgroundColor: .color(backgroundColor),
|
||||
isFullscreen: false,
|
||||
animateOut: animateOut
|
||||
),
|
||||
environment: {
|
||||
environment
|
||||
ResizableSheetComponentEnvironment(
|
||||
theme: theme,
|
||||
statusBarHeight: environment.statusBarHeight,
|
||||
safeInsets: environment.safeInsets,
|
||||
metrics: environment.metrics,
|
||||
deviceMetrics: environment.deviceMetrics,
|
||||
isDisplaying: environment.value.isVisible,
|
||||
isCentered: environment.metrics.widthClass == .regular,
|
||||
screenSize: context.availableSize,
|
||||
regularMetricsSize: CGSize(width: 430.0, height: 900.0),
|
||||
dismiss: { animated in
|
||||
dismiss(animated)
|
||||
},
|
||||
boundsUpdated: boundsUpdated
|
||||
)
|
||||
},
|
||||
availableSize: context.availableSize,
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
context.add(sheet
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
|
||||
)
|
||||
|
||||
return context.availableSize
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class SelectCraftGiftScreen: ViewControllerComponentContainer {
|
||||
public init(
|
||||
context: AccountContext,
|
||||
craftContext: CraftGiftsContext,
|
||||
resaleContext: ResaleGiftsContext,
|
||||
gift: StarGift.UniqueGift,
|
||||
genericGift: StarGift.Gift,
|
||||
selectedGiftIds: Set<Int64>,
|
||||
starsTopUpOptions: Signal<[StarsTopUpOption]?, NoError>,
|
||||
selectGift: @escaping (GiftItem) -> Void
|
||||
) {
|
||||
super.init(
|
||||
context: context,
|
||||
component: SheetContainerComponent(
|
||||
context: context,
|
||||
craftContext: craftContext,
|
||||
resaleContext: resaleContext,
|
||||
gift: gift,
|
||||
genericGift: genericGift,
|
||||
selectedGiftIds: selectedGiftIds,
|
||||
starsTopUpOptions: starsTopUpOptions,
|
||||
selectGift: selectGift
|
||||
),
|
||||
navigationBarAppearance: .none,
|
||||
statusBarStyle: .ignore,
|
||||
theme: .default
|
||||
)
|
||||
|
||||
self.navigationPresentation = .flatModal
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
fileprivate func dismissAllTooltips() {
|
||||
self.window?.forEachController({ controller in
|
||||
if let controller = controller as? TooltipScreen {
|
||||
controller.dismiss(inPlace: false)
|
||||
}
|
||||
})
|
||||
self.forEachController({ controller in
|
||||
if let controller = controller as? TooltipScreen {
|
||||
controller.dismiss(inPlace: false)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
public func dismissAnimated() {
|
||||
self.dismissAllTooltips()
|
||||
|
||||
if let view = self.node.hostView.findTaggedView(tag: ResizableSheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? ResizableSheetComponent<ViewControllerComponentContainer.Environment>.View {
|
||||
view.dismissAnimated()
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -515,7 +515,7 @@ public final class GiftItemComponent: Component {
|
||||
animationOffset = 16.0
|
||||
for attribute in gift.attributes {
|
||||
switch attribute {
|
||||
case let .model(_, file, _):
|
||||
case let .model(_, file, _, _):
|
||||
animationFile = file
|
||||
if !self.fetchedFiles.contains(file.fileId.id) {
|
||||
self.disposables.add(freeMediaFileResourceInteractiveFetched(account: component.context.account, userLocation: .other, fileReference: .standalone(media: file), resource: file.resource).start())
|
||||
@@ -606,7 +606,7 @@ public final class GiftItemComponent: Component {
|
||||
}
|
||||
for attribute in attributes {
|
||||
switch attribute {
|
||||
case let .model(_, file, _):
|
||||
case let .model(_, file, _, _):
|
||||
animationFile = file
|
||||
if !self.fetchedFiles.contains(file.fileId.id) {
|
||||
self.disposables.add(freeMediaFileResourceInteractiveFetched(account: component.context.account, userLocation: .other, fileReference: .standalone(media: file), resource: file.resource).start())
|
||||
|
||||
+2
-1
@@ -755,6 +755,7 @@ private final class GiftSetupScreenComponent: Component {
|
||||
let presentationInterfaceState = ChatPresentationInterfaceState(
|
||||
chatWallpaper: .builtin(WallpaperSettings()),
|
||||
theme: presentationData.theme,
|
||||
preferredGlassType: .default,
|
||||
strings: presentationData.strings,
|
||||
dateTimeFormat: presentationData.dateTimeFormat,
|
||||
nameDisplayOrder: presentationData.nameDisplayOrder,
|
||||
@@ -1604,7 +1605,7 @@ private final class GiftSetupScreenComponent: Component {
|
||||
guard let self, let component = self.component, let controller = self.environment?.controller(), let upgradePreview else {
|
||||
return
|
||||
}
|
||||
let previewController = component.context.sharedContext.makeGiftUpgradePreviewScreen(context: component.context, attributes: upgradePreview.attributes, peerName: peerName)
|
||||
let previewController = component.context.sharedContext.makeGiftUpgradePreviewScreen(context: component.context, gift: gift, attributes: upgradePreview.attributes, peerName: peerName)
|
||||
controller.push(previewController)
|
||||
})
|
||||
}
|
||||
|
||||
+3
-3
@@ -50,7 +50,7 @@ private func actionForAttribute(attribute: StarGift.UniqueGift.Attribute, presen
|
||||
let searchComponents = searchQuery.lowercased().components(separatedBy: .whitespaces).filter { !$0.isEmpty }
|
||||
|
||||
switch attribute {
|
||||
case let .model(name, file, _), let .pattern(name, file, _):
|
||||
case let .model(name, file, _, _), let .pattern(name, file, _):
|
||||
let attributeId: ResaleGiftsContext.Attribute
|
||||
if case .model = attribute {
|
||||
attributeId = .model(file.fileId.id)
|
||||
@@ -320,7 +320,7 @@ private final class GiftAttributeListContextItemNode: ASDisplayNode, ContextMenu
|
||||
|
||||
private func getAttributeId(from attribute: StarGift.UniqueGift.Attribute) -> AnyHashable {
|
||||
switch attribute {
|
||||
case let .model(_, file, _):
|
||||
case let .model(_, file, _, _):
|
||||
return AnyHashable("model_\(file.fileId.id)")
|
||||
case let .pattern(_, file, _):
|
||||
return AnyHashable("pattern_\(file.fileId.id)")
|
||||
@@ -582,7 +582,7 @@ private func filteredAttributes(attributes: [StarGift.UniqueGift.Attribute], que
|
||||
for attribute in attributes {
|
||||
let string: String
|
||||
switch attribute {
|
||||
case let .model(name, _, _):
|
||||
case let .model(name, _, _, _):
|
||||
string = name
|
||||
case let .pattern(name, _, _):
|
||||
string = name
|
||||
|
||||
+3
-3
@@ -537,8 +537,8 @@ final class GiftStoreScreenComponent: Component {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}.sorted(by: { lhs, rhs in
|
||||
if case let .model(_, lhsFile, _) = lhs, case let .model(_, rhsFile, _) = rhs, let lhsCount = self.state?.starGiftsState?.attributeCount[.model(lhsFile.fileId.id)], let rhsCount = self.state?.starGiftsState?.attributeCount[.model(rhsFile.fileId.id)] {
|
||||
}.sorted(by: { (lhs: StarGift.UniqueGift.Attribute, rhs: StarGift.UniqueGift.Attribute) in
|
||||
if case let .model(_, lhsFile, _, _) = lhs, case let .model(_, rhsFile, _, _) = rhs, let lhsCount = self.state?.starGiftsState?.attributeCount[.model(lhsFile.fileId.id)], let rhsCount = self.state?.starGiftsState?.attributeCount[.model(rhsFile.fileId.id)] {
|
||||
return lhsCount > rhsCount
|
||||
} else {
|
||||
return false
|
||||
@@ -1231,7 +1231,7 @@ final class GiftStoreScreenComponent: Component {
|
||||
self.context = context
|
||||
self.peerId = peerId
|
||||
self.gift = gift
|
||||
self.starGiftsContext = ResaleGiftsContext(account: context.account, giftId: gift.id)
|
||||
self.starGiftsContext = ResaleGiftsContext(account: context.account, giftId: gift.id, forCrafting: false)
|
||||
|
||||
super.init()
|
||||
|
||||
|
||||
+4
-4
@@ -164,7 +164,7 @@ private final class GiftAuctionViewSheetContent: CombinedComponent {
|
||||
}).shuffled().prefix(5))
|
||||
self.previewSymbols = randomSymbols
|
||||
|
||||
for case let .model(_, file, _) in self.previewModels where !self.fetchedFiles.contains(file.fileId.id) {
|
||||
for case let .model(_, file, _, _) in self.previewModels where !self.fetchedFiles.contains(file.fileId.id) {
|
||||
self.disposables.add(freeMediaFileResourceInteractiveFetched(account: context.account, userLocation: .other, fileReference: .standalone(media: file), resource: file.resource).start())
|
||||
self.fetchedFiles.insert(file.fileId.id)
|
||||
}
|
||||
@@ -725,9 +725,9 @@ private final class GiftAuctionViewSheetContent: CombinedComponent {
|
||||
if let genericGift {
|
||||
var attributes: [StarGift.UniqueGift.Attribute] = []
|
||||
if state.previewModelIndex == -1 {
|
||||
attributes.append(.model(name: "", file: genericGift.file, rarity: 0))
|
||||
attributes.append(.model(name: "", file: genericGift.file, rarity: .permille(0), crafted: false))
|
||||
if let background = genericGift.background {
|
||||
attributes.append(.backdrop(name: "", id: 0, innerColor: background.centerColor, outerColor: background.edgeColor, patternColor: 0, textColor: 0, rarity: 0))
|
||||
attributes.append(.backdrop(name: "", id: 0, innerColor: background.centerColor, outerColor: background.edgeColor, patternColor: 0, textColor: 0, rarity: .permille(0)))
|
||||
}
|
||||
} else if !state.previewModels.isEmpty {
|
||||
attributes.append(state.previewModels[state.previewModelIndex])
|
||||
@@ -1082,7 +1082,7 @@ private final class GiftAuctionViewSheetContent: CombinedComponent {
|
||||
guard let state, let attributes = state.giftUpgradeAttributes else {
|
||||
return
|
||||
}
|
||||
let variantsController = component.context.sharedContext.makeGiftUpgradeVariantsScreen(context: component.context, gift: .generic(gift), attributes: attributes, selectedAttributes: nil, focusedAttribute: nil)
|
||||
let variantsController = component.context.sharedContext.makeGiftUpgradeVariantsScreen(context: component.context, gift: .generic(gift), crafted: false, attributes: attributes, selectedAttributes: nil, focusedAttribute: nil)
|
||||
environment.controller()?.push(variantsController)
|
||||
}, animateScale: false),
|
||||
availableSize: CGSize(width: context.availableSize.width - 64.0, height: context.availableSize.height),
|
||||
|
||||
+4
-4
@@ -83,23 +83,23 @@ public func giftOfferAlertController(
|
||||
let tag: AnyObject?
|
||||
|
||||
switch attribute {
|
||||
case let .model(name, _, rarity):
|
||||
case let .model(name, _, rarity, _):
|
||||
id = "model"
|
||||
title = strings.Gift_Unique_Model
|
||||
value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor)
|
||||
percentage = Float(rarity) * 0.1
|
||||
percentage = Float(rarity.permilleValue) * 0.1
|
||||
tag = modelButtonTag
|
||||
case let .backdrop(name, _, _, _, _, _, rarity):
|
||||
id = "backdrop"
|
||||
title = strings.Gift_Unique_Backdrop
|
||||
value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor)
|
||||
percentage = Float(rarity) * 0.1
|
||||
percentage = Float(rarity.permilleValue) * 0.1
|
||||
tag = backdropButtonTag
|
||||
case let .pattern(name, _, rarity):
|
||||
id = "pattern"
|
||||
title = strings.Gift_Unique_Symbol
|
||||
value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor)
|
||||
percentage = Float(rarity) * 0.1
|
||||
percentage = Float(rarity.permilleValue) * 0.1
|
||||
tag = symbolButtonTag
|
||||
case .originalInfo:
|
||||
continue
|
||||
|
||||
+4
-4
@@ -68,23 +68,23 @@ public func giftTransferAlertController(
|
||||
let tag: AnyObject?
|
||||
|
||||
switch attribute {
|
||||
case let .model(name, _, rarity):
|
||||
case let .model(name, _, rarity, _):
|
||||
id = "model"
|
||||
title = strings.Gift_Unique_Model
|
||||
value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor)
|
||||
percentage = Float(rarity) * 0.1
|
||||
percentage = Float(rarity.permilleValue) * 0.1
|
||||
tag = modelButtonTag
|
||||
case let .backdrop(name, _, _, _, _, _, rarity):
|
||||
id = "backdrop"
|
||||
title = strings.Gift_Unique_Backdrop
|
||||
value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor)
|
||||
percentage = Float(rarity) * 0.1
|
||||
percentage = Float(rarity.permilleValue) * 0.1
|
||||
tag = backdropButtonTag
|
||||
case let .pattern(name, _, rarity):
|
||||
id = "pattern"
|
||||
title = strings.Gift_Unique_Symbol
|
||||
value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor)
|
||||
percentage = Float(rarity) * 0.1
|
||||
percentage = Float(rarity.permilleValue) * 0.1
|
||||
tag = symbolButtonTag
|
||||
case .originalInfo:
|
||||
continue
|
||||
|
||||
+8
-8
@@ -419,11 +419,11 @@ private final class GiftUpgradePreviewScreenComponent: Component {
|
||||
var isSelected = false
|
||||
for attribute in attributeList {
|
||||
switch attribute {
|
||||
case let .model(name, file, rarityValue):
|
||||
case let .model(name, file, rarityValue, _):
|
||||
itemId += "\(file.fileId.id)"
|
||||
if self.selectedSection == .models {
|
||||
title = name
|
||||
rarity = rarityValue
|
||||
rarity = rarityValue.permilleValue
|
||||
modelAttribute = attribute
|
||||
|
||||
isSelected = self.selectedModel == attribute
|
||||
@@ -432,7 +432,7 @@ private final class GiftUpgradePreviewScreenComponent: Component {
|
||||
itemId += "\(id)"
|
||||
if self.selectedSection == .backdrops {
|
||||
title = name
|
||||
rarity = rarityValue
|
||||
rarity = rarityValue.permilleValue
|
||||
backdropAttribute = attribute
|
||||
|
||||
isSelected = self.selectedBackdrop == attribute
|
||||
@@ -441,7 +441,7 @@ private final class GiftUpgradePreviewScreenComponent: Component {
|
||||
itemId += "\(file.fileId.id)"
|
||||
if self.selectedSection == .symbols {
|
||||
title = name
|
||||
rarity = rarityValue
|
||||
rarity = rarityValue.permilleValue
|
||||
symbolAttribute = attribute
|
||||
|
||||
isSelected = self.selectedSymbol == attribute
|
||||
@@ -1210,18 +1210,18 @@ private final class AttributeInfoComponent: Component {
|
||||
let subtitle: String
|
||||
let rarity: Int32
|
||||
switch component.attribute {
|
||||
case let .model(name, _, rarityValue):
|
||||
case let .model(name, _, rarityValue, _):
|
||||
title = name
|
||||
subtitle = component.strings.Gift_Variants_Model
|
||||
rarity = rarityValue
|
||||
rarity = rarityValue.permilleValue
|
||||
case let .backdrop(name, _, _, _, _, _, rarityValue):
|
||||
title = name
|
||||
subtitle = component.strings.Gift_Variants_Backdrop
|
||||
rarity = rarityValue
|
||||
rarity = rarityValue.permilleValue
|
||||
case let .pattern(name, _, rarityValue):
|
||||
title = name
|
||||
subtitle = component.strings.Gift_Variants_Symbol
|
||||
rarity = rarityValue
|
||||
rarity = rarityValue.permilleValue
|
||||
default:
|
||||
title = ""
|
||||
subtitle = ""
|
||||
|
||||
+9
-9
@@ -425,14 +425,14 @@ private final class GiftUpgradeVariantsScreenComponent: Component {
|
||||
var isSelected = false
|
||||
for attribute in attributeList {
|
||||
switch attribute {
|
||||
case let .model(name, file, rarityValue):
|
||||
case let .model(name, file, rarityValue, _):
|
||||
itemId += "\(file.fileId.id)"
|
||||
if self.selectedSection == .models {
|
||||
title = name
|
||||
rarity = rarityValue
|
||||
rarity = rarityValue.permilleValue
|
||||
modelAttribute = attribute
|
||||
|
||||
if case let .model(_, selectedFile, _) = self.selectedModel {
|
||||
if case let .model(_, selectedFile, _, _) = self.selectedModel {
|
||||
isSelected = file.fileId == selectedFile.fileId
|
||||
} else {
|
||||
isSelected = false
|
||||
@@ -442,7 +442,7 @@ private final class GiftUpgradeVariantsScreenComponent: Component {
|
||||
itemId += "\(id)"
|
||||
if self.selectedSection == .backdrops {
|
||||
title = name
|
||||
rarity = rarityValue
|
||||
rarity = rarityValue.permilleValue
|
||||
backdropAttribute = attribute
|
||||
|
||||
if case let .backdrop(_, selectedId, _, _, _, _, _) = self.selectedBackdrop {
|
||||
@@ -455,7 +455,7 @@ private final class GiftUpgradeVariantsScreenComponent: Component {
|
||||
itemId += "\(file.fileId.id)"
|
||||
if self.selectedSection == .symbols {
|
||||
title = name
|
||||
rarity = rarityValue
|
||||
rarity = rarityValue.permilleValue
|
||||
symbolAttribute = attribute
|
||||
|
||||
if case let .pattern(_, selectedFile, _) = self.selectedSymbol {
|
||||
@@ -1270,18 +1270,18 @@ private final class AttributeInfoComponent: Component {
|
||||
let subtitle: String
|
||||
let rarity: Int32
|
||||
switch component.attribute {
|
||||
case let .model(name, _, rarityValue):
|
||||
case let .model(name, _, rarityValue, _):
|
||||
title = name
|
||||
subtitle = component.strings.Gift_Variants_Model
|
||||
rarity = rarityValue
|
||||
rarity = rarityValue.permilleValue
|
||||
case let .backdrop(name, _, _, _, _, _, rarityValue):
|
||||
title = name
|
||||
subtitle = component.strings.Gift_Variants_Backdrop
|
||||
rarity = rarityValue
|
||||
rarity = rarityValue.permilleValue
|
||||
case let .pattern(name, _, rarityValue):
|
||||
title = name
|
||||
subtitle = component.strings.Gift_Variants_Symbol
|
||||
rarity = rarityValue
|
||||
rarity = rarityValue.permilleValue
|
||||
default:
|
||||
title = ""
|
||||
subtitle = ""
|
||||
|
||||
@@ -214,7 +214,7 @@ private final class GiftValueSheetContent: CombinedComponent {
|
||||
giftIconSubject = .starGift(gift: gift, price: "")
|
||||
case let .unique(gift):
|
||||
for attribute in gift.attributes {
|
||||
if case let .model(_, file, _) = attribute {
|
||||
if case let .model(_, file, _, _) = attribute {
|
||||
animationFile = file
|
||||
}
|
||||
}
|
||||
|
||||
+37
-27
@@ -234,8 +234,9 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
|
||||
if self.testUpgradeAnimation {
|
||||
if gift.giftId != 0 {
|
||||
self.upgradePreviewDisposable.add((context.engine.payments.starGiftUpgradePreview(giftId: gift.giftId)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] upgradePreview in
|
||||
let upgradePreviewSignal = context.engine.payments.starGiftUpgradePreview(giftId: gift.giftId)
|
||||
|> deliverOnMainQueue
|
||||
self.upgradePreviewDisposable.add(upgradePreviewSignal.start(next: { [weak self] upgradePreview in
|
||||
guard let self, let upgradePreview else {
|
||||
return
|
||||
}
|
||||
@@ -243,7 +244,7 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
|
||||
for attribute in upgradePreview.attributes {
|
||||
switch attribute {
|
||||
case let .model(_, file, _):
|
||||
case let .model(_, file, _, _):
|
||||
self.upgradePreviewDisposable.add(freeMediaFileResourceInteractiveFetched(account: self.context.account, userLocation: .other, fileReference: .standalone(media: file), resource: file.resource).start())
|
||||
case let .pattern(_, file, _):
|
||||
self.upgradePreviewDisposable.add(freeMediaFileResourceInteractiveFetched(account: self.context.account, userLocation: .other, fileReference: .standalone(media: file), resource: file.resource).start())
|
||||
@@ -261,8 +262,9 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
peerIds.append(releasedBy)
|
||||
}
|
||||
if arguments.canUpgrade || arguments.upgradeStars != nil || arguments.prepaidUpgradeHash != nil {
|
||||
self.upgradePreviewDisposable.add((context.engine.payments.starGiftUpgradePreview(giftId: gift.id)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] upgradePreview in
|
||||
let upgradePreviewSignal = context.engine.payments.starGiftUpgradePreview(giftId: gift.id)
|
||||
|> deliverOnMainQueue
|
||||
self.upgradePreviewDisposable.add(upgradePreviewSignal.start(next: { [weak self] upgradePreview in
|
||||
guard let self, let upgradePreview else {
|
||||
return
|
||||
}
|
||||
@@ -270,7 +272,7 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
|
||||
for attribute in upgradePreview.attributes {
|
||||
switch attribute {
|
||||
case let .model(_, file, _):
|
||||
case let .model(_, file, _, _):
|
||||
self.upgradePreviewDisposable.add(freeMediaFileResourceInteractiveFetched(account: self.context.account, userLocation: .other, fileReference: .standalone(media: file), resource: file.resource).start())
|
||||
case let .pattern(_, file, _):
|
||||
self.upgradePreviewDisposable.add(freeMediaFileResourceInteractiveFetched(account: self.context.account, userLocation: .other, fileReference: .standalone(media: file), resource: file.resource).start())
|
||||
@@ -501,7 +503,7 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
animationFile = gift.file
|
||||
case let .unique(gift):
|
||||
for attribute in gift.attributes {
|
||||
if case let .model(_, file, _) = attribute {
|
||||
if case let .model(_, file, _, _) = attribute {
|
||||
animationFile = file
|
||||
break
|
||||
}
|
||||
@@ -718,7 +720,7 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
let context = self.context
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
let proceed = { [weak self, weak starsContext, weak controller] in
|
||||
let proceed: () -> Void = { [weak self, weak starsContext, weak controller] in
|
||||
guard let self, let controller else {
|
||||
return
|
||||
}
|
||||
@@ -745,7 +747,7 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
let updatedAttributes = uniqueGift.attributes.filter { $0.attributeType != .originalInfo }
|
||||
self.subject = .profileGift(peerId, gift.withGift(.unique(uniqueGift.withAttributes(updatedAttributes))))
|
||||
case let .message(message):
|
||||
if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, isRefunded, isPrepaidUpgrade, peerId, senderId, savedId, resaleAmount, canTransferDate, canResaleDate, _, assigned, fromOffer) = action.action, case let .unique(uniqueGift) = gift {
|
||||
if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, isRefunded, isPrepaidUpgrade, peerId, senderId, savedId, resaleAmount, canTransferDate, canResaleDate, _, assigned, fromOffer, _, _) = action.action, case let .unique(uniqueGift) = gift {
|
||||
let updatedAttributes = uniqueGift.attributes.filter { $0.attributeType != .originalInfo }
|
||||
let updatedMedia: [Media] = [
|
||||
TelegramMediaAction(
|
||||
@@ -766,7 +768,9 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
canResaleDate: canResaleDate,
|
||||
dropOriginalDetailsStars: nil,
|
||||
assigned: assigned,
|
||||
fromOffer: fromOffer
|
||||
fromOffer: fromOffer,
|
||||
canCraftAt: nil,
|
||||
isCrafted: false
|
||||
)
|
||||
)
|
||||
]
|
||||
@@ -1008,7 +1012,10 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
|> then(
|
||||
context.engine.payments.getUniqueStarGift(slug: gift.slug)
|
||||
|> map { gift in
|
||||
return gift?.themePeerId
|
||||
return gift.themePeerId
|
||||
}
|
||||
|> `catch` { _ -> Signal<EnginePeer.Id?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -1376,6 +1383,7 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
let variantsController = self.context.sharedContext.makeGiftUpgradeVariantsScreen(
|
||||
context: self.context,
|
||||
gift: arguments.gift,
|
||||
crafted: false,
|
||||
attributes: attributes,
|
||||
selectedAttributes: selectedAttributes,
|
||||
focusedAttribute: attribute
|
||||
@@ -1443,7 +1451,7 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
}
|
||||
|
||||
if case let .unique(gift) = arguments.gift, let resellAmount = gift.resellAmounts?.first, resellAmount.amount.value > 0 {
|
||||
if arguments.reference != nil || gift.owner.peerId == context.account.peerId {
|
||||
if arguments.reference != nil || gift.owner?.peerId == context.account.peerId {
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_View_Context_ChangePrice, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/PriceTag"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] c, _ in
|
||||
@@ -1703,7 +1711,7 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
let giftTitle = "\(uniqueGift.title) #\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: presentationData.dateTimeFormat))"
|
||||
let recipientPeerId = self.recipientPeerId ?? self.context.account.peerId
|
||||
|
||||
let action: (CurrencyAmount.Currency) -> Void = { currency in
|
||||
let action: (CurrencyAmount.Currency) -> Void = { (currency: CurrencyAmount.Currency) in
|
||||
guard let resellAmount = uniqueGift.resellAmounts?.first(where: { $0.currency == currency }) else {
|
||||
guard let controller = self.getController() as? GiftViewScreen else {
|
||||
return
|
||||
@@ -1802,10 +1810,10 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
|
||||
var animationFile: TelegramMediaFile?
|
||||
for attribute in uniqueGift.attributes {
|
||||
if case let .model(_, file, _) = attribute {
|
||||
animationFile = file
|
||||
break
|
||||
}
|
||||
if case let .model(_, file, _, _) = attribute {
|
||||
animationFile = file
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if let navigationController = controller.navigationController as? NavigationController {
|
||||
@@ -3671,7 +3679,7 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
if state.pendingWear {
|
||||
var fileId: Int64?
|
||||
for attribute in uniqueGift.attributes {
|
||||
if case let .model(_, file, _) = attribute {
|
||||
if case let .model(_, file, _, _) = attribute {
|
||||
fileId = file.fileId.id
|
||||
}
|
||||
if case let .backdrop(_, _, innerColor, _, _, _, _) = attribute {
|
||||
@@ -3780,6 +3788,8 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
)
|
||||
)
|
||||
))
|
||||
case .none:
|
||||
break
|
||||
}
|
||||
|
||||
if let peerId = uniqueGift.hostPeerId, let peer = state.peerMap[peerId] {
|
||||
@@ -4113,17 +4123,17 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
var otherValuesAndPercentages: [(value: String, percentage: Float)] = []
|
||||
|
||||
switch attribute {
|
||||
case let .model(name, _, rarity):
|
||||
case let .model(name, _, rarity, _):
|
||||
id = "model"
|
||||
title = strings.Gift_Unique_Model
|
||||
value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor)
|
||||
percentage = Float(rarity) * 0.1
|
||||
percentage = Float(rarity.permilleValue) * 0.1
|
||||
tag = state.modelButtonTag
|
||||
|
||||
if state.justUpgraded, let sampleAttributes = state.upgradePreview?.attributes {
|
||||
for sampleAttribute in sampleAttributes {
|
||||
if case let .model(name, _, rarity) = sampleAttribute {
|
||||
otherValuesAndPercentages.append((name, Float(rarity) * 0.1))
|
||||
if case let .model(name, _, rarity, _) = sampleAttribute {
|
||||
otherValuesAndPercentages.append((name, Float(rarity.permilleValue) * 0.1))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4131,13 +4141,13 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
id = "backdrop"
|
||||
title = strings.Gift_Unique_Backdrop
|
||||
value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor)
|
||||
percentage = Float(rarity) * 0.1
|
||||
percentage = Float(rarity.permilleValue) * 0.1
|
||||
tag = state.backdropButtonTag
|
||||
|
||||
if state.justUpgraded, let sampleAttributes = state.upgradePreview?.attributes {
|
||||
for sampleAttribute in sampleAttributes {
|
||||
if case let .backdrop(name, _, _, _, _, _, rarity) = sampleAttribute {
|
||||
otherValuesAndPercentages.append((name, Float(rarity) * 0.1))
|
||||
otherValuesAndPercentages.append((name, Float(rarity.permilleValue) * 0.1))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4145,13 +4155,13 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
id = "pattern"
|
||||
title = strings.Gift_Unique_Symbol
|
||||
value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor)
|
||||
percentage = Float(rarity) * 0.1
|
||||
percentage = Float(rarity.permilleValue) * 0.1
|
||||
tag = state.symbolButtonTag
|
||||
|
||||
if state.justUpgraded, let sampleAttributes = state.upgradePreview?.attributes {
|
||||
for sampleAttribute in sampleAttributes {
|
||||
if case let .pattern(name, _, rarity) = sampleAttribute {
|
||||
otherValuesAndPercentages.append((name, Float(rarity) * 0.1))
|
||||
otherValuesAndPercentages.append((name, Float(rarity.permilleValue) * 0.1))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5500,7 +5510,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
||||
|
||||
let fromPeerId = senderId ?? message.author?.id
|
||||
return (message.id.peerId, fromPeerId, message.author?.debugDisplayTitle, message.author?.compactDisplayTitle, message.id, reference, message.flags.contains(.Incoming), gift, message.timestamp, convertStars, text, entities, nameHidden, savedToProfile, nil, converted, upgraded, isRefunded, canUpgrade, upgradeStars, nil, nil, nil, upgradeMessageId, nil, nil, prepaidUpgradeHash, upgradeSeparate, nil, toPeerId, number)
|
||||
case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, _, _, peerId, senderId, savedId, _, canTransferDate, canResaleDate, dropOriginalDetailsStars, _, _):
|
||||
case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, _, _, peerId, senderId, savedId, _, canTransferDate, canResaleDate, dropOriginalDetailsStars, _, _, _, _):
|
||||
var reference: StarGiftReference
|
||||
if let peerId, let savedId {
|
||||
reference = .peer(peerId: peerId, id: savedId)
|
||||
|
||||
+6
@@ -262,6 +262,12 @@ public class GlassBackgroundView: UIView {
|
||||
self.color = color
|
||||
self.innerColor = innerColor
|
||||
}
|
||||
|
||||
public init(kind: Kind, innerColor: UIColor? = nil) {
|
||||
self.kind = kind
|
||||
self.color = .clear
|
||||
self.innerColor = innerColor
|
||||
}
|
||||
}
|
||||
|
||||
public enum Shape: Equatable {
|
||||
|
||||
+17
-1
@@ -68,6 +68,7 @@ public final class GlobalControlPanelsContext {
|
||||
|
||||
public enum ChatListNotice: Equatable {
|
||||
case clearStorage(sizeFraction: Double)
|
||||
case sgUrl(id: String, title: String, text: String?, url: String, needAuth: Bool, permanent: Bool)
|
||||
case setupPassword
|
||||
case premiumUpgrade(discount: Int32)
|
||||
case premiumAnnualDiscount(discount: Int32)
|
||||
@@ -329,6 +330,7 @@ public final class GlobalControlPanelsContext {
|
||||
let starsSubscriptionsContextPromise = Promise<StarsSubscriptionsContext?>(nil)
|
||||
|
||||
let suggestedChatListNoticeSignal: Signal<ChatListNotice?, NoError> = combineLatest(
|
||||
getSGProvidedSuggestions(account: context.account),
|
||||
context.engine.notices.getServerProvidedSuggestions(),
|
||||
context.engine.notices.getServerDismissedSuggestions(),
|
||||
twoStepData,
|
||||
@@ -341,9 +343,18 @@ public final class GlobalControlPanelsContext {
|
||||
starsSubscriptionsContextPromise.get(),
|
||||
accountFreezeConfiguration
|
||||
)
|
||||
|> mapToSignal { suggestions, dismissedSuggestions, configuration, newSessionReviews, data, birthdays, starsSubscriptionsContext, accountFreezeConfiguration -> Signal<ChatListNotice?, NoError> in
|
||||
|> mapToSignal { sgSuggestionsData, suggestions, dismissedSuggestions, configuration, newSessionReviews, data, birthdays, starsSubscriptionsContext, accountFreezeConfiguration -> Signal<ChatListNotice?, NoError> in
|
||||
let (accountPeer, birthday) = data
|
||||
|
||||
|
||||
// MARK: Swiftgram
|
||||
if let sgSuggestionsData = sgSuggestionsData, let dictionary = try? JSONSerialization.jsonObject(with: sgSuggestionsData, options: []), let sgSuggestions = dictionary as? [[String: Any]], let sgSuggestion = sgSuggestions.first, let sgSuggestionId = sgSuggestion["id"] as? String {
|
||||
if let sgSuggestionType = sgSuggestion["type"] as? String, sgSuggestionType == "SG_URL", let sgSuggestionTitle = sgSuggestion["title"] as? String, let sgSuggestionUrl = sgSuggestion["url"] as? String {
|
||||
return .single(.sgUrl(id: sgSuggestionId, title: sgSuggestionTitle, text: sgSuggestion["text"] as? String, url: sgSuggestionUrl, needAuth: sgSuggestion["need_auth"] as? Bool ?? false, permanent: sgSuggestion["permanent"] as? Bool ?? false))
|
||||
|
||||
}
|
||||
}
|
||||
//
|
||||
if let newSessionReview = newSessionReviews.first {
|
||||
return .single(.reviewLogin(newSessionReview: newSessionReview, totalCount: newSessionReviews.count))
|
||||
}
|
||||
@@ -401,8 +412,12 @@ public final class GlobalControlPanelsContext {
|
||||
} else if suggestions.contains(.gracePremium) {
|
||||
return .single(.premiumGrace)
|
||||
} else if suggestions.contains(.xmasPremiumGift) {
|
||||
// MARK: Swiftgram
|
||||
if ({ return true }()) { return .single(nil) }
|
||||
return .single(.xmasPremiumGift)
|
||||
} else if suggestions.contains(.annualPremium) || suggestions.contains(.upgradePremium) || suggestions.contains(.restorePremium), let inAppPurchaseManager = context.inAppPurchaseManager {
|
||||
// MARK: Swiftgram
|
||||
if ({ return true }()) { return .single(nil) }
|
||||
return inAppPurchaseManager.availableProducts
|
||||
|> map { products -> ChatListNotice? in
|
||||
if products.count > 1 {
|
||||
@@ -475,6 +490,7 @@ public final class GlobalControlPanelsContext {
|
||||
self.notifyStateUpdated()
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
if let callManager = context.sharedContext.callManager, let peerId = groupCalls {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
sgdeps = [
|
||||
"//Swiftgram/SGSimpleSettings:SGSimpleSettings"
|
||||
]
|
||||
|
||||
swift_library(
|
||||
name = "HorizontalTabsComponent",
|
||||
module_name = "HorizontalTabsComponent",
|
||||
@@ -9,7 +13,7 @@ swift_library(
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
deps = sgdeps + [
|
||||
"//submodules/Display",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/TelegramPresentationData",
|
||||
|
||||
+19
-2
@@ -1,3 +1,4 @@
|
||||
import SGSimpleSettings
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
@@ -305,6 +306,8 @@ public final class HorizontalTabsComponent: Component {
|
||||
private var tabSwitchFraction: CGFloat = 0.0
|
||||
private var isDraggingTabs: Bool = false
|
||||
private var temporaryLiftTimer: Foundation.Timer?
|
||||
private var didTapOnAnItem: Bool = false
|
||||
private var didTapOnAnItemTimer: Foundation.Timer?
|
||||
|
||||
private var tapRecognizer: UITapGestureRecognizer?
|
||||
|
||||
@@ -531,6 +534,14 @@ public final class HorizontalTabsComponent: Component {
|
||||
for (id, itemView) in self.itemViews {
|
||||
if self.scrollView.convert(itemView.selectionFrame, to: self).contains(point) {
|
||||
if let tab = component.tabs.first(where: { $0.id == id }) {
|
||||
self.didTapOnAnItem = true
|
||||
self.didTapOnAnItemTimer?.invalidate()
|
||||
self.didTapOnAnItemTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false, block: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.didTapOnAnItem = false
|
||||
})
|
||||
tab.action()
|
||||
}
|
||||
}
|
||||
@@ -609,7 +620,7 @@ public final class HorizontalTabsComponent: Component {
|
||||
self.temporaryLiftTimer?.invalidate()
|
||||
self.temporaryLiftTimer = nil
|
||||
|
||||
if !transition.animation.isImmediate {
|
||||
if !transition.animation.isImmediate && self.didTapOnAnItem {
|
||||
self.temporaryLiftTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 0.3, repeats: false, block: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
@@ -627,6 +638,12 @@ public final class HorizontalTabsComponent: Component {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
self.didTapOnAnItem = false
|
||||
if let didTapOnAnItemTimer = self.didTapOnAnItemTimer {
|
||||
self.didTapOnAnItemTimer = nil
|
||||
didTapOnAnItemTimer.invalidate()
|
||||
}
|
||||
|
||||
self.reorderingGesture?.isEnabled = component.isEditing
|
||||
|
||||
let sizeHeight: CGFloat = availableSize.height
|
||||
@@ -1059,7 +1076,7 @@ private final class ItemComponent: Component {
|
||||
self.containerView.isGestureEnabled = component.editing == nil
|
||||
self.tapRecognizer?.isEnabled = component.editing == nil
|
||||
|
||||
let sideInset: CGFloat = 16.0
|
||||
let sideInset: CGFloat = 16.0 / (SGSimpleSettings.shared.compactFolderNames ? 2.0 : 1.0)
|
||||
let badgeSpacing: CGFloat = 5.0
|
||||
|
||||
var size = CGSize(width: sideInset, height: availableSize.height)
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
sgdeps = [
|
||||
"//Swiftgram/SGSimpleSettings:SGSimpleSettings"
|
||||
]
|
||||
|
||||
swift_library(
|
||||
name = "LegacyInstantVideoController",
|
||||
module_name = "LegacyInstantVideoController",
|
||||
@@ -9,7 +13,7 @@ swift_library(
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
deps = sgdeps + [
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Display",
|
||||
"//submodules/TelegramCore",
|
||||
|
||||
+3
@@ -1,3 +1,6 @@
|
||||
// MARK: Swiftgram
|
||||
import SGSimpleSettings
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
sgdeps = [
|
||||
"//Swiftgram/SGSimpleSettings:SGSimpleSettings"
|
||||
]
|
||||
|
||||
swift_library(
|
||||
name = "MediaEditorScreen",
|
||||
module_name = "MediaEditorScreen",
|
||||
@@ -9,7 +13,7 @@ swift_library(
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
deps = sgdeps + [
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||
"//submodules/Display:Display",
|
||||
"//submodules/Postbox:Postbox",
|
||||
@@ -70,6 +74,7 @@ swift_library(
|
||||
"//submodules/TelegramNotices",
|
||||
"//submodules/TelegramUI/Components/AttachmentFileController",
|
||||
"//submodules/SaveToCameraRoll",
|
||||
"//submodules/TelegramUI/Components/ContextControllerImpl",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import SGSimpleSettings
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
@@ -187,7 +188,7 @@ public extension MediaEditorScreenImpl {
|
||||
defer {
|
||||
TempBox.shared.dispose(tempFile)
|
||||
}
|
||||
if let imageData = compressImageToJPEG(image, quality: 0.7, tempFilePath: tempFile.path) {
|
||||
if let imageData = compressImageToJPEG(image, quality: Float(SGSimpleSettings.shared.outgoingPhotoQuality) / 100.0, tempFilePath: tempFile.path) {
|
||||
update((context.engine.messages.editStory(peerId: peer.id, id: storyItem.id, media: .image(dimensions: dimensions, data: imageData, stickers: result.stickers), mediaAreas: result.mediaAreas, text: updatedText, entities: updatedEntities, privacy: nil)
|
||||
|> deliverOnMainQueue).startStrict(next: { result in
|
||||
switch result {
|
||||
@@ -226,7 +227,7 @@ public extension MediaEditorScreenImpl {
|
||||
defer {
|
||||
TempBox.shared.dispose(tempFile)
|
||||
}
|
||||
let firstFrameImageData = firstFrameImage.flatMap { compressImageToJPEG($0, quality: 0.6, tempFilePath: tempFile.path) }
|
||||
let firstFrameImageData = firstFrameImage.flatMap { compressImageToJPEG($0, quality: Float(SGSimpleSettings.shared.outgoingPhotoQuality) / 100.0, tempFilePath: tempFile.path) }
|
||||
let firstFrameFile = firstFrameImageData.flatMap { data -> TempBoxFile? in
|
||||
let file = TempBox.shared.tempFile(fileName: "image.jpg")
|
||||
if let _ = try? data.write(to: URL(fileURLWithPath: file.path)) {
|
||||
|
||||
@@ -1244,6 +1244,7 @@ final class MediaEditorScreenComponent: Component {
|
||||
let presentationInterfaceState = ChatPresentationInterfaceState(
|
||||
chatWallpaper: .builtin(WallpaperSettings()),
|
||||
theme: presentationData.theme,
|
||||
preferredGlassType: .default,
|
||||
strings: presentationData.strings,
|
||||
dateTimeFormat: presentationData.dateTimeFormat,
|
||||
nameDisplayOrder: presentationData.nameDisplayOrder,
|
||||
@@ -3733,7 +3734,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
}
|
||||
} else if case let .gift(gift) = subject {
|
||||
isGift = true
|
||||
let media: [Media] = [TelegramMediaAction(action: .starGiftUnique(gift: .unique(gift), isUpgrade: false, isTransferred: false, savedToProfile: false, canExportDate: nil, transferStars: nil, isRefunded: false, isPrepaidUpgrade: false, peerId: nil, senderId: nil, savedId: nil, resaleAmount: nil, canTransferDate: nil, canResaleDate: nil, dropOriginalDetailsStars: nil, assigned: false, fromOffer: false))]
|
||||
let media: [Media] = [TelegramMediaAction(action: .starGiftUnique(gift: .unique(gift), isUpgrade: false, isTransferred: false, savedToProfile: false, canExportDate: nil, transferStars: nil, isRefunded: false, isPrepaidUpgrade: false, peerId: nil, senderId: nil, savedId: nil, resaleAmount: nil, canTransferDate: nil, canResaleDate: nil, dropOriginalDetailsStars: nil, assigned: false, fromOffer: false, canCraftAt: nil, isCrafted: false))]
|
||||
let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: self.context.account.peerId, namespace: Namespaces.Message.Cloud, id: -1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: media, peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
|
||||
messages = .single([message])
|
||||
} else {
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
sgDeps = [
|
||||
"//Swiftgram/SGSimpleSettings:SGSimpleSettings",
|
||||
"//Swiftgram/SGInputToolbar:SGInputToolbar"
|
||||
]
|
||||
|
||||
|
||||
swift_library(
|
||||
name = "MessageInputPanelComponent",
|
||||
module_name = "MessageInputPanelComponent",
|
||||
@@ -9,7 +15,7 @@ swift_library(
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
deps = sgDeps + [
|
||||
"//submodules/Display",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/AppBundle",
|
||||
|
||||
+128
-3
@@ -1,3 +1,8 @@
|
||||
// MARK: Swiftgram
|
||||
import class SwiftUI.UIHostingController
|
||||
import SGSimpleSettings
|
||||
import SGInputToolbar
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
@@ -603,6 +608,9 @@ public final class MessageInputPanelComponent: Component {
|
||||
private let counter = ComponentView<Empty>()
|
||||
private var header: ComponentView<Empty>?
|
||||
|
||||
// MARK: Swiftgram
|
||||
private var toolbarView: UIView?
|
||||
|
||||
private var disabledPlaceholder: ComponentView<Empty>?
|
||||
private var textClippingView = UIView()
|
||||
private let textField = ComponentView<Empty>()
|
||||
@@ -659,7 +667,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
return (self.likeButton.view as? MessageInputActionButtonComponent.View)?.likeIconView
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
init(context: AccountContext, frame: CGRect) {
|
||||
self.fieldBackgroundView = BlurredBackgroundView(color: nil, enableBlur: true)
|
||||
self.fieldBackgroundTint = UIView()
|
||||
self.fieldBackgroundTint.backgroundColor = UIColor(white: 1.0, alpha: 0.1)
|
||||
@@ -706,6 +714,9 @@ public final class MessageInputPanelComponent: Component {
|
||||
self.state?.updated()
|
||||
}
|
||||
)
|
||||
|
||||
// MARK: Swiftgram
|
||||
self.initToolbarIfNeeded(context: context)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
@@ -914,6 +925,11 @@ public final class MessageInputPanelComponent: Component {
|
||||
if result == nil, let contextQueryResultPanel = self.contextQueryResultPanel?.view, let panelResult = contextQueryResultPanel.hitTest(self.convert(point, to: contextQueryResultPanel), with: event), panelResult !== contextQueryResultPanel {
|
||||
return panelResult
|
||||
}
|
||||
|
||||
// MARK: Swiftgram
|
||||
if result == nil, let toolbarView = self.toolbarView, let toolbarResult = toolbarView.hitTest(self.convert(point, to: toolbarView), with: event) {
|
||||
return toolbarResult
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -1508,7 +1524,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
self.fieldBackgroundTint.isHidden = true
|
||||
}
|
||||
if let fieldGlassBackgroundView = self.fieldGlassBackgroundView {
|
||||
fieldGlassBackgroundView.update(size: fieldBackgroundFrame.size, cornerRadius: baseFieldHeight * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: component.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7)), transition: transition)
|
||||
fieldGlassBackgroundView.update(size: fieldBackgroundFrame.size, cornerRadius: baseFieldHeight * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: .init(kind: .panel), transition: transition)
|
||||
transition.setFrame(view: fieldGlassBackgroundView, frame: fieldBackgroundFrame)
|
||||
}
|
||||
case .videoChat:
|
||||
@@ -2983,12 +2999,15 @@ public final class MessageInputPanelComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Swiftgram
|
||||
size = self.layoutToolbar(transition: transition, layoutFromTop: layoutFromTop, size: size, availableSize: availableSize, defaultInsets: defaultInsets, textFieldSize: textFieldSize, previousComponent: previousComponent)
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
return View(context: self.context, frame: CGRect())
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
@@ -3036,3 +3055,109 @@ final class ViewForOverlayContent: UIView {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension MessageInputPanelComponent.View {
|
||||
func initToolbarIfNeeded(context: AccountContext) {
|
||||
guard #available(iOS 13.0, *) else { return }
|
||||
guard SGSimpleSettings.shared.inputToolbar else { return }
|
||||
guard context.sharedContext.immediateSGStatus.status > 1 else { return }
|
||||
guard self.toolbarView == nil else { return }
|
||||
let notificationName = Notification.Name("sgToolbarAction")
|
||||
let toolbar = ChatToolbarView(
|
||||
onQuote: { [weak self] in
|
||||
guard let _ = self else { return }
|
||||
NotificationCenter.default.post(name: notificationName, object: nil, userInfo: ["action": "quote"])
|
||||
},
|
||||
onSpoiler: { [weak self] in
|
||||
guard let _ = self else { return }
|
||||
NotificationCenter.default.post(name: notificationName, object: nil, userInfo: ["action": "spoiler"])
|
||||
},
|
||||
onBold: { [weak self] in
|
||||
guard let _ = self else { return }
|
||||
NotificationCenter.default.post(name: notificationName, object: nil, userInfo: ["action": "bold"])
|
||||
},
|
||||
onItalic: { [weak self] in
|
||||
guard let _ = self else { return }
|
||||
NotificationCenter.default.post(name: notificationName, object: nil, userInfo: ["action": "italic"])
|
||||
},
|
||||
onMonospace: { [weak self] in
|
||||
guard let _ = self else { return }
|
||||
NotificationCenter.default.post(name: notificationName, object: nil, userInfo: ["action": "monospace"])
|
||||
},
|
||||
onLink: { [weak self] in
|
||||
guard let _ = self else { return }
|
||||
NotificationCenter.default.post(name: notificationName, object: nil, userInfo: ["action": "link"])
|
||||
},
|
||||
onStrikethrough: { [weak self]
|
||||
in guard let _ = self else { return }
|
||||
NotificationCenter.default.post(name: notificationName, object: nil, userInfo: ["action": "strikethrough"])
|
||||
},
|
||||
onUnderline: { [weak self] in
|
||||
guard let _ = self else { return }
|
||||
NotificationCenter.default.post(name: notificationName, object: nil, userInfo: ["action": "underline"])
|
||||
},
|
||||
onCode: { [weak self] in
|
||||
guard let _ = self else { return }
|
||||
NotificationCenter.default.post(name: notificationName, object: nil, userInfo: ["action": "code"])
|
||||
},
|
||||
onNewLine: { [weak self] in
|
||||
guard let _ = self else { return }
|
||||
NotificationCenter.default.post(name: notificationName, object: nil, userInfo: ["action": "newline"])
|
||||
},
|
||||
// TODO(swiftgram): Binding
|
||||
showNewLine: .constant(true), //.constant(self.sendWithReturnKey)
|
||||
onClearFormatting: { [weak self] in
|
||||
guard let _ = self else { return }
|
||||
NotificationCenter.default.post(name: notificationName, object: nil, userInfo: ["action": "clearFormatting"])
|
||||
}
|
||||
).colorScheme(.dark)
|
||||
|
||||
let toolbarHostingController = UIHostingController(rootView: toolbar)
|
||||
toolbarHostingController.view.backgroundColor = .clear
|
||||
let toolbarView = toolbarHostingController.view
|
||||
self.toolbarView = toolbarView
|
||||
// assigning toolbarHostingController bugs responsivness and overrides layout
|
||||
// self.toolbarHostingController = toolbarHostingController
|
||||
|
||||
// Disable "Swipe to go back" gesture when touching scrollview
|
||||
self.interactiveTransitionGestureRecognizerTest = { [weak self] point in
|
||||
if let self, let _ = self.toolbarView?.hitTest(point, with: nil) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
if let toolbarView = self.toolbarView {
|
||||
self.addSubview(toolbarView)
|
||||
}
|
||||
}
|
||||
|
||||
func layoutToolbar(transition: ComponentTransition, layoutFromTop: Bool, size: CGSize, availableSize: CGSize, defaultInsets: UIEdgeInsets, textFieldSize: CGSize, previousComponent: MessageInputPanelComponent?) -> CGSize {
|
||||
// TODO(swiftgram): Do not show if locked formatting
|
||||
var transition = transition
|
||||
if let previousComponent = previousComponent {
|
||||
let previousLayoutFromTop = previousComponent.attachmentButtonMode == .captionDown
|
||||
if previousLayoutFromTop != layoutFromTop {
|
||||
// attachmentButtonMode changed
|
||||
transition = .immediate
|
||||
}
|
||||
}
|
||||
var size = size
|
||||
if let toolbarView = self.toolbarView {
|
||||
let toolbarHeight: CGFloat = 44.0
|
||||
let toolbarSpacing: CGFloat = 1.0
|
||||
let toolbarSize = CGSize(width: availableSize.width, height: toolbarHeight)
|
||||
let hasFirstResponder = self.hasFirstResponder()
|
||||
transition.setAlpha(view: toolbarView, alpha: hasFirstResponder ? 1.0 : 0.0)
|
||||
if layoutFromTop {
|
||||
transition.setFrame(view: toolbarView, frame: CGRect(origin: CGPoint(x: .zero, y: availableSize.height + toolbarSpacing), size: toolbarSize))
|
||||
} else {
|
||||
transition.setFrame(view: toolbarView, frame: CGRect(origin: CGPoint(x: .zero, y: textFieldSize.height + defaultInsets.top + toolbarSpacing), size: toolbarSize))
|
||||
if hasFirstResponder {
|
||||
size.height += toolbarHeight + toolbarSpacing
|
||||
}
|
||||
}
|
||||
}
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "AccountPeerContextItem",
|
||||
module_name = "AccountPeerContextItem",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Display",
|
||||
"//submodules/Postbox",
|
||||
"//submodules/TelegramCore",
|
||||
"//submodules/PresentationDataUtils",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/TelegramUI/Components/EmojiStatusComponent",
|
||||
"//submodules/AvatarNode",
|
||||
"//submodules/ContextUI",
|
||||
"//submodules/AccountContext",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import ComponentFlow
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import PresentationDataUtils
|
||||
import ContextUI
|
||||
import AvatarNode
|
||||
import EmojiStatusComponent
|
||||
import AccountContext
|
||||
|
||||
public final class AccountPeerContextItem: ContextMenuCustomItem {
|
||||
let context: AccountContext
|
||||
let account: Account
|
||||
let peer: EnginePeer
|
||||
let action: (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void) -> Void
|
||||
|
||||
public init(context: AccountContext, account: Account, peer: EnginePeer, action: @escaping (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void) -> Void) {
|
||||
self.context = context
|
||||
self.account = account
|
||||
self.peer = peer
|
||||
self.action = action
|
||||
}
|
||||
|
||||
public func node(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode {
|
||||
return AccountPeerContextItemNode(presentationData: presentationData, item: self, getController: getController, actionSelected: actionSelected)
|
||||
}
|
||||
}
|
||||
|
||||
private final class AccountPeerContextItemNode: ASDisplayNode, ContextMenuCustomNode {
|
||||
private let item: AccountPeerContextItem
|
||||
private let presentationData: PresentationData
|
||||
private let getController: () -> ContextControllerProtocol?
|
||||
private let actionSelected: (ContextMenuActionResult) -> Void
|
||||
|
||||
private let buttonNode: HighlightTrackingButtonNode
|
||||
private let textNode: ImmediateTextNode
|
||||
private let avatarNode: AvatarNode
|
||||
private let emojiStatusView: ComponentView<Empty>
|
||||
|
||||
init(presentationData: PresentationData, item: AccountPeerContextItem, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) {
|
||||
self.item = item
|
||||
self.presentationData = presentationData
|
||||
self.getController = getController
|
||||
self.actionSelected = actionSelected
|
||||
|
||||
let textFont = Font.regular(presentationData.listsFontSize.baseDisplaySize * 17.0 / 17.0)
|
||||
|
||||
self.textNode = ImmediateTextNode()
|
||||
self.textNode.isAccessibilityElement = false
|
||||
self.textNode.isUserInteractionEnabled = false
|
||||
self.textNode.displaysAsynchronously = false
|
||||
let peerTitle = item.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
|
||||
self.textNode.attributedText = NSAttributedString(string: peerTitle, font: textFont, textColor: presentationData.theme.contextMenu.primaryColor)
|
||||
self.textNode.maximumNumberOfLines = 1
|
||||
|
||||
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 14.0))
|
||||
|
||||
self.emojiStatusView = ComponentView<Empty>()
|
||||
|
||||
self.buttonNode = HighlightTrackingButtonNode()
|
||||
self.buttonNode.isAccessibilityElement = true
|
||||
self.buttonNode.accessibilityLabel = peerTitle
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.textNode)
|
||||
self.addSubnode(self.avatarNode)
|
||||
self.addSubnode(self.buttonNode)
|
||||
|
||||
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
|
||||
}
|
||||
|
||||
func updateLayout(constrainedWidth: CGFloat, constrainedHeight: CGFloat) -> (CGSize, (CGSize, ContainedViewLayoutTransition) -> Void) {
|
||||
let sideInset: CGFloat = 18.0
|
||||
let iconSideInset: CGFloat = 20.0
|
||||
let verticalInset: CGFloat = 11.0
|
||||
|
||||
let iconSize = CGSize(width: 28.0, height: 28.0)
|
||||
|
||||
let standardIconWidth: CGFloat = 32.0
|
||||
var rightTextInset: CGFloat = sideInset
|
||||
if !iconSize.width.isZero {
|
||||
rightTextInset = max(iconSize.width, standardIconWidth) + iconSideInset + sideInset - 12.0
|
||||
}
|
||||
|
||||
self.avatarNode.setPeer(context: self.item.context, account: self.item.account, theme: self.presentationData.theme, peer: self.item.peer)
|
||||
|
||||
if self.item.peer.emojiStatus != nil {
|
||||
rightTextInset += 32.0
|
||||
}
|
||||
|
||||
let textSize = self.textNode.updateLayout(CGSize(width: constrainedWidth - sideInset - rightTextInset, height: .greatestFiniteMagnitude))
|
||||
|
||||
return (CGSize(width: textSize.width + sideInset + rightTextInset, height: verticalInset * 2.0 + textSize.height), { size, transition in
|
||||
let verticalOrigin = floor((size.height - textSize.height) / 2.0)
|
||||
let textFrame = CGRect(origin: CGPoint(x: iconSideInset + 40.0, y: verticalOrigin), size: textSize)
|
||||
transition.updateFrameAdditive(node: self.textNode, frame: textFrame)
|
||||
|
||||
var iconContent: EmojiStatusComponent.Content?
|
||||
if case let .user(user) = self.item.peer {
|
||||
if let emojiStatus = user.emojiStatus {
|
||||
iconContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 28.0, height: 28.0), placeholderColor: self.presentationData.theme.list.mediaPlaceholderColor, themeColor: self.presentationData.theme.list.itemAccentColor, loopMode: .forever)
|
||||
} else if user.isPremium {
|
||||
iconContent = .premium(color: self.presentationData.theme.list.itemAccentColor)
|
||||
}
|
||||
} else if case let .channel(channel) = self.item.peer {
|
||||
if let emojiStatus = channel.emojiStatus {
|
||||
iconContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 28.0, height: 28.0), placeholderColor: self.presentationData.theme.list.mediaPlaceholderColor, themeColor: self.presentationData.theme.list.itemAccentColor, loopMode: .forever)
|
||||
}
|
||||
}
|
||||
if let iconContent {
|
||||
let emojiStatusSize = self.emojiStatusView.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(EmojiStatusComponent(
|
||||
context: self.item.context,
|
||||
animationCache: self.item.context.animationCache,
|
||||
animationRenderer: self.item.context.animationRenderer,
|
||||
content: iconContent,
|
||||
isVisibleForAnimations: true,
|
||||
action: nil
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 24.0, height: 24.0)
|
||||
)
|
||||
if let view = self.emojiStatusView.view {
|
||||
if view.superview == nil {
|
||||
self.view.addSubview(view)
|
||||
}
|
||||
transition.updateFrame(view: view, frame: CGRect(origin: CGPoint(x: textFrame.maxX + 2.0, y: textFrame.minY + floor((textFrame.height - emojiStatusSize.height) / 2.0)), size: emojiStatusSize))
|
||||
}
|
||||
}
|
||||
|
||||
transition.updateFrame(node: self.avatarNode, frame: CGRect(origin: CGPoint(x: iconSideInset + floor((standardIconWidth - iconSize.width) / 2.0), y: floor((size.height - iconSize.height) / 2.0)), size: iconSize))
|
||||
|
||||
transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)))
|
||||
})
|
||||
}
|
||||
|
||||
func updateTheme(presentationData: PresentationData) {
|
||||
if let attributedText = self.textNode.attributedText {
|
||||
let updatedAttributedText = NSMutableAttributedString(attributedString: attributedText)
|
||||
updatedAttributedText.addAttribute(.foregroundColor, value: presentationData.theme.contextMenu.primaryColor.cgColor, range: NSRange(location: 0, length: updatedAttributedText.length))
|
||||
self.textNode.attributedText = updatedAttributedText
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func buttonPressed() {
|
||||
self.performAction()
|
||||
}
|
||||
|
||||
func canBeHighlighted() -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func setIsHighlighted(_ value: Bool) {
|
||||
}
|
||||
|
||||
func updateIsHighlighted(isHighlighted: Bool) {
|
||||
self.setIsHighlighted(isHighlighted)
|
||||
}
|
||||
|
||||
func performAction() {
|
||||
guard let controller = self.getController() else {
|
||||
return
|
||||
}
|
||||
self.item.action(controller, { [weak self] result in
|
||||
self?.actionSelected(result)
|
||||
})
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -504,7 +504,7 @@ private class GiftIconLayer: SimpleLayer {
|
||||
file = gift.file
|
||||
case let .unique(gift):
|
||||
for attribute in gift.attributes {
|
||||
if case let .model(_, fileValue, _) = attribute {
|
||||
if case let .model(_, fileValue, _, _) = attribute {
|
||||
file = fileValue
|
||||
} else if case let .backdrop(_, _, innerColor, _, _, _, _) = attribute {
|
||||
color = UIColor(rgb: UInt32(bitPattern: innerColor))
|
||||
@@ -570,7 +570,7 @@ private class GiftIconLayer: SimpleLayer {
|
||||
file = gift.file
|
||||
case let .unique(gift):
|
||||
for attribute in gift.attributes {
|
||||
if case let .model(_, fileValue, _) = attribute {
|
||||
if case let .model(_, fileValue, _, _) = attribute {
|
||||
file = fileValue
|
||||
} else if case let .backdrop(_, _, innerColor, _, _, _, _) = attribute {
|
||||
color = UIColor(rgb: UInt32(bitPattern: innerColor))
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
sgdeps = [
|
||||
"//Swiftgram/SGSettingsUI:SGSettingsUI",
|
||||
"//Swiftgram/SGStrings:SGStrings",
|
||||
"//Swiftgram/SGSimpleSettings:SGSimpleSettings",
|
||||
|
||||
"//Swiftgram/SGRegDate:SGRegDate",
|
||||
"//Swiftgram/SGRegDateScheme:SGRegDateScheme",
|
||||
"//Swiftgram/SGDebugUI:SGDebugUI",
|
||||
]
|
||||
|
||||
|
||||
swift_library(
|
||||
name = "PeerInfoScreen",
|
||||
module_name = "PeerInfoScreen",
|
||||
@@ -9,7 +20,7 @@ swift_library(
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
deps = sgdeps + [
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/AccountUtils",
|
||||
"//submodules/ActionSheetPeerItem",
|
||||
@@ -173,6 +184,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/AvatarComponent",
|
||||
"//submodules/TelegramUI/Components/AlertComponent/AlertTransferHeaderComponent",
|
||||
"//submodules/TelegramUI/Components/HorizontalTabsComponent",
|
||||
"//submodules/TelegramUI/Components/PeerInfo/AccountPeerContextItem",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
+59
-7
@@ -1,3 +1,5 @@
|
||||
import SGRegDateScheme
|
||||
import SGRegDate
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Postbox
|
||||
@@ -383,6 +385,8 @@ final class PeerInfoPersonalChannelData: Equatable {
|
||||
}
|
||||
|
||||
final class PeerInfoScreenData {
|
||||
let regDate: RegDate?
|
||||
let channelCreationTimestamp: Int32?
|
||||
let peer: Peer?
|
||||
let chatPeer: Peer?
|
||||
let savedMessagesPeer: Peer?
|
||||
@@ -437,6 +441,8 @@ final class PeerInfoScreenData {
|
||||
}
|
||||
|
||||
init(
|
||||
regDate: RegDate? = nil,
|
||||
channelCreationTimestamp: Int32? = nil,
|
||||
peer: Peer?,
|
||||
chatPeer: Peer?,
|
||||
savedMessagesPeer: Peer?,
|
||||
@@ -480,6 +486,8 @@ final class PeerInfoScreenData {
|
||||
savedMusicContext: ProfileSavedMusicContext?,
|
||||
savedMusicState: ProfileSavedMusicContext.State?
|
||||
) {
|
||||
self.regDate = regDate
|
||||
self.channelCreationTimestamp = channelCreationTimestamp
|
||||
self.peer = peer
|
||||
self.chatPeer = chatPeer
|
||||
self.savedMessagesPeer = savedMessagesPeer
|
||||
@@ -937,6 +945,7 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id,
|
||||
combineLatest(notificationExceptions, notificationsAuthorizationStatus.get(), notificationsWarningSuppressed.get()),
|
||||
combineLatest(context.account.viewTracker.featuredStickerPacks(), archivedStickerPacks),
|
||||
hasPassport,
|
||||
(context.watchManager?.watchAppInstalled ?? .single(false)),
|
||||
context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]),
|
||||
context.engine.notices.getServerProvidedSuggestions(),
|
||||
context.engine.data.get(
|
||||
@@ -955,7 +964,7 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id,
|
||||
starsState,
|
||||
tonState
|
||||
)
|
||||
|> map { peerView, accountsAndPeers, accountSessions, privacySettings, sharedPreferences, notifications, stickerPacks, hasPassport, accountPreferences, suggestions, limits, hasPassword, isPowerSavingEnabled, hasStories, bots, personalChannel, starsState, tonState -> PeerInfoScreenData in
|
||||
|> map { peerView, accountsAndPeers, accountSessions, privacySettings, sharedPreferences, notifications, stickerPacks, hasPassport, hasWatchApp, accountPreferences, suggestions, limits, hasPassword, isPowerSavingEnabled, hasStories, bots, personalChannel, starsState, tonState -> PeerInfoScreenData in
|
||||
let (notificationExceptions, notificationsAuthorizationStatus, notificationsWarningSuppressed) = notifications
|
||||
let (featuredStickerPacks, archivedStickerPacks) = stickerPacks
|
||||
|
||||
@@ -968,6 +977,10 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id,
|
||||
|
||||
var enableQRLogin = false
|
||||
let appConfiguration = accountPreferences.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self)
|
||||
// MARK: Swiftgram
|
||||
if let appConfiguration, appConfiguration.sgWebSettings.global.qrLogin {
|
||||
enableQRLogin = true
|
||||
}
|
||||
if let appConfiguration, let data = appConfiguration.data, let enableQR = data["qr_login_camera"] as? Bool, enableQR {
|
||||
enableQRLogin = true
|
||||
}
|
||||
@@ -998,7 +1011,7 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id,
|
||||
userLimits: peer?.isPremium == true ? limits.1 : limits.0,
|
||||
bots: bots,
|
||||
hasPassport: hasPassport,
|
||||
hasWatchApp: false,
|
||||
hasWatchApp: hasWatchApp,
|
||||
enableQRLogin: enableQRLogin
|
||||
)
|
||||
|
||||
@@ -1443,6 +1456,7 @@ func peerInfoScreenData(
|
||||
let savedMusicContext = ProfileSavedMusicContext(account: context.account, peerId: peerId)
|
||||
|
||||
return combineLatest(
|
||||
Signal<RegDate?, NoError>.single(nil) |> then (getRegDate(context: context, peerId: peerId.id._internalGetInt64Value())),
|
||||
context.account.viewTracker.peerView(peerId, updateData: true),
|
||||
peerInfoAvailableMediaPanes(context: context, peerId: peerId, chatLocation: chatLocation, isMyProfile: isMyProfile, chatLocationContextHolder: chatLocationContextHolder, sharedMediaFromForumTopic: sharedMediaFromForumTopic),
|
||||
context.engine.data.subscribe(TelegramEngine.EngineData.Item.NotificationSettings.Global()),
|
||||
@@ -1465,7 +1479,7 @@ func peerInfoScreenData(
|
||||
webAppPermissions,
|
||||
savedMusicContext.state
|
||||
)
|
||||
|> map { peerView, availablePanes, globalNotificationSettings, encryptionKeyFingerprint, status, hasStories, hasStoryArchive, recommendedBots, accountIsPremium, savedMessagesPeer, hasSavedMessagesChats, hasSavedMessages, hasSavedMessageTags, hasBotPreviewItems, personalChannel, privacySettings, starsRevenueContextAndState, revenueContextAndState, premiumGiftOptions, webAppPermissions, savedMusicState -> PeerInfoScreenData in
|
||||
|> map { regDate, peerView, availablePanes, globalNotificationSettings, encryptionKeyFingerprint, status, hasStories, hasStoryArchive, recommendedBots, accountIsPremium, savedMessagesPeer, hasSavedMessagesChats, hasSavedMessages, hasSavedMessageTags, hasBotPreviewItems, personalChannel, privacySettings, starsRevenueContextAndState, revenueContextAndState, premiumGiftOptions, webAppPermissions, savedMusicState -> PeerInfoScreenData in
|
||||
var availablePanes = availablePanes
|
||||
if isMyProfile {
|
||||
availablePanes?.insert(.stories, at: 0)
|
||||
@@ -1562,6 +1576,7 @@ func peerInfoScreenData(
|
||||
}
|
||||
|
||||
return PeerInfoScreenData(
|
||||
regDate: regDate,
|
||||
peer: peer,
|
||||
chatPeer: peerView.peers[peerId],
|
||||
savedMessagesPeer: savedMessagesPeer?._asPeer(),
|
||||
@@ -1715,6 +1730,7 @@ func peerInfoScreenData(
|
||||
let personalChannel = peerInfoPersonalOrLinkedChannel(context: context, peerId: peerId, isSettings: false)
|
||||
|
||||
return combineLatest(
|
||||
getFirstMessage(context: context, peerId: peerId),
|
||||
context.account.viewTracker.peerView(peerId, updateData: true),
|
||||
peerInfoAvailableMediaPanes(context: context, peerId: peerId, chatLocation: chatLocation, isMyProfile: false, chatLocationContextHolder: chatLocationContextHolder, sharedMediaFromForumTopic: sharedMediaFromForumTopic),
|
||||
context.engine.data.subscribe(TelegramEngine.EngineData.Item.NotificationSettings.Global()),
|
||||
@@ -1735,7 +1751,7 @@ func peerInfoScreenData(
|
||||
profileGiftsContext.state,
|
||||
personalChannel
|
||||
)
|
||||
|> map { peerView, availablePanes, globalNotificationSettings, status, currentInvitationsContext, invitations, currentRequestsContext, requests, hasStories, accountIsPremium, recommendedChannels, hasSavedMessages, hasSavedMessagesChats, hasSavedMessageTags, isPremiumRequiredForStoryPosting, starsRevenueContextAndState, revenueContextAndState, profileGiftsState, personalChannel -> PeerInfoScreenData in
|
||||
|> map { firstMessage, peerView, availablePanes, globalNotificationSettings, status, currentInvitationsContext, invitations, currentRequestsContext, requests, hasStories, accountIsPremium, recommendedChannels, hasSavedMessages, hasSavedMessagesChats, hasSavedMessageTags, isPremiumRequiredForStoryPosting, starsRevenueContextAndState, revenueContextAndState, profileGiftsState, personalChannel -> PeerInfoScreenData in
|
||||
var availablePanes = availablePanes
|
||||
if let hasStories {
|
||||
if hasStories {
|
||||
@@ -1807,6 +1823,7 @@ func peerInfoScreenData(
|
||||
}
|
||||
|
||||
return PeerInfoScreenData(
|
||||
channelCreationTimestamp: firstMessage?.timestamp,
|
||||
peer: peerView.peers[peerId],
|
||||
chatPeer: peerView.peers[peerId],
|
||||
savedMessagesPeer: nil,
|
||||
@@ -2049,6 +2066,7 @@ func peerInfoScreenData(
|
||||
let isPremiumRequiredForStoryPosting: Signal<Bool, NoError> = isPremiumRequiredForStoryPosting(context: context)
|
||||
|
||||
return combineLatest(queue: .mainQueue(),
|
||||
Signal<Message?, NoError>.single(nil) |> then (getFirstMessage(context: context, peerId: peerId)),
|
||||
context.account.viewTracker.peerView(groupId, updateData: true),
|
||||
peerInfoAvailableMediaPanes(context: context, peerId: groupId, chatLocation: chatLocation, isMyProfile: false, chatLocationContextHolder: chatLocationContextHolder, sharedMediaFromForumTopic: sharedMediaFromForumTopic),
|
||||
context.engine.data.subscribe(TelegramEngine.EngineData.Item.NotificationSettings.Global()),
|
||||
@@ -2068,7 +2086,7 @@ func peerInfoScreenData(
|
||||
isPremiumRequiredForStoryPosting,
|
||||
starsRevenueContextAndState
|
||||
)
|
||||
|> mapToSignal { peerView, availablePanes, globalNotificationSettings, status, membersData, currentInvitationsContext, invitations, currentRequestsContext, requests, hasStories, threadData, preferencesView, accountIsPremium, hasSavedMessages, hasSavedMessagesChats, hasSavedMessageTags, isPremiumRequiredForStoryPosting, starsRevenueContextAndState -> Signal<PeerInfoScreenData, NoError> in
|
||||
|> mapToSignal { firstMessage, peerView, availablePanes, globalNotificationSettings, status, membersData, currentInvitationsContext, invitations, currentRequestsContext, requests, hasStories, threadData, preferencesView, accountIsPremium, hasSavedMessages, hasSavedMessagesChats, hasSavedMessageTags, isPremiumRequiredForStoryPosting, starsRevenueContextAndState -> Signal<PeerInfoScreenData, NoError> in
|
||||
var discussionPeer: Peer?
|
||||
if case let .known(maybeLinkedDiscussionPeerId) = (peerView.cachedData as? CachedChannelData)?.linkedDiscussionPeerId, let linkedDiscussionPeerId = maybeLinkedDiscussionPeerId, let peer = peerView.peers[linkedDiscussionPeerId] {
|
||||
discussionPeer = peer
|
||||
@@ -2142,7 +2160,24 @@ func peerInfoScreenData(
|
||||
|
||||
let appConfiguration: AppConfiguration = preferencesView.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) ?? .defaultValue
|
||||
|
||||
// MARK: Swiftgram
|
||||
var channelCreationTimestamp = firstMessage?.timestamp
|
||||
if groupId.namespace == Namespaces.Peer.CloudChannel, let firstMessage {
|
||||
for media in firstMessage.media {
|
||||
if let action = media as? TelegramMediaAction {
|
||||
if case let .channelMigratedFromGroup(_, legacyGroupId) = action.action {
|
||||
if let legacyGroup = firstMessage.peers[legacyGroupId] as? TelegramGroup {
|
||||
if legacyGroup.creationDate != 0 {
|
||||
channelCreationTimestamp = legacyGroup.creationDate
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return .single(PeerInfoScreenData(
|
||||
channelCreationTimestamp: channelCreationTimestamp,
|
||||
peer: peerView.peers[groupId],
|
||||
chatPeer: peerView.peers[groupId],
|
||||
savedMessagesPeer: nil,
|
||||
@@ -2425,8 +2460,8 @@ func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFro
|
||||
result.append(.message)
|
||||
}
|
||||
result.append(.mute)
|
||||
if case let .broadcast(info) = channel.info, info.flags.contains(.hasMonoforum), !channel.hasPermission(.manageDirect) {
|
||||
} else if hasDiscussion {
|
||||
/* /* MARK: Swiftgram */ if case let .broadcast(info) = channel.info, info.flags.contains(.hasMonoforum), !channel.hasPermission(.manageDirect) {
|
||||
} else*/ if hasDiscussion {
|
||||
result.append(.discussion)
|
||||
}
|
||||
result.append(.search)
|
||||
@@ -2596,3 +2631,20 @@ private func isPremiumRequiredForStoryPosting(context: AccountContext) -> Signal
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// MARK: Swiftgram
|
||||
private func getFirstMessage(context: AccountContext, peerId: PeerId) -> Signal<Message?, NoError> {
|
||||
return context.engine.messages.getMessagesLoadIfNecessary([MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: 1)])
|
||||
|> `catch` { _ in
|
||||
return .single(.result([]))
|
||||
}
|
||||
|> mapToSignal { result -> Signal<[Message], NoError> in
|
||||
guard case let .result(result) = result else {
|
||||
return .complete()
|
||||
}
|
||||
return .single(result)
|
||||
}
|
||||
|> map { $0.first }
|
||||
}
|
||||
|
||||
|
||||
+10
-6
@@ -28,6 +28,7 @@ import AnimationCache
|
||||
import MultiAnimationRenderer
|
||||
import ComponentDisplayAdapters
|
||||
import ChatTitleView
|
||||
import SGSimpleSettings
|
||||
import AppBundle
|
||||
import AvatarVideoNode
|
||||
import PeerInfoVisualMediaPaneNode
|
||||
@@ -207,7 +208,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
||||
private var currentStarRating: TelegramStarRating?
|
||||
private var currentPendingStarRating: TelegramStarPendingRating?
|
||||
|
||||
init(context: AccountContext, controller: PeerInfoScreenImpl, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, isMediaOnly: Bool, isSettings: Bool, isMyProfile: Bool, forumTopicThreadId: Int64?, chatLocation: ChatLocation) {
|
||||
init(hidePhoneInSettings: Bool = false, context: AccountContext, controller: PeerInfoScreenImpl, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, isMediaOnly: Bool, isSettings: Bool, isMyProfile: Bool, forumTopicThreadId: Int64?, chatLocation: ChatLocation) {
|
||||
self.context = context
|
||||
self.controller = controller
|
||||
self.isAvatarExpanded = avatarInitiallyExpanded
|
||||
@@ -1236,11 +1237,15 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
||||
smallTitleAttributes = MultiScaleTextState.Attributes(font: Font.medium(28.0), color: .white, shadowColor: titleShadowColor)
|
||||
|
||||
if self.isSettings, let user = peer as? TelegramUser {
|
||||
var subtitle = formatPhoneNumber(context: self.context, number: user.phone ?? "")
|
||||
|
||||
if let mainUsername = user.addressName, !mainUsername.isEmpty {
|
||||
subtitle = "\(subtitle) • @\(mainUsername)"
|
||||
let hidePhoneInSettings = SGSimpleSettings.shared.hidePhoneInSettings
|
||||
var subtitleComponents: [String] = []
|
||||
if !hidePhoneInSettings, let phone = user.phone, !phone.isEmpty {
|
||||
subtitleComponents.append(formatPhoneNumber(context: self.context, number: phone))
|
||||
}
|
||||
if let mainUsername = user.addressName, !mainUsername.isEmpty {
|
||||
subtitleComponents.append("@\(mainUsername)")
|
||||
}
|
||||
let subtitle = subtitleComponents.joined(separator: " • ")
|
||||
subtitleStringText = subtitle
|
||||
subtitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(17.0), color: .white)
|
||||
smallSubtitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(16.0), color: .white, shadowColor: titleShadowColor)
|
||||
@@ -2859,4 +2864,3 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
||||
transition.updateAnchorPoint(layer: self.avatarListNode.maskNode.layer, anchorPoint: maskAnchorPoint)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
@@ -9,6 +9,7 @@ import AccountContext
|
||||
import StatisticsUI
|
||||
|
||||
final class PeerInfoInteraction {
|
||||
let notifyTextCopied: () -> Void
|
||||
let openChat: (EnginePeer.Id?) -> Void
|
||||
let openUsername: (String, Bool, Promise<Bool>?) -> Void
|
||||
let openPhone: (String, ASDisplayNode, ContextGesture?, Promise<Bool>?) -> Void
|
||||
@@ -85,6 +86,7 @@ final class PeerInfoInteraction {
|
||||
let getController: () -> ViewController?
|
||||
|
||||
init(
|
||||
notifyTextCopied: @escaping () -> Void,
|
||||
openUsername: @escaping (String, Bool, Promise<Bool>?) -> Void,
|
||||
openPhone: @escaping (String, ASDisplayNode, ContextGesture?, Promise<Bool>?) -> Void,
|
||||
editingOpenNotificationSettings: @escaping () -> Void,
|
||||
@@ -160,6 +162,7 @@ final class PeerInfoInteraction {
|
||||
displayAutoTranslateLocked: @escaping () -> Void,
|
||||
getController: @escaping () -> ViewController?
|
||||
) {
|
||||
self.notifyTextCopied = notifyTextCopied
|
||||
self.openUsername = openUsername
|
||||
self.openPhone = openPhone
|
||||
self.editingOpenNotificationSettings = editingOpenNotificationSettings
|
||||
|
||||
+3
-3
@@ -129,7 +129,7 @@ private final class GiftsTabItemComponent: Component {
|
||||
file = gift.file
|
||||
case let .unique(gift):
|
||||
for attribute in gift.attributes {
|
||||
if case let .model(_, fileValue, _) = attribute {
|
||||
if case let .model(_, fileValue, _, _) = attribute {
|
||||
file = fileValue
|
||||
}
|
||||
}
|
||||
@@ -270,7 +270,7 @@ final class PeerInfoPaneTabsContainerPaneNode: ASDisplayNode {
|
||||
file = gift.file
|
||||
case let .unique(gift):
|
||||
for attribute in gift.attributes {
|
||||
if case let .model(_, fileValue, _) = attribute {
|
||||
if case let .model(_, fileValue, _, _) = attribute {
|
||||
file = fileValue
|
||||
}
|
||||
}
|
||||
@@ -653,7 +653,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat
|
||||
|
||||
private let initialPaneKey: PeerInfoPaneKey?
|
||||
|
||||
init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, peerId: PeerId, chatLocation: ChatLocation, sharedMediaFromForumTopic: (EnginePeer.Id, Int64)?, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>, isMediaOnly: Bool, initialPaneKey: PeerInfoPaneKey?, initialStoryFolderId: Int64?, initialGiftCollectionId: Int64?) {
|
||||
init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, peerId: PeerId, chatLocation: ChatLocation, sharedMediaFromForumTopic: (EnginePeer.Id, Int64)?, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>, isMediaOnly: Bool, initialPaneKey: PeerInfoPaneKey?, initialStoryFolderId: Int64?, initialGiftCollectionId: Int64?, switchToMediaTarget: PeerInfoSwitchToMediaTarget? = nil) {
|
||||
self.context = context
|
||||
self.updatedPresentationData = updatedPresentationData
|
||||
self.peerId = peerId
|
||||
|
||||
+171
-5
@@ -1,3 +1,8 @@
|
||||
// MARK: Swiftgram
|
||||
import SGSimpleSettings
|
||||
import SGSettingsUI
|
||||
import SGStrings
|
||||
import CountrySelectionUI
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
@@ -19,9 +24,10 @@ import PeerNameColorItem
|
||||
import BoostLevelIconComponent
|
||||
|
||||
private let enabledPublicBioEntities: EnabledEntityTypes = [.allUrl, .mention, .hashtag]
|
||||
private let enabledPrivateBioEntities: EnabledEntityTypes = [.internalUrl, .mention, .hashtag]
|
||||
private let enabledPrivateBioEntities: EnabledEntityTypes = [.allUrl, .mention, .hashtag] // MARK: Swiftgram
|
||||
|
||||
enum InfoSection: Int, CaseIterable {
|
||||
case swiftgram
|
||||
case groupLocation
|
||||
case calls
|
||||
case personalChannel
|
||||
@@ -35,12 +41,19 @@ enum InfoSection: Int, CaseIterable {
|
||||
case botAffiliateProgram
|
||||
}
|
||||
|
||||
func infoItems(data: PeerInfoScreenData?, context: AccountContext, presentationData: PresentationData, interaction: PeerInfoInteraction, nearbyPeerDistance: Int32?, reactionSourceMessageId: MessageId?, callMessages: [Message], chatLocation: ChatLocation, isOpenedFromChat: Bool, isMyProfile: Bool) -> [(AnyHashable, [PeerInfoScreenItem])] {
|
||||
func infoItems(nearestChatParticipant: (String?, Int32?), showProfileId: Bool, data: PeerInfoScreenData?, context: AccountContext, presentationData: PresentationData, interaction: PeerInfoInteraction, nearbyPeerDistance: Int32?, reactionSourceMessageId: MessageId?, callMessages: [Message], chatLocation: ChatLocation, isOpenedFromChat: Bool, isMyProfile: Bool) -> [(AnyHashable, [PeerInfoScreenItem])] {
|
||||
guard let data = data else {
|
||||
return []
|
||||
}
|
||||
|
||||
var currentPeerInfoSection: InfoSection = .peerInfo
|
||||
|
||||
// MARK: Swiftgram
|
||||
var sgItemId = 0
|
||||
var idText = ""
|
||||
var isMutualContact = false
|
||||
// var isUser = false
|
||||
// let lang = presentationData.strings.baseLanguageCode
|
||||
|
||||
var items: [InfoSection: [PeerInfoScreenItem]] = [:]
|
||||
for section in InfoSection.allCases {
|
||||
@@ -98,6 +111,11 @@ func infoItems(data: PeerInfoScreenData?, context: AccountContext, presentationD
|
||||
let ItemBotAddToChatInfo = 9003
|
||||
let ItemVerification = 9004
|
||||
|
||||
// MARK: Swiftgram
|
||||
isMutualContact = user.flags.contains(.mutualContact)
|
||||
idText = String(user.id.id._internalGetInt64Value())
|
||||
// isUser = true
|
||||
|
||||
if !callMessages.isEmpty {
|
||||
items[.calls]!.append(PeerInfoScreenCallListItem(id: ItemCallList, messages: callMessages))
|
||||
}
|
||||
@@ -122,7 +140,7 @@ func infoItems(data: PeerInfoScreenData?, context: AccountContext, presentationD
|
||||
}))
|
||||
}
|
||||
|
||||
if let phone = user.phone {
|
||||
if let phone = user.phone, !(SGSimpleSettings.shared.hidePhoneInSettings && isMyProfile) {
|
||||
let formattedPhone = formatPhoneNumber(context: context, number: phone)
|
||||
let label: String
|
||||
if formattedPhone.hasPrefix("+888 ") {
|
||||
@@ -516,6 +534,10 @@ func infoItems(data: PeerInfoScreenData?, context: AccountContext, presentationD
|
||||
}
|
||||
}
|
||||
} else if let channel = data.peer as? TelegramChannel {
|
||||
// MARK: Swiftgram
|
||||
idText = "-100" + String(channel.id.id._internalGetInt64Value())
|
||||
let ItemSGRecentActions = 20
|
||||
|
||||
let ItemUsername = 1
|
||||
let ItemUsernameInfo = 2
|
||||
let ItemAbout = 3
|
||||
@@ -681,7 +703,7 @@ func infoItems(data: PeerInfoScreenData?, context: AccountContext, presentationD
|
||||
|
||||
if case .broadcast = channel.info {
|
||||
var canEditMembers = false
|
||||
if channel.hasPermission(.banMembers) {
|
||||
if channel.adminRights != nil || channel.flags.contains(.isCreator) { // MARK: Swiftgram
|
||||
canEditMembers = true
|
||||
}
|
||||
if canEditMembers {
|
||||
@@ -763,6 +785,14 @@ func infoItems(data: PeerInfoScreenData?, context: AccountContext, presentationD
|
||||
items[section]!.append(PeerInfoScreenDisclosureItem(id: ItemEdit, label: .none, text: settingsTitle, icon: UIImage(bundleImageName: "Chat/Info/SettingsIcon"), action: {
|
||||
interaction.openEditing()
|
||||
}))
|
||||
|
||||
// MARK: Swiftgram
|
||||
if channel.hasPermission(.banMembers) || channel.flags.contains(.isCreator) {
|
||||
items[section]!.append(PeerInfoScreenDisclosureItem(id: ItemSGRecentActions, label: .none, text: presentationData.strings.Group_Info_AdminLog, icon: UIImage(bundleImageName: "Chat/Info/RecentActionsIcon"), action: {
|
||||
interaction.openRecentActions()
|
||||
}))
|
||||
}
|
||||
//
|
||||
}
|
||||
|
||||
if channel.hasPermission(.manageDirect), let personalChannel = data.personalChannel {
|
||||
@@ -782,6 +812,9 @@ func infoItems(data: PeerInfoScreenData?, context: AccountContext, presentationD
|
||||
}
|
||||
}
|
||||
} else if let group = data.peer as? TelegramGroup {
|
||||
// MARK: Swiftgram
|
||||
idText = String(group.id.id._internalGetInt64Value())
|
||||
|
||||
if let cachedData = data.cachedData as? CachedGroupData {
|
||||
let aboutText: String?
|
||||
if group.isFake {
|
||||
@@ -852,6 +885,139 @@ func infoItems(data: PeerInfoScreenData?, context: AccountContext, presentationD
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Swiftgram
|
||||
if showProfileId {
|
||||
items[.swiftgram]!.append(PeerInfoScreenLabeledValueItem(id: sgItemId, label: "id: \(idText)", text: "", textColor: .primary, action: nil, longTapAction: { sourceNode in
|
||||
interaction.openPeerInfoContextMenu(.copy(idText), sourceNode, nil)
|
||||
}, requestLayout: { _ in
|
||||
interaction.requestLayout(false)
|
||||
}))
|
||||
sgItemId += 1
|
||||
}
|
||||
|
||||
if SGSimpleSettings.shared.showDC {
|
||||
var dcId: Int? = nil
|
||||
// var dcLocation: String = ""
|
||||
var phoneCountryText = ""
|
||||
|
||||
var dcLabel = ""
|
||||
var dcText: String = ""
|
||||
|
||||
if let cachedData = data.cachedData as? CachedUserData, let phoneCountry = cachedData.peerStatusSettings?.phoneCountry {
|
||||
var countryName = ""
|
||||
let countriesConfiguration = context.currentCountriesConfiguration.with { $0 }
|
||||
if let country = countriesConfiguration.countries.first(where: { $0.id == phoneCountry }) {
|
||||
countryName = country.localizedName ?? country.name
|
||||
} else if phoneCountry == "FT" {
|
||||
countryName = presentationData.strings.Chat_NonContactUser_AnonymousNumber
|
||||
} else if phoneCountry == "TS" {
|
||||
countryName = "Test"
|
||||
}
|
||||
phoneCountryText = emojiFlagForISOCountryCode(phoneCountry) + " " + countryName
|
||||
}
|
||||
if let peer = data.peer, let smallProfileImage = peer.smallProfileImage, let cloudResource = smallProfileImage.resource as? CloudPeerPhotoSizeMediaResource {
|
||||
dcId = cloudResource.datacenterId
|
||||
|
||||
// switch (dcId) {
|
||||
// case 1:
|
||||
// dcLocation = "Miami"
|
||||
// case 2:
|
||||
// dcLocation = "Amsterdam"
|
||||
// case 3:
|
||||
// dcLocation = "Miami"
|
||||
// case 4:
|
||||
// dcLocation = "Amsterdam"
|
||||
// case 5:
|
||||
// dcLocation = "Singapore"
|
||||
// default:
|
||||
// break
|
||||
// }
|
||||
}
|
||||
|
||||
if let dcId = dcId {
|
||||
dcLabel = "dc: \(dcId)"
|
||||
if phoneCountryText.isEmpty {
|
||||
// if !dcLocation.isEmpty {
|
||||
// dcLabel += " \(dcLocation)"
|
||||
// }
|
||||
} else {
|
||||
dcText = "\(phoneCountryText)"
|
||||
}
|
||||
} else if !phoneCountryText.isEmpty {
|
||||
dcLabel = "dc: ?"
|
||||
dcText = phoneCountryText
|
||||
}
|
||||
|
||||
if !dcText.isEmpty || !dcLabel.isEmpty {
|
||||
items[.swiftgram]!.append(PeerInfoScreenLabeledValueItem(id: sgItemId, label: dcLabel, text: dcText, textColor: .primary, action: nil, longTapAction: { sourceNode in
|
||||
interaction.openPeerInfoContextMenu(.aboutDC, sourceNode, nil)
|
||||
}, requestLayout: { _ in
|
||||
interaction.requestLayout(false)
|
||||
}))
|
||||
sgItemId += 1
|
||||
}
|
||||
}
|
||||
|
||||
if SGSimpleSettings.shared.showCreationDate {
|
||||
if let channelCreationTimestamp = data.channelCreationTimestamp {
|
||||
let creationDateString = stringForDate(timestamp: channelCreationTimestamp, strings: presentationData.strings)
|
||||
items[.swiftgram]!.append(PeerInfoScreenLabeledValueItem(id: sgItemId, label: i18n("Chat.Created", presentationData.strings.baseLanguageCode, creationDateString), text: "", action: nil, longTapAction: { sourceNode in
|
||||
interaction.openPeerInfoContextMenu(.copy(creationDateString), sourceNode, nil)
|
||||
}, requestLayout: { _ in
|
||||
interaction.requestLayout(false)
|
||||
}))
|
||||
sgItemId += 1
|
||||
}
|
||||
}
|
||||
|
||||
if let invitedAt = nearestChatParticipant.1 {
|
||||
let joinedDateString = stringForDate(timestamp: invitedAt, strings: presentationData.strings)
|
||||
items[.swiftgram]!.append(PeerInfoScreenLabeledValueItem(id: sgItemId, label: i18n("Chat.JoinedDateTitle", presentationData.strings.baseLanguageCode, nearestChatParticipant.0 ?? "chat") , text: joinedDateString, action: nil, longTapAction: { sourceNode in
|
||||
interaction.openPeerInfoContextMenu(.copy(joinedDateString), sourceNode, nil)
|
||||
}, requestLayout: { _ in
|
||||
interaction.requestLayout(false)
|
||||
}))
|
||||
sgItemId += 1
|
||||
}
|
||||
|
||||
if SGSimpleSettings.shared.showRegDate {
|
||||
var regDateString = ""
|
||||
if let cachedData = data.cachedData as? CachedUserData, let registrationDate = cachedData.peerStatusSettings?.registrationDate {
|
||||
let components = registrationDate.components(separatedBy: ".")
|
||||
if components.count == 2, let first = Int32(components[0]), let second = Int32(components[1]) {
|
||||
let month = first - 1
|
||||
let year = second - 1900
|
||||
regDateString = stringForMonth(strings: presentationData.strings, month: month, ofYear: year)
|
||||
}
|
||||
}
|
||||
if let regDate = data.regDate, regDateString.isEmpty {
|
||||
let regTimestamp = Int32((regDate.from + regDate.to) / 2)
|
||||
switch (context.currentAppConfiguration.with { $0 }.sgWebSettings.global.regdateFormat) {
|
||||
case "year":
|
||||
regDateString = stringForDateWithoutDayAndMonth(date: Date(timeIntervalSince1970: Double(regTimestamp)), strings: presentationData.strings)
|
||||
case "month":
|
||||
regDateString = stringForDateWithoutDay(date: Date(timeIntervalSince1970: Double(regTimestamp)), strings: presentationData.strings)
|
||||
default:
|
||||
regDateString = stringForDate(timestamp: regTimestamp, strings: presentationData.strings)
|
||||
}
|
||||
}
|
||||
if !regDateString.isEmpty {
|
||||
items[.swiftgram]!.append(PeerInfoScreenLabeledValueItem(id: sgItemId, label: i18n("Chat.RegDate", presentationData.strings.baseLanguageCode), text: regDateString, action: nil, longTapAction: { sourceNode in
|
||||
interaction.openPeerInfoContextMenu(.copy(regDateString), sourceNode, nil)
|
||||
}, requestLayout: { _ in
|
||||
interaction.requestLayout(false)
|
||||
}))
|
||||
sgItemId += 1
|
||||
}
|
||||
}
|
||||
if isMutualContact {
|
||||
items[.swiftgram]!.append(PeerInfoScreenLabeledValueItem(id: sgItemId, label: i18n("MutualContact.Label", presentationData.strings.baseLanguageCode), text: "", action: nil, longTapAction: { _ in }, requestLayout: { _ in
|
||||
interaction.requestLayout(false)
|
||||
}))
|
||||
sgItemId += 1
|
||||
}
|
||||
|
||||
|
||||
var result: [(AnyHashable, [PeerInfoScreenItem])] = []
|
||||
for section in InfoSection.allCases {
|
||||
if let sectionItems = items[section], !sectionItems.isEmpty {
|
||||
@@ -1226,7 +1392,7 @@ func editingItems(data: PeerInfoScreenData?, boostStatus: ChannelBoostStatus?, s
|
||||
}
|
||||
|
||||
var canEditMembers = false
|
||||
if channel.hasPermission(.banMembers) && (channel.adminRights != nil || channel.flags.contains(.isCreator)) {
|
||||
if /*channel.hasPermission(.banMembers) &&*/ (channel.adminRights != nil || channel.flags.contains(.isCreator)) { // MARK: Swiftgram
|
||||
canEditMembers = true
|
||||
}
|
||||
if canEditMembers {
|
||||
|
||||
+306
-87
@@ -1,3 +1,9 @@
|
||||
// MARK: Swiftgram
|
||||
import SGDebugUI
|
||||
import SGSimpleSettings
|
||||
import SGSettingsUI
|
||||
import SGStrings
|
||||
import CountrySelectionUI
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
@@ -114,6 +120,7 @@ import GiftViewScreen
|
||||
import PeerMessagesMediaPlaylist
|
||||
import EdgeEffect
|
||||
import Pasteboard
|
||||
import AccountPeerContextItem
|
||||
|
||||
public enum PeerInfoAvatarEditingMode {
|
||||
case generic
|
||||
@@ -144,6 +151,8 @@ enum PeerInfoMemberAction {
|
||||
}
|
||||
|
||||
enum PeerInfoContextSubject {
|
||||
case copy(String)
|
||||
case aboutDC
|
||||
case bio
|
||||
case phone(String)
|
||||
case link(customLink: String?)
|
||||
@@ -153,6 +162,9 @@ enum PeerInfoContextSubject {
|
||||
}
|
||||
|
||||
enum PeerInfoSettingsSection {
|
||||
case swiftgram
|
||||
case swiftgramPro
|
||||
case ghostgram
|
||||
case avatar
|
||||
case edit
|
||||
case proxy
|
||||
@@ -188,7 +200,6 @@ enum PeerInfoSettingsSection {
|
||||
case premiumManagement
|
||||
case stars
|
||||
case ton
|
||||
case ghostgram
|
||||
}
|
||||
|
||||
enum PeerInfoReportType {
|
||||
@@ -213,13 +224,14 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
let chatLocation: ChatLocation
|
||||
let chatLocationContextHolder: Atomic<ChatLocationContextHolder?>
|
||||
let switchToStoryFolder: Int64?
|
||||
let switchToMediaTarget: PeerInfoSwitchToMediaTarget?
|
||||
let switchToGiftsTarget: PeerInfoSwitchToGiftsTarget?
|
||||
let sharedMediaFromForumTopic: (EnginePeer.Id, Int64)?
|
||||
|
||||
let isSettings: Bool
|
||||
let isMyProfile: Bool
|
||||
let isMediaOnly: Bool
|
||||
let initialExpandPanes: Bool
|
||||
var initialExpandPanes: Bool
|
||||
|
||||
private(set) var presentationData: PresentationData
|
||||
|
||||
@@ -229,6 +241,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
let edgeEffectView: EdgeEffectView
|
||||
|
||||
let headerNode: PeerInfoHeaderNode
|
||||
var underHeaderContentsAlpha: CGFloat = 1.0
|
||||
var regularSections: [AnyHashable: PeerInfoScreenItemSectionContainerNode] = [:]
|
||||
var editingSections: [AnyHashable: PeerInfoScreenItemSectionContainerNode] = [:]
|
||||
let paneContainerNode: PeerInfoPaneContainerNode
|
||||
@@ -262,6 +275,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
let enqueueMediaMessageDisposable = MetaDisposable()
|
||||
|
||||
private(set) var validLayout: (ContainerViewLayout, CGFloat)?
|
||||
private(set) var nearestChatParticipant: (String?, Int32?) = (nil, nil)
|
||||
private(set) var showProfileId: Bool = SGSimpleSettings.shared.showProfileId // MARK: Swiftgram
|
||||
private(set) var data: PeerInfoScreenData?
|
||||
|
||||
var state = PeerInfoState(
|
||||
@@ -311,7 +326,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
let twoStepAuthData = Promise<TwoStepAuthData?>(nil)
|
||||
let supportPeerDisposable = MetaDisposable()
|
||||
let tipsPeerDisposable = MetaDisposable()
|
||||
|
||||
let cachedFaq = Promise<ResolvedUrl?>(nil)
|
||||
var didSetCachedFaq = false
|
||||
|
||||
weak var copyProtectionTooltipController: TooltipController?
|
||||
weak var emojiStatusSelectionController: ViewController?
|
||||
@@ -343,7 +360,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
}
|
||||
private var didSetReady = false
|
||||
|
||||
init(controller: PeerInfoScreenImpl, context: AccountContext, peerId: PeerId, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, nearbyPeerDistance: Int32?, reactionSourceMessageId: MessageId?, callMessages: [Message], isSettings: Bool, isMyProfile: Bool, hintGroupInCommon: PeerId?, requestsContext: PeerInvitationImportersContext?, profileGiftsContext: ProfileGiftsContext?, starsContext: StarsContext?, tonContext: StarsContext?, chatLocation: ChatLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>, switchToGiftsTarget: PeerInfoSwitchToGiftsTarget?, switchToStoryFolder: Int64?, initialPaneKey: PeerInfoPaneKey?, sharedMediaFromForumTopic: (EnginePeer.Id, Int64)?) {
|
||||
init(hidePhoneInSettings: Bool /* MARK: Swiftgram */, controller: PeerInfoScreenImpl, context: AccountContext, peerId: PeerId, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, nearbyPeerDistance: Int32?, reactionSourceMessageId: MessageId?, callMessages: [Message], isSettings: Bool, isMyProfile: Bool, hintGroupInCommon: PeerId?, requestsContext: PeerInvitationImportersContext?, profileGiftsContext: ProfileGiftsContext?, starsContext: StarsContext?, tonContext: StarsContext?, chatLocation: ChatLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>, switchToGiftsTarget: PeerInfoSwitchToGiftsTarget?, switchToStoryFolder: Int64?, switchToMediaTarget: PeerInfoSwitchToMediaTarget?, initialPaneKey: PeerInfoPaneKey?, sharedMediaFromForumTopic: (EnginePeer.Id, Int64)?) {
|
||||
self.controller = controller
|
||||
self.context = context
|
||||
self.peerId = peerId
|
||||
@@ -360,6 +377,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
self.isMediaOnly = context.account.peerId == peerId && !isSettings && !isMyProfile
|
||||
self.initialExpandPanes = initialPaneKey != nil
|
||||
self.switchToStoryFolder = switchToStoryFolder
|
||||
self.switchToMediaTarget = switchToMediaTarget
|
||||
self.switchToGiftsTarget = switchToGiftsTarget
|
||||
self.sharedMediaFromForumTopic = sharedMediaFromForumTopic
|
||||
|
||||
@@ -373,7 +391,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
if case let .replyThread(message) = chatLocation {
|
||||
forumTopicThreadId = message.threadId
|
||||
}
|
||||
self.headerNode = PeerInfoHeaderNode(context: context, controller: controller, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, isMediaOnly: self.isMediaOnly, isSettings: isSettings, isMyProfile: isMyProfile, forumTopicThreadId: forumTopicThreadId, chatLocation: self.chatLocation)
|
||||
self.headerNode = PeerInfoHeaderNode(hidePhoneInSettings: hidePhoneInSettings, context: context, controller: controller, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, isMediaOnly: self.isMediaOnly, isSettings: isSettings, isMyProfile: isMyProfile, forumTopicThreadId: forumTopicThreadId, chatLocation: self.chatLocation)
|
||||
|
||||
var switchToGiftCollection: Int64?
|
||||
switch switchToGiftsTarget {
|
||||
@@ -383,13 +401,17 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
break
|
||||
}
|
||||
|
||||
self.paneContainerNode = PeerInfoPaneContainerNode(context: context, updatedPresentationData: controller.updatedPresentationData, peerId: peerId, chatLocation: chatLocation, sharedMediaFromForumTopic: sharedMediaFromForumTopic, chatLocationContextHolder: chatLocationContextHolder, isMediaOnly: self.isMediaOnly, initialPaneKey: initialPaneKey, initialStoryFolderId: switchToStoryFolder, initialGiftCollectionId: switchToGiftCollection)
|
||||
self.paneContainerNode = PeerInfoPaneContainerNode(context: context, updatedPresentationData: controller.updatedPresentationData, peerId: peerId, chatLocation: chatLocation, sharedMediaFromForumTopic: sharedMediaFromForumTopic, chatLocationContextHolder: chatLocationContextHolder, isMediaOnly: self.isMediaOnly, initialPaneKey: initialPaneKey, initialStoryFolderId: switchToStoryFolder, initialGiftCollectionId: switchToGiftCollection, switchToMediaTarget: switchToMediaTarget)
|
||||
|
||||
super.init()
|
||||
|
||||
self.paneContainerNode.parentController = controller
|
||||
|
||||
self._interaction = PeerInfoInteraction(
|
||||
notifyTextCopied: { [weak self] in
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
self?.controller?.present(UndoOverlayController(presentationData: presentationData, content: .copy(text: presentationData.strings.Conversation_TextCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current)
|
||||
},
|
||||
openUsername: { [weak self] value, isMainUsername, progress in
|
||||
self?.openUsername(value: value, isMainUsername: isMainUsername, progress: progress)
|
||||
},
|
||||
@@ -851,7 +873,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
})))
|
||||
}
|
||||
|
||||
let controller = ContextController(presentationData: strongSelf.presentationData, source: .extracted(MessageContextExtractedContentSource(sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture)
|
||||
let controller = makeContextController(presentationData: strongSelf.presentationData, source: .extracted(MessageContextExtractedContentSource(sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture)
|
||||
strongSelf.controller?.window?.presentInGlobalOverlay(controller)
|
||||
})
|
||||
}, openMessageReactionContextMenu: { _, _, _, _ in
|
||||
@@ -1007,7 +1029,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
switch previewData {
|
||||
case let .gallery(gallery):
|
||||
gallery.setHintWillBePresentedInPreviewingContext(true)
|
||||
let contextController = ContextController(presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: gallery, sourceNode: node, sourceRect: rect)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture)
|
||||
let contextController = makeContextController(presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: gallery, sourceNode: node, sourceRect: rect)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture)
|
||||
strongSelf.controller?.presentInGlobalOverlay(contextController)
|
||||
case .instantPage:
|
||||
break
|
||||
@@ -1294,7 +1316,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
}))
|
||||
]
|
||||
}
|
||||
let contextController = ContextController(presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, passthroughTouches: true)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
|
||||
let contextController = makeContextController(presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, passthroughTouches: true)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
|
||||
controller.presentInGlobalOverlay(contextController)
|
||||
}
|
||||
|
||||
@@ -1590,35 +1612,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
strongSelf.controller?.push(controller)
|
||||
}
|
||||
} else {
|
||||
(strongSelf.controller?.parent as? TabBarController)?.updateIsTabBarHidden(true, transition: .animated(duration: 0.3, curve: .linear))
|
||||
strongSelf.state = strongSelf.state.withIsEditing(true)
|
||||
var updateOnCompletion = false
|
||||
if strongSelf.headerNode.isAvatarExpanded {
|
||||
updateOnCompletion = true
|
||||
strongSelf.headerNode.skipCollapseCompletion = true
|
||||
strongSelf.headerNode.avatarListNode.avatarContainerNode.canAttachVideo = false
|
||||
strongSelf.headerNode.editingContentNode.avatarNode.canAttachVideo = false
|
||||
strongSelf.headerNode.avatarListNode.listContainerNode.isCollapsing = true
|
||||
strongSelf.headerNode.updateIsAvatarExpanded(false, transition: .immediate)
|
||||
strongSelf.updateNavigationExpansionPresentation(isExpanded: false, animated: true)
|
||||
}
|
||||
if let (layout, navigationHeight) = strongSelf.validLayout {
|
||||
strongSelf.scrollNode.view.setContentOffset(CGPoint(), animated: false)
|
||||
strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false)
|
||||
}
|
||||
UIView.transition(with: strongSelf.view, duration: 0.3, options: [.transitionCrossDissolve], animations: {
|
||||
}, completion: { _ in
|
||||
if updateOnCompletion {
|
||||
strongSelf.headerNode.skipCollapseCompletion = false
|
||||
strongSelf.headerNode.avatarListNode.listContainerNode.isCollapsing = false
|
||||
strongSelf.headerNode.avatarListNode.avatarContainerNode.canAttachVideo = true
|
||||
strongSelf.headerNode.editingContentNode.avatarNode.canAttachVideo = true
|
||||
strongSelf.headerNode.editingContentNode.avatarNode.reset()
|
||||
if let (layout, navigationHeight) = strongSelf.validLayout {
|
||||
strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false)
|
||||
}
|
||||
}
|
||||
})
|
||||
strongSelf.activateEdit()
|
||||
}
|
||||
case .done, .cancel:
|
||||
strongSelf.view.endEditing(true)
|
||||
@@ -2003,7 +1997,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
UIView.transition(with: strongSelf.view, duration: 0.3, options: [.transitionCrossDissolve], animations: {
|
||||
}, completion: nil)
|
||||
}
|
||||
(strongSelf.controller?.parent as? TabBarController)?.updateIsTabBarHidden(false, transition: .animated(duration: 0.3, curve: .linear))
|
||||
(strongSelf.controller?.parent as? TabBarController)?.updateIsTabBarHidden(SGSimpleSettings.shared.hideTabBar ? true : false, transition: .animated(duration: 0.3, curve: .linear))
|
||||
case .select:
|
||||
strongSelf.state = strongSelf.state.withSelectedMessageIds(Set())
|
||||
if let (layout, navigationHeight) = strongSelf.validLayout {
|
||||
@@ -2060,6 +2054,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.underHeaderContentsAlpha = alpha
|
||||
if !self.state.isEditing {
|
||||
for (_, section) in self.regularSections {
|
||||
transition.updateAlpha(node: section, alpha: alpha)
|
||||
@@ -2094,9 +2089,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
|> map { data -> Bool in
|
||||
return data?.hasSecretValues ?? false
|
||||
}
|
||||
|
||||
self.cachedFaq.set(.single(nil) |> then(cachedFaqInstantPage(context: self.context) |> map(Optional.init)))
|
||||
|
||||
|
||||
screenData = peerInfoScreenSettingsData(context: context, peerId: peerId, accountsAndPeers: self.accountsAndPeers.get(), activeSessionsContextAndCount: self.activeSessionsContextAndCount.get(), notificationExceptions: self.notificationExceptions.get(), privacySettings: self.privacySettings.get(), archivedStickerPacks: self.archivedPacks.get(), hasPassport: hasPassport, starsContext: starsContext, tonContext: tonContext)
|
||||
|
||||
|
||||
@@ -2326,7 +2319,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
}, synchronousLoad: true)
|
||||
galleryController.setHintWillBePresentedInPreviewingContext(true)
|
||||
|
||||
let contextController = ContextController(presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: galleryController, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
|
||||
let contextController = makeContextController(presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: galleryController, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
|
||||
strongSelf.controller?.presentInGlobalOverlay(contextController)
|
||||
}
|
||||
|
||||
@@ -2456,14 +2449,29 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
self?.updateNavigation(transition: .immediate, additive: true, animateHeader: true)
|
||||
}
|
||||
|
||||
let nearestChatParticipantSignal = .single((nil, nil)) |> then(self.fetchNearestChatParticipant()) |> distinctUntilChanged { lhs, rhs in
|
||||
if lhs.0 != rhs.0 {
|
||||
return false
|
||||
}
|
||||
if lhs.1 != rhs.1 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
self.dataDisposable = combineLatest(
|
||||
queue: Queue.mainQueue(),
|
||||
nearestChatParticipantSignal,
|
||||
screenData,
|
||||
self.forceIsContactPromise.get()
|
||||
).startStrict(next: { [weak self] data, forceIsContact in
|
||||
).startStrict(next: { [weak self] nearestChatParticipant, data, forceIsContact in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
// MARK: Swiftgram
|
||||
strongSelf.showProfileId = SGSimpleSettings.shared.showProfileId
|
||||
//
|
||||
strongSelf.nearestChatParticipant = nearestChatParticipant
|
||||
if data.isContact && forceIsContact {
|
||||
strongSelf.forceIsContactPromise.set(false)
|
||||
} else {
|
||||
@@ -2645,6 +2653,38 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
|
||||
var canAttachVideo: Bool?
|
||||
|
||||
func activateEdit() {
|
||||
(self.controller?.parent as? TabBarController)?.updateIsTabBarHidden(true, transition: .animated(duration: 0.3, curve: .linear))
|
||||
self.state = self.state.withIsEditing(true)
|
||||
var updateOnCompletion = false
|
||||
if self.headerNode.isAvatarExpanded {
|
||||
updateOnCompletion = true
|
||||
self.headerNode.skipCollapseCompletion = true
|
||||
self.headerNode.avatarListNode.avatarContainerNode.canAttachVideo = false
|
||||
self.headerNode.editingContentNode.avatarNode.canAttachVideo = false
|
||||
self.headerNode.avatarListNode.listContainerNode.isCollapsing = true
|
||||
self.headerNode.updateIsAvatarExpanded(false, transition: .immediate)
|
||||
self.updateNavigationExpansionPresentation(isExpanded: false, animated: true)
|
||||
}
|
||||
if let (layout, navigationHeight) = self.validLayout {
|
||||
self.scrollNode.view.setContentOffset(CGPoint(), animated: false)
|
||||
self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false)
|
||||
}
|
||||
UIView.transition(with: self.view, duration: 0.3, options: [.transitionCrossDissolve], animations: {
|
||||
}, completion: { _ in
|
||||
if updateOnCompletion {
|
||||
self.headerNode.skipCollapseCompletion = false
|
||||
self.headerNode.avatarListNode.listContainerNode.isCollapsing = false
|
||||
self.headerNode.avatarListNode.avatarContainerNode.canAttachVideo = true
|
||||
self.headerNode.editingContentNode.avatarNode.canAttachVideo = true
|
||||
self.headerNode.editingContentNode.avatarNode.reset()
|
||||
if let (layout, navigationHeight) = self.validLayout {
|
||||
self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func updateData(_ data: PeerInfoScreenData) {
|
||||
let previousData = self.data
|
||||
var previousMemberCount: Int?
|
||||
@@ -2820,6 +2860,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
self.ignoreScrolling = true
|
||||
transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(x: 0.0, y: paneAreaExpansionFinalPoint), size: self.scrollNode.bounds.size))
|
||||
self.ignoreScrolling = false
|
||||
self.headerNode.headerEdgeEffectContainer.center = CGPoint(x: 0.0, y: self.scrollNode.view.contentOffset.y)
|
||||
self.updateNavigation(transition: transition, additive: false, animateHeader: true)
|
||||
if let (layout, navigationHeight) = self.validLayout {
|
||||
self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: transition, additive: true)
|
||||
@@ -4111,7 +4152,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
}
|
||||
}
|
||||
|
||||
private func openParticipantsSection(section: PeerInfoParticipantsSection) {
|
||||
public func openParticipantsSection(section: PeerInfoParticipantsSection) { // MARK: Swiftgram
|
||||
guard let data = self.data, let peer = data.peer else {
|
||||
return
|
||||
}
|
||||
@@ -4660,7 +4701,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
}
|
||||
|
||||
if let rootController = self.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface {
|
||||
let coordinator = rootController.openStoryCamera(customTarget: self.peerId == self.context.account.peerId ? nil : .peer(self.peerId), resumeLiveStream: false, transitionIn: cameraTransitionIn, transitionedIn: {}, transitionOut: self.storyCameraTransitionOut())
|
||||
let coordinator = rootController.openStoryCamera(mode: .photo, customTarget: self.peerId == self.context.account.peerId ? nil : .peer(self.peerId), resumeLiveStream: false, transitionIn: cameraTransitionIn, transitionedIn: {}, transitionOut: self.storyCameraTransitionOut())
|
||||
coordinator?.animateIn()
|
||||
}
|
||||
case .channelBoostRequired:
|
||||
@@ -4799,35 +4840,57 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
self.headerNode.navigationButtonContainer.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue)
|
||||
|
||||
if self.isSettings {
|
||||
self.setupFaqIfNeeded()
|
||||
|
||||
if let settings = self.data?.globalSettings {
|
||||
self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, mode: .navigation, placeholder: self.presentationData.strings.Settings_Search, hasBackground: true, hasSeparator: true, contentNode: SettingsSearchContainerNode(context: self.context, openResult: { [weak self] result in
|
||||
if let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController {
|
||||
result.present(strongSelf.context, navigationController, { [weak self] mode, controller in
|
||||
if let strongSelf = self {
|
||||
switch mode {
|
||||
case .push:
|
||||
if let controller = controller {
|
||||
strongSelf.controller?.push(controller)
|
||||
self.searchDisplayController = SearchDisplayController(
|
||||
presentationData: self.presentationData,
|
||||
mode: .navigation,
|
||||
placeholder: self.presentationData.strings.Settings_Search,
|
||||
hasBackground: true,
|
||||
hasSeparator: true,
|
||||
contentNode: SettingsSearchContainerNode(
|
||||
context: self.context,
|
||||
openResult: { [weak self] result in
|
||||
if let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController {
|
||||
result.present(strongSelf.context, navigationController, { [weak self] mode, controller in
|
||||
if let strongSelf = self {
|
||||
switch mode {
|
||||
case .push:
|
||||
if let controller = controller {
|
||||
strongSelf.controller?.push(controller)
|
||||
}
|
||||
case .modal:
|
||||
if let controller = controller {
|
||||
strongSelf.controller?.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet, completion: { [weak self] in
|
||||
self?.deactivateSearch()
|
||||
}))
|
||||
}
|
||||
case .immediate:
|
||||
if let controller = controller {
|
||||
strongSelf.controller?.present(controller, in: .window(.root), with: nil)
|
||||
}
|
||||
case .dismiss:
|
||||
strongSelf.deactivateSearch()
|
||||
}
|
||||
case .modal:
|
||||
if let controller = controller {
|
||||
strongSelf.controller?.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet, completion: { [weak self] in
|
||||
self?.deactivateSearch()
|
||||
}))
|
||||
}
|
||||
case .immediate:
|
||||
if let controller = controller {
|
||||
strongSelf.controller?.present(controller, in: .window(.root), with: nil)
|
||||
}
|
||||
case .dismiss:
|
||||
strongSelf.deactivateSearch()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}, resolvedFaqUrl: self.cachedFaq.get(), exceptionsList: .single(settings.notificationExceptions), archivedStickerPacks: .single(settings.archivedStickerPacks), privacySettings: .single(settings.privacySettings), hasTwoStepAuth: self.hasTwoStepAuth.get(), twoStepAuthData: self.twoStepAccessConfiguration.get(), activeSessionsContext: self.activeSessionsContextAndCount.get() |> map { $0?.0 }, webSessionsContext: self.activeSessionsContextAndCount.get() |> map { $0?.2 }), cancel: { [weak self] in
|
||||
self?.deactivateSearch()
|
||||
}, searchBarIsExternal: true)
|
||||
},
|
||||
resolvedFaqUrl: self.cachedFaq.get(),
|
||||
exceptionsList: .single(settings.notificationExceptions),
|
||||
archivedStickerPacks: .single(settings.archivedStickerPacks),
|
||||
privacySettings: .single(settings.privacySettings),
|
||||
hasTwoStepAuth: self.hasTwoStepAuth.get(),
|
||||
twoStepAuthData: self.twoStepAccessConfiguration.get(),
|
||||
activeSessionsContext: self.activeSessionsContextAndCount.get() |> map { $0?.0 },
|
||||
webSessionsContext: self.activeSessionsContextAndCount.get() |> map { $0?.2 }
|
||||
),
|
||||
cancel: { [weak self] in
|
||||
self?.deactivateSearch()
|
||||
},
|
||||
searchBarIsExternal: true
|
||||
)
|
||||
}
|
||||
} else if let currentPaneKey = self.paneContainerNode.currentPaneKey, case .members = currentPaneKey {
|
||||
self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, mode: .navigation, placeholder: self.presentationData.strings.Common_Search, hasBackground: true, hasSeparator: true, contentNode: ChannelMembersSearchContainerNode(context: self.context, forceTheme: nil, peerId: self.peerId, mode: .searchMembers, filters: [], searchContext: self.groupMembersSearchContext, openPeer: { [weak self] peer, participant in
|
||||
@@ -4971,8 +5034,10 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
self.searchDisplayController = nil
|
||||
searchDisplayController.deactivate(placeholder: nil)
|
||||
|
||||
controller.dismissAllTooltips()
|
||||
|
||||
if self.isSettings {
|
||||
(self.controller?.parent as? TabBarController)?.updateIsTabBarHidden(false, transition: .animated(duration: 0.4, curve: .spring))
|
||||
(self.controller?.parent as? TabBarController)?.updateIsTabBarHidden(SGSimpleSettings.shared.hideTabBar ? true : false, transition: .animated(duration: 0.4, curve: .spring))
|
||||
controller.updateTabBarSearchState(ViewController.TabBarSearchState(isActive: false), transition: .animated(duration: 0.4, curve: .spring))
|
||||
}
|
||||
|
||||
@@ -5076,7 +5141,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
|
||||
let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: strongSelf.peerId), subject: .message(id: .id(index.id), highlight: nil, timecode: nil, setupReply: false), botStart: nil, mode: .standard(.previewing), params: nil)
|
||||
chatController.canReadHistory.set(false)
|
||||
let contextController = ContextController(presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: sourceNode, sourceRect: sourceRect, passthroughTouches: true)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
|
||||
let contextController = makeContextController(presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: sourceNode, sourceRect: sourceRect, passthroughTouches: true)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
|
||||
strongSelf.controller?.presentInGlobalOverlay(contextController)
|
||||
}
|
||||
)
|
||||
@@ -5268,7 +5333,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
insets.left += sectionInset
|
||||
insets.right += sectionInset
|
||||
|
||||
let items = self.isSettings ? settingsItems(data: self.data, context: self.context, presentationData: self.presentationData, interaction: self.interaction, isExpanded: self.headerNode.isAvatarExpanded) : infoItems(data: self.data, context: self.context, presentationData: self.presentationData, interaction: self.interaction, nearbyPeerDistance: self.nearbyPeerDistance, reactionSourceMessageId: self.reactionSourceMessageId, callMessages: self.callMessages, chatLocation: self.chatLocation, isOpenedFromChat: self.isOpenedFromChat, isMyProfile: self.isMyProfile)
|
||||
let items = self.isSettings ? settingsItems(showProfileId: self.showProfileId, data: self.data, context: self.context, presentationData: self.presentationData, interaction: self.interaction, isExpanded: self.headerNode.isAvatarExpanded) : infoItems(nearestChatParticipant: self.nearestChatParticipant, showProfileId: self.showProfileId, data: self.data, context: self.context, presentationData: self.presentationData, interaction: self.interaction, nearbyPeerDistance: self.nearbyPeerDistance, reactionSourceMessageId: self.reactionSourceMessageId, callMessages: self.callMessages, chatLocation: self.chatLocation, isOpenedFromChat: self.isOpenedFromChat, isMyProfile: self.isMyProfile)
|
||||
|
||||
contentHeight += headerHeight
|
||||
if !((self.isSettings || self.isMyProfile) && self.state.isEditing) {
|
||||
@@ -5294,7 +5359,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
|
||||
if wasAdded && transition.isAnimated && (self.isSettings || self.isMyProfile) && !self.state.isEditing {
|
||||
sectionNode.alpha = 0.0
|
||||
transition.updateAlpha(node: sectionNode, alpha: 1.0, delay: 0.1)
|
||||
transition.updateAlpha(node: sectionNode, alpha: self.underHeaderContentsAlpha, delay: 0.1)
|
||||
}
|
||||
|
||||
let sectionWidth = layout.size.width - insets.left - insets.right
|
||||
@@ -5313,7 +5378,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
|
||||
if wasAdded && transition.isAnimated && (self.isSettings || self.isMyProfile) && !self.state.isEditing {
|
||||
} else {
|
||||
transition.updateAlpha(node: sectionNode, alpha: self.state.isEditing ? 0.0 : 1.0)
|
||||
transition.updateAlpha(node: sectionNode, alpha: self.state.isEditing ? 0.0 : self.underHeaderContentsAlpha)
|
||||
}
|
||||
if !sectionHeight.isZero && !self.state.isEditing {
|
||||
contentHeight += sectionHeight
|
||||
@@ -5689,7 +5754,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
|
||||
let navigationBarHeight: CGFloat = !self.isSettings && layout.isModalOverlay ? 68.0 : 60.0
|
||||
let paneContainerTopInset = navigationBarHeight + (layout.statusBarHeight ?? 0.0)
|
||||
self.paneContainerNode.update(size: self.paneContainerNode.bounds.size, sideInset: layout.safeInsets.left, topInset: paneContainerTopInset, bottomInset: bottomInset, deviceMetrics: layout.deviceMetrics, visibleHeight: visibleHeight, expansionFraction: effectiveAreaExpansionFraction, presentationData: self.presentationData, data: self.data, areTabsHidden: self.headerNode.customNavigationContentNode != nil, disableTabSwitching: disableTabSwitching, navigationHeight: navigationHeight, transition: transition)
|
||||
self.paneContainerNode.update(size: self.paneContainerNode.bounds.size, sideInset: layout.safeInsets.left, topInset: paneContainerTopInset, bottomInset: bottomInset, deviceMetrics: layout.deviceMetrics, visibleHeight: visibleHeight, expansionFraction: self.initialExpandPanes ? 1.0 : effectiveAreaExpansionFraction, presentationData: self.presentationData, data: self.data, areTabsHidden: self.headerNode.customNavigationContentNode != nil, disableTabSwitching: disableTabSwitching, navigationHeight: navigationHeight, transition: transition)
|
||||
|
||||
transition.updateFrame(node: self.headerNode.navigationButtonContainer, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left, y: layout.statusBarHeight ?? 0.0), size: CGSize(width: layout.size.width - layout.safeInsets.left * 2.0, height: navigationBarHeight)))
|
||||
var searchBarContainerY: CGFloat = layout.statusBarHeight ?? 0.0
|
||||
@@ -5705,6 +5770,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
} else {
|
||||
if self.isSettings {
|
||||
leftNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .qrCode, isForExpandedView: false))
|
||||
if SGSimpleSettings.shared.hideTabBar { leftNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .back, isForExpandedView: false)) }
|
||||
rightNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .edit, isForExpandedView: false))
|
||||
} else if self.isMyProfile {
|
||||
rightNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .edit, isForExpandedView: false))
|
||||
@@ -6159,6 +6225,21 @@ public enum PeerInfoSwitchToGiftsTarget {
|
||||
case collection(Int64)
|
||||
}
|
||||
|
||||
public struct PeerInfoSwitchToMediaTarget {
|
||||
public enum Kind {
|
||||
case photoVideo
|
||||
case file
|
||||
}
|
||||
|
||||
public let kind: Kind
|
||||
public let messageIndex: EngineMessage.Index
|
||||
|
||||
public init(kind: Kind, messageIndex: EngineMessage.Index) {
|
||||
self.kind = kind
|
||||
self.messageIndex = messageIndex
|
||||
}
|
||||
}
|
||||
|
||||
public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortcutResponder {
|
||||
let context: AccountContext
|
||||
let updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?
|
||||
@@ -6179,6 +6260,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc
|
||||
private let switchToGiftsTarget: PeerInfoSwitchToGiftsTarget?
|
||||
private let switchToGroupsInCommon: Bool
|
||||
private let switchToStoryFolder: Int64?
|
||||
private let switchToMediaTarget: PeerInfoSwitchToMediaTarget?
|
||||
private let sharedMediaFromForumTopic: (EnginePeer.Id, Int64)?
|
||||
let chatLocation: ChatLocation
|
||||
private let chatLocationContextHolder = Atomic<ChatLocationContextHolder?>(value: nil)
|
||||
@@ -6199,6 +6281,8 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc
|
||||
|
||||
var avatarPickerHolder: Any?
|
||||
|
||||
private let hidePhoneInSettings: Bool
|
||||
|
||||
var controllerNode: PeerInfoScreenNode {
|
||||
return self.displayNode as! PeerInfoScreenNode
|
||||
}
|
||||
@@ -6214,6 +6298,10 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc
|
||||
return self.controllerNode.privacySettings
|
||||
}
|
||||
|
||||
public var twoStepAuthData: Promise<TwoStepAuthData?> {
|
||||
return self.controllerNode.twoStepAuthData
|
||||
}
|
||||
|
||||
override public var customNavigationData: CustomViewControllerNavigationData? {
|
||||
get {
|
||||
if !self.isSettings {
|
||||
@@ -6237,8 +6325,9 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc
|
||||
var didAppear: Bool = false
|
||||
|
||||
private var validLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)?
|
||||
|
||||
|
||||
public init(
|
||||
hidePhoneInSettings: Bool = SGSimpleSettings.defaultValues[SGSimpleSettings.Keys.hidePhoneInSettings.rawValue] as! Bool,
|
||||
context: AccountContext,
|
||||
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?,
|
||||
peerId: PeerId,
|
||||
@@ -6258,8 +6347,10 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc
|
||||
switchToGiftsTarget: PeerInfoSwitchToGiftsTarget? = nil,
|
||||
switchToGroupsInCommon: Bool = false,
|
||||
switchToStoryFolder: Int64? = nil,
|
||||
switchToMediaTarget: PeerInfoSwitchToMediaTarget? = nil,
|
||||
) {
|
||||
self.context = context
|
||||
self.hidePhoneInSettings = hidePhoneInSettings
|
||||
self.updatedPresentationData = updatedPresentationData
|
||||
self.peerId = peerId
|
||||
self.avatarInitiallyExpanded = avatarInitiallyExpanded
|
||||
@@ -6276,6 +6367,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc
|
||||
self.switchToGiftsTarget = switchToGiftsTarget
|
||||
self.switchToGroupsInCommon = switchToGroupsInCommon
|
||||
self.switchToStoryFolder = switchToStoryFolder
|
||||
self.switchToMediaTarget = switchToMediaTarget
|
||||
self.sharedMediaFromForumTopic = sharedMediaFromForumTopic
|
||||
|
||||
if let forumTopicThread = forumTopicThread {
|
||||
@@ -6621,8 +6713,15 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc
|
||||
initialPaneKey = .groupsInCommon
|
||||
} else if self.switchToStoryFolder != nil {
|
||||
initialPaneKey = .stories
|
||||
} else if let switchToMediaTarget = self.switchToMediaTarget {
|
||||
switch switchToMediaTarget.kind {
|
||||
case .photoVideo:
|
||||
initialPaneKey = .media
|
||||
case .file:
|
||||
initialPaneKey = .files
|
||||
}
|
||||
}
|
||||
self.displayNode = PeerInfoScreenNode(controller: self, context: self.context, peerId: self.peerId, avatarInitiallyExpanded: self.avatarInitiallyExpanded, isOpenedFromChat: self.isOpenedFromChat, nearbyPeerDistance: self.nearbyPeerDistance, reactionSourceMessageId: self.reactionSourceMessageId, callMessages: self.callMessages, isSettings: self.isSettings, isMyProfile: self.isMyProfile, hintGroupInCommon: self.hintGroupInCommon, requestsContext: self.requestsContext, profileGiftsContext: self.profileGiftsContext, starsContext: self.starsContext, tonContext: self.tonContext, chatLocation: self.chatLocation, chatLocationContextHolder: self.chatLocationContextHolder, switchToGiftsTarget: self.switchToGiftsTarget, switchToStoryFolder: self.switchToStoryFolder, initialPaneKey: initialPaneKey, sharedMediaFromForumTopic: self.sharedMediaFromForumTopic)
|
||||
self.displayNode = PeerInfoScreenNode(hidePhoneInSettings: self.hidePhoneInSettings, controller: self, context: self.context, peerId: self.peerId, avatarInitiallyExpanded: self.avatarInitiallyExpanded, isOpenedFromChat: self.isOpenedFromChat, nearbyPeerDistance: self.nearbyPeerDistance, reactionSourceMessageId: self.reactionSourceMessageId, callMessages: self.callMessages, isSettings: self.isSettings, isMyProfile: self.isMyProfile, hintGroupInCommon: self.hintGroupInCommon, requestsContext: self.requestsContext, profileGiftsContext: self.profileGiftsContext, starsContext: self.starsContext, tonContext: self.tonContext, chatLocation: self.chatLocation, chatLocationContextHolder: self.chatLocationContextHolder, switchToGiftsTarget: self.switchToGiftsTarget, switchToStoryFolder: self.switchToStoryFolder, switchToMediaTarget: self.switchToMediaTarget, initialPaneKey: initialPaneKey, sharedMediaFromForumTopic: self.sharedMediaFromForumTopic)
|
||||
self.controllerNode.accountsAndPeers.set(self.accountsAndPeers.get() |> map { $0.1 })
|
||||
self.controllerNode.activeSessionsContextAndCount.set(self.activeSessionsContextAndCount.get())
|
||||
self.cachedDataPromise.set(self.controllerNode.cachedDataPromise.get())
|
||||
@@ -6656,7 +6755,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc
|
||||
}
|
||||
}
|
||||
|
||||
private func dismissAllTooltips() {
|
||||
fileprivate func dismissAllTooltips() {
|
||||
self.window?.forEachController({ controller in
|
||||
if let controller = controller as? UndoOverlayController, !controller.keepOnParentDismissal {
|
||||
controller.dismissWithCommitAction()
|
||||
@@ -6687,6 +6786,10 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc
|
||||
}
|
||||
}
|
||||
|
||||
public func activateEdit() {
|
||||
self.controllerNode.activateEdit()
|
||||
}
|
||||
|
||||
public func openAvatarSetup(completedWithUploadingImage: @escaping (UIImage, Signal<PeerInfoAvatarUploadStatus, NoError>) -> UIView?) {
|
||||
let proceed = { [weak self] in
|
||||
self?.openAvatarForEditing(completedWithUploadingImage: completedWithUploadingImage)
|
||||
@@ -6828,7 +6931,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc
|
||||
}))
|
||||
})))
|
||||
}
|
||||
let contextController = ContextController(presentationData: presentationData, source: .reference(PeerInfoControllerContextReferenceContentSource(controller: parentController, sourceView: backButtonView, insets: UIEdgeInsets(), contentInsets: UIEdgeInsets(top: 0.0, left: -15.0, bottom: 0.0, right: -15.0))), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
|
||||
let contextController = makeContextController(presentationData: presentationData, source: .reference(PeerInfoControllerContextReferenceContentSource(controller: parentController, sourceView: backButtonView, insets: UIEdgeInsets(), contentInsets: UIEdgeInsets(top: 0.0, left: -15.0, bottom: 0.0, right: -15.0))), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
|
||||
parentController.presentInGlobalOverlay(contextController)
|
||||
})
|
||||
}
|
||||
@@ -6867,6 +6970,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc
|
||||
}
|
||||
|
||||
self.controllerNode.refreshHasPersonalChannelsIfNeeded()
|
||||
self.controllerNode.initialExpandPanes = false
|
||||
}
|
||||
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
@@ -6886,6 +6990,22 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc
|
||||
let strings = self.presentationData.strings
|
||||
|
||||
var items: [ContextMenuItem] = []
|
||||
|
||||
// MARK: Swiftgram
|
||||
#if DEBUG
|
||||
items.append(.action(ContextMenuActionItem(text: "Swiftgram Debug", icon: { theme in
|
||||
return generateTintedImage(image: nil, color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] _, f in
|
||||
guard let self = self else {
|
||||
return
|
||||
}
|
||||
self.push(sgDebugController(context: self.context))
|
||||
|
||||
f(.dismissWithoutContent)
|
||||
})))
|
||||
#endif
|
||||
//
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: strings.Settings_AddAccount, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] _, f in
|
||||
@@ -6929,7 +7049,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc
|
||||
})))*/
|
||||
}
|
||||
|
||||
let controller = ContextController(presentationData: self.presentationData, source: .reference(SettingsTabBarContextReferenceContentSource(controller: self, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture)
|
||||
let controller = makeContextController(presentationData: self.presentationData, source: .reference(SettingsTabBarContextReferenceContentSource(controller: self, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture)
|
||||
self.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller)
|
||||
}
|
||||
|
||||
@@ -6964,6 +7084,10 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc
|
||||
}
|
||||
}
|
||||
|
||||
public func openEmojiStatusSetup() {
|
||||
self.controllerNode.openSettings(section: .emojiStatus)
|
||||
}
|
||||
|
||||
public func openBirthdaySetup() {
|
||||
self.controllerNode.interaction.updateIsEditingBirthdate(true)
|
||||
self.controllerNode.headerNode.navigationButtonContainer.performAction?(.edit, nil, nil)
|
||||
@@ -7044,7 +7168,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc
|
||||
})))
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let contextController = ContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: sourceController, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
|
||||
let contextController = makeContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: sourceController, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
|
||||
sourceController.presentInGlobalOverlay(contextController)
|
||||
})
|
||||
}
|
||||
@@ -7178,15 +7302,20 @@ final class PeerInfoContextExtractedContentSource: ContextExtractedContentSource
|
||||
|
||||
final class PeerInfoContextReferenceContentSource: ContextReferenceContentSource {
|
||||
private let controller: ViewController
|
||||
private let sourceNode: ContextReferenceContentNode
|
||||
private let sourceView: UIView
|
||||
|
||||
init(controller: ViewController, sourceNode: ContextReferenceContentNode) {
|
||||
init(controller: ViewController, sourceNode: ASDisplayNode) {
|
||||
self.controller = controller
|
||||
self.sourceNode = sourceNode
|
||||
self.sourceView = sourceNode.view
|
||||
}
|
||||
|
||||
init(controller: ViewController, sourceView: UIView) {
|
||||
self.controller = controller
|
||||
self.sourceView = sourceView
|
||||
}
|
||||
|
||||
func transitionInfo() -> ContextControllerReferenceViewInfo? {
|
||||
return ContextControllerReferenceViewInfo(referenceView: self.sourceNode.view, contentAreaInScreenSpace: UIScreen.main.bounds)
|
||||
return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7259,3 +7388,93 @@ struct ClearPeerHistory {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// MARK: Swiftgram
|
||||
extension PeerInfoScreenImpl {
|
||||
|
||||
public func tabBarItemContextActionRawUIView(sourceView: UIView, gesture: ContextGesture?) {
|
||||
guard let (maybePrimary, other) = self.accountsAndPeersValue, let primary = maybePrimary else {
|
||||
return
|
||||
}
|
||||
|
||||
let strings = self.presentationData.strings
|
||||
|
||||
var items: [ContextMenuItem] = []
|
||||
|
||||
// MARK: Swiftgram
|
||||
#if DEBUG
|
||||
items.append(.action(ContextMenuActionItem(text: "Swiftgram Debug", icon: { theme in
|
||||
return generateTintedImage(image: nil, color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] _, f in
|
||||
guard let self = self else {
|
||||
return
|
||||
}
|
||||
self.push(sgDebugController(context: self.context))
|
||||
|
||||
f(.dismissWithoutContent)
|
||||
})))
|
||||
#endif
|
||||
//
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: strings.Settings_AddAccount, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] _, f in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.controllerNode.openSettings(section: .addAccount)
|
||||
f(.dismissWithoutContent)
|
||||
})))
|
||||
|
||||
|
||||
items.append(.custom(AccountPeerContextItem(context: self.context, account: self.context.account, peer: primary.1, action: { _, f in
|
||||
f(.default)
|
||||
}), true))
|
||||
|
||||
if !other.isEmpty {
|
||||
items.append(.separator)
|
||||
}
|
||||
|
||||
for account in other {
|
||||
let id = account.0.account.id
|
||||
items.append(.custom(AccountPeerContextItem(context: self.context, account: account.0.account, peer: account.1, action: { [weak self] _, f in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.controllerNode.switchToAccount(id: id)
|
||||
f(.dismissWithoutContent)
|
||||
}), true))
|
||||
}
|
||||
|
||||
let controller = makeContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: self, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
|
||||
self.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller)
|
||||
}
|
||||
}
|
||||
|
||||
extension PeerInfoScreenNode {
|
||||
|
||||
public func fetchNearestChatParticipant() -> Signal<(String?, Int32?), NoError> {
|
||||
guard let navigationController = self.controller?.navigationController as? NavigationController else {
|
||||
return .single((nil, nil))
|
||||
}
|
||||
|
||||
for controller in navigationController.viewControllers.reversed() {
|
||||
if let chatController = controller as? ChatController, let chatPeerId = chatController.chatLocation.peerId, [Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel].contains(chatPeerId.namespace) {
|
||||
return self.context.engine.peers.fetchChannelParticipant(peerId: chatPeerId, participantId: self.peerId)
|
||||
|> mapToSignal { participant -> Signal<(String?, Int32?), NoError> in
|
||||
if let participant = participant, case let .member(_, invitedAt, _, _, _, _) = participant {
|
||||
return .single((chatController.overlayTitle, invitedAt))
|
||||
} else {
|
||||
return .single((nil, nil))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
return .single((nil, nil))
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -463,7 +463,7 @@ extension PeerInfoScreenImpl {
|
||||
case .accept:
|
||||
(strongSelf.parentController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .image(image: image, title: strongSelf.presentationData.strings.Conversation_SuggestedPhotoSuccess, text: strongSelf.presentationData.strings.Conversation_SuggestedPhotoSuccessText, round: true, undoText: nil), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] action in
|
||||
if case .info = action {
|
||||
self?.parentController?.openSettings()
|
||||
self?.parentController?.openSettings(edit: false)
|
||||
}
|
||||
return false
|
||||
}), in: .current)
|
||||
@@ -662,7 +662,7 @@ extension PeerInfoScreenImpl {
|
||||
case .accept:
|
||||
(strongSelf.parentController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .image(image: image, title: strongSelf.presentationData.strings.Conversation_SuggestedVideoSuccess, text: strongSelf.presentationData.strings.Conversation_SuggestedVideoSuccessText, round: true, undoText: nil), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] action in
|
||||
if case .info = action {
|
||||
self?.parentController?.openSettings()
|
||||
self?.parentController?.openSettings(edit: false)
|
||||
}
|
||||
return false
|
||||
}), in: .current)
|
||||
@@ -874,7 +874,7 @@ extension PeerInfoScreenImpl {
|
||||
case .accept:
|
||||
(strongSelf.parentController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .image(image: image, title: strongSelf.presentationData.strings.Conversation_SuggestedVideoSuccess, text: strongSelf.presentationData.strings.Conversation_SuggestedVideoSuccessText, round: true, undoText: nil), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] action in
|
||||
if case .info = action {
|
||||
self?.parentController?.openSettings()
|
||||
self?.parentController?.openSettings(edit: false)
|
||||
}
|
||||
return false
|
||||
}), in: .current)
|
||||
|
||||
+33
@@ -18,6 +18,39 @@ extension PeerInfoScreenNode {
|
||||
}
|
||||
let context = self.context
|
||||
switch subject {
|
||||
case let .copy(text):
|
||||
let contextMenuController = makeContextMenuController(actions: [ContextMenuAction(content: .text(title: self.presentationData.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.presentationData.strings.Conversation_ContextMenuCopy), action: { [weak self] in
|
||||
UIPasteboard.general.string = text
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
self?.controller?.present(UndoOverlayController(presentationData: presentationData, content: .copy(text: presentationData.strings.Conversation_TextCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current)
|
||||
})])
|
||||
controller.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self, weak sourceNode] in
|
||||
if let controller = self?.controller, let sourceNode = sourceNode {
|
||||
var rect = sourceNode.bounds.insetBy(dx: 0.0, dy: 2.0)
|
||||
if let sourceRect = sourceRect {
|
||||
rect = sourceRect.insetBy(dx: 0.0, dy: 2.0)
|
||||
}
|
||||
return (sourceNode, rect, controller.displayNode, controller.view.bounds)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}))
|
||||
case .aboutDC:
|
||||
let contextMenuController = makeContextMenuController(actions: [ContextMenuAction(content: .text(title: self.presentationData.strings.Passport_InfoLearnMore, accessibilityLabel: self.presentationData.strings.Passport_InfoLearnMore), action: { [weak self] in
|
||||
self?.openUrl(url: "https://core.telegram.org/api/datacenter", concealed: false, external: false)
|
||||
})])
|
||||
controller.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self, weak sourceNode] in
|
||||
if let controller = self?.controller, let sourceNode = sourceNode {
|
||||
var rect = sourceNode.bounds.insetBy(dx: 0.0, dy: 2.0)
|
||||
if let sourceRect = sourceRect {
|
||||
rect = sourceRect.insetBy(dx: 0.0, dy: 2.0)
|
||||
}
|
||||
return (sourceNode, rect, controller.displayNode, controller.view.bounds)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}))
|
||||
case .birthday:
|
||||
if let cachedData = data.cachedData as? CachedUserData, let birthday = cachedData.birthday {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user