mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 13:35:19 +00:00
Various improvements
This commit is contained in:
parent
8ec6964dfe
commit
37c91f89c5
@ -13218,3 +13218,6 @@ Sorry for the inconvenience.";
|
||||
"Gift.Convert.Period.Unavailable.Text" = "Sorry, you can't convert this gift.\n\nStars can only be claimed within %@ after receiving a gift.";
|
||||
"Gift.Convert.Period.Unavailable.Days_1" = "%@ day";
|
||||
"Gift.Convert.Period.Unavailable.Days_any" = "%@ days";
|
||||
|
||||
"Gift.Send.TitleTo" = "Gift to %@";
|
||||
"Gift.Send.SendShort" = "Send";
|
||||
|
@ -524,7 +524,7 @@ final class AttachmentContainer: ASDisplayNode, ASGestureRecognizerDelegate {
|
||||
}
|
||||
|
||||
var containerTopInset: CGFloat
|
||||
if isLandscape {
|
||||
if isLandscape || controllers.last?.isFullscreen == true {
|
||||
containerTopInset = 0.0
|
||||
containerLayout = layout
|
||||
|
||||
|
@ -124,7 +124,7 @@ public protocol AttachmentContainable: ViewController, MinimizableController {
|
||||
var isInnerPanGestureEnabled: (() -> Bool)? { get }
|
||||
var mediaPickerContext: AttachmentMediaPickerContext? { get }
|
||||
var getCurrentSendMessageContextMediaPreview: (() -> ChatSendMessageContextScreenMediaPreview?)? { get }
|
||||
|
||||
|
||||
func isContainerPanningUpdated(_ panning: Bool)
|
||||
|
||||
func resetForReuse()
|
||||
@ -165,6 +165,10 @@ public extension AttachmentContainable {
|
||||
return nil
|
||||
}
|
||||
|
||||
var isFullscreen: Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
var minimizedTopEdgeOffset: CGFloat? {
|
||||
return nil
|
||||
}
|
||||
@ -363,6 +367,10 @@ public class AttachmentController: ViewController, MinimizableController {
|
||||
public var minimizedIcon: UIImage? {
|
||||
return self.mainController.minimizedIcon
|
||||
}
|
||||
|
||||
public var isFullscreen: Bool {
|
||||
return self.mainController.isFullscreen
|
||||
}
|
||||
|
||||
private final class Node: ASDisplayNode {
|
||||
private weak var controller: AttachmentController?
|
||||
@ -1268,6 +1276,10 @@ public class AttachmentController: ViewController, MinimizableController {
|
||||
|
||||
public var ensureUnfocused = true
|
||||
|
||||
public func requestMinimize(topEdgeOffset: CGFloat?, initialVelocity: CGFloat?) {
|
||||
self.node.minimize()
|
||||
}
|
||||
|
||||
public override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
|
||||
if self.ensureUnfocused {
|
||||
self.view.endEditing(true)
|
||||
@ -1397,7 +1409,9 @@ public class AttachmentController: ViewController, MinimizableController {
|
||||
public func makeContentSnapshotView() -> UIView? {
|
||||
let snapshotView = self.view.snapshotView(afterScreenUpdates: false)
|
||||
if let contentSnapshotView = self.mainController.makeContentSnapshotView() {
|
||||
contentSnapshotView.frame = contentSnapshotView.frame.offsetBy(dx: 0.0, dy: 64.0 + 56.0)
|
||||
if !self.mainController.isFullscreen {
|
||||
contentSnapshotView.frame = contentSnapshotView.frame.offsetBy(dx: 0.0, dy: 64.0 + 56.0)
|
||||
}
|
||||
snapshotView?.addSubview(contentSnapshotView)
|
||||
}
|
||||
return snapshotView
|
||||
|
@ -638,8 +638,11 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
|
||||
self.webView.reloadInputViews()
|
||||
}
|
||||
|
||||
self.webView.customBottomInset = safeInsets.bottom * (1.0 - insets.bottom / fullInsets.bottom)
|
||||
|
||||
if fullInsets.bottom.isZero {
|
||||
self.webView.customBottomInset = safeInsets.bottom
|
||||
} else {
|
||||
self.webView.customBottomInset = safeInsets.bottom * (1.0 - insets.bottom / fullInsets.bottom)
|
||||
}
|
||||
// self.webView.scrollView.scrollIndicatorInsets = UIEdgeInsets(top: 0.0, left: -insets.left, bottom: 0.0, right: -insets.right)
|
||||
// self.webView.scrollView.horizontalScrollIndicatorInsets = UIEdgeInsets(top: 0.0, left: -insets.left, bottom: 0.0, right: -insets.right)
|
||||
|
||||
|
@ -30,6 +30,7 @@ public protocol MinimizableController: ViewController {
|
||||
var isMinimizable: Bool { get }
|
||||
var minimizedIcon: UIImage? { get }
|
||||
var minimizedProgress: Float? { get }
|
||||
var isFullscreen: Bool { get }
|
||||
|
||||
func requestMinimize(topEdgeOffset: CGFloat?, initialVelocity: CGFloat?)
|
||||
func makeContentSnapshotView() -> UIView?
|
||||
@ -41,6 +42,10 @@ public protocol MinimizableController: ViewController {
|
||||
}
|
||||
|
||||
public extension MinimizableController {
|
||||
var isFullscreen: Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
var minimizedTopEdgeOffset: CGFloat? {
|
||||
return nil
|
||||
}
|
||||
|
@ -13,18 +13,27 @@ public final class MoreButtonNode: ASDisplayNode {
|
||||
case search
|
||||
}
|
||||
|
||||
private let encircled: Bool
|
||||
private let duration: Double = 0.21
|
||||
public var iconState: State = .search
|
||||
|
||||
init() {
|
||||
super.init(size: CGSize(width: 30.0, height: 30.0))
|
||||
init(size: CGSize = CGSize(width: 30.0, height: 30.0), encircled: Bool) {
|
||||
self.encircled = encircled
|
||||
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("anim_moretosearch"), frames: .range(startFrame: 90, endFrame: 90), duration: 0.0))
|
||||
super.init(size: size)
|
||||
|
||||
if self.encircled {
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("anim_moretosearch"), frames: .range(startFrame: 90, endFrame: 90), duration: 0.0))
|
||||
} else {
|
||||
self.iconState = .more
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("anim_baremoredots"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.0))
|
||||
}
|
||||
}
|
||||
|
||||
func play() {
|
||||
if case .more = self.iconState {
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("anim_moredots"), frames: .range(startFrame: 0, endFrame: 46), duration: 0.76))
|
||||
let animationName = self.encircled ? "anim_moredots" : "anim_baremoredots"
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local(animationName), frames: .range(startFrame: 0, endFrame: 46), duration: 0.76))
|
||||
}
|
||||
}
|
||||
|
||||
@ -81,6 +90,7 @@ public final class MoreButtonNode: ASDisplayNode {
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
private let size: CGSize
|
||||
|
||||
public func updateColor(_ color: UIColor?, transition: ContainedViewLayoutTransition) {
|
||||
self.color = color
|
||||
@ -104,15 +114,16 @@ public final class MoreButtonNode: ASDisplayNode {
|
||||
self.iconNode.customColor = color
|
||||
}
|
||||
|
||||
public init(theme: PresentationTheme) {
|
||||
public init(theme: PresentationTheme, size: CGSize = CGSize(width: 30.0, height: 30.0), encircled: Bool = true) {
|
||||
self.theme = theme
|
||||
self.size = size
|
||||
|
||||
self.contextSourceNode = ContextReferenceContentNode()
|
||||
self.containerNode = ContextControllerSourceNode()
|
||||
self.containerNode.animateScale = false
|
||||
|
||||
self.buttonNode = HighlightableButtonNode()
|
||||
self.iconNode = MoreIconNode()
|
||||
self.iconNode = MoreIconNode(size: size, encircled: encircled)
|
||||
self.iconNode.customColor = self.theme.rootController.navigationBar.buttonColor
|
||||
|
||||
super.init()
|
||||
@ -143,7 +154,7 @@ public final class MoreButtonNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
override public func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
|
||||
let animationSize = CGSize(width: 30.0, height: 30.0)
|
||||
let animationSize = self.size
|
||||
let inset: CGFloat = 0.0
|
||||
let iconFrame = CGRect(origin: CGPoint(x: inset + 6.0, y: floor((constrainedSize.height - animationSize.height) / 2.0) + 1.0), size: animationSize)
|
||||
|
||||
|
@ -50,6 +50,18 @@ extension JSON {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public init?(dictionary: [String: Any]) {
|
||||
var values: [String: JSON] = [:]
|
||||
for (key, value) in dictionary {
|
||||
if let v = JSON(value) {
|
||||
values[key] = v
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
self = .dictionary(values)
|
||||
}
|
||||
}
|
||||
|
||||
extension JSON: Collection {
|
||||
@ -125,7 +137,7 @@ extension JSON {
|
||||
get {
|
||||
switch self {
|
||||
case .null:
|
||||
return 0
|
||||
return NSNull()
|
||||
case let .number(value):
|
||||
return value
|
||||
case let .string(value):
|
||||
@ -172,6 +184,18 @@ extension JSON {
|
||||
}
|
||||
}
|
||||
|
||||
extension JSON {
|
||||
public var string: String? {
|
||||
guard let jsonData = try? JSONSerialization.data(withJSONObject: self.value) else {
|
||||
return nil
|
||||
}
|
||||
guard let jsonDataString = String(data: jsonData, encoding: .utf8) else {
|
||||
return nil
|
||||
}
|
||||
return jsonDataString
|
||||
}
|
||||
}
|
||||
|
||||
extension JSON: ExpressibleByDictionaryLiteral {
|
||||
public init(dictionaryLiteral elements: (String, Any)...) {
|
||||
self = .dictionary(elements.reduce([String: JSON]()) { (dictionary, element) in
|
||||
@ -195,6 +219,12 @@ private protocol JSONValue {
|
||||
var jsonValue: JSON { get }
|
||||
}
|
||||
|
||||
extension NSNull: JSONElement, JSONValue {
|
||||
var jsonValue: JSON {
|
||||
return .null
|
||||
}
|
||||
}
|
||||
|
||||
extension Int: JSONElement, JSONValue {
|
||||
var jsonValue: JSON {
|
||||
return .number(Double(self))
|
||||
|
@ -48,7 +48,11 @@ public func formatTonAmountText(_ value: Int64, dateTimeFormat: PresentationDate
|
||||
}
|
||||
|
||||
if let dotIndex = balanceText.range(of: dateTimeFormat.decimalSeparator) {
|
||||
balanceText = String(balanceText[balanceText.startIndex ..< min(balanceText.endIndex, balanceText.index(dotIndex.upperBound, offsetBy: 2))])
|
||||
if let endIndex = balanceText.index(dotIndex.upperBound, offsetBy: 2, limitedBy: balanceText.endIndex) {
|
||||
balanceText = String(balanceText[balanceText.startIndex..<endIndex])
|
||||
} else {
|
||||
balanceText = String(balanceText[balanceText.startIndex..<balanceText.endIndex])
|
||||
}
|
||||
|
||||
let integerPartString = balanceText[..<dotIndex.lowerBound]
|
||||
if let integerPart = Int32(integerPartString) {
|
||||
|
@ -1290,7 +1290,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
||||
id: subscriptionFormId,
|
||||
canSaveCredentials: false,
|
||||
passwordMissing: false,
|
||||
invoice: BotPaymentInvoice(isTest: false, requestedFields: [], currency: "XTR", prices: [BotPaymentPrice(label: "", amount: subscriptionPricing.amount)], tip: nil, termsInfo: nil),
|
||||
invoice: BotPaymentInvoice(isTest: false, requestedFields: [], currency: "XTR", prices: [BotPaymentPrice(label: "", amount: subscriptionPricing.amount)], tip: nil, termsInfo: nil, subscriptionPeriod: subscriptionPricing.period),
|
||||
paymentBotId: channel.id,
|
||||
providerId: nil,
|
||||
url: nil,
|
||||
|
@ -44,6 +44,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode",
|
||||
"//submodules/InAppPurchaseManager",
|
||||
"//submodules/Components/BlurredBackgroundComponent",
|
||||
"//submodules/ProgressNavigationButtonNode",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -30,6 +30,7 @@ import AudioToolbox
|
||||
import TextFormat
|
||||
import InAppPurchaseManager
|
||||
import BlurredBackgroundComponent
|
||||
import ProgressNavigationButtonNode
|
||||
|
||||
final class GiftSetupScreenComponent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
@ -203,7 +204,7 @@ final class GiftSetupScreenComponent: Component {
|
||||
self.buttonSeparator.opacity = Float(bottomPanelAlpha)
|
||||
}
|
||||
|
||||
func proceed() {
|
||||
@objc private func proceed() {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
@ -215,7 +216,7 @@ final class GiftSetupScreenComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
func proceedWithPremiumGift() {
|
||||
private func proceedWithPremiumGift() {
|
||||
guard let component = self.component, case let .premium(product) = component.subject, let storeProduct = product.storeProduct, let inAppPurchaseManager = component.context.inAppPurchaseManager else {
|
||||
return
|
||||
}
|
||||
@ -304,7 +305,7 @@ final class GiftSetupScreenComponent: Component {
|
||||
})
|
||||
}
|
||||
|
||||
func proceedWithStarGift() {
|
||||
private func proceedWithStarGift() {
|
||||
guard let component = self.component, case let .starGift(starGift) = component.subject, let starsContext = component.context.starsContext, let starsState = starsContext.currentState else {
|
||||
return
|
||||
}
|
||||
@ -450,6 +451,8 @@ final class GiftSetupScreenComponent: Component {
|
||||
self.isUpdating = false
|
||||
}
|
||||
|
||||
let peerName = self.peerMap[component.peerId]?.compactDisplayTitle ?? ""
|
||||
|
||||
if self.component == nil {
|
||||
let _ = (component.context.engine.data.get(
|
||||
TelegramEngine.EngineData.Item.Peer.Peer(id: component.peerId),
|
||||
@ -588,26 +591,16 @@ final class GiftSetupScreenComponent: Component {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let alphaTransition: ComponentTransition
|
||||
if !transition.animation.isImmediate {
|
||||
alphaTransition = .easeInOut(duration: 0.25)
|
||||
} else {
|
||||
alphaTransition = .immediate
|
||||
}
|
||||
|
||||
if themeUpdated {
|
||||
self.backgroundColor = environment.theme.list.blocksBackgroundColor
|
||||
}
|
||||
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
let _ = alphaTransition
|
||||
let _ = presentationData
|
||||
|
||||
|
||||
let navigationTitleSize = self.navigationTitle.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: environment.strings.Gift_Send_Title, font: Font.semibold(17.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)),
|
||||
text: .plain(NSAttributedString(string: environment.strings.Gift_Send_TitleTo(peerName).string, font: Font.semibold(17.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)),
|
||||
horizontalAlignment: .center
|
||||
)),
|
||||
environment: {},
|
||||
@ -720,7 +713,6 @@ final class GiftSetupScreenComponent: Component {
|
||||
))))
|
||||
self.resetText = nil
|
||||
|
||||
let peerName = self.peerMap[component.peerId]?.compactDisplayTitle ?? ""
|
||||
let introFooter: AnyComponent<Empty>?
|
||||
switch component.subject {
|
||||
case .premium:
|
||||
@ -960,6 +952,20 @@ final class GiftSetupScreenComponent: Component {
|
||||
buttonView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - buttonSize.width) / 2.0), y: availableSize.height - bottomPanelHeight + bottomPanelPadding), size: buttonSize)
|
||||
}
|
||||
|
||||
let controller = environment.controller()
|
||||
if inputHeight > 10.0 {
|
||||
if self.inProgress {
|
||||
let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: environment.theme.rootController.navigationBar.accentTextColor))
|
||||
controller?.navigationItem.rightBarButtonItem = item
|
||||
} else {
|
||||
let rightBarButtonItem = UIBarButtonItem(title: environment.strings.Gift_Send_SendShort, style: .done, target: self, action: #selector(self.proceed))
|
||||
rightBarButtonItem.isEnabled = buttonIsEnabled
|
||||
controller?.navigationItem.setRightBarButton(rightBarButtonItem, animated: controller?.navigationItem.rightBarButtonItem == nil)
|
||||
}
|
||||
} else {
|
||||
controller?.navigationItem.setRightBarButton(nil, animated: true)
|
||||
}
|
||||
|
||||
if self.textInputState.isEditing, let emojiSuggestion = self.textInputState.currentEmojiSuggestion, emojiSuggestion.disposable == nil {
|
||||
emojiSuggestion.disposable = (EmojiSuggestionsComponent.suggestionData(context: component.context, isSavedMessages: false, query: emojiSuggestion.position.value)
|
||||
|> deliverOnMainQueue).start(next: { [weak self, weak emojiSuggestion] result in
|
||||
@ -1090,7 +1096,6 @@ final class GiftSetupScreenComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let previousBounds = self.scrollView.bounds
|
||||
|
||||
self.recenterOnTag = nil
|
||||
|
@ -1077,13 +1077,23 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll
|
||||
transition.updateTransform(node: itemNode, transform: CATransform3DIdentity)
|
||||
|
||||
if let _ = itemNode.snapshotView {
|
||||
if itemNode.item.controller.minimizedTopEdgeOffset == nil, let snapshotView = itemNode.snapshotView, snapshotView.frame.origin.y == -12.0 {
|
||||
if itemNode.item.controller.isFullscreen {
|
||||
if layout.size.width < layout.size.height {
|
||||
let snapshotFrame = itemNode.snapshotContainerView.frame.offsetBy(dx: 0.0, dy: 64.0)
|
||||
transition.updateFrame(view: itemNode.snapshotContainerView, frame: snapshotFrame)
|
||||
}
|
||||
} else if itemNode.item.controller.minimizedTopEdgeOffset == nil, let snapshotView = itemNode.snapshotView, snapshotView.frame.origin.y == -12.0 {
|
||||
let snapshotFrame = snapshotView.frame.offsetBy(dx: 0.0, dy: 12.0)
|
||||
transition.updateFrame(view: snapshotView, frame: snapshotFrame)
|
||||
}
|
||||
}
|
||||
|
||||
transition.updatePosition(node: itemNode, position: CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0 + topInset + self.scrollView.contentOffset.y), completion: { _ in
|
||||
var maximizeTopInset = 0.0
|
||||
if !itemNode.item.controller.isFullscreen {
|
||||
maximizeTopInset = topInset
|
||||
}
|
||||
|
||||
transition.updatePosition(node: itemNode, position: CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0 + maximizeTopInset + self.scrollView.contentOffset.y), completion: { _ in
|
||||
self.isApplyingTransition = false
|
||||
if self.currentTransition == currentTransition {
|
||||
self.currentTransition = nil
|
||||
|
@ -15,6 +15,7 @@ import DeviceAccess
|
||||
import PeerInfoVisualMediaPaneNode
|
||||
import PhotoResources
|
||||
import PeerInfoPaneNode
|
||||
import WebUI
|
||||
|
||||
enum PeerInfoUpdatingAvatar {
|
||||
case none
|
||||
@ -386,6 +387,7 @@ final class PeerInfoScreenData {
|
||||
let revenueStatsContext: RevenueStatsContext?
|
||||
let profileGiftsContext: ProfileGiftsContext?
|
||||
let premiumGiftOptions: [PremiumGiftCodeOption]
|
||||
let webAppPermissions: WebAppPermissionsState?
|
||||
|
||||
let _isContact: Bool
|
||||
var forceIsContact: Bool = false
|
||||
@ -434,7 +436,8 @@ final class PeerInfoScreenData {
|
||||
revenueStatsState: RevenueStats?,
|
||||
revenueStatsContext: RevenueStatsContext?,
|
||||
profileGiftsContext: ProfileGiftsContext?,
|
||||
premiumGiftOptions: [PremiumGiftCodeOption]
|
||||
premiumGiftOptions: [PremiumGiftCodeOption],
|
||||
webAppPermissions: WebAppPermissionsState?
|
||||
) {
|
||||
self.peer = peer
|
||||
self.chatPeer = chatPeer
|
||||
@ -472,6 +475,7 @@ final class PeerInfoScreenData {
|
||||
self.revenueStatsContext = revenueStatsContext
|
||||
self.profileGiftsContext = profileGiftsContext
|
||||
self.premiumGiftOptions = premiumGiftOptions
|
||||
self.webAppPermissions = webAppPermissions
|
||||
}
|
||||
}
|
||||
|
||||
@ -967,7 +971,8 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id,
|
||||
revenueStatsState: nil,
|
||||
revenueStatsContext: nil,
|
||||
profileGiftsContext: nil,
|
||||
premiumGiftOptions: []
|
||||
premiumGiftOptions: [],
|
||||
webAppPermissions: nil
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1015,7 +1020,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
|
||||
revenueStatsState: nil,
|
||||
revenueStatsContext: nil,
|
||||
profileGiftsContext: nil,
|
||||
premiumGiftOptions: []
|
||||
premiumGiftOptions: [],
|
||||
webAppPermissions: nil
|
||||
))
|
||||
case let .user(userPeerId, secretChatId, kind):
|
||||
let groupsInCommon: GroupsInCommonContext?
|
||||
@ -1310,7 +1316,16 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
|
||||
return (revenueStatsContext, state.stats)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let webAppPermissions: Signal<WebAppPermissionsState?, NoError> = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|
||||
|> mapToSignal { peer -> Signal<WebAppPermissionsState?, NoError> in
|
||||
if let peer, case let .user(user) = peer, let _ = user.botInfo {
|
||||
return webAppPermissionsState(context: context, peerId: peerId)
|
||||
} else {
|
||||
return .single(nil)
|
||||
}
|
||||
}
|
||||
|
||||
return combineLatest(
|
||||
context.account.viewTracker.peerView(peerId, updateData: true),
|
||||
peerInfoAvailableMediaPanes(context: context, peerId: peerId, chatLocation: chatLocation, isMyProfile: isMyProfile, chatLocationContextHolder: chatLocationContextHolder),
|
||||
@ -1329,9 +1344,10 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
|
||||
privacySettings,
|
||||
starsRevenueContextAndState,
|
||||
revenueContextAndState,
|
||||
premiumGiftOptions
|
||||
premiumGiftOptions,
|
||||
webAppPermissions
|
||||
)
|
||||
|> map { peerView, availablePanes, globalNotificationSettings, encryptionKeyFingerprint, status, hasStories, hasStoryArchive, accountIsPremium, savedMessagesPeer, hasSavedMessagesChats, hasSavedMessages, hasSavedMessageTags, hasBotPreviewItems, personalChannel, privacySettings, starsRevenueContextAndState, revenueContextAndState, premiumGiftOptions -> PeerInfoScreenData in
|
||||
|> map { peerView, availablePanes, globalNotificationSettings, encryptionKeyFingerprint, status, hasStories, hasStoryArchive, accountIsPremium, savedMessagesPeer, hasSavedMessagesChats, hasSavedMessages, hasSavedMessageTags, hasBotPreviewItems, personalChannel, privacySettings, starsRevenueContextAndState, revenueContextAndState, premiumGiftOptions, webAppPermissions -> PeerInfoScreenData in
|
||||
var availablePanes = availablePanes
|
||||
if isMyProfile {
|
||||
availablePanes?.insert(.stories, at: 0)
|
||||
@ -1450,7 +1466,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
|
||||
revenueStatsState: revenueContextAndState.1,
|
||||
revenueStatsContext: revenueContextAndState.0,
|
||||
profileGiftsContext: profileGiftsContext,
|
||||
premiumGiftOptions: premiumGiftOptions
|
||||
premiumGiftOptions: premiumGiftOptions,
|
||||
webAppPermissions: webAppPermissions
|
||||
)
|
||||
}
|
||||
case .channel:
|
||||
@ -1662,7 +1679,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
|
||||
revenueStatsState: revenueContextAndState.1,
|
||||
revenueStatsContext: revenueContextAndState.0,
|
||||
profileGiftsContext: nil,
|
||||
premiumGiftOptions: []
|
||||
premiumGiftOptions: [],
|
||||
webAppPermissions: nil
|
||||
)
|
||||
}
|
||||
case let .group(groupId):
|
||||
@ -1965,7 +1983,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
|
||||
revenueStatsState: nil,
|
||||
revenueStatsContext: nil,
|
||||
profileGiftsContext: nil,
|
||||
premiumGiftOptions: []
|
||||
premiumGiftOptions: [],
|
||||
webAppPermissions: nil
|
||||
))
|
||||
}
|
||||
}
|
||||
|
@ -1209,6 +1209,7 @@ private enum InfoSection: Int, CaseIterable {
|
||||
case personalChannel
|
||||
case peerInfo
|
||||
case balances
|
||||
case permissions
|
||||
case peerInfoTrailing
|
||||
case peerMembers
|
||||
}
|
||||
@ -1540,7 +1541,23 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
|
||||
} else {
|
||||
print()
|
||||
}
|
||||
|
||||
|
||||
if let _ = user.botInfo {
|
||||
//TODO:localize
|
||||
items[.permissions]!.append(PeerInfoScreenHeaderItem(id: 30, text: "ALLOW ACCESS TO"))
|
||||
// items[.permissions]!.append(PeerInfoScreenSwitchItem(id: 31, text: "Emoji Status", value: false, icon: UIImage(bundleImageName: "Chat/Info/Status"), isLocked: false, toggled: { value in
|
||||
//
|
||||
// }))
|
||||
|
||||
if data.webAppPermissions?.location?.isRequested == true {
|
||||
items[.permissions]!.append(PeerInfoScreenSwitchItem(id: 32, text: "Geolocation", value: data.webAppPermissions?.location?.isAllowed ?? false, icon: UIImage(bundleImageName: "Chat/Info/Location"), isLocked: false, toggled: { value in
|
||||
let _ = updateWebAppPermissionsStateInteractively(context: context, peerId: user.id) { current in
|
||||
return WebAppPermissionsState(location: WebAppPermissionsState.Location(isRequested: true, isAllowed: value))
|
||||
}.start()
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
if let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) {
|
||||
items[currentPeerInfoSection]!.append(PeerInfoScreenDisclosureItem(id: 10, label: .none, text: presentationData.strings.Bot_Settings, icon: UIImage(bundleImageName: "Chat/Info/SettingsIcon"), action: {
|
||||
interaction.openEditing()
|
||||
|
@ -317,14 +317,18 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
|
||||
return nil
|
||||
})
|
||||
|
||||
let scrollOffset: CGFloat = max(0.0, size.height - params.visibleHeight)
|
||||
|
||||
var scrollOffset: CGFloat = max(0.0, size.height - params.visibleHeight)
|
||||
|
||||
let buttonSideInset = sideInset + 16.0
|
||||
let buttonSize = CGSize(width: size.width - buttonSideInset * 2.0, height: 50.0)
|
||||
let bottomPanelHeight = bottomInset + buttonSize.height + 8.0
|
||||
if params.visibleHeight < 110.0 {
|
||||
scrollOffset -= bottomPanelHeight
|
||||
}
|
||||
|
||||
transition.setFrame(view: unlockButton.view, frame: CGRect(origin: CGPoint(x: buttonSideInset, y: size.height - bottomInset - buttonSize.height - scrollOffset), size: buttonSize))
|
||||
let _ = unlockButton.updateLayout(width: buttonSize.width, transition: .immediate)
|
||||
|
||||
let bottomPanelHeight = bottomInset + buttonSize.height + 8.0
|
||||
transition.setFrame(view: unlockBackground.view, frame: CGRect(x: 0.0, y: size.height - bottomInset - buttonSize.height - 8.0 - scrollOffset, width: size.width, height: bottomPanelHeight))
|
||||
unlockBackground.update(size: CGSize(width: size.width, height: bottomPanelHeight), transition: transition.containedViewLayoutTransition)
|
||||
transition.setFrame(view: unlockSeparator.view, frame: CGRect(x: 0.0, y: size.height - bottomInset - buttonSize.height - 8.0 - scrollOffset, width: size.width, height: UIScreenPixel))
|
||||
|
@ -12,11 +12,18 @@ public final class PremiumPeerShortcutComponent: Component {
|
||||
let context: AccountContext
|
||||
let theme: PresentationTheme
|
||||
let peer: EnginePeer
|
||||
let icon: TelegramMediaFile?
|
||||
|
||||
public init(context: AccountContext, theme: PresentationTheme, peer: EnginePeer) {
|
||||
public init(
|
||||
context: AccountContext,
|
||||
theme: PresentationTheme,
|
||||
peer: EnginePeer,
|
||||
icon: TelegramMediaFile? = nil
|
||||
) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.peer = peer
|
||||
self.icon = icon
|
||||
}
|
||||
|
||||
public static func ==(lhs: PremiumPeerShortcutComponent, rhs: PremiumPeerShortcutComponent) -> Bool {
|
||||
|
@ -254,6 +254,7 @@ public final class StarsImageComponent: Component {
|
||||
case extendedMedia([TelegramExtendedMedia])
|
||||
case transactionPeer(StarsContext.State.Transaction.Peer)
|
||||
case gift(Int64)
|
||||
case color(UIColor)
|
||||
|
||||
public static func == (lhs: StarsImageComponent.Subject, rhs: StarsImageComponent.Subject) -> Bool {
|
||||
switch lhs {
|
||||
@ -293,6 +294,12 @@ public final class StarsImageComponent: Component {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .color(lhsColor):
|
||||
if case let .color(rhsColor) = rhs, lhsColor == rhsColor {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -307,6 +314,7 @@ public final class StarsImageComponent: Component {
|
||||
public let diameter: CGFloat
|
||||
public let backgroundColor: UIColor
|
||||
public let icon: Icon?
|
||||
public let value: Int64?
|
||||
public let action: ((@escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void)?
|
||||
|
||||
public init(
|
||||
@ -316,6 +324,7 @@ public final class StarsImageComponent: Component {
|
||||
diameter: CGFloat,
|
||||
backgroundColor: UIColor,
|
||||
icon: Icon? = nil,
|
||||
value: Int64? = nil,
|
||||
action: ((@escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void)? = nil
|
||||
) {
|
||||
self.context = context
|
||||
@ -324,6 +333,7 @@ public final class StarsImageComponent: Component {
|
||||
self.diameter = diameter
|
||||
self.backgroundColor = backgroundColor
|
||||
self.icon = icon
|
||||
self.value = value
|
||||
self.action = action
|
||||
}
|
||||
|
||||
@ -346,6 +356,9 @@ public final class StarsImageComponent: Component {
|
||||
if lhs.icon != rhs.icon {
|
||||
return false
|
||||
}
|
||||
if lhs.value != rhs.value {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -368,10 +381,14 @@ public final class StarsImageComponent: Component {
|
||||
private var dustNode: MediaDustNode?
|
||||
private var button: UIControl?
|
||||
|
||||
private var amountIconView: UIImageView?
|
||||
private var amountBackgroundView = ComponentView<Empty>()
|
||||
private let amountView = ComponentView<Empty>()
|
||||
|
||||
private var animationNode: AnimatedStickerNode?
|
||||
|
||||
private var lockView: UIImageView?
|
||||
private var countView = ComponentView<Empty>()
|
||||
private let countView = ComponentView<Empty>()
|
||||
|
||||
private let fetchDisposable = MetaDisposable()
|
||||
private var hiddenMediaDisposable: Disposable?
|
||||
@ -471,6 +488,21 @@ public final class StarsImageComponent: Component {
|
||||
switch component.subject {
|
||||
case .none:
|
||||
break
|
||||
case let .color(color):
|
||||
let imageNode: TransformImageNode
|
||||
if let current = self.imageNode {
|
||||
imageNode = current
|
||||
} else {
|
||||
imageNode = TransformImageNode()
|
||||
imageNode.contentAnimations = [.firstUpdate, .subsequentUpdates]
|
||||
containerNode.view.addSubview(imageNode.view)
|
||||
self.imageNode = imageNode
|
||||
|
||||
imageNode.setSignal(solidColorImage(color))
|
||||
}
|
||||
|
||||
imageNode.frame = imageFrame
|
||||
imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: 16.0), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))()
|
||||
case let .photo(photo):
|
||||
let imageNode: TransformImageNode
|
||||
if let current = self.imageNode {
|
||||
@ -873,6 +905,62 @@ public final class StarsImageComponent: Component {
|
||||
smallIconOutlineView.removeFromSuperview()
|
||||
}
|
||||
|
||||
if let amount = component.value {
|
||||
let smallIconView: UIImageView
|
||||
if let current = self.amountIconView {
|
||||
smallIconView = current
|
||||
} else {
|
||||
smallIconView = UIImageView()
|
||||
self.amountIconView = smallIconView
|
||||
|
||||
smallIconView.image = UIImage(bundleImageName: "Premium/SendStarsPeerBadgeStarIcon")?.withRenderingMode(.alwaysTemplate)
|
||||
smallIconView.tintColor = .white
|
||||
}
|
||||
|
||||
let countSize = self.amountView.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
Text(text: "\(amount)", font: Font.with(size: 12.0, design: .round, weight: .bold), color: .white)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: imageFrame.size
|
||||
)
|
||||
|
||||
let iconSize = CGSize(width: 11.0, height: 11.0)
|
||||
let iconSpacing: CGFloat = 1.0
|
||||
|
||||
let totalLabelWidth = iconSize.width + iconSpacing + countSize.width
|
||||
let iconFrame = CGRect(origin: CGPoint(x: imageFrame.minX + floorToScreenPixels((imageFrame.width - totalLabelWidth) / 2.0), y: imageFrame.maxY - countSize.height + 4.0), size: iconSize)
|
||||
smallIconView.frame = iconFrame
|
||||
|
||||
let countFrame = CGRect(origin: CGPoint(x: imageFrame.minX + floorToScreenPixels((imageFrame.width - totalLabelWidth) / 2.0) + iconSize.width + iconSpacing, y: imageFrame.maxY - countSize.height + 2.0), size: countSize)
|
||||
|
||||
let amountBackgroundFrame = CGRect(origin: CGPoint(x: imageFrame.minX + floorToScreenPixels((imageFrame.width - totalLabelWidth) / 2.0) - 7.0, y: imageFrame.maxY - countSize.height - 3.0), size: CGSize(width: totalLabelWidth + 14.0, height: countFrame.height + 10.0))
|
||||
|
||||
let _ = self.amountBackgroundView.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
RoundedRectangle(colors: [UIColor(rgb: 0xffaa01)], cornerRadius: amountBackgroundFrame.height / 2.0, gradientDirection: .horizontal, stroke: 2.0 - UIScreenPixel, strokeColor: component.backgroundColor, size: amountBackgroundFrame.size)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: amountBackgroundFrame.size
|
||||
)
|
||||
if let backgroundView = self.amountBackgroundView.view {
|
||||
if backgroundView.superview == nil {
|
||||
containerNode.view.addSubview(backgroundView)
|
||||
}
|
||||
backgroundView.frame = amountBackgroundFrame
|
||||
}
|
||||
|
||||
if let countView = self.amountView.view {
|
||||
if countView.superview == nil {
|
||||
containerNode.view.addSubview(countView)
|
||||
containerNode.view.addSubview(smallIconView)
|
||||
}
|
||||
countView.frame = countFrame
|
||||
}
|
||||
}
|
||||
|
||||
if let _ = component.action {
|
||||
if self.button == nil {
|
||||
let button = UIControl(frame: imageFrame)
|
||||
|
@ -154,6 +154,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
|
||||
let table = Child(TableComponent.self)
|
||||
let additional = Child(BalancedTextComponent.self)
|
||||
let status = Child(BalancedTextComponent.self)
|
||||
let cancelButton = Child(SolidRoundedButtonComponent.self)
|
||||
let button = Child(SolidRoundedButtonComponent.self)
|
||||
|
||||
let transactionStatusBackgound = Child(RoundedRectangle.self)
|
||||
@ -201,7 +202,8 @@ private final class StarsTransactionSheetContent: CombinedComponent {
|
||||
var boostsText: String?
|
||||
let additionalText = strings.Stars_Transaction_Terms
|
||||
var buttonText: String? = strings.Common_OK
|
||||
var buttonIsDestructive = false
|
||||
|
||||
var cancelButtonText: String?
|
||||
var statusText: String?
|
||||
var statusIsDestructive = false
|
||||
|
||||
@ -223,6 +225,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
|
||||
var isSubscription = false
|
||||
var isSubscriber = false
|
||||
var isSubscriptionFee = false
|
||||
var isBotSubscription = false
|
||||
var isCancelled = false
|
||||
var isReaction = false
|
||||
var giveawayMessageId: MessageId?
|
||||
@ -256,7 +259,16 @@ private final class StarsTransactionSheetContent: CombinedComponent {
|
||||
transactionPeer = .peer(peer)
|
||||
isSubscriber = true
|
||||
case let .subscription(subscription):
|
||||
titleText = strings.Stars_Transaction_Subscription_Title
|
||||
if case let .user(user) = subscription.peer, user.botInfo != nil {
|
||||
isBotSubscription = true
|
||||
}
|
||||
if let title = subscription.title {
|
||||
titleText = title
|
||||
} else {
|
||||
titleText = strings.Stars_Transaction_Subscription_Title
|
||||
}
|
||||
photo = subscription.photo
|
||||
|
||||
descriptionText = ""
|
||||
count = subscription.pricing.amount
|
||||
date = subscription.untilDate
|
||||
@ -320,8 +332,8 @@ private final class StarsTransactionSheetContent: CombinedComponent {
|
||||
}
|
||||
} else {
|
||||
statusText = strings.Stars_Transaction_Subscription_Active(stringForMediumDate(timestamp: subscription.untilDate, strings: strings, dateTimeFormat: dateTimeFormat, withTime: false)).string
|
||||
buttonText = strings.Stars_Transaction_Subscription_Cancel
|
||||
buttonIsDestructive = true
|
||||
cancelButtonText = strings.Stars_Transaction_Subscription_Cancel
|
||||
buttonText = strings.Common_OK
|
||||
}
|
||||
}
|
||||
case let .transaction(transaction, parentPeer):
|
||||
@ -571,7 +583,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
|
||||
)
|
||||
|
||||
let imageSubject: StarsImageComponent.Subject
|
||||
let imageIcon: StarsImageComponent.Icon?
|
||||
var imageIcon: StarsImageComponent.Icon?
|
||||
if isGift {
|
||||
imageSubject = .gift(count)
|
||||
} else if !media.isEmpty {
|
||||
@ -590,6 +602,11 @@ private final class StarsTransactionSheetContent: CombinedComponent {
|
||||
} else {
|
||||
imageIcon = nil
|
||||
}
|
||||
|
||||
if isSubscription && "".isEmpty {
|
||||
imageIcon = nil
|
||||
}
|
||||
|
||||
var starChild: _UpdatedChildComponent
|
||||
if let giftAnimation {
|
||||
starChild = gift.update(
|
||||
@ -690,7 +707,12 @@ private final class StarsTransactionSheetContent: CombinedComponent {
|
||||
} else if let toPeer {
|
||||
let title: String
|
||||
if isSubscription {
|
||||
title = strings.Stars_Transaction_Subscription_Subscription
|
||||
if isBotSubscription {
|
||||
//TODO:localize
|
||||
title = "Bot"
|
||||
} else {
|
||||
title = strings.Stars_Transaction_Subscription_Subscription
|
||||
}
|
||||
} else if isSubscriber {
|
||||
title = strings.Stars_Transaction_Subscription_Subscriber
|
||||
} else {
|
||||
@ -724,6 +746,16 @@ private final class StarsTransactionSheetContent: CombinedComponent {
|
||||
)
|
||||
)
|
||||
))
|
||||
if case let .subscription(subscription) = component.subject, let title = subscription.title {
|
||||
//TODO:localize
|
||||
tableItems.append(.init(
|
||||
id: "subscription",
|
||||
title: "Subscription",
|
||||
component: AnyComponent(
|
||||
MultilineTextComponent(text: .plain(NSAttributedString(string: title, font: tableFont, textColor: tableTextColor)))
|
||||
)
|
||||
))
|
||||
}
|
||||
} else if let via {
|
||||
tableItems.append(.init(
|
||||
id: "via",
|
||||
@ -1069,12 +1101,12 @@ private final class StarsTransactionSheetContent: CombinedComponent {
|
||||
originY += status.size.height + (statusIsDestructive ? 23.0 : 13.0)
|
||||
}
|
||||
|
||||
if let buttonText {
|
||||
let button = button.update(
|
||||
if let cancelButtonText {
|
||||
let cancelButton = cancelButton.update(
|
||||
component: SolidRoundedButtonComponent(
|
||||
title: buttonText,
|
||||
theme: buttonIsDestructive ? SolidRoundedButtonComponent.Theme(backgroundColor: .clear, foregroundColor: destructiveColor) : SolidRoundedButtonComponent.Theme(theme: theme),
|
||||
font: buttonIsDestructive ? .regular : .bold,
|
||||
title: cancelButtonText,
|
||||
theme: SolidRoundedButtonComponent.Theme(backgroundColor: .clear, foregroundColor: linkColor),
|
||||
font: .regular,
|
||||
fontSize: 17.0,
|
||||
height: 50.0,
|
||||
cornerRadius: 10.0,
|
||||
@ -1094,6 +1126,39 @@ private final class StarsTransactionSheetContent: CombinedComponent {
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
let cancelButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: originY), size: cancelButton.size)
|
||||
context.add(cancelButton
|
||||
.position(CGPoint(x: cancelButtonFrame.midX, y: cancelButtonFrame.midY))
|
||||
)
|
||||
originY += cancelButton.size.height
|
||||
originY += 8.0
|
||||
}
|
||||
|
||||
if let buttonText {
|
||||
let button = button.update(
|
||||
component: SolidRoundedButtonComponent(
|
||||
title: buttonText,
|
||||
theme: SolidRoundedButtonComponent.Theme(theme: theme),
|
||||
font: .bold,
|
||||
fontSize: 17.0,
|
||||
height: 50.0,
|
||||
cornerRadius: 10.0,
|
||||
gloss: false,
|
||||
iconName: nil,
|
||||
animationName: nil,
|
||||
iconPosition: .left,
|
||||
isLoading: state.inProgress,
|
||||
action: {
|
||||
component.cancel(true)
|
||||
if isSubscription && cancelButtonText == nil {
|
||||
component.updateSubscription()
|
||||
}
|
||||
}
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0),
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: originY), size: button.size)
|
||||
context.add(button
|
||||
.position(CGPoint(x: buttonFrame.midX, y: buttonFrame.midY))
|
||||
|
@ -698,8 +698,33 @@ final class StarsTransactionsScreenComponent: Component {
|
||||
isExpired = true
|
||||
}
|
||||
}
|
||||
|
||||
if let title = subscription.title {
|
||||
let nameComponent = AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: title,
|
||||
font: Font.regular(fontBaseDisplaySize * 16.0 / 17.0),
|
||||
textColor: environment.theme.list.itemPrimaryTextColor
|
||||
))
|
||||
))
|
||||
var nameGroupComponent: AnyComponent<Empty>
|
||||
if let _ = subscription.photo {
|
||||
nameGroupComponent = AnyComponent(
|
||||
HStack([
|
||||
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(RoundedRectangle(color: .lightGray, cornerRadius: 3.0, size: CGSize(width: 19.0, height: 19.0)))),
|
||||
AnyComponentWithIdentity(id: AnyHashable(1), component: nameComponent)
|
||||
], spacing: 6.0)
|
||||
)
|
||||
} else {
|
||||
nameGroupComponent = nameComponent
|
||||
}
|
||||
titleComponents.append(
|
||||
AnyComponentWithIdentity(id: AnyHashable(1), component: nameGroupComponent)
|
||||
)
|
||||
}
|
||||
|
||||
titleComponents.append(
|
||||
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(
|
||||
AnyComponentWithIdentity(id: AnyHashable(2), component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: dateText,
|
||||
font: Font.regular(floor(fontBaseDisplaySize * 15.0 / 17.0)),
|
||||
|
@ -19,6 +19,7 @@ import AccountContext
|
||||
import PresentationDataUtils
|
||||
import StarsImageComponent
|
||||
import ConfettiEffect
|
||||
import PremiumPeerShortcutComponent
|
||||
|
||||
private final class SheetContent: CombinedComponent {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
@ -263,6 +264,8 @@ private final class SheetContent: CombinedComponent {
|
||||
let star = Child(StarsImageComponent.self)
|
||||
let closeButton = Child(Button.self)
|
||||
let title = Child(Text.self)
|
||||
let peerShortcut = Child(PremiumPeerShortcutComponent.self)
|
||||
|
||||
let text = Child(BalancedTextComponent.self)
|
||||
let button = Child(ButtonComponent.self)
|
||||
let balanceTitle = Child(MultilineTextComponent.self)
|
||||
@ -297,14 +300,25 @@ private final class SheetContent: CombinedComponent {
|
||||
if let photo = component.invoice.photo {
|
||||
subject = .photo(photo)
|
||||
} else {
|
||||
subject = .transactionPeer(.peer(peer))
|
||||
if "".isEmpty {
|
||||
subject = .color(.lightGray)
|
||||
} else {
|
||||
subject = .transactionPeer(.peer(peer))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
subject = .none
|
||||
}
|
||||
|
||||
var isBot = false
|
||||
if case let .user(user) = state.botPeer, user.botInfo != nil {
|
||||
isBot = true
|
||||
}
|
||||
|
||||
var isSubscription = false
|
||||
if case .starsChatSubscription = context.component.source {
|
||||
if case .starsChatSubscription = component.source {
|
||||
isSubscription = true
|
||||
} else if "".isEmpty {
|
||||
isSubscription = true
|
||||
}
|
||||
let star = star.update(
|
||||
@ -314,7 +328,8 @@ private final class SheetContent: CombinedComponent {
|
||||
theme: theme,
|
||||
diameter: 90.0,
|
||||
backgroundColor: theme.actionSheet.opaqueItemBackgroundColor,
|
||||
icon: isSubscription ? .star : nil
|
||||
icon: isSubscription && !isBot ? .star : nil,
|
||||
value: isBot ? component.invoice.totalAmount : nil
|
||||
),
|
||||
availableSize: CGSize(width: min(414.0, context.availableSize.width), height: 220.0),
|
||||
transition: context.transition
|
||||
@ -350,7 +365,11 @@ private final class SheetContent: CombinedComponent {
|
||||
|
||||
let titleString: String
|
||||
if isSubscription {
|
||||
titleString = strings.Stars_Transfer_Subscribe_Channel_Title
|
||||
if isBot {
|
||||
titleString = "Subscription Name"
|
||||
} else {
|
||||
titleString = strings.Stars_Transfer_Subscribe_Channel_Title
|
||||
}
|
||||
} else {
|
||||
titleString = strings.Stars_Transfer_Title
|
||||
}
|
||||
@ -365,6 +384,24 @@ private final class SheetContent: CombinedComponent {
|
||||
)
|
||||
contentSize.height += title.size.height
|
||||
contentSize.height += 13.0
|
||||
|
||||
if isBot, let peer = state.botPeer {
|
||||
contentSize.height -= 3.0
|
||||
let peerShortcut = peerShortcut.update(
|
||||
component: PremiumPeerShortcutComponent(
|
||||
context: component.context,
|
||||
theme: theme,
|
||||
peer: peer
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width - 32.0, height: context.availableSize.height),
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(peerShortcut
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + peerShortcut.size.height / 2.0))
|
||||
)
|
||||
contentSize.height += peerShortcut.size.height
|
||||
contentSize.height += 13.0
|
||||
}
|
||||
|
||||
let textFont = Font.regular(15.0)
|
||||
let boldTextFont = Font.semibold(15.0)
|
||||
@ -378,6 +415,8 @@ private final class SheetContent: CombinedComponent {
|
||||
let infoText: String
|
||||
if case .starsChatSubscription = context.component.source {
|
||||
infoText = strings.Stars_Transfer_SubscribeInfo(state.botPeer?.compactDisplayTitle ?? "", strings.Stars_Transfer_Info_Stars(Int32(amount))).string
|
||||
} else if "".isEmpty {
|
||||
infoText = "Do you want to subscribe to **Subscription Name** in **\(state.botPeer?.compactDisplayTitle ?? "")** for **\(strings.Stars_Transfer_Info_Stars(Int32(amount)))** per month?"
|
||||
} else if !component.extendedMedia.isEmpty {
|
||||
var description: String = ""
|
||||
var photoCount: Int32 = 0
|
||||
@ -499,7 +538,11 @@ private final class SheetContent: CombinedComponent {
|
||||
let amountString = presentationStringsFormattedNumber(Int32(amount), presentationData.dateTimeFormat.groupingSeparator)
|
||||
let buttonAttributedString: NSMutableAttributedString
|
||||
if case .starsChatSubscription = component.source {
|
||||
buttonAttributedString = NSMutableAttributedString(string: strings.Stars_Transfer_Subscribe, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)
|
||||
//TODO:localize
|
||||
buttonAttributedString = NSMutableAttributedString(string: "Subscribe for # \(amountString) / month", font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)
|
||||
//buttonAttributedString = NSMutableAttributedString(string: strings.Stars_Transfer_Subscribe, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)
|
||||
} else if "".isEmpty {
|
||||
buttonAttributedString = NSMutableAttributedString(string: "Subscribe for # \(amountString) / month", font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)
|
||||
} else {
|
||||
buttonAttributedString = NSMutableAttributedString(string: "\(strings.Stars_Transfer_Pay) # \(amountString)", font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)
|
||||
}
|
||||
|
@ -30,7 +30,6 @@ swift_library(
|
||||
"//submodules/MediaPickerUI",
|
||||
"//submodules/LegacyMediaPickerUI",
|
||||
"//submodules/LocationUI",
|
||||
"//submodules/WebUI",
|
||||
"//submodules/TelegramUI/Components/ChatScheduleTimeController",
|
||||
"//submodules/TelegramUI/Components/ChatTimerScreen",
|
||||
"//submodules/TextFormat",
|
||||
|
@ -12,7 +12,6 @@ import MediaPickerUI
|
||||
import LegacyMediaPickerUI
|
||||
import LocationUI
|
||||
import ChatEntityKeyboardInputNode
|
||||
import WebUI
|
||||
import ChatScheduleTimeController
|
||||
import TextFormat
|
||||
import PhoneNumberFormat
|
||||
@ -1808,37 +1807,38 @@ final class StoryItemSetContainerSendMessage {
|
||||
//TODO:gift controller
|
||||
break
|
||||
case let .app(bot):
|
||||
let params = WebAppParameters(source: .attachMenu, peerId: peer.id, botId: bot.peer.id, botName: bot.shortName, botVerified: bot.peer.isVerified, url: nil, queryId: nil, payload: nil, buttonText: nil, keepAliveSignal: nil, forceHasSettings: false, fullSize: true)
|
||||
let theme = component.theme
|
||||
let updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>) = (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) })
|
||||
let controller = WebAppController(context: component.context, updatedPresentationData: updatedPresentationData, params: params, replyToMessageId: nil, threadId: nil)
|
||||
controller.openUrl = { [weak self] url, _, _, _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let _ = self
|
||||
//self?.openUrl(url, concealed: true, forceExternal: true)
|
||||
}
|
||||
controller.getNavigationController = { [weak view] in
|
||||
guard let view, let controller = view.component?.controller() else {
|
||||
return nil
|
||||
}
|
||||
return controller.navigationController as? NavigationController
|
||||
}
|
||||
controller.completion = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let _ = self
|
||||
/*if let strongSelf = self {
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
|
||||
$0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) }
|
||||
})
|
||||
strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory()
|
||||
}*/
|
||||
}
|
||||
completion(controller, controller.mediaPickerContext)
|
||||
self.controllerNavigationDisposable.set(nil)
|
||||
let _ = bot
|
||||
// let params = WebAppParameters(source: .attachMenu, peerId: peer.id, botId: bot.peer.id, botName: bot.shortName, botVerified: bot.peer.isVerified, url: nil, queryId: nil, payload: nil, buttonText: nil, keepAliveSignal: nil, forceHasSettings: false, fullSize: true)
|
||||
// let theme = component.theme
|
||||
// let updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>) = (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) })
|
||||
// let controller = WebAppController(context: component.context, updatedPresentationData: updatedPresentationData, params: params, replyToMessageId: nil, threadId: nil)
|
||||
// controller.openUrl = { [weak self] url, _, _, _ in
|
||||
// guard let self else {
|
||||
// return
|
||||
// }
|
||||
// let _ = self
|
||||
// //self?.openUrl(url, concealed: true, forceExternal: true)
|
||||
// }
|
||||
// controller.getNavigationController = { [weak view] in
|
||||
// guard let view, let controller = view.component?.controller() else {
|
||||
// return nil
|
||||
// }
|
||||
// return controller.navigationController as? NavigationController
|
||||
// }
|
||||
// controller.completion = { [weak self] in
|
||||
// guard let self else {
|
||||
// return
|
||||
// }
|
||||
// let _ = self
|
||||
// /*if let strongSelf = self {
|
||||
// strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
|
||||
// $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) }
|
||||
// })
|
||||
// strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory()
|
||||
// }*/
|
||||
// }
|
||||
// completion(controller, controller.mediaPickerContext)
|
||||
// self.controllerNavigationDisposable.set(nil)
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
12
submodules/TelegramUI/Images.xcassets/Chat/Info/Location.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Info/Location.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "location.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
137
submodules/TelegramUI/Images.xcassets/Chat/Info/Location.imageset/location.pdf
vendored
Normal file
137
submodules/TelegramUI/Images.xcassets/Chat/Info/Location.imageset/location.pdf
vendored
Normal file
@ -0,0 +1,137 @@
|
||||
%PDF-1.7
|
||||
|
||||
1 0 obj
|
||||
<< /Filter /FlateDecode
|
||||
/Type /XObject
|
||||
/Length 2 0 R
|
||||
/Group << /Type /Group
|
||||
/S /Transparency
|
||||
>>
|
||||
/Subtype /Form
|
||||
/Resources << >>
|
||||
/BBox [ 0.000000 0.000000 30.000000 30.000000 ]
|
||||
>>
|
||||
stream
|
||||
xmTKŠc1Üû^Œ°å<C2B0>ìcÌ4³H74}è’ã'-’Jé©TÒ³ó2¥1Ý௡ל<C397>ë3$ʳ' »ŒZ2ÀLàrü¹¾<C2B9>Æsˆ˜ÉÔˆZ™I8•.(ÂoDäJ}¤Ö”tžFî”Ef)ñ‚ÑZjê#²P<C2B2>m!j©Î‘µÞ&,Ì©÷È“¸Hª
¥<>JžµŽX¬™Ëìv*gb|9‡¶æÞÙGpûκÿƒ³N?Çfü@žµ×¹Â¡þ®ãg>ÃY}Ï›1<E280BA>³ocës?{ÍÁø½›`¿´y°_îÚƒù·^nß(wo^Ë6ÉG0ïÆ™uc¶óÆ}ÜÆÍîùNíÇë- £›èk›W+·ã|úl<C3BA>ð?|„¯ð/|ã¨#‡ðâ7wRàLÍñ‰Ð;iÙW wÒj¶ãJN&¹èBÀ(—MQ(ã‚÷vPY7¢<37>1¶S¯¬*s#Þ‚*³KœÂA¤Ò <C392>Kn¤—sêèk¤ùk8˜¬b;ÅHíÇ<C3AD>¹°áÍ–8Ÿ äÖGçrz"¬"‘þ³UÌåÜáä`ëS¦AfÑ¥¤ú\ ”þ+-i6#ëà9Ssêhë¤ù35›Â𣸸³Ù/ɬ#=
|
||||
endstream
|
||||
endobj
|
||||
|
||||
2 0 obj
|
||||
470
|
||||
endobj
|
||||
|
||||
3 0 obj
|
||||
<< /Type /XObject
|
||||
/Length 4 0 R
|
||||
/Group << /Type /Group
|
||||
/S /Transparency
|
||||
>>
|
||||
/Subtype /Form
|
||||
/Resources << >>
|
||||
/BBox [ 0.000000 0.000000 30.000000 30.000000 ]
|
||||
>>
|
||||
stream
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm
|
||||
0.000000 0.000000 0.000000 scn
|
||||
0.000000 18.799999 m
|
||||
0.000000 22.720367 0.000000 24.680552 0.762954 26.177933 c
|
||||
1.434068 27.495068 2.504932 28.565931 3.822066 29.237045 c
|
||||
5.319448 30.000000 7.279633 30.000000 11.200000 30.000000 c
|
||||
18.799999 30.000000 l
|
||||
22.720367 30.000000 24.680552 30.000000 26.177933 29.237045 c
|
||||
27.495068 28.565931 28.565931 27.495068 29.237045 26.177933 c
|
||||
30.000000 24.680552 30.000000 22.720367 30.000000 18.799999 c
|
||||
30.000000 11.200001 l
|
||||
30.000000 7.279633 30.000000 5.319448 29.237045 3.822067 c
|
||||
28.565931 2.504932 27.495068 1.434069 26.177933 0.762955 c
|
||||
24.680552 0.000000 22.720367 0.000000 18.799999 0.000000 c
|
||||
11.200000 0.000000 l
|
||||
7.279633 0.000000 5.319448 0.000000 3.822066 0.762955 c
|
||||
2.504932 1.434069 1.434068 2.504932 0.762954 3.822067 c
|
||||
0.000000 5.319448 0.000000 7.279633 0.000000 11.200001 c
|
||||
0.000000 18.799999 l
|
||||
h
|
||||
f
|
||||
n
|
||||
Q
|
||||
|
||||
endstream
|
||||
endobj
|
||||
|
||||
4 0 obj
|
||||
916
|
||||
endobj
|
||||
|
||||
5 0 obj
|
||||
<< /XObject << /X1 1 0 R >>
|
||||
/ExtGState << /E1 << /SMask << /Type /Mask
|
||||
/G 3 0 R
|
||||
/S /Alpha
|
||||
>>
|
||||
/Type /ExtGState
|
||||
>> >>
|
||||
>>
|
||||
endobj
|
||||
|
||||
6 0 obj
|
||||
<< /Length 7 0 R >>
|
||||
stream
|
||||
/DeviceRGB CS
|
||||
/DeviceRGB cs
|
||||
q
|
||||
/E1 gs
|
||||
/X1 Do
|
||||
Q
|
||||
|
||||
endstream
|
||||
endobj
|
||||
|
||||
7 0 obj
|
||||
46
|
||||
endobj
|
||||
|
||||
8 0 obj
|
||||
<< /Annots []
|
||||
/Type /Page
|
||||
/MediaBox [ 0.000000 0.000000 30.000000 30.000000 ]
|
||||
/Resources 5 0 R
|
||||
/Contents 6 0 R
|
||||
/Parent 9 0 R
|
||||
>>
|
||||
endobj
|
||||
|
||||
9 0 obj
|
||||
<< /Kids [ 8 0 R ]
|
||||
/Count 1
|
||||
/Type /Pages
|
||||
>>
|
||||
endobj
|
||||
|
||||
10 0 obj
|
||||
<< /Pages 9 0 R
|
||||
/Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
|
||||
xref
|
||||
0 11
|
||||
0000000000 65535 f
|
||||
0000000010 00000 n
|
||||
0000000754 00000 n
|
||||
0000000776 00000 n
|
||||
0000001940 00000 n
|
||||
0000001962 00000 n
|
||||
0000002260 00000 n
|
||||
0000002362 00000 n
|
||||
0000002383 00000 n
|
||||
0000002556 00000 n
|
||||
0000002630 00000 n
|
||||
trailer
|
||||
<< /ID [ (some) (id) ]
|
||||
/Root 10 0 R
|
||||
/Size 11
|
||||
>>
|
||||
startxref
|
||||
2690
|
||||
%%EOF
|
12
submodules/TelegramUI/Images.xcassets/Chat/Info/Status.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Info/Status.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "status (3).pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Chat/Info/Status.imageset/status (3).pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Chat/Info/Status.imageset/status (3).pdf
vendored
Normal file
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Instant View/MinimizeArrow.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Instant View/MinimizeArrow.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "miniappminimize_30.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
92
submodules/TelegramUI/Images.xcassets/Instant View/MinimizeArrow.imageset/miniappminimize_30.pdf
vendored
Normal file
92
submodules/TelegramUI/Images.xcassets/Instant View/MinimizeArrow.imageset/miniappminimize_30.pdf
vendored
Normal file
@ -0,0 +1,92 @@
|
||||
%PDF-1.7
|
||||
|
||||
1 0 obj
|
||||
<< >>
|
||||
endobj
|
||||
|
||||
2 0 obj
|
||||
<< /Length 3 0 R >>
|
||||
stream
|
||||
/DeviceRGB CS
|
||||
/DeviceRGB cs
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 8.500000 8.804749 cm
|
||||
0.000000 0.000000 0.000000 scn
|
||||
0.707107 9.402358 m
|
||||
0.316583 9.792883 -0.316583 9.792883 -0.707107 9.402358 c
|
||||
-1.097631 9.011834 -1.097631 8.378669 -0.707107 7.988145 c
|
||||
0.707107 9.402358 l
|
||||
h
|
||||
6.500000 2.195251 m
|
||||
5.792893 1.488145 l
|
||||
6.183417 1.097620 6.816583 1.097620 7.207107 1.488145 c
|
||||
6.500000 2.195251 l
|
||||
h
|
||||
13.707107 7.988145 m
|
||||
14.097631 8.378669 14.097631 9.011834 13.707107 9.402358 c
|
||||
13.316583 9.792883 12.683417 9.792883 12.292893 9.402358 c
|
||||
13.707107 7.988145 l
|
||||
h
|
||||
-0.707107 7.988145 m
|
||||
5.792893 1.488145 l
|
||||
7.207107 2.902358 l
|
||||
0.707107 9.402358 l
|
||||
-0.707107 7.988145 l
|
||||
h
|
||||
7.207107 1.488145 m
|
||||
13.707107 7.988145 l
|
||||
12.292893 9.402358 l
|
||||
5.792893 2.902358 l
|
||||
7.207107 1.488145 l
|
||||
h
|
||||
f
|
||||
n
|
||||
Q
|
||||
|
||||
endstream
|
||||
endobj
|
||||
|
||||
3 0 obj
|
||||
772
|
||||
endobj
|
||||
|
||||
4 0 obj
|
||||
<< /Annots []
|
||||
/Type /Page
|
||||
/MediaBox [ 0.000000 0.000000 30.000000 30.000000 ]
|
||||
/Resources 1 0 R
|
||||
/Contents 2 0 R
|
||||
/Parent 5 0 R
|
||||
>>
|
||||
endobj
|
||||
|
||||
5 0 obj
|
||||
<< /Kids [ 4 0 R ]
|
||||
/Count 1
|
||||
/Type /Pages
|
||||
>>
|
||||
endobj
|
||||
|
||||
6 0 obj
|
||||
<< /Pages 5 0 R
|
||||
/Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
|
||||
xref
|
||||
0 7
|
||||
0000000000 65535 f
|
||||
0000000010 00000 n
|
||||
0000000034 00000 n
|
||||
0000000862 00000 n
|
||||
0000000884 00000 n
|
||||
0000001057 00000 n
|
||||
0000001131 00000 n
|
||||
trailer
|
||||
<< /ID [ (some) (id) ]
|
||||
/Root 6 0 R
|
||||
/Size 7
|
||||
>>
|
||||
startxref
|
||||
1190
|
||||
%%EOF
|
12
submodules/TelegramUI/Images.xcassets/Instant View/Verified.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Instant View/Verified.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "miniappverify_14.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Instant View/Verified.imageset/miniappverify_14.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Instant View/Verified.imageset/miniappverify_14.pdf
vendored
Normal file
Binary file not shown.
@ -0,0 +1 @@
|
||||
{"nm":"Main Scene","ddd":0,"h":300,"w":300,"meta":{"g":"@lottiefiles/creator 1.31.1"},"layers":[{"ty":4,"nm":"more","sr":1,"st":-38,"op":47,"ip":-35,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[150,150],"ix":1},"s":{"a":0,"k":[100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[150,150],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"3","ix":2,"cix":2,"np":2,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[-8.284,0],[0,8.284],[8.284,0],[0,-8.284]],"o":[[8.284,0],[0,-8.284],[-8.284,0],[0,8.284]],"v":[[50,15],[65,0],[50,-15],[35,0]]},"ix":2}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[1,1,1],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[50,0],"ix":1},"s":{"a":1,"k":[{"o":{"x":0.5,"y":0},"i":{"x":0.5,"y":1},"s":[100,100],"t":15},{"o":{"x":0.5,"y":0},"i":{"x":0.5,"y":1},"s":[125,125],"t":25},{"o":{"x":0.5,"y":0},"i":{"x":0.5,"y":1},"s":[95,95],"t":35},{"s":[100,100],"t":45}],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":1,"k":[{"o":{"x":0.5,"y":0},"i":{"x":0.5,"y":1},"s":[200,150],"t":10},{"o":{"x":0.5,"y":0},"i":{"x":0.5,"y":1},"s":[200,120],"t":20},{"o":{"x":0.5,"y":0},"i":{"x":0.5,"y":1},"s":[200,162],"t":30},{"s":[200,150],"t":40}],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]},{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"2","ix":3,"cix":2,"np":2,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 2","ix":1,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[-8.284,0],[0,8.284],[8.284,0],[0,-8.284]],"o":[[8.284,0],[0,-8.284],[-8.284,0],[0,8.284]],"v":[[0.5,15],[15.5,0],[0.5,-15],[-14.5,0]]},"ix":2}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[1,1,1],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"o":{"x":0.5,"y":0},"i":{"x":0.5,"y":1},"s":[100,100],"t":10},{"o":{"x":0.5,"y":0},"i":{"x":0.5,"y":1},"s":[125,125],"t":20},{"o":{"x":0.5,"y":0},"i":{"x":0.5,"y":1},"s":[95,95],"t":30},{"s":[100,100],"t":40}],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":1,"k":[{"o":{"x":0.5,"y":0},"i":{"x":0.5,"y":1},"s":[150,150],"t":5},{"o":{"x":0.5,"y":0},"i":{"x":0.5,"y":1},"s":[150,120],"t":15},{"o":{"x":0.5,"y":0},"i":{"x":0.5,"y":1},"s":[150,162],"t":25},{"s":[150,150],"t":35}],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]},{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"1","ix":4,"cix":2,"np":2,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 3","ix":1,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[-8.284,0],[0,8.284],[8.284,0],[0,-8.284]],"o":[[8.284,0],[0,-8.284],[-8.284,0],[0,8.284]],"v":[[-49,15],[-34,0],[-49,-15],[-64,0]]},"ix":2}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[1,1,1],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[-50,0],"ix":1},"s":{"a":1,"k":[{"o":{"x":0.5,"y":0},"i":{"x":0.5,"y":1},"s":[100,100],"t":5},{"o":{"x":0.5,"y":0},"i":{"x":0.5,"y":1},"s":[125,125],"t":15},{"o":{"x":0.5,"y":0},"i":{"x":0.5,"y":1},"s":[95,95],"t":25},{"s":[100,100],"t":35}],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":1,"k":[{"o":{"x":0.5,"y":0},"i":{"x":0.5,"y":1},"s":[100,150],"t":0},{"o":{"x":0.5,"y":0},"i":{"x":0.5,"y":1},"s":[100,120],"t":10},{"o":{"x":0.5,"y":0},"i":{"x":0.5,"y":1},"s":[100,162],"t":20},{"s":[100,150],"t":30}],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":1}],"v":"5.7.0","fr":60,"op":60,"ip":0,"assets":[]}
|
@ -0,0 +1 @@
|
||||
{"nm":"Main Scene","ddd":0,"h":99,"w":99,"meta":{"g":"@lottiefiles/creator 1.31.1"},"layers":[{"ty":4,"nm":"Artboard Copy 3 Outlines","sr":1,"st":0,"op":60,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[49.5,49.5,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[49.5,49.5],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Group 1","ix":1,"cix":2,"np":3,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":1,"k":[{"o":{"x":0.2,"y":0},"i":{"x":0.2,"y":1},"s":[{"c":false,"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-18.672,-16.5],[-2.172,0],[-18.672,16.5]]}],"t":0},{"o":{"x":0.8,"y":0},"i":{"x":0.8,"y":1},"s":[{"c":false,"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-8.25,-16.5],[8.25,0],[-8.25,16.5]]}],"t":15},{"o":{"x":0.2,"y":0},"i":{"x":0.2,"y":1},"s":[{"c":false,"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-8.25,-16.5],[8.25,0],[-8.25,16.5]]}],"t":30},{"s":[{"c":false,"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-18.672,-16.5],[-2.172,0],[-18.672,16.5]]}],"t":45}],"ix":2}},{"ty":"tm","bm":0,"hd":false,"mn":"ADBE Vector Filter - Trim","nm":"Trim Paths 1","ix":2,"e":{"a":1,"k":[{"o":{"x":0.2,"y":0},"i":{"x":0.2,"y":1},"s":[50],"t":0},{"o":{"x":0.8,"y":0},"i":{"x":0.8,"y":1},"s":[100],"t":15},{"o":{"x":0.2,"y":0},"i":{"x":0.2,"y":1},"s":[100],"t":30},{"s":[50],"t":45}],"ix":2},"o":{"a":0,"k":0,"ix":3},"s":{"a":1,"k":[{"o":{"x":0.2,"y":0},"i":{"x":0.2,"y":1},"s":[50],"t":0},{"o":{"x":0.8,"y":0},"i":{"x":0.8,"y":1},"s":[0],"t":15},{"o":{"x":0.2,"y":0},"i":{"x":0.2,"y":1},"s":[0],"t":30},{"s":[50],"t":45}],"ix":1},"m":1},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":2,"lj":2,"ml":1,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":6,"ix":5},"c":{"a":0,"k":[1,1,1],"ix":3}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[41.25,49.5],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]},{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Group 2","ix":2,"cix":2,"np":2,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":1,"k":[{"o":{"x":0.2,"y":0},"i":{"x":0.2,"y":1},"s":[{"c":false,"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-1.5,-17.25],[-18.75,0],[-1.5,17.25]]}],"t":0},{"o":{"x":0.8,"y":0},"i":{"x":0.8,"y":1},"s":[{"c":false,"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[8.25,-16.5],[-8.25,0],[8.25,16.5]]}],"t":15},{"o":{"x":0.2,"y":0},"i":{"x":0.2,"y":1},"s":[{"c":false,"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[8.25,-16.5],[-8.25,0],[8.25,16.5]]}],"t":30},{"s":[{"c":false,"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-1.5,-17.25],[-18.75,0],[-1.5,17.25]]}],"t":45}],"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":2,"lj":2,"ml":1,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":6,"ix":5},"c":{"a":0,"k":[1,1,1],"ix":3}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[57.75,49.5],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":1}],"v":"5.7.0","fr":60,"op":60,"ip":0,"assets":[]}
|
@ -103,12 +103,18 @@ func openWebAppImpl(context: AccountContext, parentController: ViewController, u
|
||||
}
|
||||
|
||||
var fullSize = false
|
||||
var isFullscreen = false
|
||||
if isTelegramMeLink(url), let internalUrl = parseFullInternalUrl(sharedContext: context.sharedContext, url: url), case .peer(_, .appStart) = internalUrl {
|
||||
fullSize = !url.contains("?mode=compact")
|
||||
if url.contains("&mode=fullscreen") {
|
||||
isFullscreen = true
|
||||
fullSize = true
|
||||
} else {
|
||||
fullSize = !url.contains("?mode=compact")
|
||||
}
|
||||
}
|
||||
|
||||
var presentImpl: ((ViewController, Any?) -> Void)?
|
||||
let params = WebAppParameters(source: .menu, peerId: peer.id, botId: peer.id, botName: botName, botVerified: botVerified, url: url, queryId: nil, payload: nil, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false, fullSize: fullSize)
|
||||
let params = WebAppParameters(source: .menu, peerId: peer.id, botId: peer.id, botName: botName, botVerified: botVerified, url: url, queryId: nil, payload: nil, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false, fullSize: fullSize, isFullscreen: isFullscreen)
|
||||
let controller = standaloneWebAppController(context: context, updatedPresentationData: updatedPresentationData, params: params, threadId: threadId, openUrl: { [weak parentController] url, concealed, forceUpdate, commit in
|
||||
ChatControllerImpl.botOpenUrl(context: context, peerId: peer.id, controller: parentController as? ChatControllerImpl, url: url, concealed: concealed, forceUpdate: forceUpdate, present: { c, a in
|
||||
presentImpl?(c, a)
|
||||
@ -196,7 +202,7 @@ func openWebAppImpl(context: AccountContext, parentController: ViewController, u
|
||||
} else {
|
||||
source = url.isEmpty ? .generic : .simple
|
||||
}
|
||||
let params = WebAppParameters(source: source, peerId: peer.id, botId: botId, botName: botName, botVerified: botVerified, url: result.url, queryId: nil, payload: payload, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false, fullSize: result.flags.contains(.fullSize))
|
||||
let params = WebAppParameters(source: source, peerId: peer.id, botId: botId, botName: botName, botVerified: botVerified, url: result.url, queryId: nil, payload: payload, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false, fullSize: result.flags.contains(.fullSize), isFullscreen: result.flags.contains(.fullScreen))
|
||||
let controller = standaloneWebAppController(context: context, updatedPresentationData: updatedPresentationData, params: params, threadId: threadId, openUrl: { [weak parentController] url, concealed, forceUpdate, commit in
|
||||
ChatControllerImpl.botOpenUrl(context: context, peerId: peer.id, controller: parentController as? ChatControllerImpl, url: url, concealed: concealed, forceUpdate: forceUpdate, present: { c, a in
|
||||
presentImpl?(c, a)
|
||||
@ -242,7 +248,7 @@ func openWebAppImpl(context: AccountContext, parentController: ViewController, u
|
||||
return
|
||||
}
|
||||
var presentImpl: ((ViewController, Any?) -> Void)?
|
||||
let params = WebAppParameters(source: .button, peerId: peer.id, botId: peer.id, botName: botName, botVerified: botVerified, url: result.url, queryId: result.queryId, payload: nil, buttonText: buttonText, keepAliveSignal: result.keepAliveSignal, forceHasSettings: false, fullSize: result.flags.contains(.fullSize))
|
||||
let params = WebAppParameters(source: .button, peerId: peer.id, botId: peer.id, botName: botName, botVerified: botVerified, url: result.url, queryId: result.queryId, payload: nil, buttonText: buttonText, keepAliveSignal: result.keepAliveSignal, forceHasSettings: false, fullSize: result.flags.contains(.fullSize), isFullscreen: result.flags.contains(.fullScreen))
|
||||
let controller = standaloneWebAppController(context: context, updatedPresentationData: updatedPresentationData, params: params, threadId: threadId, openUrl: { [weak parentController] url, concealed, forceUpdate, commit in
|
||||
ChatControllerImpl.botOpenUrl(context: context, peerId: peer.id, controller: parentController as? ChatControllerImpl, url: url, concealed: concealed, forceUpdate: forceUpdate, present: { c, a in
|
||||
presentImpl?(c, a)
|
||||
@ -472,7 +478,7 @@ public extension ChatControllerImpl {
|
||||
return
|
||||
}
|
||||
let context = strongSelf.context
|
||||
let params = WebAppParameters(source: .generic, peerId: peerId, botId: botPeer.id, botName: botApp.title, botVerified: botPeer.isVerified, url: result.url, queryId: 0, payload: payload, buttonText: "", keepAliveSignal: nil, forceHasSettings: botApp.flags.contains(.hasSettings), fullSize: result.flags.contains(.fullSize))
|
||||
let params = WebAppParameters(source: .generic, peerId: peerId, botId: botPeer.id, botName: botApp.title, botVerified: botPeer.isVerified, url: result.url, queryId: 0, payload: payload, buttonText: "", keepAliveSignal: nil, forceHasSettings: botApp.flags.contains(.hasSettings), fullSize: result.flags.contains(.fullSize), isFullscreen: result.flags.contains(.fullScreen))
|
||||
var presentImpl: ((ViewController, Any?) -> Void)?
|
||||
let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, threadId: strongSelf.chatLocation.threadId, openUrl: { [weak self] url, concealed, forceUpdate, commit in
|
||||
ChatControllerImpl.botOpenUrl(context: context, peerId: peerId, controller: self, url: url, concealed: concealed, forceUpdate: forceUpdate, present: { c, a in
|
||||
|
@ -613,7 +613,7 @@ extension ChatControllerImpl {
|
||||
payload = botPayload
|
||||
fromAttachMenu = false
|
||||
}
|
||||
let params = WebAppParameters(source: fromAttachMenu ? .attachMenu : .generic, peerId: peer.id, botId: bot.peer.id, botName: bot.shortName, botVerified: bot.peer.isVerified, url: nil, queryId: nil, payload: payload, buttonText: nil, keepAliveSignal: nil, forceHasSettings: false, fullSize: false)
|
||||
let params = WebAppParameters(source: fromAttachMenu ? .attachMenu : .generic, peerId: peer.id, botId: bot.peer.id, botName: bot.shortName, botVerified: bot.peer.isVerified, url: nil, queryId: nil, payload: payload, buttonText: nil, keepAliveSignal: nil, forceHasSettings: false, fullSize: false, isFullscreen: false)
|
||||
let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject
|
||||
let controller = WebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, replyToMessageId: replyMessageSubject?.messageId, threadId: strongSelf.chatLocation.threadId)
|
||||
controller.openUrl = { [weak self] url, concealed, forceUpdate, commit in
|
||||
|
@ -308,7 +308,7 @@ func openResolvedUrlImpl(
|
||||
id: subscriptionFormId,
|
||||
canSaveCredentials: false,
|
||||
passwordMissing: false,
|
||||
invoice: BotPaymentInvoice(isTest: false, requestedFields: [], currency: "XTR", prices: [BotPaymentPrice(label: "", amount: subscriptionPricing.amount)], tip: nil, termsInfo: nil),
|
||||
invoice: BotPaymentInvoice(isTest: false, requestedFields: [], currency: "XTR", prices: [BotPaymentPrice(label: "", amount: subscriptionPricing.amount)], tip: nil, termsInfo: nil, subscriptionPeriod: subscriptionPricing.period),
|
||||
paymentBotId: channel.id,
|
||||
providerId: nil,
|
||||
url: nil,
|
||||
|
@ -79,6 +79,7 @@ private enum ApplicationSpecificItemCacheCollectionIdValues: Int8 {
|
||||
case storySource = 11
|
||||
case mediaEditorState = 12
|
||||
case shareWithPeersState = 13
|
||||
case webAppPermissionsState = 14
|
||||
}
|
||||
|
||||
public struct ApplicationSpecificItemCacheCollectionId {
|
||||
@ -94,6 +95,7 @@ public struct ApplicationSpecificItemCacheCollectionId {
|
||||
public static let storySource = applicationSpecificItemCacheCollectionId(ApplicationSpecificItemCacheCollectionIdValues.storySource.rawValue)
|
||||
public static let mediaEditorState = applicationSpecificItemCacheCollectionId(ApplicationSpecificItemCacheCollectionIdValues.mediaEditorState.rawValue)
|
||||
public static let shareWithPeersState = applicationSpecificItemCacheCollectionId(ApplicationSpecificItemCacheCollectionIdValues.shareWithPeersState.rawValue)
|
||||
public static let webAppPermissionsState = applicationSpecificItemCacheCollectionId(ApplicationSpecificItemCacheCollectionIdValues.webAppPermissionsState.rawValue)
|
||||
}
|
||||
|
||||
private enum ApplicationSpecificOrderedItemListCollectionIdValues: Int32 {
|
||||
|
@ -41,6 +41,13 @@ swift_library(
|
||||
"//submodules/UndoUI",
|
||||
"//submodules/OverlayStatusController",
|
||||
"//submodules/TelegramUIPreferences",
|
||||
"//submodules/Components/LottieAnimationComponent",
|
||||
"//submodules/TelegramUI/Components/EmojiTextAttachmentView",
|
||||
"//submodules/TelegramUI/Components/Gifts/GiftAnimationComponent",
|
||||
"//submodules/TelegramUI/Components/ListItemComponentAdaptor",
|
||||
"//submodules/TelegramUI/Components/ListSectionComponent",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatMessageItemImpl",
|
||||
"//submodules/DeviceLocationManager",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
352
submodules/WebUI/Sources/FullscreenControlsComponent.swift
Normal file
352
submodules/WebUI/Sources/FullscreenControlsComponent.swift
Normal file
@ -0,0 +1,352 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import ComponentFlow
|
||||
import BundleIconComponent
|
||||
import MultilineTextComponent
|
||||
import MoreButtonNode
|
||||
import AccountContext
|
||||
import TelegramPresentationData
|
||||
import LottieAnimationComponent
|
||||
|
||||
final class FullscreenControlsComponent: Component {
|
||||
let context: AccountContext
|
||||
let title: String
|
||||
let isVerified: Bool
|
||||
let insets: UIEdgeInsets
|
||||
var hasBack: Bool
|
||||
let backPressed: () -> Void
|
||||
let minimizePressed: () -> Void
|
||||
let morePressed: (ASDisplayNode, ContextGesture?) -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
title: String,
|
||||
isVerified: Bool,
|
||||
insets: UIEdgeInsets,
|
||||
hasBack: Bool,
|
||||
backPressed: @escaping () -> Void,
|
||||
minimizePressed: @escaping () -> Void,
|
||||
morePressed: @escaping (ASDisplayNode, ContextGesture?) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.title = title
|
||||
self.isVerified = isVerified
|
||||
self.insets = insets
|
||||
self.hasBack = hasBack
|
||||
self.backPressed = backPressed
|
||||
self.minimizePressed = minimizePressed
|
||||
self.morePressed = morePressed
|
||||
}
|
||||
|
||||
static func ==(lhs: FullscreenControlsComponent, rhs: FullscreenControlsComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.title != rhs.title {
|
||||
return false
|
||||
}
|
||||
if lhs.isVerified != rhs.isVerified {
|
||||
return false
|
||||
}
|
||||
if lhs.insets != rhs.insets {
|
||||
return false
|
||||
}
|
||||
if lhs.hasBack != rhs.hasBack {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private let leftBackgroundView: BlurredBackgroundView
|
||||
private let rightBackgroundView: BlurredBackgroundView
|
||||
|
||||
private let closeIcon = ComponentView<Empty>()
|
||||
private let leftButton = HighlightTrackingButton()
|
||||
|
||||
private let titleClippingView = UIView()
|
||||
private let title = ComponentView<Empty>()
|
||||
private let credibility = ComponentView<Empty>()
|
||||
private let buttonTitle = ComponentView<Empty>()
|
||||
private let minimizeButton = ComponentView<Empty>()
|
||||
private let moreNode = MoreButtonNode(theme: defaultPresentationTheme, size: CGSize(width: 36.0, height: 36.0), encircled: false)
|
||||
|
||||
private var displayTitle = true
|
||||
private var timer: Timer?
|
||||
|
||||
private var component: FullscreenControlsComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.leftBackgroundView = BlurredBackgroundView(color: nil)
|
||||
self.rightBackgroundView = BlurredBackgroundView(color: nil)
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.titleClippingView.clipsToBounds = true
|
||||
self.titleClippingView.isUserInteractionEnabled = false
|
||||
|
||||
self.leftBackgroundView.clipsToBounds = true
|
||||
self.addSubview(self.leftBackgroundView)
|
||||
self.addSubview(self.leftButton)
|
||||
|
||||
self.addSubview(self.titleClippingView)
|
||||
|
||||
self.rightBackgroundView.clipsToBounds = true
|
||||
self.addSubview(self.rightBackgroundView)
|
||||
|
||||
self.addSubview(self.moreNode.view)
|
||||
|
||||
self.moreNode.updateColor(.white, transition: .immediate)
|
||||
|
||||
self.leftButton.highligthedChanged = { [weak self] highlighted in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if highlighted {
|
||||
if let view = self.closeIcon.view {
|
||||
view.layer.removeAnimation(forKey: "opacity")
|
||||
view.alpha = 0.6
|
||||
}
|
||||
if let view = self.buttonTitle.view {
|
||||
view.layer.removeAnimation(forKey: "opacity")
|
||||
view.alpha = 0.6
|
||||
}
|
||||
} else {
|
||||
if let view = self.closeIcon.view {
|
||||
view.alpha = 1.0
|
||||
view.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2)
|
||||
}
|
||||
if let view = self.buttonTitle.view {
|
||||
view.alpha = 1.0
|
||||
view.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
}
|
||||
self.leftButton.addTarget(self, action: #selector(self.closePressed), for: .touchUpInside)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.timer?.invalidate()
|
||||
}
|
||||
|
||||
@objc private func closePressed() {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.backPressed()
|
||||
}
|
||||
|
||||
@objc private func timerEvent() {
|
||||
self.timer?.invalidate()
|
||||
self.timer = nil
|
||||
|
||||
self.displayTitle = false
|
||||
self.state?.updated(transition: .spring(duration: 0.3))
|
||||
}
|
||||
|
||||
func update(component: FullscreenControlsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
let isFirstTime = self.component == nil
|
||||
let previousComponent = self.component
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let sideInset: CGFloat = 16.0
|
||||
let leftBackgroundSize = CGSize(width: 30.0, height: 30.0)
|
||||
let rightBackgroundSize = CGSize(width: 72.0, height: 30.0)
|
||||
|
||||
self.leftBackgroundView.updateColor(color: UIColor(white: 0.67, alpha: 0.35), transition: transition.containedViewLayoutTransition)
|
||||
self.rightBackgroundView.updateColor(color: UIColor(white: 0.67, alpha: 0.35), transition: transition.containedViewLayoutTransition)
|
||||
|
||||
let rightBackgroundFrame = CGRect(origin: CGPoint(x: availableSize.width - component.insets.right - sideInset - rightBackgroundSize.width, y: 0.0), size: rightBackgroundSize)
|
||||
self.rightBackgroundView.update(size: rightBackgroundSize, cornerRadius: rightBackgroundFrame.height / 2.0, transition: transition.containedViewLayoutTransition)
|
||||
transition.setFrame(view: self.rightBackgroundView, frame: rightBackgroundFrame)
|
||||
|
||||
var isAnimatingTextTransition = false
|
||||
|
||||
var additionalLeftWidth: CGFloat = 0.0
|
||||
let titleSize = self.title.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: component.title, font: Font.with(size: 13.0, design: .round, weight: .semibold), textColor: .white)))),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
let titleFrame = CGRect(origin: CGPoint(x: self.displayTitle ? 3.0 : -titleSize.width - 15.0, y: floorToScreenPixels((leftBackgroundSize.height - titleSize.height) / 2.0)), size: titleSize)
|
||||
if let view = self.title.view {
|
||||
if view.superview == nil {
|
||||
self.titleClippingView.addSubview(view)
|
||||
}
|
||||
|
||||
if !view.alpha.isZero && !self.displayTitle {
|
||||
isAnimatingTextTransition = true
|
||||
}
|
||||
|
||||
transition.setFrame(view: view, frame: titleFrame)
|
||||
transition.setAlpha(view: view, alpha: self.displayTitle ? 1.0 : 0.0)
|
||||
}
|
||||
|
||||
let buttonTitleUpdated = (previousComponent?.hasBack ?? false) != component.hasBack
|
||||
let animationMultiplier = !component.hasBack ? -1.0 : 1.0
|
||||
if buttonTitleUpdated {
|
||||
isAnimatingTextTransition = true
|
||||
|
||||
if let view = self.buttonTitle.view, let snapshotView = view.snapshotView(afterScreenUpdates: false) {
|
||||
snapshotView.frame = view.frame
|
||||
self.titleClippingView.addSubview(snapshotView)
|
||||
snapshotView.layer.animatePosition(from: .zero, to: CGPoint(x: -(snapshotView.frame.width * 1.5) * animationMultiplier, y: 0.0), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true, completion: { _ in
|
||||
snapshotView.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let buttonTitleSize = self.buttonTitle.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: component.hasBack ? "Back" : "Close", font: Font.with(size: 13.0, design: .round, weight: .semibold), textColor: .white)))),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
|
||||
if self.displayTitle {
|
||||
additionalLeftWidth += titleSize.width + 10.0
|
||||
} else {
|
||||
additionalLeftWidth += buttonTitleSize.width + 10.0
|
||||
}
|
||||
|
||||
let buttonTitleFrame = CGRect(origin: CGPoint(x: self.displayTitle ? leftBackgroundSize.width + additionalLeftWidth + 3.0 : 3.0, y: floorToScreenPixels((leftBackgroundSize.height - buttonTitleSize.height) / 2.0)), size: buttonTitleSize)
|
||||
if let view = self.buttonTitle.view {
|
||||
if view.superview == nil {
|
||||
self.titleClippingView.addSubview(view)
|
||||
}
|
||||
transition.setFrame(view: view, frame: buttonTitleFrame)
|
||||
|
||||
if buttonTitleUpdated {
|
||||
view.layer.animatePosition(from: CGPoint(x: (view.frame.width * 1.5) * animationMultiplier, y: 0.0), to: .zero, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||
}
|
||||
}
|
||||
|
||||
if component.isVerified {
|
||||
let credibilitySize = self.credibility.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(BundleIconComponent(name: "Instant View/Verified", tintColor: .white)),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
if let view = self.credibility.view {
|
||||
if view.superview == nil {
|
||||
view.alpha = 0.6
|
||||
self.titleClippingView.addSubview(view)
|
||||
}
|
||||
let credibilityFrame = CGRect(origin: CGPoint(x: titleFrame.maxX + 2.0, y: floorToScreenPixels((leftBackgroundSize.height - credibilitySize.height) / 2.0)), size: credibilitySize)
|
||||
transition.setFrame(view: view, frame: credibilityFrame)
|
||||
}
|
||||
if self.displayTitle {
|
||||
additionalLeftWidth += credibilitySize.width + 2.0
|
||||
}
|
||||
}
|
||||
|
||||
var leftBackgroundTransition = transition
|
||||
if buttonTitleUpdated {
|
||||
leftBackgroundTransition = .spring(duration: 0.3)
|
||||
}
|
||||
|
||||
let leftBackgroundFrame = CGRect(origin: CGPoint(x: sideInset + component.insets.left, y: 0.0), size: CGSize(width: leftBackgroundSize.width + additionalLeftWidth, height: leftBackgroundSize.height))
|
||||
self.leftBackgroundView.update(size: leftBackgroundFrame.size, cornerRadius: leftBackgroundSize.height / 2.0, transition: leftBackgroundTransition.containedViewLayoutTransition)
|
||||
leftBackgroundTransition.setFrame(view: self.leftBackgroundView, frame: leftBackgroundFrame)
|
||||
self.leftButton.frame = leftBackgroundFrame
|
||||
|
||||
if isAnimatingTextTransition, self.titleClippingView.mask == nil {
|
||||
if let maskImage = generateGradientImage(size: CGSize(width: 42.0, height: 10.0), colors: [UIColor.clear, UIColor.black, UIColor.black, UIColor.clear], locations: [0.0, 0.1, 0.9, 1.0], direction: .horizontal) {
|
||||
let maskView = UIImageView(image: maskImage.stretchableImage(withLeftCapWidth: 4, topCapHeight: 0))
|
||||
self.titleClippingView.mask = maskView
|
||||
maskView.frame = CGRect(origin: .zero, size: CGSize(width: self.titleClippingView.bounds.width, height: self.titleClippingView.bounds.height))
|
||||
}
|
||||
}
|
||||
|
||||
transition.setFrame(view: self.titleClippingView, frame: CGRect(origin: CGPoint(x: sideInset + component.insets.left + leftBackgroundSize.height - 3.0, y: 0.0), size: CGSize(width: leftBackgroundFrame.width - leftBackgroundSize.height, height: leftBackgroundSize.height)))
|
||||
if let maskView = self.titleClippingView.mask {
|
||||
leftBackgroundTransition.setFrame(view: maskView, frame: CGRect(origin: .zero, size: CGSize(width: self.titleClippingView.bounds.width, height: self.titleClippingView.bounds.height)), completion: { _ in
|
||||
self.titleClippingView.mask = nil
|
||||
})
|
||||
}
|
||||
|
||||
let backButtonSize = self.closeIcon.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
LottieAnimationComponent(
|
||||
animation: LottieAnimationComponent.AnimationItem(
|
||||
name: "web_backToCancel",
|
||||
mode: .animating(loop: false),
|
||||
range: component.hasBack ? (0.5, 1.0) : (0.0, 0.5)
|
||||
),
|
||||
colors: ["__allcolors__": .white],
|
||||
size: CGSize(width: 30.0, height: 30.0)
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 30.0, height: 30.0)
|
||||
)
|
||||
if let view = self.closeIcon.view {
|
||||
if view.superview == nil {
|
||||
view.isUserInteractionEnabled = false
|
||||
self.addSubview(view)
|
||||
}
|
||||
let buttonFrame = CGRect(origin: CGPoint(x: leftBackgroundFrame.minX, y: 0.0), size: backButtonSize)
|
||||
transition.setFrame(view: view, frame: buttonFrame)
|
||||
}
|
||||
|
||||
let minimizeButtonSize = self.minimizeButton.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(Button(
|
||||
content: AnyComponent(
|
||||
BundleIconComponent(name: "Instant View/MinimizeArrow", tintColor: .white)
|
||||
),
|
||||
action: { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.minimizePressed()
|
||||
}
|
||||
).minSize(CGSize(width: 30.0, height: 30.0))),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 30.0, height: 30.0)
|
||||
)
|
||||
if let view = self.minimizeButton.view {
|
||||
if view.superview == nil {
|
||||
self.addSubview(view)
|
||||
}
|
||||
let buttonFrame = CGRect(origin: CGPoint(x: rightBackgroundFrame.minX + 2.0, y: 0.0), size: minimizeButtonSize)
|
||||
transition.setFrame(view: view, frame: buttonFrame)
|
||||
}
|
||||
|
||||
transition.setFrame(view: self.moreNode.view, frame: CGRect(origin: CGPoint(x: rightBackgroundFrame.maxX - 42.0, y: -4.0), size: CGSize(width: 36.0, height: 36.0)))
|
||||
self.moreNode.action = { [weak self] node, gesture in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.morePressed(node, gesture)
|
||||
}
|
||||
|
||||
if isFirstTime {
|
||||
let timer = Timer(timeInterval: 2.5, target: self, selector: #selector(self.timerEvent), userInfo: nil, repeats: false)
|
||||
self.timer = timer
|
||||
RunLoop.main.add(timer, forMode: .common)
|
||||
}
|
||||
|
||||
return CGSize(width: availableSize.width, height: leftBackgroundSize.height)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ import AsyncDisplayKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import ComponentFlow
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import AttachmentUI
|
||||
@ -30,6 +31,8 @@ import UndoUI
|
||||
import AvatarNode
|
||||
import OverlayStatusController
|
||||
import TelegramUIPreferences
|
||||
import CoreMotion
|
||||
import DeviceLocationManager
|
||||
|
||||
private let durgerKingBotIds: [Int64] = [5104055776, 2200339955]
|
||||
|
||||
@ -64,6 +67,7 @@ public struct WebAppParameters {
|
||||
let keepAliveSignal: Signal<Never, KeepWebViewError>?
|
||||
let forceHasSettings: Bool
|
||||
let fullSize: Bool
|
||||
let isFullscreen: Bool
|
||||
|
||||
public init(
|
||||
source: Source,
|
||||
@ -77,7 +81,8 @@ public struct WebAppParameters {
|
||||
buttonText: String?,
|
||||
keepAliveSignal: Signal<Never, KeepWebViewError>?,
|
||||
forceHasSettings: Bool,
|
||||
fullSize: Bool
|
||||
fullSize: Bool,
|
||||
isFullscreen: Bool = false
|
||||
) {
|
||||
self.source = source
|
||||
self.peerId = peerId
|
||||
@ -91,6 +96,11 @@ public struct WebAppParameters {
|
||||
self.keepAliveSignal = keepAliveSignal
|
||||
self.forceHasSettings = forceHasSettings
|
||||
self.fullSize = fullSize
|
||||
// #if DEBUG
|
||||
// self.isFullscreen = true
|
||||
// #else
|
||||
self.isFullscreen = isFullscreen
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
|
||||
@ -136,6 +146,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
fileprivate var webView: WebAppWebView?
|
||||
private var placeholderIcon: (UIImage, Bool)?
|
||||
private var placeholderNode: ShimmerEffectNode?
|
||||
private var fullscreenControls: ComponentView<Empty>?
|
||||
|
||||
fileprivate let loadingProgressPromise = Promise<CGFloat?>(nil)
|
||||
|
||||
@ -158,6 +169,8 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
private var queryId: Int64?
|
||||
fileprivate let canMinimize = true
|
||||
|
||||
private var hasBackButton = false
|
||||
|
||||
private var placeholderDisposable = MetaDisposable()
|
||||
private var keepAliveDisposable: Disposable?
|
||||
private var paymentDisposable: Disposable?
|
||||
@ -180,7 +193,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.headerBackgroundNode = ASDisplayNode()
|
||||
self.topOverscrollNode = ASDisplayNode()
|
||||
|
||||
|
||||
super.init()
|
||||
|
||||
if self.presentationData.theme.list.plainBackgroundColor.rgb == 0x000000 {
|
||||
@ -321,6 +334,13 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
self.paymentDisposable?.dispose()
|
||||
|
||||
self.webView?.removeObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress))
|
||||
|
||||
if self.motionManager.isAccelerometerActive {
|
||||
self.motionManager.stopAccelerometerUpdates()
|
||||
}
|
||||
if self.motionManager.isGyroActive {
|
||||
self.motionManager.stopGyroUpdates()
|
||||
}
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
@ -593,7 +613,15 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
let previousLayout = self.validLayout?.0
|
||||
self.validLayout = (layout, navigationBarHeight)
|
||||
|
||||
|
||||
guard let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
|
||||
self.controller?.navigationBar?.alpha = controller.isFullscreen ? 0.0 : 1.0
|
||||
transition.updateAlpha(node: self.topOverscrollNode, alpha: controller.isFullscreen ? 0.0 : 1.0)
|
||||
transition.updateAlpha(node: self.headerBackgroundNode, alpha: controller.isFullscreen ? 0.0 : 1.0)
|
||||
|
||||
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: .zero, size: layout.size))
|
||||
transition.updateFrame(node: self.headerBackgroundNode, frame: CGRect(origin: .zero, size: CGSize(width: layout.size.width, height: navigationBarHeight)))
|
||||
transition.updateFrame(node: self.topOverscrollNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -1000.0), size: CGSize(width: layout.size.width, height: 1000.0)))
|
||||
@ -606,8 +634,10 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
scrollInset.bottom = 0.0
|
||||
}
|
||||
|
||||
let frame = CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight), size: CGSize(width: layout.size.width, height: max(1.0, layout.size.height - navigationBarHeight - frameBottomInset)))
|
||||
if !webView.frame.width.isZero && webView.frame != frame {
|
||||
let topInset: CGFloat = controller.isFullscreen ? 0.0 : navigationBarHeight
|
||||
|
||||
let webViewFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: layout.size.width, height: max(1.0, layout.size.height - topInset - frameBottomInset)))
|
||||
if !webView.frame.width.isZero && webView.frame != webViewFrame {
|
||||
self.updateWebViewWhenStable = true
|
||||
}
|
||||
|
||||
@ -615,7 +645,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
if let inputHeight = self.validLayout?.0.inputHeight, inputHeight > 44.0 {
|
||||
bottomInset = max(bottomInset, inputHeight)
|
||||
}
|
||||
let viewportFrame = CGRect(origin: CGPoint(x: layout.safeInsets.left, y: navigationBarHeight), size: CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right, height: max(1.0, layout.size.height - navigationBarHeight - bottomInset)))
|
||||
let viewportFrame = CGRect(origin: CGPoint(x: layout.safeInsets.left, y: topInset), size: CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right, height: max(1.0, layout.size.height - topInset - bottomInset)))
|
||||
|
||||
if webView.scrollView.contentInset != scrollInset {
|
||||
webView.scrollView.contentInset = scrollInset
|
||||
@ -628,16 +658,29 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
}, transition: transition)
|
||||
Queue.mainQueue().after(0.4, {
|
||||
if let inputHeight = self.validLayout?.0.inputHeight, inputHeight > 44.0 {
|
||||
transition.updateFrame(view: webView, frame: frame)
|
||||
transition.updateFrame(view: webView, frame: webViewFrame)
|
||||
Queue.mainQueue().after(0.1) {
|
||||
self.targetContentOffset = nil
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
transition.updateFrame(view: webView, frame: frame)
|
||||
transition.updateFrame(view: webView, frame: webViewFrame)
|
||||
}
|
||||
|
||||
var customInsets: UIEdgeInsets = .zero
|
||||
if controller.isFullscreen {
|
||||
customInsets.top = layout.statusBarHeight ?? 0.0
|
||||
}
|
||||
if layout.intrinsicInsets.bottom > 44.0 {
|
||||
customInsets.bottom = 0.0
|
||||
} else {
|
||||
customInsets.bottom = layout.intrinsicInsets.bottom
|
||||
}
|
||||
customInsets.left = layout.safeInsets.left
|
||||
customInsets.right = layout.safeInsets.left
|
||||
webView.customInsets = customInsets
|
||||
|
||||
if let controller = self.controller {
|
||||
webView.updateMetrics(height: viewportFrame.height, isExpanded: controller.isContainerExpanded(), isStable: !controller.isContainerPanning(), transition: transition)
|
||||
if self.updateWebViewWhenStable && !controller.isContainerPanning() {
|
||||
@ -645,13 +688,6 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
webView.setNeedsLayout()
|
||||
}
|
||||
}
|
||||
|
||||
if layout.intrinsicInsets.bottom > 44.0 {
|
||||
webView.customBottomInset = 0.0
|
||||
} else {
|
||||
webView.customBottomInset = layout.intrinsicInsets.bottom
|
||||
}
|
||||
webView.customSideInset = layout.safeInsets.left
|
||||
}
|
||||
|
||||
if let placeholderNode = self.placeholderNode {
|
||||
@ -674,6 +710,66 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
placeholderNode.updateAbsoluteRect(placeholderFrame, within: layout.size)
|
||||
}
|
||||
|
||||
if controller.isFullscreen {
|
||||
var added = false
|
||||
let fullscreenControls: ComponentView<Empty>
|
||||
if let current = self.fullscreenControls {
|
||||
fullscreenControls = current
|
||||
} else {
|
||||
fullscreenControls = ComponentView<Empty>()
|
||||
self.fullscreenControls = fullscreenControls
|
||||
added = true
|
||||
}
|
||||
|
||||
let componentTransition: ComponentTransition = added ? .immediate : ComponentTransition(transition)
|
||||
let controlsSize = fullscreenControls.update(
|
||||
transition: componentTransition,
|
||||
component: AnyComponent(
|
||||
FullscreenControlsComponent(
|
||||
context: self.context,
|
||||
title: controller.botName,
|
||||
isVerified: controller.botVerified,
|
||||
insets: UIEdgeInsets(top: 0.0, left: layout.safeInsets.left, bottom: 0.0, right: layout.safeInsets.right),
|
||||
hasBack: self.hasBackButton,
|
||||
backPressed: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.controller?.cancelPressed()
|
||||
},
|
||||
minimizePressed: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.controller?.requestMinimize(topEdgeOffset: nil, initialVelocity: nil)
|
||||
},
|
||||
morePressed: { [weak self] node, gesture in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.controller?.morePressed(node: node, gesture: gesture)
|
||||
}
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: layout.size
|
||||
)
|
||||
if let view = fullscreenControls.view {
|
||||
if view.superview == nil {
|
||||
self.view.addSubview(view)
|
||||
}
|
||||
transition.updateFrame(view: view, frame: CGRect(origin: CGPoint(x: 0.0, y: (layout.statusBarHeight ?? 0.0) + 8.0), size: controlsSize))
|
||||
if added {
|
||||
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||
}
|
||||
}
|
||||
} else if let fullscreenControls = self.fullscreenControls {
|
||||
self.fullscreenControls = nil
|
||||
fullscreenControls.view?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
|
||||
fullscreenControls.view?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
|
||||
if let previousLayout = previousLayout, (previousLayout.inputHeight ?? 0.0).isZero, let inputHeight = layout.inputHeight, inputHeight > 44.0 {
|
||||
Queue.mainQueue().justDispatch {
|
||||
self.controller?.requestAttachmentMenuExpansion()
|
||||
@ -691,6 +787,12 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
|
||||
private weak var currentQrCodeScannerScreen: QrCodeScanScreen?
|
||||
|
||||
func requestLayout(transition: ContainedViewLayoutTransition) {
|
||||
if let (layout, navigationBarHeight) = self.validLayout {
|
||||
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
private var delayedScriptMessages: [WKScriptMessage] = []
|
||||
private func handleScriptMessage(_ message: WKScriptMessage) {
|
||||
guard let controller = self.controller else {
|
||||
@ -786,9 +888,9 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
}
|
||||
}
|
||||
case "web_app_request_viewport":
|
||||
if let (layout, navigationBarHeight) = self.validLayout {
|
||||
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
|
||||
}
|
||||
self.requestLayout(transition: .immediate)
|
||||
case "web_app_request_safe_area":
|
||||
self.requestLayout(transition: .immediate)
|
||||
case "web_app_request_theme":
|
||||
self.sendThemeChangedEvent()
|
||||
case "web_app_expand":
|
||||
@ -939,7 +1041,11 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
}
|
||||
case "web_app_setup_back_button":
|
||||
if let json = json, let isVisible = json["is_visible"] as? Bool {
|
||||
self.hasBackButton = isVisible
|
||||
self.controller?.cancelButtonNode.setState(isVisible ? .back : .cancel, animated: true)
|
||||
if controller.isFullscreen {
|
||||
self.requestLayout(transition: .immediate)
|
||||
}
|
||||
}
|
||||
case "web_app_trigger_haptic_feedback":
|
||||
if let json = json, let type = json["type"] as? String {
|
||||
@ -1234,6 +1340,43 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
}
|
||||
})
|
||||
}
|
||||
case "web_app_request_fullscreen":
|
||||
self.setIsFullscreen(true)
|
||||
case "web_app_exit_fullscreen":
|
||||
self.setIsFullscreen(false)
|
||||
case "web_app_start_accelerometer":
|
||||
if let json = json, let refreshRate = json["refresh_rate"] as? Double {
|
||||
self.setIsAccelerometerActive(true, refreshRate: refreshRate)
|
||||
}
|
||||
case "web_app_stop_accelerometer":
|
||||
self.setIsAccelerometerActive(false)
|
||||
case "web_app_start_device_orientation":
|
||||
if let json = json, let refreshRate = json["refresh_rate"] as? Double {
|
||||
self.setIsDeviceOrientationActive(true, refreshRate: refreshRate)
|
||||
}
|
||||
case "web_app_stop_device_orientation":
|
||||
self.setIsDeviceOrientationActive(false)
|
||||
case "web_app_start_gyroscope":
|
||||
if let json = json, let refreshRate = json["refresh_rate"] as? Double {
|
||||
self.setIsGyroscopeActive(true, refreshRate: refreshRate)
|
||||
}
|
||||
case "web_app_stop_gyroscope":
|
||||
self.setIsGyroscopeActive(false)
|
||||
case "web_app_set_emoji_status":
|
||||
if let json = json, let emojiIdString = json["custom_emoji_id"] as? String, let emojiId = Int64(emojiIdString) {
|
||||
let expirationDate = json["expiration_date"] as? Double
|
||||
self.setEmojiStatus(emojiId, expirationDate: expirationDate.flatMap { Int32($0) })
|
||||
}
|
||||
case "web_app_add_to_home_screen":
|
||||
self.addToHomeScreen()
|
||||
case "web_app_check_home_screen":
|
||||
self.webView?.sendEvent(name: "home_screen_checked", data: "{status: \"unknown\"}")
|
||||
case "web_app_request_location":
|
||||
self.requestLocation()
|
||||
case "web_app_check_location":
|
||||
self.checkLocation()
|
||||
case "web_app_open_location_settings":
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
@ -1469,11 +1612,16 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
}
|
||||
|
||||
fileprivate func shareAccountContact() {
|
||||
guard let controller = self.controller, let botId = self.controller?.botId, let botName = self.controller?.botName else {
|
||||
if "".isEmpty, let controller = self.controller {
|
||||
let previewController = WebAppMessagePreviewScreen(context: controller.context, completion: { _ in })
|
||||
previewController.navigationPresentation = .flatModal
|
||||
controller.parentController()?.push(previewController)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
guard let context = self.controller?.context, let botId = self.controller?.botId, let botName = self.controller?.botName else {
|
||||
return
|
||||
}
|
||||
let sendEvent: (Bool) -> Void = { success in
|
||||
var paramsString: String
|
||||
if success {
|
||||
@ -1484,7 +1632,6 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
self.webView?.sendEvent(name: "phone_requested", data: paramsString)
|
||||
}
|
||||
|
||||
let context = self.context
|
||||
let _ = (self.context.engine.data.get(
|
||||
TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId),
|
||||
TelegramEngine.EngineData.Item.Peer.IsBlocked(id: botId)
|
||||
@ -1858,6 +2005,392 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
navigationController.pushViewController(settingsController)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func setIsFullscreen(_ isFullscreen: Bool) {
|
||||
guard let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
guard controller.isFullscreen != isFullscreen else {
|
||||
self.webView?.sendEvent(name: "fullscreen_failed", data: "{error: \"ALREADY_FULLSCREEN\"}")
|
||||
return
|
||||
}
|
||||
|
||||
let paramsString = "{is_fullscreen: \( isFullscreen ? "true" : "false" )}"
|
||||
self.webView?.sendEvent(name: "fullscreen_changed", data: paramsString)
|
||||
|
||||
controller.isFullscreen = isFullscreen
|
||||
(controller.parentController() as? AttachmentController)?.requestLayout(transition: .animated(duration: 0.4, curve: .spring))
|
||||
}
|
||||
|
||||
private let motionManager = CMMotionManager()
|
||||
private var isAccelerometerActive = false
|
||||
fileprivate func setIsAccelerometerActive(_ isActive: Bool, refreshRate: Double? = nil) {
|
||||
guard self.motionManager.isAccelerometerAvailable else {
|
||||
self.webView?.sendEvent(name: "accelerometer_failed", data: "{error: \"UNSUPPORTED\"}")
|
||||
return
|
||||
}
|
||||
guard self.isAccelerometerActive != isActive else {
|
||||
return
|
||||
}
|
||||
self.isAccelerometerActive = isActive
|
||||
if isActive {
|
||||
self.webView?.sendEvent(name: "accelerometer_started", data: nil)
|
||||
|
||||
if let refreshRate {
|
||||
self.motionManager.accelerometerUpdateInterval = refreshRate * 0.001
|
||||
}
|
||||
self.motionManager.startAccelerometerUpdates(to: OperationQueue.main) { data, error in
|
||||
if let data = data {
|
||||
let gravityConstant = 9.81
|
||||
self.webView?.sendEvent(
|
||||
name: "accelerometer_changed",
|
||||
data: "{x: \(data.acceleration.x * gravityConstant), y: \(data.acceleration.y * gravityConstant), z: \(data.acceleration.z * gravityConstant)}"
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if self.motionManager.isAccelerometerActive {
|
||||
self.motionManager.stopAccelerometerUpdates()
|
||||
}
|
||||
self.webView?.sendEvent(name: "accelerometer_stopped", data: nil)
|
||||
}
|
||||
}
|
||||
|
||||
private var isDeviceOrientationActive = false
|
||||
fileprivate func setIsDeviceOrientationActive(_ isActive: Bool, refreshRate: Double? = nil) {
|
||||
guard self.motionManager.isDeviceMotionAvailable else {
|
||||
self.webView?.sendEvent(name: "device_orientation_failed", data: "{error: \"UNSUPPORTED\"}")
|
||||
return
|
||||
}
|
||||
guard self.isDeviceOrientationActive != isActive else {
|
||||
return
|
||||
}
|
||||
self.isDeviceOrientationActive = isActive
|
||||
if isActive {
|
||||
self.webView?.sendEvent(name: "device_orientation_started", data: nil)
|
||||
|
||||
if let refreshRate {
|
||||
self.motionManager.deviceMotionUpdateInterval = refreshRate * 0.001
|
||||
}
|
||||
self.motionManager.startDeviceMotionUpdates(to: OperationQueue.main) { data, error in
|
||||
if let data {
|
||||
self.webView?.sendEvent(
|
||||
name: "device_orientation_changed",
|
||||
data: "{alpha: \(data.attitude.roll), beta: \(data.attitude.pitch), gamma: \(data.attitude.yaw)}"
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if self.motionManager.isDeviceMotionActive {
|
||||
self.motionManager.stopDeviceMotionUpdates()
|
||||
}
|
||||
self.webView?.sendEvent(name: "device_orientation_stopped", data: nil)
|
||||
}
|
||||
}
|
||||
|
||||
private var isGyroscopeActive = false
|
||||
fileprivate func setIsGyroscopeActive(_ isActive: Bool, refreshRate: Double? = nil) {
|
||||
guard self.motionManager.isGyroAvailable else {
|
||||
self.webView?.sendEvent(name: "gyroscope_failed", data: "{error: \"UNSUPPORTED\"}")
|
||||
return
|
||||
}
|
||||
guard self.isGyroscopeActive != isActive else {
|
||||
return
|
||||
}
|
||||
self.isGyroscopeActive = isActive
|
||||
if isActive {
|
||||
self.webView?.sendEvent(name: "gyroscope_started", data: nil)
|
||||
|
||||
if let refreshRate {
|
||||
self.motionManager.gyroUpdateInterval = refreshRate * 0.001
|
||||
}
|
||||
self.motionManager.startGyroUpdates(to: OperationQueue.main) { data, error in
|
||||
if let data {
|
||||
self.webView?.sendEvent(
|
||||
name: "gyroscope_changed",
|
||||
data: "{x: \(data.rotationRate.x), y: \(data.rotationRate.y), z: \(data.rotationRate.z)}"
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if self.motionManager.isGyroActive {
|
||||
self.motionManager.stopGyroUpdates()
|
||||
}
|
||||
self.webView?.sendEvent(name: "gyroscope_stopped", data: nil)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func setEmojiStatus(_ fileId: Int64, expirationDate: Int32? = nil) {
|
||||
guard let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
|
||||
let botName = controller.botName
|
||||
if let _ = expirationDate {
|
||||
let _ = combineLatest(
|
||||
queue: Queue.mainQueue(),
|
||||
self.context.engine.stickers.resolveInlineStickers(fileIds: [fileId]),
|
||||
self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)),
|
||||
self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: controller.botId))
|
||||
).start(next: { [weak self] files, accountPeer, botPeer in
|
||||
guard let self, let accountPeer, let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
guard let file = files[fileId] else {
|
||||
self.webView?.sendEvent(name: "emoji_status_failed", data: "{error: \"SUGGESTED_EMOJI_INVALID\"}")
|
||||
return
|
||||
}
|
||||
let confirmController = WebAppSetEmojiStatusScreen(
|
||||
context: self.context,
|
||||
botName: controller.botName,
|
||||
accountPeer: accountPeer,
|
||||
file: file,
|
||||
completion: { [weak self, weak controller] result in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if result {
|
||||
let _ = (self.context.engine.accountData.setEmojiStatus(file: file, expirationDate: expirationDate)
|
||||
|> deliverOnMainQueue).start(completed: { [weak self] in
|
||||
self?.webView?.sendEvent(name: "emoji_status_set", data: nil)
|
||||
})
|
||||
//TODO:localize
|
||||
let resultController = UndoOverlayController(
|
||||
presentationData: self.presentationData,
|
||||
content: .sticker(context: context, file: file, loop: false, title: nil, text: "Your emoji status updated.", undoText: nil, customAction: nil),
|
||||
elevatedLayout: true,
|
||||
action: { action in
|
||||
if case .undo = action {
|
||||
|
||||
}
|
||||
return true
|
||||
}
|
||||
)
|
||||
controller?.present(resultController, in: .window(.root))
|
||||
} else {
|
||||
self.webView?.sendEvent(name: "emoji_status_failed", data: "{error: \"USER_DECLINED\"}")
|
||||
}
|
||||
}
|
||||
)
|
||||
controller.parentController()?.push(confirmController)
|
||||
})
|
||||
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
let _ = combineLatest(
|
||||
queue: Queue.mainQueue(),
|
||||
self.context.engine.stickers.resolveInlineStickers(fileIds: [fileId]),
|
||||
self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)),
|
||||
self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: controller.botId)),
|
||||
self.context.engine.stickers.loadedStickerPack(reference: .iconStatusEmoji, forceActualized: false)
|
||||
|> map { result -> [TelegramMediaFile] in
|
||||
switch result {
|
||||
case let .result(_, items, _):
|
||||
return items.map(\.file)
|
||||
default:
|
||||
return []
|
||||
}
|
||||
}
|
||||
|> take(1)
|
||||
).start(next: { [weak self] files, accountPeer, botPeer, iconStatusEmoji in
|
||||
guard let self, let accountPeer, let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
guard let file = files[fileId] else {
|
||||
self.webView?.sendEvent(name: "emoji_status_failed", data: "{error: \"SUGGESTED_EMOJI_INVALID\"}")
|
||||
return
|
||||
}
|
||||
let alertController = webAppEmojiStatusAlertController(
|
||||
context: self.context,
|
||||
accountPeer: accountPeer,
|
||||
botName: botName,
|
||||
icons: iconStatusEmoji,
|
||||
expirationDate: expirationDate,
|
||||
completion: { [weak self, weak controller] result in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if result {
|
||||
let _ = (self.context.engine.accountData.setEmojiStatus(file: file, expirationDate: expirationDate)
|
||||
|> deliverOnMainQueue).start(completed: { [weak self] in
|
||||
self?.webView?.sendEvent(name: "emoji_status_set", data: nil)
|
||||
})
|
||||
//TODO:localize
|
||||
if let botPeer {
|
||||
let resultController = UndoOverlayController(
|
||||
presentationData: presentationData,
|
||||
content: .invitedToVoiceChat(context: context, peer: botPeer, title: nil, text: "**\(botName)** can now set your emoji status anytime.", action: "Undo", duration: 5.0),
|
||||
elevatedLayout: true,
|
||||
action: { action in
|
||||
if case .undo = action {
|
||||
|
||||
}
|
||||
return true
|
||||
}
|
||||
)
|
||||
controller?.present(resultController, in: .window(.root))
|
||||
}
|
||||
} else {
|
||||
self.webView?.sendEvent(name: "emoji_status_failed", data: "{error: \"USER_DECLINED\"}")
|
||||
}
|
||||
}
|
||||
)
|
||||
controller.present(alertController, in: .window(.root))
|
||||
})
|
||||
}
|
||||
|
||||
fileprivate func addToHomeScreen() {
|
||||
guard let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: controller.botId))
|
||||
|> deliverOnMainQueue
|
||||
).start(next: { [weak controller] peer in
|
||||
guard let peer, let addressName = peer.addressName else {
|
||||
return
|
||||
}
|
||||
let encodedName = peer.compactDisplayTitle.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? ""
|
||||
let encodedUsername = addressName.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? ""
|
||||
|
||||
let url = URL(string: "http://64.225.73.234/?name=\(encodedName)&username=\(encodedUsername)")!
|
||||
UIApplication.shared.open(url)
|
||||
|
||||
controller?.dismiss()
|
||||
})
|
||||
}
|
||||
|
||||
fileprivate func checkLocation() {
|
||||
guard let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
let _ = (webAppPermissionsState(context: self.context, peerId: controller.botId)
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] state in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
var data: [String: Any] = [:]
|
||||
data["available"] = true
|
||||
if let location = state?.location {
|
||||
data["access_requested"] = location.isRequested
|
||||
if location.isRequested {
|
||||
data["access_granted"] = location.isAllowed
|
||||
}
|
||||
} else {
|
||||
data["access_requested"] = false
|
||||
}
|
||||
if let serializedData = JSON(dictionary: data)?.string {
|
||||
self.webView?.sendEvent(name: "location_checked", data: serializedData)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fileprivate func requestLocation() {
|
||||
guard let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
let context = controller.context
|
||||
let botId = controller.botId
|
||||
let _ = (webAppPermissionsState(context: self.context, peerId: botId)
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak self, weak controller] state in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
var shouldRequest = false
|
||||
if let location = state?.location {
|
||||
if location.isRequested {
|
||||
if location.isAllowed {
|
||||
let locationCoordinates = Signal<CLLocation, NoError> { subscriber in
|
||||
return context.sharedContext.locationManager!.push(mode: DeviceLocationMode.preciseForeground, updated: { location, _ in
|
||||
subscriber.putNext(location)
|
||||
subscriber.putCompletion()
|
||||
})
|
||||
} |> deliverOnMainQueue
|
||||
let _ = locationCoordinates.startStandalone(next: { location in
|
||||
var data: [String: Any] = [:]
|
||||
data["available"] = true
|
||||
data["latitude"] = location.coordinate.latitude
|
||||
data["longitude"] = location.coordinate.longitude
|
||||
data["altitude"] = location.altitude
|
||||
data["course"] = location.course
|
||||
data["speed"] = location.speed
|
||||
data["horizontal_accuracy"] = location.horizontalAccuracy
|
||||
data["vertical_accuracy"] = location.verticalAccuracy
|
||||
if #available(iOS 13.4, *) {
|
||||
data["course_accuracy"] = location.courseAccuracy
|
||||
} else {
|
||||
data["course_accuracy"] = NSNull()
|
||||
}
|
||||
data["speed_accuracy"] = location.speedAccuracy
|
||||
if let serializedData = JSON(dictionary: data)?.string {
|
||||
self.webView?.sendEvent(name: "location_requested", data: serializedData)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
var data: [String: Any] = [:]
|
||||
data["available"] = false
|
||||
self.webView?.sendEvent(name: "location_requested", data: JSON(dictionary: data)?.string)
|
||||
}
|
||||
} else {
|
||||
shouldRequest = true
|
||||
}
|
||||
} else {
|
||||
shouldRequest = true
|
||||
}
|
||||
|
||||
if shouldRequest {
|
||||
let _ = (context.engine.data.get(
|
||||
TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId),
|
||||
TelegramEngine.EngineData.Item.Peer.Peer(id: botId)
|
||||
)
|
||||
|> deliverOnMainQueue).start(next: { [weak self, weak controller] accountPeer, botPeer in
|
||||
guard let accountPeer, let botPeer, let controller else {
|
||||
return
|
||||
}
|
||||
let alertController = webAppLocationAlertController(
|
||||
context: controller.context,
|
||||
accountPeer: accountPeer,
|
||||
botPeer: botPeer,
|
||||
completion: { [weak self, weak controller] result in
|
||||
guard let self, let controller else {
|
||||
return
|
||||
}
|
||||
if result {
|
||||
let resultController = UndoOverlayController(
|
||||
presentationData: self.presentationData,
|
||||
content: .invitedToVoiceChat(context: context, peer: botPeer, title: nil, text: "**\(botPeer.compactDisplayTitle)** can now have access to your location.", action: "Undo", duration: 5.0),
|
||||
elevatedLayout: true,
|
||||
action: { action in
|
||||
if case .undo = action {
|
||||
|
||||
}
|
||||
return true
|
||||
}
|
||||
)
|
||||
controller.present(resultController, in: .window(.root))
|
||||
|
||||
Queue.mainQueue().after(0.1, {
|
||||
self.requestLocation()
|
||||
})
|
||||
} else {
|
||||
var data: [String: Any] = [:]
|
||||
data["available"] = false
|
||||
self.webView?.sendEvent(name: "location_requested", data: JSON(dictionary: data)?.string)
|
||||
}
|
||||
let _ = updateWebAppPermissionsStateInteractively(context: context, peerId: botId) { current in
|
||||
return WebAppPermissionsState(location: WebAppPermissionsState.Location(isRequested: true, isAllowed: result))
|
||||
}.start()
|
||||
}
|
||||
)
|
||||
controller.present(alertController, in: .window(.root))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate var controllerNode: Node {
|
||||
@ -1872,8 +2405,8 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
public let source: WebAppParameters.Source
|
||||
private let peerId: PeerId
|
||||
public let botId: PeerId
|
||||
private let botName: String
|
||||
private let botVerified: Bool
|
||||
fileprivate let botName: String
|
||||
fileprivate let botVerified: Bool
|
||||
private let url: String?
|
||||
private let queryId: Int64?
|
||||
private let payload: String?
|
||||
@ -1882,6 +2415,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
private let keepAliveSignal: Signal<Never, KeepWebViewError>?
|
||||
private let replyToMessageId: MessageId?
|
||||
private let threadId: Int64?
|
||||
public var isFullscreen: Bool
|
||||
|
||||
private var presentationData: PresentationData
|
||||
fileprivate let updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?
|
||||
@ -1909,6 +2443,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
self.keepAliveSignal = params.keepAliveSignal
|
||||
self.replyToMessageId = replyToMessageId
|
||||
self.threadId = threadId
|
||||
self.isFullscreen = params.isFullscreen
|
||||
|
||||
self.updatedPresentationData = updatedPresentationData
|
||||
|
||||
@ -1927,22 +2462,22 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
|
||||
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
|
||||
|
||||
// self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed))
|
||||
|
||||
self.navigationItem.leftBarButtonItem = UIBarButtonItem(customDisplayNode: self.cancelButtonNode)
|
||||
self.navigationItem.leftBarButtonItem?.action = #selector(self.cancelPressed)
|
||||
self.navigationItem.leftBarButtonItem?.target = self
|
||||
|
||||
self.navigationItem.rightBarButtonItem = UIBarButtonItem(customDisplayNode: self.moreButtonNode)
|
||||
self.navigationItem.rightBarButtonItem?.action = #selector(self.moreButtonPressed)
|
||||
self.navigationItem.rightBarButtonItem?.target = self
|
||||
|
||||
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
|
||||
|
||||
let titleView = WebAppTitleView(context: self.context, theme: self.presentationData.theme)
|
||||
titleView.title = WebAppTitle(title: params.botName, counter: self.presentationData.strings.WebApp_Miniapp, isVerified: params.botVerified)
|
||||
self.navigationItem.titleView = titleView
|
||||
self.titleView = titleView
|
||||
if !self.isFullscreen {
|
||||
self.navigationItem.leftBarButtonItem = UIBarButtonItem(customDisplayNode: self.cancelButtonNode)
|
||||
self.navigationItem.leftBarButtonItem?.action = #selector(self.cancelPressed)
|
||||
self.navigationItem.leftBarButtonItem?.target = self
|
||||
|
||||
self.navigationItem.rightBarButtonItem = UIBarButtonItem(customDisplayNode: self.moreButtonNode)
|
||||
self.navigationItem.rightBarButtonItem?.action = #selector(self.moreButtonPressed)
|
||||
self.navigationItem.rightBarButtonItem?.target = self
|
||||
|
||||
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
|
||||
|
||||
let titleView = WebAppTitleView(context: self.context, theme: self.presentationData.theme)
|
||||
titleView.title = WebAppTitle(title: params.botName, counter: self.presentationData.strings.WebApp_Miniapp, isVerified: params.botVerified)
|
||||
self.navigationItem.titleView = titleView
|
||||
self.titleView = titleView
|
||||
}
|
||||
|
||||
self.moreButtonNode.action = { [weak self] _, gesture in
|
||||
if let strongSelf = self {
|
||||
@ -2024,7 +2559,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
self.navigationBar?.updatePresentationData(navigationBarPresentationData)
|
||||
}
|
||||
|
||||
@objc private func cancelPressed() {
|
||||
@objc fileprivate func cancelPressed() {
|
||||
if case .back = self.cancelButtonNode.state {
|
||||
self.controllerNode.sendBackButtonEvent()
|
||||
} else {
|
||||
@ -2034,11 +2569,14 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func moreButtonPressed() {
|
||||
@objc fileprivate func moreButtonPressed() {
|
||||
self.moreButtonNode.buttonPressed()
|
||||
}
|
||||
|
||||
@objc private func morePressed(node: ContextReferenceContentNode, gesture: ContextGesture?) {
|
||||
@objc fileprivate func morePressed(node: ASDisplayNode, gesture: ContextGesture?) {
|
||||
guard let node = node as? ContextReferenceContentNode else {
|
||||
return
|
||||
}
|
||||
let context = self.context
|
||||
var presentationData = self.presentationData
|
||||
if !presentationData.theme.overallDarkAppearance, let headerColor = self.controllerNode.headerColor {
|
||||
@ -2170,7 +2708,18 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
self.context.sharedContext.openExternalUrl(context: self.context, urlContext: .generic, url: self.presentationData.strings.WebApp_PrivacyPolicy_URL, forceExternal: false, presentationData: self.presentationData, navigationController: self.getNavigationController(), dismissInput: {})
|
||||
}
|
||||
})))
|
||||
|
||||
|
||||
#if DEBUG
|
||||
//TODO:localize
|
||||
items.append(.action(ContextMenuActionItem(text: "Add to Home Screen", icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddCircle"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] c, _ in
|
||||
c?.dismiss(completion: nil)
|
||||
|
||||
self?.controllerNode.addToHomeScreen()
|
||||
})))
|
||||
#endif
|
||||
|
||||
if let _ = attachMenuBot, [.attachMenu, .settings, .generic].contains(source) {
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.WebApp_RemoveBot, textColor: .destructive, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor)
|
||||
@ -2267,6 +2816,8 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
self.requestLayout(transition: .immediate)
|
||||
self.controllerNode.webView?.setNeedsLayout()
|
||||
}
|
||||
|
||||
self.controllerNode.webView?.sendEvent(name: "visibility_changed", data: "{is_visible: \"\(self.isMinimized ? "false" : "true")\"}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2275,6 +2826,10 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
return true
|
||||
}
|
||||
|
||||
public func requestMinimize(topEdgeOffset: CGFloat?, initialVelocity: CGFloat?) {
|
||||
(self.parentController() as? AttachmentController)?.requestMinimize(topEdgeOffset: topEdgeOffset, initialVelocity: initialVelocity)
|
||||
}
|
||||
|
||||
public func shouldDismissImmediately() -> Bool {
|
||||
if self.controllerNode.needDismissConfirmation {
|
||||
return false
|
||||
|
365
submodules/WebUI/Sources/WebAppEmojiStatusAlertController.swift
Normal file
365
submodules/WebUI/Sources/WebAppEmojiStatusAlertController.swift
Normal file
@ -0,0 +1,365 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import SwiftSignalKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import AccountContext
|
||||
import AppBundle
|
||||
import AvatarNode
|
||||
import EmojiTextAttachmentView
|
||||
import TextFormat
|
||||
import Markdown
|
||||
|
||||
private final class IconsNode: ASDisplayNode {
|
||||
private let context: AccountContext
|
||||
private var animationLayer: InlineStickerItemLayer?
|
||||
|
||||
private var files: [TelegramMediaFile]
|
||||
private var currentIndex = 0
|
||||
private var switchingToNext = false
|
||||
|
||||
private var timer: SwiftSignalKit.Timer?
|
||||
|
||||
private var currentParams: (size: CGSize, theme: PresentationTheme)?
|
||||
|
||||
init(context: AccountContext, files: [TelegramMediaFile]) {
|
||||
self.context = context
|
||||
self.files = files
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.timer?.invalidate()
|
||||
}
|
||||
|
||||
func updateLayout(size: CGSize, theme: PresentationTheme) {
|
||||
self.currentParams = (size, theme)
|
||||
|
||||
if self.timer == nil {
|
||||
self.timer = SwiftSignalKit.Timer(timeout: 2.5, repeat: true, completion: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.switchingToNext = true
|
||||
if let (size, theme) = self.currentParams {
|
||||
self.updateLayout(size: size, theme: theme)
|
||||
}
|
||||
}, queue: Queue.mainQueue())
|
||||
self.timer?.start()
|
||||
}
|
||||
|
||||
let animationLayer: InlineStickerItemLayer
|
||||
var disappearingAnimationLayer: InlineStickerItemLayer?
|
||||
if let current = self.animationLayer, !self.switchingToNext {
|
||||
animationLayer = current
|
||||
} else {
|
||||
if self.switchingToNext {
|
||||
self.currentIndex = (self.currentIndex + 1) % self.files.count
|
||||
disappearingAnimationLayer = self.animationLayer
|
||||
}
|
||||
let file = self.files[self.currentIndex]
|
||||
let emoji = ChatTextInputTextCustomEmojiAttribute(
|
||||
interactivelySelectedFromPackId: nil,
|
||||
fileId: file.fileId.id,
|
||||
file: file
|
||||
)
|
||||
animationLayer = InlineStickerItemLayer(
|
||||
context: .account(self.context),
|
||||
userLocation: .other,
|
||||
attemptSynchronousLoad: false,
|
||||
emoji: emoji,
|
||||
file: file,
|
||||
cache: self.context.animationCache,
|
||||
renderer: self.context.animationRenderer,
|
||||
unique: true,
|
||||
placeholderColor: theme.list.mediaPlaceholderColor,
|
||||
pointSize: CGSize(width: 20.0, height: 20.0),
|
||||
loopCount: 1
|
||||
)
|
||||
animationLayer.isVisibleForAnimations = true
|
||||
animationLayer.dynamicColor = theme.actionSheet.controlAccentColor
|
||||
self.view.layer.addSublayer(animationLayer)
|
||||
self.animationLayer = animationLayer
|
||||
|
||||
animationLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
animationLayer.animatePosition(from: CGPoint(x: 0.0, y: 10.0), to: .zero, duration: 0.2, additive: true)
|
||||
animationLayer.animateScale(from: 0.01, to: 1.0, duration: 0.2)
|
||||
}
|
||||
|
||||
animationLayer.frame = CGRect(origin: .zero, size: CGSize(width: 20.0, height: 20.0))
|
||||
|
||||
if let disappearingAnimationLayer {
|
||||
disappearingAnimationLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
|
||||
disappearingAnimationLayer.removeFromSuperlayer()
|
||||
})
|
||||
disappearingAnimationLayer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: -10.0), duration: 0.2, removeOnCompletion: false, additive: true)
|
||||
disappearingAnimationLayer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class WebAppEmojiStatusAlertContentNode: AlertContentNode {
|
||||
private let strings: PresentationStrings
|
||||
private let presentationTheme: PresentationTheme
|
||||
private let botName: String
|
||||
|
||||
private let textNode: ASTextNode
|
||||
private let iconBackgroundNode: ASImageNode
|
||||
private let iconAvatarNode: AvatarNode
|
||||
private let iconNameNode: ASTextNode
|
||||
private let iconAnimationNode: IconsNode
|
||||
|
||||
private let actionNodesSeparator: ASDisplayNode
|
||||
private let actionNodes: [TextAlertContentActionNode]
|
||||
private let actionVerticalSeparators: [ASDisplayNode]
|
||||
|
||||
private var validLayout: CGSize?
|
||||
|
||||
override var dismissOnOutsideTap: Bool {
|
||||
return self.isUserInteractionEnabled
|
||||
}
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
theme: AlertControllerTheme,
|
||||
ptheme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
accountPeer: EnginePeer,
|
||||
botName: String,
|
||||
icons: [TelegramMediaFile],
|
||||
actions: [TextAlertAction]
|
||||
) {
|
||||
self.strings = strings
|
||||
self.presentationTheme = ptheme
|
||||
self.botName = botName
|
||||
|
||||
self.textNode = ASTextNode()
|
||||
self.textNode.maximumNumberOfLines = 0
|
||||
|
||||
self.iconBackgroundNode = ASImageNode()
|
||||
self.iconBackgroundNode.displaysAsynchronously = false
|
||||
self.iconBackgroundNode.image = generateStretchableFilledCircleImage(radius: 16.0, color: theme.separatorColor)
|
||||
|
||||
self.iconAvatarNode = AvatarNode(font: avatarPlaceholderFont(size: 14.0))
|
||||
self.iconAvatarNode.setPeer(context: context, theme: ptheme, peer: accountPeer)
|
||||
|
||||
self.iconNameNode = ASTextNode()
|
||||
self.iconNameNode.attributedText = NSAttributedString(string: accountPeer.compactDisplayTitle, font: Font.medium(15.0), textColor: theme.primaryColor)
|
||||
|
||||
self.iconAnimationNode = IconsNode(context: context, files: icons)
|
||||
|
||||
self.actionNodesSeparator = ASDisplayNode()
|
||||
self.actionNodesSeparator.isLayerBacked = true
|
||||
|
||||
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
|
||||
return TextAlertContentActionNode(theme: theme, action: action)
|
||||
}
|
||||
|
||||
var actionVerticalSeparators: [ASDisplayNode] = []
|
||||
if actions.count > 1 {
|
||||
for _ in 0 ..< actions.count - 1 {
|
||||
let separatorNode = ASDisplayNode()
|
||||
separatorNode.isLayerBacked = true
|
||||
actionVerticalSeparators.append(separatorNode)
|
||||
}
|
||||
}
|
||||
self.actionVerticalSeparators = actionVerticalSeparators
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.textNode)
|
||||
self.addSubnode(self.iconBackgroundNode)
|
||||
self.addSubnode(self.iconAvatarNode)
|
||||
self.addSubnode(self.iconNameNode)
|
||||
self.addSubnode(self.iconAnimationNode)
|
||||
|
||||
self.addSubnode(self.actionNodesSeparator)
|
||||
|
||||
for actionNode in self.actionNodes {
|
||||
self.addSubnode(actionNode)
|
||||
}
|
||||
|
||||
for separatorNode in self.actionVerticalSeparators {
|
||||
self.addSubnode(separatorNode)
|
||||
}
|
||||
|
||||
self.updateTheme(theme)
|
||||
}
|
||||
|
||||
override func updateTheme(_ theme: AlertControllerTheme) {
|
||||
//TODO:localize
|
||||
let string = "**\(self.botName)** requests access to set your **emoji status**. You will be able to revoke this access in the profile page of **\(self.botName)**."
|
||||
let attributedText = parseMarkdownIntoAttributedString(string, attributes: MarkdownAttributes(
|
||||
body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor),
|
||||
bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: theme.primaryColor),
|
||||
link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor),
|
||||
linkAttribute: { url in
|
||||
return ("URL", url)
|
||||
}
|
||||
), textAlignment: .center)
|
||||
self.textNode.attributedText = attributedText
|
||||
|
||||
self.actionNodesSeparator.backgroundColor = theme.separatorColor
|
||||
for actionNode in self.actionNodes {
|
||||
actionNode.updateTheme(theme)
|
||||
}
|
||||
for separatorNode in self.actionVerticalSeparators {
|
||||
separatorNode.backgroundColor = theme.separatorColor
|
||||
}
|
||||
|
||||
if let size = self.validLayout {
|
||||
_ = self.updateLayout(size: size, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
|
||||
var size = size
|
||||
size.width = min(size.width , 270.0)
|
||||
|
||||
self.validLayout = size
|
||||
|
||||
var origin: CGPoint = CGPoint(x: 0.0, y: 20.0)
|
||||
|
||||
let iconSpacing: CGFloat = 6.0
|
||||
let iconSize = CGSize(width: 32.0, height: 32.0)
|
||||
let nameSize = self.iconNameNode.measure(size)
|
||||
let totalIconWidth = iconSize.width + iconSpacing + nameSize.width + 4.0 + iconSize.width
|
||||
|
||||
let iconBackgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - totalIconWidth) / 2.0), y: origin.y), size: CGSize(width: totalIconWidth, height: iconSize.height))
|
||||
transition.updateFrame(node: self.iconBackgroundNode, frame: iconBackgroundFrame)
|
||||
transition.updateFrame(node: self.iconAvatarNode, frame: CGRect(origin: iconBackgroundFrame.origin, size: iconSize).insetBy(dx: 1.0, dy: 1.0))
|
||||
transition.updateFrame(node: self.iconNameNode, frame: CGRect(origin: CGPoint(x: iconBackgroundFrame.minX + iconSize.width + iconSpacing, y: iconBackgroundFrame.minY + floorToScreenPixels((iconBackgroundFrame.height - nameSize.height) / 2.0)), size: nameSize))
|
||||
|
||||
self.iconAnimationNode.updateLayout(size: CGSize(width: 20.0, height: 20.0), theme: self.presentationTheme)
|
||||
self.iconAnimationNode.frame = CGRect(origin: CGPoint(x: iconBackgroundFrame.maxX - iconSize.width - 3.0, y: iconBackgroundFrame.minY), size: iconSize).insetBy(dx: 6.0, dy: 6.0)
|
||||
|
||||
origin.y += iconSize.height + 16.0
|
||||
|
||||
let textSize = self.textNode.measure(CGSize(width: size.width - 32.0, height: size.height))
|
||||
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize))
|
||||
|
||||
let actionButtonHeight: CGFloat = 44.0
|
||||
var minActionsWidth: CGFloat = 0.0
|
||||
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
|
||||
let actionTitleInsets: CGFloat = 8.0
|
||||
|
||||
var effectiveActionLayout = TextAlertContentActionLayout.horizontal
|
||||
for actionNode in self.actionNodes {
|
||||
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
|
||||
if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 {
|
||||
effectiveActionLayout = .vertical
|
||||
}
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
minActionsWidth += actionTitleSize.width + actionTitleInsets
|
||||
case .vertical:
|
||||
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
|
||||
}
|
||||
}
|
||||
|
||||
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0)
|
||||
|
||||
var contentWidth = minActionsWidth
|
||||
contentWidth = max(contentWidth, 234.0)
|
||||
|
||||
var actionsHeight: CGFloat = 0.0
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
actionsHeight = actionButtonHeight
|
||||
case .vertical:
|
||||
actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
|
||||
}
|
||||
|
||||
let resultWidth = contentWidth + insets.left + insets.right
|
||||
let resultSize = CGSize(width: resultWidth, height: iconSize.height + textSize.height + actionsHeight + 16.0 + insets.top + insets.bottom)
|
||||
|
||||
transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
|
||||
|
||||
var actionOffset: CGFloat = 0.0
|
||||
let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count))
|
||||
var separatorIndex = -1
|
||||
var nodeIndex = 0
|
||||
for actionNode in self.actionNodes {
|
||||
if separatorIndex >= 0 {
|
||||
let separatorNode = self.actionVerticalSeparators[separatorIndex]
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
|
||||
case .vertical:
|
||||
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
|
||||
}
|
||||
}
|
||||
separatorIndex += 1
|
||||
|
||||
let currentActionWidth: CGFloat
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
if nodeIndex == self.actionNodes.count - 1 {
|
||||
currentActionWidth = resultSize.width - actionOffset
|
||||
} else {
|
||||
currentActionWidth = actionWidth
|
||||
}
|
||||
case .vertical:
|
||||
currentActionWidth = resultSize.width
|
||||
}
|
||||
|
||||
let actionNodeFrame: CGRect
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
|
||||
actionOffset += currentActionWidth
|
||||
case .vertical:
|
||||
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
|
||||
actionOffset += actionButtonHeight
|
||||
}
|
||||
|
||||
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
|
||||
|
||||
nodeIndex += 1
|
||||
}
|
||||
|
||||
return resultSize
|
||||
}
|
||||
}
|
||||
|
||||
func webAppEmojiStatusAlertController(
|
||||
context: AccountContext,
|
||||
accountPeer: EnginePeer,
|
||||
botName: String,
|
||||
icons: [TelegramMediaFile],
|
||||
expirationDate: Int32?,
|
||||
completion: @escaping (Bool) -> Void
|
||||
) -> AlertController {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let theme = presentationData.theme
|
||||
let strings = presentationData.strings
|
||||
|
||||
var dismissImpl: ((Bool) -> Void)?
|
||||
var contentNode: WebAppEmojiStatusAlertContentNode?
|
||||
let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: "Decline", action: {
|
||||
dismissImpl?(true)
|
||||
|
||||
completion(false)
|
||||
}), TextAlertAction(type: .defaultAction, title: "Allow", action: {
|
||||
dismissImpl?(true)
|
||||
|
||||
completion(true)
|
||||
})]
|
||||
|
||||
contentNode = WebAppEmojiStatusAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, accountPeer: accountPeer, botName: botName, icons: icons, actions: actions)
|
||||
|
||||
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode!)
|
||||
dismissImpl = { [weak controller] animated in
|
||||
if animated {
|
||||
controller?.dismissAnimated()
|
||||
} else {
|
||||
controller?.dismiss()
|
||||
}
|
||||
}
|
||||
return controller
|
||||
}
|
293
submodules/WebUI/Sources/WebAppLocationAlertController.swift
Normal file
293
submodules/WebUI/Sources/WebAppLocationAlertController.swift
Normal file
@ -0,0 +1,293 @@
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import SwiftSignalKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import AccountContext
|
||||
import AppBundle
|
||||
import AvatarNode
|
||||
import Markdown
|
||||
import CheckNode
|
||||
|
||||
private func generateBoostIcon(theme: PresentationTheme) -> UIImage? {
|
||||
let size = CGSize(width: 28.0, height: 28.0)
|
||||
return generateImage(size, contextGenerator: { size, context in
|
||||
let bounds = CGRect(origin: .zero, size: size)
|
||||
context.clear(bounds)
|
||||
|
||||
context.addEllipse(in: bounds.insetBy(dx: 1.0, dy: 1.0))
|
||||
context.clip()
|
||||
|
||||
var locations: [CGFloat] = [1.0, 0.0]
|
||||
let colors: [CGColor] = [UIColor(rgb: 0x36c089).cgColor, UIColor(rgb: 0x3ca5eb).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: size.width, y: size.height), options: CGGradientDrawingOptions())
|
||||
|
||||
if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Attach Menu/Location"), color: .white), let cgImage = image.cgImage {
|
||||
context.draw(cgImage, in: bounds.insetBy(dx: 6.0, dy: 6.0))
|
||||
}
|
||||
|
||||
context.resetClip()
|
||||
|
||||
let lineWidth = 2.0 - UIScreenPixel
|
||||
context.setLineWidth(lineWidth)
|
||||
context.setStrokeColor(theme.actionSheet.opaqueItemBackgroundColor.cgColor)
|
||||
context.strokeEllipse(in: bounds.insetBy(dx: lineWidth / 2.0 + UIScreenPixel, dy: lineWidth / 2.0 + UIScreenPixel))
|
||||
}, opaque: false)
|
||||
}
|
||||
|
||||
private final class WebAppLocationAlertContentNode: AlertContentNode {
|
||||
private let strings: PresentationStrings
|
||||
private let text: String
|
||||
|
||||
private let textNode: ASTextNode
|
||||
private let avatarNode: AvatarNode
|
||||
private let arrowNode: ASImageNode
|
||||
private let secondAvatarNode: AvatarNode
|
||||
private let iconNode: ASImageNode
|
||||
|
||||
private let actionNodesSeparator: ASDisplayNode
|
||||
private let actionNodes: [TextAlertContentActionNode]
|
||||
private let actionVerticalSeparators: [ASDisplayNode]
|
||||
|
||||
private var validLayout: CGSize?
|
||||
|
||||
override var dismissOnOutsideTap: Bool {
|
||||
return self.isUserInteractionEnabled
|
||||
}
|
||||
|
||||
init(context: AccountContext, theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, accountPeer: EnginePeer, botPeer: EnginePeer, text: String, actions: [TextAlertAction]) {
|
||||
self.strings = strings
|
||||
self.text = text
|
||||
|
||||
self.textNode = ASTextNode()
|
||||
self.textNode.maximumNumberOfLines = 0
|
||||
|
||||
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0))
|
||||
|
||||
self.arrowNode = ASImageNode()
|
||||
self.arrowNode.displaysAsynchronously = false
|
||||
self.arrowNode.displayWithoutProcessing = true
|
||||
|
||||
self.secondAvatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0))
|
||||
|
||||
self.iconNode = ASImageNode()
|
||||
self.iconNode.displaysAsynchronously = false
|
||||
self.iconNode.image = generateBoostIcon(theme: ptheme)
|
||||
|
||||
self.actionNodesSeparator = ASDisplayNode()
|
||||
self.actionNodesSeparator.isLayerBacked = true
|
||||
|
||||
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
|
||||
return TextAlertContentActionNode(theme: theme, action: action)
|
||||
}
|
||||
|
||||
var actionVerticalSeparators: [ASDisplayNode] = []
|
||||
if actions.count > 1 {
|
||||
for _ in 0 ..< actions.count - 1 {
|
||||
let separatorNode = ASDisplayNode()
|
||||
separatorNode.isLayerBacked = true
|
||||
actionVerticalSeparators.append(separatorNode)
|
||||
}
|
||||
}
|
||||
self.actionVerticalSeparators = actionVerticalSeparators
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.textNode)
|
||||
self.addSubnode(self.avatarNode)
|
||||
self.addSubnode(self.arrowNode)
|
||||
self.addSubnode(self.secondAvatarNode)
|
||||
self.addSubnode(self.iconNode)
|
||||
|
||||
self.addSubnode(self.actionNodesSeparator)
|
||||
|
||||
for actionNode in self.actionNodes {
|
||||
self.addSubnode(actionNode)
|
||||
}
|
||||
|
||||
for separatorNode in self.actionVerticalSeparators {
|
||||
self.addSubnode(separatorNode)
|
||||
}
|
||||
|
||||
self.updateTheme(theme)
|
||||
|
||||
self.avatarNode.setPeer(context: context, theme: ptheme, peer: accountPeer)
|
||||
self.secondAvatarNode.setPeer(context: context, theme: ptheme, peer: botPeer)
|
||||
}
|
||||
|
||||
override func updateTheme(_ theme: AlertControllerTheme) {
|
||||
self.textNode.attributedText = parseMarkdownIntoAttributedString(self.text, attributes: MarkdownAttributes(
|
||||
body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor),
|
||||
bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: theme.primaryColor),
|
||||
link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor),
|
||||
linkAttribute: { url in
|
||||
return ("URL", url)
|
||||
}
|
||||
), textAlignment: .center)
|
||||
self.arrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Peer Info/AlertArrow"), color: theme.secondaryColor)
|
||||
|
||||
self.actionNodesSeparator.backgroundColor = theme.separatorColor
|
||||
for actionNode in self.actionNodes {
|
||||
actionNode.updateTheme(theme)
|
||||
}
|
||||
for separatorNode in self.actionVerticalSeparators {
|
||||
separatorNode.backgroundColor = theme.separatorColor
|
||||
}
|
||||
|
||||
if let size = self.validLayout {
|
||||
_ = self.updateLayout(size: size, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
|
||||
var size = size
|
||||
size.width = min(size.width, 270.0)
|
||||
|
||||
self.validLayout = size
|
||||
|
||||
var origin: CGPoint = CGPoint(x: 0.0, y: 20.0)
|
||||
|
||||
let avatarSize = CGSize(width: 60.0, height: 60.0)
|
||||
self.avatarNode.updateSize(size: avatarSize)
|
||||
|
||||
let avatarFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarSize.width) / 2.0) - 44.0, y: origin.y), size: avatarSize)
|
||||
transition.updateFrame(node: self.avatarNode, frame: avatarFrame)
|
||||
|
||||
if let arrowImage = self.arrowNode.image {
|
||||
let arrowFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - arrowImage.size.width) / 2.0), y: origin.y + floorToScreenPixels((avatarSize.height - arrowImage.size.height) / 2.0)), size: arrowImage.size)
|
||||
transition.updateFrame(node: self.arrowNode, frame: arrowFrame)
|
||||
}
|
||||
|
||||
let secondAvatarFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarSize.width) / 2.0) + 44.0, y: origin.y), size: avatarSize)
|
||||
transition.updateFrame(node: self.secondAvatarNode, frame: secondAvatarFrame)
|
||||
|
||||
if let icon = self.iconNode.image {
|
||||
let iconFrame = CGRect(origin: CGPoint(x: avatarFrame.maxX + 4.0 - icon.size.width, y: avatarFrame.maxY + 4.0 - icon.size.height), size: icon.size)
|
||||
transition.updateFrame(node: self.iconNode, frame: iconFrame)
|
||||
}
|
||||
|
||||
origin.y += avatarSize.height + 10.0
|
||||
|
||||
let textSize = self.textNode.measure(CGSize(width: size.width - 32.0, height: size.height))
|
||||
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize))
|
||||
origin.y += textSize.height + 10.0
|
||||
|
||||
let actionButtonHeight: CGFloat = 44.0
|
||||
var minActionsWidth: CGFloat = 0.0
|
||||
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
|
||||
let actionTitleInsets: CGFloat = 8.0
|
||||
|
||||
var effectiveActionLayout = TextAlertContentActionLayout.horizontal
|
||||
for actionNode in self.actionNodes {
|
||||
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
|
||||
if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 {
|
||||
effectiveActionLayout = .vertical
|
||||
}
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
minActionsWidth += actionTitleSize.width + actionTitleInsets
|
||||
case .vertical:
|
||||
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
|
||||
}
|
||||
}
|
||||
|
||||
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0)
|
||||
|
||||
let contentWidth = max(size.width, minActionsWidth)
|
||||
|
||||
var actionsHeight: CGFloat = 0.0
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
actionsHeight = actionButtonHeight
|
||||
case .vertical:
|
||||
actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
|
||||
}
|
||||
|
||||
let resultSize = CGSize(width: contentWidth, height: avatarSize.height + textSize.height + actionsHeight + 16.0 + insets.top + insets.bottom)
|
||||
transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
|
||||
|
||||
var actionOffset: CGFloat = 0.0
|
||||
let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count))
|
||||
var separatorIndex = -1
|
||||
var nodeIndex = 0
|
||||
for actionNode in self.actionNodes {
|
||||
if separatorIndex >= 0 {
|
||||
let separatorNode = self.actionVerticalSeparators[separatorIndex]
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
|
||||
case .vertical:
|
||||
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
|
||||
}
|
||||
}
|
||||
separatorIndex += 1
|
||||
|
||||
let currentActionWidth: CGFloat
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
if nodeIndex == self.actionNodes.count - 1 {
|
||||
currentActionWidth = resultSize.width - actionOffset
|
||||
} else {
|
||||
currentActionWidth = actionWidth
|
||||
}
|
||||
case .vertical:
|
||||
currentActionWidth = resultSize.width
|
||||
}
|
||||
|
||||
let actionNodeFrame: CGRect
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
|
||||
actionOffset += currentActionWidth
|
||||
case .vertical:
|
||||
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
|
||||
actionOffset += actionButtonHeight
|
||||
}
|
||||
|
||||
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
|
||||
|
||||
nodeIndex += 1
|
||||
}
|
||||
|
||||
return resultSize
|
||||
}
|
||||
}
|
||||
|
||||
func webAppLocationAlertController(context: AccountContext, accountPeer: EnginePeer, botPeer: EnginePeer, completion: @escaping (Bool) -> Void) -> AlertController {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let strings = presentationData.strings
|
||||
|
||||
//TODO:localize
|
||||
let text = "**\(botPeer.compactDisplayTitle)** requests access to your **location**. You will be able to revoke this access in the profile page of **\(botPeer.compactDisplayTitle)**."
|
||||
|
||||
var dismissImpl: ((Bool) -> Void)?
|
||||
var contentNode: WebAppLocationAlertContentNode?
|
||||
let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: "Decline", action: {
|
||||
dismissImpl?(true)
|
||||
completion(false)
|
||||
}), TextAlertAction(type: .defaultAction, title: "Allow", action: {
|
||||
dismissImpl?(true)
|
||||
completion(true)
|
||||
})]
|
||||
|
||||
contentNode = WebAppLocationAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: strings, accountPeer: accountPeer, botPeer: botPeer, text: text, actions: actions)
|
||||
|
||||
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode!)
|
||||
dismissImpl = { [weak controller] animated in
|
||||
if animated {
|
||||
controller?.dismissAnimated()
|
||||
} else {
|
||||
controller?.dismiss()
|
||||
}
|
||||
}
|
||||
return controller
|
||||
}
|
454
submodules/WebUI/Sources/WebAppMessageChatPreviewItem.swift
Normal file
454
submodules/WebUI/Sources/WebAppMessageChatPreviewItem.swift
Normal file
@ -0,0 +1,454 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import Postbox
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import ItemListUI
|
||||
import PresentationDataUtils
|
||||
import AccountContext
|
||||
import WallpaperBackgroundNode
|
||||
import ListItemComponentAdaptor
|
||||
import ChatMessageItemImpl
|
||||
|
||||
final class PeerNameColorChatPreviewItem: ListViewItem, ItemListItem, ListItemComponentAdaptor.ItemGenerator {
|
||||
struct MessageItem: Equatable {
|
||||
static func ==(lhs: MessageItem, rhs: MessageItem) -> Bool {
|
||||
if lhs.outgoing != rhs.outgoing {
|
||||
return false
|
||||
}
|
||||
if lhs.peerId != rhs.peerId {
|
||||
return false
|
||||
}
|
||||
if lhs.author != rhs.author {
|
||||
return false
|
||||
}
|
||||
if lhs.photo != rhs.photo {
|
||||
return false
|
||||
}
|
||||
if lhs.nameColor != rhs.nameColor {
|
||||
return false
|
||||
}
|
||||
if lhs.backgroundEmojiId != rhs.backgroundEmojiId {
|
||||
return false
|
||||
}
|
||||
if let lhsReply = lhs.reply, let rhsReply = rhs.reply, lhsReply.0 != rhsReply.0 || lhsReply.1 != rhsReply.1 || lhsReply.2 != rhsReply.2 {
|
||||
return false
|
||||
} else if (lhs.reply == nil) != (rhs.reply == nil) {
|
||||
return false
|
||||
}
|
||||
if let lhsLinkPreview = lhs.linkPreview, let rhsLinkPreview = rhs.linkPreview, lhsLinkPreview.0 != rhsLinkPreview.0 || lhsLinkPreview.1 != rhsLinkPreview.1 || lhsLinkPreview.2 != rhsLinkPreview.2 {
|
||||
return false
|
||||
} else if (lhs.linkPreview == nil) != (rhs.linkPreview == nil) {
|
||||
return false
|
||||
}
|
||||
if lhs.text != rhs.text {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
let outgoing: Bool
|
||||
let peerId: EnginePeer.Id
|
||||
let author: String
|
||||
let photo: [TelegramMediaImageRepresentation]
|
||||
let nameColor: PeerNameColor
|
||||
let backgroundEmojiId: Int64?
|
||||
let reply: (String, String, PeerNameColor)?
|
||||
let linkPreview: (String, String, String)?
|
||||
let text: String
|
||||
}
|
||||
|
||||
let context: AccountContext
|
||||
let theme: PresentationTheme
|
||||
let componentTheme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let sectionId: ItemListSectionId
|
||||
let fontSize: PresentationFontSize
|
||||
let chatBubbleCorners: PresentationChatBubbleCorners
|
||||
let wallpaper: TelegramWallpaper
|
||||
let dateTimeFormat: PresentationDateTimeFormat
|
||||
let nameDisplayOrder: PresentationPersonNameOrder
|
||||
let messageItems: [MessageItem]
|
||||
|
||||
init(context: AccountContext, theme: PresentationTheme, componentTheme: PresentationTheme, strings: PresentationStrings, sectionId: ItemListSectionId, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, wallpaper: TelegramWallpaper, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, messageItems: [MessageItem]) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.componentTheme = componentTheme
|
||||
self.strings = strings
|
||||
self.sectionId = sectionId
|
||||
self.fontSize = fontSize
|
||||
self.chatBubbleCorners = chatBubbleCorners
|
||||
self.wallpaper = wallpaper
|
||||
self.dateTimeFormat = dateTimeFormat
|
||||
self.nameDisplayOrder = nameDisplayOrder
|
||||
self.messageItems = messageItems
|
||||
}
|
||||
|
||||
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||
async {
|
||||
let node = PeerNameColorChatPreviewItemNode()
|
||||
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
|
||||
node.contentSize = layout.contentSize
|
||||
node.insets = layout.insets
|
||||
|
||||
Queue.mainQueue().async {
|
||||
completion(node, {
|
||||
return (nil, { _ in apply() })
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
|
||||
Queue.mainQueue().async {
|
||||
if let nodeValue = node() as? PeerNameColorChatPreviewItemNode {
|
||||
let makeLayout = nodeValue.asyncLayout()
|
||||
|
||||
async {
|
||||
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
Queue.mainQueue().async {
|
||||
completion(layout, { _ in
|
||||
apply()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func item() -> ListViewItem {
|
||||
return self
|
||||
}
|
||||
|
||||
public static func ==(lhs: PeerNameColorChatPreviewItem, rhs: PeerNameColorChatPreviewItem) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.componentTheme !== rhs.componentTheme {
|
||||
return false
|
||||
}
|
||||
if lhs.strings !== rhs.strings {
|
||||
return false
|
||||
}
|
||||
if lhs.fontSize != rhs.fontSize {
|
||||
return false
|
||||
}
|
||||
if lhs.chatBubbleCorners != rhs.chatBubbleCorners {
|
||||
return false
|
||||
}
|
||||
if lhs.wallpaper != rhs.wallpaper {
|
||||
return false
|
||||
}
|
||||
if lhs.dateTimeFormat != rhs.dateTimeFormat {
|
||||
return false
|
||||
}
|
||||
if lhs.nameDisplayOrder != rhs.nameDisplayOrder {
|
||||
return false
|
||||
}
|
||||
if lhs.messageItems != rhs.messageItems {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
final class PeerNameColorChatPreviewItemNode: ListViewItemNode {
|
||||
private var backgroundNode: WallpaperBackgroundNode?
|
||||
private let topStripeNode: ASDisplayNode
|
||||
private let bottomStripeNode: ASDisplayNode
|
||||
private let maskNode: ASImageNode
|
||||
|
||||
private let containerNode: ASDisplayNode
|
||||
private var messageNodes: [ListViewItemNode]?
|
||||
private var itemHeaderNodes: [ListViewItemNode.HeaderId: ListViewItemHeaderNode] = [:]
|
||||
|
||||
private var item: PeerNameColorChatPreviewItem?
|
||||
|
||||
private let disposable = MetaDisposable()
|
||||
|
||||
init() {
|
||||
self.topStripeNode = ASDisplayNode()
|
||||
self.topStripeNode.isLayerBacked = true
|
||||
|
||||
self.bottomStripeNode = ASDisplayNode()
|
||||
self.bottomStripeNode.isLayerBacked = true
|
||||
|
||||
self.maskNode = ASImageNode()
|
||||
|
||||
self.containerNode = ASDisplayNode()
|
||||
self.containerNode.subnodeTransform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0)
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
|
||||
self.clipsToBounds = true
|
||||
self.isUserInteractionEnabled = false
|
||||
|
||||
self.addSubnode(self.containerNode)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.disposable.dispose()
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: PeerNameColorChatPreviewItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
let currentNodes = self.messageNodes
|
||||
|
||||
var currentBackgroundNode = self.backgroundNode
|
||||
|
||||
let currentItem = self.item
|
||||
|
||||
return { item, params, neighbors in
|
||||
if currentBackgroundNode == nil {
|
||||
currentBackgroundNode = createWallpaperBackgroundNode(context: item.context, forChatDisplay: false)
|
||||
currentBackgroundNode?.update(wallpaper: item.wallpaper, animated: false)
|
||||
currentBackgroundNode?.updateBubbleTheme(bubbleTheme: item.componentTheme, bubbleCorners: item.chatBubbleCorners)
|
||||
}
|
||||
|
||||
var insets: UIEdgeInsets
|
||||
let separatorHeight = UIScreenPixel
|
||||
|
||||
let peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(1))
|
||||
|
||||
var items: [ListViewItem] = []
|
||||
for messageItem in item.messageItems.reversed() {
|
||||
let authorPeerId = messageItem.peerId
|
||||
let replyAuthorPeerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(10))
|
||||
|
||||
var peers = SimpleDictionary<PeerId, Peer>()
|
||||
var messages = SimpleDictionary<MessageId, Message>()
|
||||
|
||||
peers[authorPeerId] = TelegramUser(id: authorPeerId, accessHash: nil, firstName: messageItem.author, lastName: "", username: nil, phone: nil, photo: messageItem.photo, botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: messageItem.nameColor, backgroundEmojiId: messageItem.backgroundEmojiId, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil)
|
||||
|
||||
|
||||
let replyMessageId = MessageId(peerId: peerId, namespace: 0, id: 3)
|
||||
if let (replyAuthor, text, replyColor) = messageItem.reply {
|
||||
peers[replyAuthorPeerId] = TelegramUser(id: authorPeerId, accessHash: nil, firstName: replyAuthor, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: replyColor, backgroundEmojiId: messageItem.backgroundEmojiId, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil)
|
||||
|
||||
messages[replyMessageId] = Message(stableId: 3, stableVersion: 0, id: replyMessageId, globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[replyAuthorPeerId], text: text, attributes: [], media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
|
||||
}
|
||||
|
||||
var media: [Media] = []
|
||||
if let (site, title, text) = messageItem.linkPreview, params.width > 320.0 {
|
||||
media.append(TelegramMediaWebpage(webpageId: MediaId(namespace: 0, id: 0), content: .Loaded(TelegramMediaWebpageLoadedContent(url: "", displayUrl: "", hash: 0, type: nil, websiteName: site, title: title, text: text, embedUrl: nil, embedType: nil, embedSize: nil, duration: nil, author: nil, isMediaLargeByDefault: nil, image: nil, file: nil, story: nil, attributes: [], instantPage: nil))))
|
||||
}
|
||||
|
||||
var attributes: [MessageAttribute] = []
|
||||
if messageItem.reply != nil {
|
||||
attributes.append(ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil, isQuote: false))
|
||||
}
|
||||
|
||||
attributes.append(InlineBotMessageAttribute(peerId: nil, title: "Test Attach"))
|
||||
|
||||
let message = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: messageItem.outgoing ? [] : [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[authorPeerId], text: messageItem.text, attributes: attributes, media: media, peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
|
||||
items.append(item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [message], theme: item.componentTheme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true, isStandalone: false))
|
||||
}
|
||||
|
||||
var nodes: [ListViewItemNode] = []
|
||||
if let messageNodes = currentNodes {
|
||||
nodes = messageNodes
|
||||
for i in 0 ..< items.count {
|
||||
let itemNode = messageNodes[i]
|
||||
items[i].updateNode(async: { $0() }, node: {
|
||||
return itemNode
|
||||
}, params: params, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], animation: .None, completion: { (layout, apply) in
|
||||
let nodeFrame = CGRect(origin: itemNode.frame.origin, size: CGSize(width: layout.size.width, height: layout.size.height))
|
||||
|
||||
itemNode.contentSize = layout.contentSize
|
||||
itemNode.insets = layout.insets
|
||||
itemNode.frame = nodeFrame
|
||||
itemNode.isUserInteractionEnabled = false
|
||||
|
||||
Queue.mainQueue().after(0.01) {
|
||||
apply(ListViewItemApply(isOnScreen: true))
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
var messageNodes: [ListViewItemNode] = []
|
||||
for i in 0 ..< items.count {
|
||||
var itemNode: ListViewItemNode?
|
||||
items[i].nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: false, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], completion: { node, apply in
|
||||
itemNode = node
|
||||
apply().1(ListViewItemApply(isOnScreen: true))
|
||||
})
|
||||
itemNode!.isUserInteractionEnabled = false
|
||||
messageNodes.append(itemNode!)
|
||||
}
|
||||
nodes = messageNodes
|
||||
}
|
||||
|
||||
var contentSize = CGSize(width: params.width, height: 4.0 + 4.0)
|
||||
for node in nodes {
|
||||
contentSize.height += node.frame.size.height
|
||||
}
|
||||
insets = itemListNeighborsGroupedInsets(neighbors, params)
|
||||
if params.width <= 320.0 {
|
||||
insets.top = 0.0
|
||||
}
|
||||
|
||||
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
|
||||
let layoutSize = layout.size
|
||||
|
||||
let leftInset = params.leftInset
|
||||
let rightInset = params.leftInset
|
||||
|
||||
return (layout, { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.item = item
|
||||
|
||||
if let currentBackgroundNode {
|
||||
currentBackgroundNode.update(wallpaper: item.wallpaper, animated: false)
|
||||
currentBackgroundNode.updateBubbleTheme(bubbleTheme: item.theme, bubbleCorners: item.chatBubbleCorners)
|
||||
}
|
||||
|
||||
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: contentSize)
|
||||
|
||||
if let currentItem, currentItem.messageItems.first?.nameColor != item.messageItems.first?.nameColor || currentItem.messageItems.first?.backgroundEmojiId != item.messageItems.first?.backgroundEmojiId || currentItem.theme !== item.theme || currentItem.wallpaper != item.wallpaper {
|
||||
if let snapshot = strongSelf.view.snapshotView(afterScreenUpdates: false) {
|
||||
snapshot.frame = CGRect(origin: CGPoint(x: 0.0, y: -insets.top), size: snapshot.frame.size)
|
||||
strongSelf.view.addSubview(snapshot)
|
||||
snapshot.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, delay: 0.25, removeOnCompletion: false, completion: { _ in
|
||||
snapshot.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
strongSelf.messageNodes = nodes
|
||||
var topOffset: CGFloat = 4.0
|
||||
for node in nodes {
|
||||
if node.supernode == nil {
|
||||
strongSelf.containerNode.addSubnode(node)
|
||||
}
|
||||
node.updateFrame(CGRect(origin: CGPoint(x: 0.0, y: topOffset), size: node.frame.size), within: layoutSize)
|
||||
topOffset += node.frame.size.height
|
||||
|
||||
if let header = node.headers()?.first(where: { $0 is ChatMessageAvatarHeader }) {
|
||||
let headerFrame = CGRect(origin: CGPoint(x: 0.0, y: 3.0 + node.frame.minY), size: CGSize(width: layoutSize.width, height: header.height))
|
||||
let stickLocationDistanceFactor: CGFloat = 0.0
|
||||
|
||||
let id = header.id
|
||||
let headerNode: ListViewItemHeaderNode
|
||||
if let current = strongSelf.itemHeaderNodes[id] {
|
||||
headerNode = current
|
||||
headerNode.updateFrame(headerFrame, within: layoutSize)
|
||||
|
||||
if headerNode.item !== header {
|
||||
header.updateNode(headerNode, previous: nil, next: nil)
|
||||
headerNode.item = header
|
||||
}
|
||||
headerNode.updateLayoutInternal(size: headerFrame.size, leftInset: leftInset, rightInset: rightInset)
|
||||
headerNode.updateStickDistanceFactor(stickLocationDistanceFactor, transition: .immediate)
|
||||
} else {
|
||||
headerNode = header.node(synchronousLoad: true)
|
||||
if headerNode.item !== header {
|
||||
header.updateNode(headerNode, previous: nil, next: nil)
|
||||
headerNode.item = header
|
||||
}
|
||||
headerNode.frame = headerFrame
|
||||
headerNode.updateLayoutInternal(size: headerFrame.size, leftInset: leftInset, rightInset: rightInset)
|
||||
strongSelf.itemHeaderNodes[id] = headerNode
|
||||
|
||||
strongSelf.containerNode.addSubnode(headerNode)
|
||||
headerNode.updateStickDistanceFactor(stickLocationDistanceFactor, transition: .immediate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let currentBackgroundNode = currentBackgroundNode, strongSelf.backgroundNode !== currentBackgroundNode {
|
||||
strongSelf.backgroundNode = currentBackgroundNode
|
||||
strongSelf.insertSubnode(currentBackgroundNode, at: 0)
|
||||
}
|
||||
|
||||
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
|
||||
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
|
||||
|
||||
if strongSelf.topStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
|
||||
}
|
||||
if strongSelf.bottomStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
|
||||
}
|
||||
if strongSelf.maskNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
|
||||
}
|
||||
|
||||
if params.isStandalone {
|
||||
strongSelf.topStripeNode.isHidden = true
|
||||
strongSelf.bottomStripeNode.isHidden = true
|
||||
strongSelf.maskNode.isHidden = true
|
||||
} else {
|
||||
let hasCorners = itemListHasRoundedBlockLayout(params)
|
||||
|
||||
var hasTopCorners = false
|
||||
var hasBottomCorners = false
|
||||
|
||||
switch neighbors.top {
|
||||
case .sameSection(false):
|
||||
strongSelf.topStripeNode.isHidden = true
|
||||
default:
|
||||
hasTopCorners = true
|
||||
strongSelf.topStripeNode.isHidden = hasCorners
|
||||
}
|
||||
let bottomStripeInset: CGFloat
|
||||
let bottomStripeOffset: CGFloat
|
||||
switch neighbors.bottom {
|
||||
case .sameSection(false):
|
||||
bottomStripeInset = 0.0
|
||||
bottomStripeOffset = -separatorHeight
|
||||
strongSelf.bottomStripeNode.isHidden = false
|
||||
default:
|
||||
bottomStripeInset = 0.0
|
||||
bottomStripeOffset = 0.0
|
||||
hasBottomCorners = true
|
||||
strongSelf.bottomStripeNode.isHidden = hasCorners
|
||||
}
|
||||
|
||||
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.componentTheme, top: hasTopCorners, bottom: hasBottomCorners) : nil
|
||||
|
||||
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
|
||||
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
|
||||
}
|
||||
|
||||
let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
|
||||
|
||||
let displayMode: WallpaperDisplayMode
|
||||
if abs(params.availableHeight - params.width) < 100.0, params.availableHeight > 700.0 {
|
||||
displayMode = .halfAspectFill
|
||||
} else {
|
||||
if backgroundFrame.width > backgroundFrame.height * 4.0 {
|
||||
if params.availableHeight < 700.0 {
|
||||
displayMode = .halfAspectFill
|
||||
} else {
|
||||
displayMode = .aspectFill
|
||||
}
|
||||
} else {
|
||||
displayMode = .aspectFill
|
||||
}
|
||||
}
|
||||
|
||||
if let backgroundNode = strongSelf.backgroundNode {
|
||||
backgroundNode.frame = backgroundFrame
|
||||
backgroundNode.updateLayout(size: backgroundNode.bounds.size, displayMode: displayMode, transition: .immediate)
|
||||
}
|
||||
strongSelf.maskNode.frame = backgroundFrame.insetBy(dx: params.leftInset, dy: 0.0)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
}
|
||||
|
||||
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||
}
|
||||
}
|
337
submodules/WebUI/Sources/WebAppMessagePreviewScreen.swift
Normal file
337
submodules/WebUI/Sources/WebAppMessagePreviewScreen.swift
Normal file
@ -0,0 +1,337 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import Markdown
|
||||
import TextFormat
|
||||
import TelegramPresentationData
|
||||
import ViewControllerComponent
|
||||
import SheetComponent
|
||||
import BalancedTextComponent
|
||||
import MultilineTextComponent
|
||||
import BundleIconComponent
|
||||
import ButtonComponent
|
||||
import ItemListUI
|
||||
import AccountContext
|
||||
import PresentationDataUtils
|
||||
import ListSectionComponent
|
||||
import ListItemComponentAdaptor
|
||||
import TelegramStringFormatting
|
||||
|
||||
private final class SheetContent: CombinedComponent {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
||||
let context: AccountContext
|
||||
let dismiss: () -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
dismiss: @escaping () -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.dismiss = dismiss
|
||||
}
|
||||
|
||||
static func ==(lhs: SheetContent, rhs: SheetContent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
static var body: Body {
|
||||
let closeButton = Child(Button.self)
|
||||
let title = Child(Text.self)
|
||||
let amountSection = Child(ListSectionComponent.self)
|
||||
let button = Child(ButtonComponent.self)
|
||||
|
||||
return { context in
|
||||
let environment = context.environment[EnvironmentType.self]
|
||||
let component = context.component
|
||||
|
||||
let controller = environment.controller
|
||||
|
||||
let theme = environment.theme.withModalBlocksBackground()
|
||||
let strings = environment.strings
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
let sideInset: CGFloat = 16.0
|
||||
var contentSize = CGSize(width: context.availableSize.width, height: 18.0)
|
||||
|
||||
let constrainedTitleWidth = context.availableSize.width - 16.0 * 2.0
|
||||
|
||||
let closeButton = closeButton.update(
|
||||
component: Button(
|
||||
content: AnyComponent(Text(text: "Cancel", font: Font.regular(17.0), color: theme.actionSheet.controlAccentColor)),
|
||||
action: {
|
||||
component.dismiss()
|
||||
}
|
||||
),
|
||||
availableSize: CGSize(width: 120.0, height: 30.0),
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(closeButton
|
||||
.position(CGPoint(x: closeButton.size.width / 2.0 + sideInset, y: 28.0))
|
||||
)
|
||||
|
||||
let title = title.update(
|
||||
component: Text(text: "Share Message", font: Font.bold(17.0), color: theme.list.itemPrimaryTextColor),
|
||||
availableSize: CGSize(width: constrainedTitleWidth, height: context.availableSize.height),
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(title
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + title.size.height / 2.0))
|
||||
)
|
||||
contentSize.height += title.size.height
|
||||
contentSize.height += 40.0
|
||||
|
||||
let amountFont = Font.regular(13.0)
|
||||
let amountTextColor = theme.list.freeTextColor
|
||||
let amountMarkdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: amountFont, textColor: amountTextColor), bold: MarkdownAttributeSet(font: amountFont, textColor: amountTextColor), link: MarkdownAttributeSet(font: amountFont, textColor: theme.list.itemAccentColor), linkAttribute: { contents in
|
||||
return (TelegramTextAttributes.URL, contents)
|
||||
})
|
||||
|
||||
let amountInfoString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString("Test Attach mini app suggests you to send this message to a chat you select.", attributes: amountMarkdownAttributes, textAlignment: .natural))
|
||||
let amountFooter = AnyComponent(MultilineTextComponent(
|
||||
text: .plain(amountInfoString),
|
||||
maximumNumberOfLines: 0,
|
||||
highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.1),
|
||||
highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0),
|
||||
highlightAction: { attributes in
|
||||
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
|
||||
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
},
|
||||
tapAction: { attributes, _ in
|
||||
if let controller = controller() as? WebAppMessagePreviewScreen, let navigationController = controller.navigationController as? NavigationController {
|
||||
component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: strings.Stars_PaidContent_AmountInfo_URL, forceExternal: false, presentationData: presentationData, navigationController: navigationController, dismissInput: {})
|
||||
}
|
||||
}
|
||||
))
|
||||
|
||||
let messageItem = PeerNameColorChatPreviewItem.MessageItem(
|
||||
outgoing: true,
|
||||
peerId: EnginePeer.Id(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(0)),
|
||||
author: "",
|
||||
photo: [],
|
||||
nameColor: .blue,
|
||||
backgroundEmojiId: nil,
|
||||
reply: nil,
|
||||
linkPreview: nil,
|
||||
text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
|
||||
)
|
||||
|
||||
let listItemParams = ListViewItemLayoutParams(width: context.availableSize.width - sideInset * 2.0, leftInset: 0.0, rightInset: 0.0, availableHeight: 10000.0, isStandalone: true)
|
||||
|
||||
let amountSection = amountSection.update(
|
||||
component: ListSectionComponent(
|
||||
theme: theme,
|
||||
header: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: "Message Preview".uppercased(),
|
||||
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
|
||||
textColor: theme.list.freeTextColor
|
||||
)),
|
||||
maximumNumberOfLines: 0
|
||||
)),
|
||||
footer: amountFooter,
|
||||
items: [
|
||||
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListItemComponentAdaptor(
|
||||
itemGenerator: PeerNameColorChatPreviewItem(
|
||||
context: component.context,
|
||||
theme: environment.theme,
|
||||
componentTheme: environment.theme,
|
||||
strings: environment.strings,
|
||||
sectionId: 0,
|
||||
fontSize: presentationData.chatFontSize,
|
||||
chatBubbleCorners: presentationData.chatBubbleCorners,
|
||||
wallpaper: presentationData.chatWallpaper,
|
||||
dateTimeFormat: environment.dateTimeFormat,
|
||||
nameDisplayOrder: presentationData.nameDisplayOrder,
|
||||
messageItems: [messageItem]
|
||||
),
|
||||
params: listItemParams
|
||||
)))
|
||||
]
|
||||
),
|
||||
environment: {},
|
||||
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude),
|
||||
transition: context.transition
|
||||
)
|
||||
context.add(amountSection
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + amountSection.size.height / 2.0))
|
||||
.clipsToBounds(true)
|
||||
.cornerRadius(10.0)
|
||||
)
|
||||
contentSize.height += amountSection.size.height
|
||||
contentSize.height += 32.0
|
||||
|
||||
let buttonString: String = "Share With..."
|
||||
let buttonAttributedString = NSMutableAttributedString(string: buttonString, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)
|
||||
|
||||
let button = button.update(
|
||||
component: ButtonComponent(
|
||||
background: ButtonComponent.Background(
|
||||
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(buttonAttributedString)))
|
||||
),
|
||||
isEnabled: true,
|
||||
displaysProgress: false,
|
||||
action: {
|
||||
if let controller = controller() as? WebAppMessagePreviewScreen {
|
||||
let _ = controller
|
||||
}
|
||||
}
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50),
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(button
|
||||
.clipsToBounds(true)
|
||||
.cornerRadius(10.0)
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + button.size.height / 2.0))
|
||||
)
|
||||
contentSize.height += button.size.height
|
||||
contentSize.height += 15.0
|
||||
|
||||
contentSize.height += max(environment.inputHeight, environment.safeInsets.bottom)
|
||||
|
||||
return contentSize
|
||||
}
|
||||
}
|
||||
|
||||
final class State: ComponentState {
|
||||
var cachedCloseImage: (UIImage, PresentationTheme)?
|
||||
}
|
||||
|
||||
func makeState() -> State {
|
||||
return State()
|
||||
}
|
||||
}
|
||||
|
||||
private final class WebAppMessagePreviewSheetComponent: CombinedComponent {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
||||
private let context: AccountContext
|
||||
|
||||
init(
|
||||
context: AccountContext
|
||||
) {
|
||||
self.context = context
|
||||
}
|
||||
|
||||
static func ==(lhs: WebAppMessagePreviewSheetComponent, rhs: WebAppMessagePreviewSheetComponent) -> 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>(SheetContent(
|
||||
context: context.component.context,
|
||||
dismiss: {
|
||||
animateOut.invoke(Action { _ in
|
||||
if let controller = controller() {
|
||||
controller.dismiss(completion: nil)
|
||||
}
|
||||
})
|
||||
}
|
||||
)),
|
||||
backgroundColor: .color(environment.theme.list.blocksBackgroundColor),
|
||||
followContentSizeChanges: false,
|
||||
clipsContent: true,
|
||||
isScrollEnabled: false,
|
||||
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 final class WebAppMessagePreviewScreen: ViewControllerComponentContainer {
|
||||
private let context: AccountContext
|
||||
fileprivate let completion: (Bool) -> Void
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
completion: @escaping (Bool) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.completion = completion
|
||||
|
||||
super.init(
|
||||
context: context,
|
||||
component: WebAppMessagePreviewSheetComponent(
|
||||
context: context
|
||||
),
|
||||
navigationBarAppearance: .none,
|
||||
statusBarStyle: .ignore,
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
98
submodules/WebUI/Sources/WebAppPermissions.swift
Normal file
98
submodules/WebUI/Sources/WebAppPermissions.swift
Normal file
@ -0,0 +1,98 @@
|
||||
import Foundation
|
||||
import NaturalLanguage
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import AccountContext
|
||||
import TelegramUIPreferences
|
||||
|
||||
public struct WebAppPermissionsState: Codable {
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case location
|
||||
}
|
||||
|
||||
public struct Location: Codable {
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case isRequested
|
||||
case isAllowed
|
||||
}
|
||||
|
||||
public let isRequested: Bool
|
||||
public let isAllowed: Bool
|
||||
|
||||
public init(
|
||||
isRequested: Bool,
|
||||
isAllowed: Bool
|
||||
) {
|
||||
self.isRequested = isRequested
|
||||
self.isAllowed = isAllowed
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
self.isRequested = try container.decode(Bool.self, forKey: .isRequested)
|
||||
self.isAllowed = try container.decode(Bool.self, forKey: .isAllowed)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
try container.encode(self.isRequested, forKey: .isRequested)
|
||||
try container.encode(self.isAllowed, forKey: .isAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
public let location: Location?
|
||||
|
||||
public init(
|
||||
location: Location?
|
||||
) {
|
||||
self.location = location
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
self.location = try container.decode(WebAppPermissionsState.Location.self, forKey: .location)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
try container.encodeIfPresent(self.location, forKey: .location)
|
||||
}
|
||||
}
|
||||
|
||||
public func webAppPermissionsState(context: AccountContext, peerId: EnginePeer.Id) -> Signal<WebAppPermissionsState?, NoError> {
|
||||
let key = EngineDataBuffer(length: 8)
|
||||
key.setInt64(0, value: peerId.id._internalGetInt64Value())
|
||||
|
||||
return context.engine.data.subscribe(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: ApplicationSpecificItemCacheCollectionId.webAppPermissionsState, id: key))
|
||||
|> map { entry -> WebAppPermissionsState? in
|
||||
return entry?.get(WebAppPermissionsState.self)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateWebAppPermissionsState(context: AccountContext, peerId: EnginePeer.Id, state: WebAppPermissionsState?) -> Signal<Never, NoError> {
|
||||
let key = EngineDataBuffer(length: 8)
|
||||
key.setInt64(0, value: peerId.id._internalGetInt64Value())
|
||||
|
||||
if let state {
|
||||
return context.engine.itemCache.put(collectionId: ApplicationSpecificItemCacheCollectionId.webAppPermissionsState, id: key, item: state)
|
||||
} else {
|
||||
return context.engine.itemCache.remove(collectionId: ApplicationSpecificItemCacheCollectionId.webAppPermissionsState, id: key)
|
||||
}
|
||||
}
|
||||
|
||||
public func updateWebAppPermissionsStateInteractively(context: AccountContext, peerId: EnginePeer.Id, _ f: @escaping (WebAppPermissionsState?) -> WebAppPermissionsState?) -> Signal<Never, NoError> {
|
||||
let key = EngineDataBuffer(length: 8)
|
||||
key.setInt64(0, value: peerId.id._internalGetInt64Value())
|
||||
|
||||
return context.engine.data.get(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: ApplicationSpecificItemCacheCollectionId.webAppPermissionsState, id: key))
|
||||
|> map { entry -> WebAppPermissionsState? in
|
||||
return entry?.get(WebAppPermissionsState.self)
|
||||
}
|
||||
|> mapToSignal { current -> Signal<Never, NoError> in
|
||||
return updateWebAppPermissionsState(context: context, peerId: peerId, state: f(current))
|
||||
}
|
||||
}
|
390
submodules/WebUI/Sources/WebAppSetEmojiStatusScreen.swift
Normal file
390
submodules/WebUI/Sources/WebAppSetEmojiStatusScreen.swift
Normal file
@ -0,0 +1,390 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import Markdown
|
||||
import TextFormat
|
||||
import TelegramPresentationData
|
||||
import ViewControllerComponent
|
||||
import SheetComponent
|
||||
import BalancedTextComponent
|
||||
import MultilineTextComponent
|
||||
import BundleIconComponent
|
||||
import ButtonComponent
|
||||
import AccountContext
|
||||
import PresentationDataUtils
|
||||
import PremiumPeerShortcutComponent
|
||||
import GiftAnimationComponent
|
||||
|
||||
private final class SheetContent: CombinedComponent {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
||||
let context: AccountContext
|
||||
let botName: String
|
||||
let accountPeer: EnginePeer
|
||||
let file: TelegramMediaFile
|
||||
let dismiss: () -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
botName: String,
|
||||
accountPeer: EnginePeer,
|
||||
file: TelegramMediaFile,
|
||||
dismiss: @escaping () -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.botName = botName
|
||||
self.accountPeer = accountPeer
|
||||
self.file = file
|
||||
self.dismiss = dismiss
|
||||
}
|
||||
|
||||
static func ==(lhs: SheetContent, rhs: SheetContent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.botName != rhs.botName {
|
||||
return false
|
||||
}
|
||||
if lhs.file != rhs.file {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class State: ComponentState {
|
||||
var cachedCloseImage: (UIImage, PresentationTheme)?
|
||||
}
|
||||
|
||||
func makeState() -> State {
|
||||
return State()
|
||||
}
|
||||
|
||||
static var body: Body {
|
||||
let background = Child(RoundedRectangle.self)
|
||||
let animation = Child(GiftAnimationComponent.self)
|
||||
let closeButton = Child(Button.self)
|
||||
let title = Child(Text.self)
|
||||
let text = Child(BalancedTextComponent.self)
|
||||
|
||||
let peerShortcut = Child(PremiumPeerShortcutComponent.self)
|
||||
let button = Child(ButtonComponent.self)
|
||||
|
||||
|
||||
return { context in
|
||||
let environment = context.environment[EnvironmentType.self]
|
||||
let component = context.component
|
||||
let state = context.state
|
||||
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||
let theme = presentationData.theme
|
||||
|
||||
var contentSize = CGSize(width: context.availableSize.width, height: 18.0)
|
||||
|
||||
let background = background.update(
|
||||
component: RoundedRectangle(color: theme.actionSheet.opaqueItemBackgroundColor, cornerRadius: 8.0),
|
||||
availableSize: CGSize(width: context.availableSize.width, height: 1000.0),
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(background
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: background.size.height / 2.0))
|
||||
)
|
||||
|
||||
let animation = animation.update(
|
||||
component: GiftAnimationComponent(
|
||||
context: component.context,
|
||||
theme: environment.theme,
|
||||
file: component.file
|
||||
),
|
||||
availableSize: CGSize(width: 128.0, height: 128.0),
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(animation
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: animation.size.height / 2.0 + 12.0))
|
||||
)
|
||||
|
||||
let closeImage: UIImage
|
||||
if let (image, cacheTheme) = state.cachedCloseImage, theme === cacheTheme {
|
||||
closeImage = image
|
||||
} else {
|
||||
closeImage = generateCloseButtonImage(backgroundColor: UIColor(rgb: 0x808084, alpha: 0.1), foregroundColor: theme.actionSheet.inputClearButtonColor)!
|
||||
state.cachedCloseImage = (closeImage, theme)
|
||||
}
|
||||
let closeButton = closeButton.update(
|
||||
component: Button(
|
||||
content: AnyComponent(Image(image: closeImage)),
|
||||
action: {
|
||||
component.dismiss()
|
||||
}
|
||||
),
|
||||
availableSize: CGSize(width: 30.0, height: 30.0),
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(closeButton
|
||||
.position(CGPoint(x: context.availableSize.width - closeButton.size.width, y: 28.0))
|
||||
)
|
||||
|
||||
let constrainedTitleWidth = context.availableSize.width - 16.0 * 2.0
|
||||
|
||||
contentSize.height += 128.0
|
||||
|
||||
let title = title.update(
|
||||
component: Text(text: "Set Emoji Status", font: Font.bold(24.0), color: theme.list.itemPrimaryTextColor),
|
||||
availableSize: CGSize(width: constrainedTitleWidth, height: context.availableSize.height),
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(title
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + title.size.height / 2.0))
|
||||
)
|
||||
contentSize.height += title.size.height
|
||||
contentSize.height += 13.0
|
||||
|
||||
let textFont = Font.regular(15.0)
|
||||
let boldTextFont = Font.semibold(15.0)
|
||||
let textColor = theme.actionSheet.primaryTextColor
|
||||
let linkColor = theme.actionSheet.controlAccentColor
|
||||
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in
|
||||
return (TelegramTextAttributes.URL, contents)
|
||||
})
|
||||
|
||||
let text = text.update(
|
||||
component: BalancedTextComponent(
|
||||
text: .markdown(
|
||||
text: "Do you want to set this emoji status suggested by **\(component.botName)**?",
|
||||
attributes: markdownAttributes
|
||||
),
|
||||
horizontalAlignment: .center,
|
||||
maximumNumberOfLines: 0,
|
||||
lineSpacing: 0.2
|
||||
),
|
||||
availableSize: CGSize(width: constrainedTitleWidth, height: context.availableSize.height),
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(text
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + text.size.height / 2.0))
|
||||
)
|
||||
contentSize.height += text.size.height
|
||||
contentSize.height += 15.0
|
||||
|
||||
let peerShortcut = peerShortcut.update(
|
||||
component: PremiumPeerShortcutComponent(
|
||||
context: component.context,
|
||||
theme: theme,
|
||||
peer: component.accountPeer
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width - 32.0, height: context.availableSize.height),
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(peerShortcut
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + peerShortcut.size.height / 2.0))
|
||||
)
|
||||
contentSize.height += peerShortcut.size.height
|
||||
contentSize.height += 32.0
|
||||
|
||||
let controller = environment.controller() as? WebAppSetEmojiStatusScreen
|
||||
|
||||
let button = button.update(
|
||||
component: ButtonComponent(
|
||||
background: ButtonComponent.Background(
|
||||
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: "Confirm", font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center))))
|
||||
),
|
||||
isEnabled: true,
|
||||
displaysProgress: false,
|
||||
action: { [weak controller] in
|
||||
controller?.complete(result: true)
|
||||
controller?.dismissAnimated()
|
||||
}
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width - 16.0 * 2.0, height: 50),
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(button
|
||||
.clipsToBounds(true)
|
||||
.cornerRadius(10.0)
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + button.size.height / 2.0))
|
||||
)
|
||||
contentSize.height += button.size.height
|
||||
|
||||
contentSize.height += 48.0
|
||||
|
||||
return contentSize
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class WebAppSetEmojiStatusSheetComponent: CombinedComponent {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
||||
private let context: AccountContext
|
||||
private let botName: String
|
||||
private let accountPeer: EnginePeer
|
||||
private let file: TelegramMediaFile
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
botName: String,
|
||||
accountPeer: EnginePeer,
|
||||
file: TelegramMediaFile
|
||||
) {
|
||||
self.context = context
|
||||
self.botName = botName
|
||||
self.accountPeer = accountPeer
|
||||
self.file = file
|
||||
}
|
||||
|
||||
static func ==(lhs: WebAppSetEmojiStatusSheetComponent, rhs: WebAppSetEmojiStatusSheetComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.botName != rhs.botName {
|
||||
return false
|
||||
}
|
||||
if lhs.accountPeer != rhs.accountPeer {
|
||||
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>(SheetContent(
|
||||
context: context.component.context,
|
||||
botName: context.component.botName,
|
||||
accountPeer: context.component.accountPeer,
|
||||
file: context.component.file,
|
||||
dismiss: {
|
||||
animateOut.invoke(Action { _ in
|
||||
if let controller = controller() {
|
||||
controller.dismiss(completion: nil)
|
||||
}
|
||||
})
|
||||
}
|
||||
)),
|
||||
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 final class WebAppSetEmojiStatusScreen: ViewControllerComponentContainer {
|
||||
private let context: AccountContext
|
||||
private let completion: (Bool) -> Void
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
botName: String,
|
||||
accountPeer: EnginePeer,
|
||||
file: TelegramMediaFile,
|
||||
completion: @escaping (Bool) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.completion = completion
|
||||
|
||||
super.init(
|
||||
context: context,
|
||||
component: WebAppSetEmojiStatusSheetComponent(
|
||||
context: context,
|
||||
botName: botName,
|
||||
accountPeer: accountPeer,
|
||||
file: file
|
||||
),
|
||||
navigationBarAppearance: .none,
|
||||
statusBarStyle: .ignore,
|
||||
theme: .default
|
||||
)
|
||||
|
||||
self.navigationPresentation = .flatModal
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private var didComplete = false
|
||||
fileprivate func complete(result: Bool) {
|
||||
guard !self.didComplete else {
|
||||
return
|
||||
}
|
||||
self.didComplete = true
|
||||
self.completion(result)
|
||||
}
|
||||
|
||||
public func dismissAnimated() {
|
||||
if let view = self.node.hostView.findTaggedView(tag: SheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? SheetComponent<ViewControllerComponentContainer.Environment>.View {
|
||||
view.dismissAnimated()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? {
|
||||
return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
context.setFillColor(backgroundColor.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
context.setLineWidth(2.0)
|
||||
context.setLineCap(.round)
|
||||
context.setStrokeColor(foregroundColor.cgColor)
|
||||
|
||||
context.move(to: CGPoint(x: 10.0, y: 10.0))
|
||||
context.addLine(to: CGPoint(x: 20.0, y: 20.0))
|
||||
context.strokePath()
|
||||
|
||||
context.move(to: CGPoint(x: 20.0, y: 10.0))
|
||||
context.addLine(to: CGPoint(x: 10.0, y: 20.0))
|
||||
context.strokePath()
|
||||
})
|
||||
}
|
@ -92,24 +92,16 @@ function tgBrowserDisconnectObserver() {
|
||||
final class WebAppWebView: WKWebView {
|
||||
var handleScriptMessage: (WKScriptMessage) -> Void = { _ in }
|
||||
|
||||
var customSideInset: CGFloat = 0.0 {
|
||||
var customInsets: UIEdgeInsets = .zero {
|
||||
didSet {
|
||||
if self.customSideInset != oldValue {
|
||||
if self.customInsets != oldValue {
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var customBottomInset: CGFloat = 0.0 {
|
||||
didSet {
|
||||
if self.customBottomInset != oldValue {
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override var safeAreaInsets: UIEdgeInsets {
|
||||
return UIEdgeInsets(top: 0.0, left: self.customSideInset, bottom: self.customBottomInset, right: self.customSideInset)
|
||||
return UIEdgeInsets(top: self.customInsets.top, left: self.customInsets.left, bottom: self.customInsets.bottom, right: self.customInsets.right)
|
||||
}
|
||||
|
||||
init(account: Account) {
|
||||
@ -241,8 +233,11 @@ final class WebAppWebView: WKWebView {
|
||||
}
|
||||
|
||||
func updateMetrics(height: CGFloat, isExpanded: Bool, isStable: Bool, transition: ContainedViewLayoutTransition) {
|
||||
let data = "{height:\(height), is_expanded:\(isExpanded ? "true" : "false"), is_state_stable:\(isStable ? "true" : "false")}"
|
||||
self.sendEvent(name: "viewport_changed", data: data)
|
||||
let viewportData = "{height:\(height), is_expanded:\(isExpanded ? "true" : "false"), is_state_stable:\(isStable ? "true" : "false")}"
|
||||
self.sendEvent(name: "viewport_changed", data: viewportData)
|
||||
|
||||
let safeInsetsData = "{top:\(self.customInsets.top), bottom:\(self.customInsets.bottom), left:\(self.customInsets.left), right:\(self.customInsets.right)}"
|
||||
self.sendEvent(name: "safe_area_changed", data: safeInsetsData)
|
||||
}
|
||||
|
||||
var lastTouchTimestamp: Double?
|
||||
|
Loading…
x
Reference in New Issue
Block a user