Update Ghostgram features

This commit is contained in:
ichmagmaus 812
2026-03-07 18:15:32 +01:00
parent 1a3303b059
commit 24a7ec39d9
902 changed files with 148302 additions and 62355 deletions
+40 -4
View File
@@ -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",
],
)
@@ -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)
}
}
@@ -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)
}
}
@@ -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: [],
@@ -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",
@@ -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
@@ -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",
@@ -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
}
@@ -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,
@@ -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) {
@@ -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",
@@ -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)
}
}
@@ -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",
@@ -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",
@@ -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()
@@ -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) {
@@ -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,
@@ -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,
@@ -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,
@@ -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)
}
}
@@ -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: [],
@@ -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
}
@@ -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,
@@ -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
@@ -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
}
}
}))
@@ -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
@@ -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,
@@ -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
}
}
@@ -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)
@@ -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)
@@ -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
@@ -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,
@@ -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)
@@ -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",
],
)
@@ -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
}
}
@@ -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)
}
}
@@ -0,0 +1,6 @@
import ContextUI
public typealias ContextControllerImpl = ContextController
public typealias ContextControllerActionsStackNodeImpl = ContextControllerActionsStackNode
public typealias PeekControllerImpl = PeekController
public typealias PinchControllerImpl = PinchController
@@ -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)
}
@@ -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)
}
}
@@ -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)
}
}
@@ -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",
@@ -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,
@@ -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",
],
)
@@ -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
}
}
@@ -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
)
}
@@ -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)
}
}
}
@@ -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()
}
}
}
@@ -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())
@@ -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)
})
}
@@ -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
@@ -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()
@@ -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),
@@ -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
@@ -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
@@ -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 = ""
@@ -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
}
}
@@ -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)
@@ -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 {
@@ -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",
@@ -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",
@@ -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",
@@ -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",
],
)
@@ -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)
})
}
}
@@ -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",
@@ -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 }
}
@@ -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)
}
}
@@ -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
@@ -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
@@ -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 {
@@ -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))
}
}
@@ -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)
@@ -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