mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-10-09 03:20:48 +00:00
Cherry-pick various improvements
This commit is contained in:
parent
1e51780884
commit
4e0da9eaa4
@ -9899,3 +9899,12 @@ Sorry for the inconvenience.";
|
||||
"SessionReview.Title" = "New Login Prevented";
|
||||
"SessionReview.Text" = "We have terminated the login attempt from **%1$@**, **%2$@**";
|
||||
"SessionReview.OkAction" = "Got it";
|
||||
|
||||
"WebApp.DisclaimerTitle" = "Terms of Use";
|
||||
"WebApp.DisclaimerText" = "You are about to use a mini app operated by an independent party not affiliated with Telegram. You must agree to the Terms of Use of mini apps to continue.";
|
||||
"WebApp.DisclaimerAgree" = "I agree to the [Terms of Use]()";
|
||||
"WebApp.DisclaimerContinue" = "Continue";
|
||||
"WebApp.Disclaimer_URL" = "https://telegram.org/tos/mini-apps";
|
||||
|
||||
"WebApp.ShortcutsAdded" = "**%@** shortcut added in attachment menu.";
|
||||
"WebApp.ShortcutsSettingsAdded" = "**%@** shortcut added in attachment menu and Settings.";
|
||||
|
@ -869,7 +869,7 @@ public protocol SharedAccountContext: AnyObject {
|
||||
func makeSetupTwoFactorAuthController(context: AccountContext) -> ViewController
|
||||
func makeStorageManagementController(context: AccountContext) -> ViewController
|
||||
func makeAttachmentFileController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, bannedSendMedia: (Int32, Bool)?, presentGallery: @escaping () -> Void, presentFiles: @escaping () -> Void, send: @escaping (AnyMediaReference) -> Void) -> AttachmentFileController
|
||||
func makeGalleryCaptionPanelView(context: AccountContext, chatLocation: ChatLocation, customEmojiAvailable: Bool, present: @escaping (ViewController) -> Void, presentInGlobalOverlay: @escaping (ViewController) -> Void) -> NSObject?
|
||||
func makeGalleryCaptionPanelView(context: AccountContext, chatLocation: ChatLocation, isScheduledMessages: Bool, customEmojiAvailable: Bool, present: @escaping (ViewController) -> Void, presentInGlobalOverlay: @escaping (ViewController) -> Void) -> NSObject?
|
||||
func makeHashtagSearchController(context: AccountContext, peer: EnginePeer?, query: String, all: Bool) -> ViewController
|
||||
func makeMyStoriesController(context: AccountContext, isArchive: Bool) -> ViewController
|
||||
func makeArchiveSettingsController(context: AccountContext) -> ViewController
|
||||
|
@ -82,6 +82,7 @@ final class AttachmentTextInputActionButtonsNode: ASDisplayNode {
|
||||
super.didLoad()
|
||||
|
||||
let gestureRecognizer = ContextGesture(target: nil, action: nil)
|
||||
gestureRecognizer.isEnabled = self.sendButtonLongPressEnabled
|
||||
self.gestureRecognizer = gestureRecognizer
|
||||
self.sendButton.view.addGestureRecognizer(gestureRecognizer)
|
||||
gestureRecognizer.activated = { [weak self] recognizer, _ in
|
||||
|
@ -327,7 +327,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
||||
|
||||
private var maxCaptionLength: Int32?
|
||||
|
||||
public init(context: AccountContext, presentationInterfaceState: ChatPresentationInterfaceState, isCaption: Bool = false, isAttachment: Bool = false, presentController: @escaping (ViewController) -> Void, makeEntityInputView: @escaping () -> AttachmentTextInputPanelInputView?) {
|
||||
public init(context: AccountContext, presentationInterfaceState: ChatPresentationInterfaceState, isCaption: Bool = false, isAttachment: Bool = false, isScheduledMessages: Bool = false, presentController: @escaping (ViewController) -> Void, makeEntityInputView: @escaping () -> AttachmentTextInputPanelInputView?) {
|
||||
self.context = context
|
||||
self.presentationInterfaceState = presentationInterfaceState
|
||||
self.isCaption = isCaption
|
||||
@ -374,8 +374,13 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
||||
|
||||
super.init()
|
||||
|
||||
self.actionButtons.sendButtonLongPressed = { [weak self] node, gesture in
|
||||
self?.interfaceInteraction?.displaySendMessageOptions(node, gesture)
|
||||
if !isScheduledMessages {
|
||||
self.actionButtons.sendButtonLongPressed = { [weak self] node, gesture in
|
||||
self?.interfaceInteraction?.displaySendMessageOptions(node, gesture)
|
||||
}
|
||||
self.actionButtons.sendButtonLongPressEnabled = true
|
||||
} else {
|
||||
self.actionButtons.sendButtonLongPressEnabled = false
|
||||
}
|
||||
|
||||
self.actionButtons.sendButton.addTarget(self, action: #selector(self.sendButtonPressed), forControlEvents: .touchUpInside)
|
||||
@ -461,7 +466,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
||||
}
|
||||
}
|
||||
|
||||
public func setTimeout(_ timeout: Int32) {
|
||||
public func setTimeout(_ timeout: Int32, isVideo: Bool) {
|
||||
}
|
||||
|
||||
public func animate(_ view: UIView, frame: CGRect) {
|
||||
@ -471,8 +476,9 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
||||
public func onAnimateOut() {
|
||||
}
|
||||
|
||||
public func dismissInput() {
|
||||
public func dismissInput() -> Bool {
|
||||
self.ensureUnfocused()
|
||||
return true
|
||||
}
|
||||
|
||||
public func baseHeight() -> CGFloat {
|
||||
@ -727,8 +733,6 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
||||
}
|
||||
self.textPlaceholderNode.frame = CGRect(origin: self.textPlaceholderNode.frame.origin, size: placeholderSize)
|
||||
}
|
||||
|
||||
self.actionButtons.sendButtonLongPressEnabled = true
|
||||
}
|
||||
|
||||
let sendButtonHasApplyIcon = self.isCaption || interfaceState.interfaceState.editMessage != nil
|
||||
@ -1692,7 +1696,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
||||
}
|
||||
}
|
||||
if let sendPressed = self.sendPressed, let presentationInterfaceState = self.effectivePresentationInterfaceState?() {
|
||||
self.dismissInput()
|
||||
let _ = self.dismissInput()
|
||||
let effectiveInputText = presentationInterfaceState.interfaceState.composeInputState.inputText
|
||||
sendPressed(effectiveInputText)
|
||||
return
|
||||
|
@ -19,7 +19,7 @@ public enum AttachmentButtonType: Equatable {
|
||||
case location
|
||||
case contact
|
||||
case poll
|
||||
case app(EnginePeer, String, [AttachMenuBots.Bot.IconName: TelegramMediaFile])
|
||||
case app(AttachMenuBot)
|
||||
case gift
|
||||
case standalone
|
||||
|
||||
@ -55,8 +55,8 @@ public enum AttachmentButtonType: Equatable {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .app(lhsPeer, lhsTitle, lhsIcons):
|
||||
if case let .app(rhsPeer, rhsTitle, rhsIcons) = rhs, lhsPeer == rhsPeer, lhsTitle == rhsTitle, lhsIcons == rhsIcons {
|
||||
case let .app(lhsBot):
|
||||
if case let .app(rhsBot) = rhs, lhsBot.peer.id == rhsBot.peer.id {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
@ -179,6 +179,7 @@ public class AttachmentController: ViewController {
|
||||
private let context: AccountContext
|
||||
private let updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?
|
||||
private let chatLocation: ChatLocation?
|
||||
private let isScheduledMessages: Bool
|
||||
private let buttons: [AttachmentButtonType]
|
||||
private let initialButton: AttachmentButtonType
|
||||
private let fromMenu: Bool
|
||||
@ -294,7 +295,7 @@ public class AttachmentController: ViewController {
|
||||
|
||||
self.container = AttachmentContainer()
|
||||
self.container.canHaveKeyboardFocus = true
|
||||
self.panel = AttachmentPanel(context: controller.context, chatLocation: controller.chatLocation, updatedPresentationData: controller.updatedPresentationData, makeEntityInputView: makeEntityInputView)
|
||||
self.panel = AttachmentPanel(context: controller.context, chatLocation: controller.chatLocation, isScheduledMessages: controller.isScheduledMessages, updatedPresentationData: controller.updatedPresentationData, makeEntityInputView: makeEntityInputView)
|
||||
self.panel.fromMenu = controller.fromMenu
|
||||
self.panel.isStandalone = controller.isStandalone
|
||||
|
||||
@ -445,9 +446,9 @@ public class AttachmentController: ViewController {
|
||||
|
||||
if let controller = self.controller {
|
||||
let _ = self.switchToController(controller.initialButton)
|
||||
if case let .app(bot, _, _) = controller.initialButton {
|
||||
if case let .app(bot) = controller.initialButton {
|
||||
if let index = controller.buttons.firstIndex(where: {
|
||||
if case let .app(otherBot, _, _) = $0, otherBot.id == bot.id {
|
||||
if case let .app(otherBot) = $0, otherBot.peer.id == bot.peer.id {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
@ -907,10 +908,11 @@ public class AttachmentController: ViewController {
|
||||
|
||||
public var getSourceRect: (() -> CGRect?)?
|
||||
|
||||
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, chatLocation: ChatLocation?, buttons: [AttachmentButtonType], initialButton: AttachmentButtonType = .gallery, fromMenu: Bool = false, hasTextInput: Bool = true, makeEntityInputView: @escaping () -> AttachmentTextInputPanelInputView? = { return nil}) {
|
||||
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, chatLocation: ChatLocation?, isScheduledMessages: Bool = false, buttons: [AttachmentButtonType], initialButton: AttachmentButtonType = .gallery, fromMenu: Bool = false, hasTextInput: Bool = true, makeEntityInputView: @escaping () -> AttachmentTextInputPanelInputView? = { return nil}) {
|
||||
self.context = context
|
||||
self.updatedPresentationData = updatedPresentationData
|
||||
self.chatLocation = chatLocation
|
||||
self.isScheduledMessages = isScheduledMessages
|
||||
self.buttons = buttons
|
||||
self.initialButton = initialButton
|
||||
self.fromMenu = fromMenu
|
||||
|
@ -200,15 +200,15 @@ private final class AttachButtonComponent: CombinedComponent {
|
||||
case .gift:
|
||||
name = strings.Attachment_Gift
|
||||
imageName = "Chat/Attach Menu/Gift"
|
||||
case let .app(peer, appName, appIcons):
|
||||
botPeer = peer
|
||||
name = appName
|
||||
case let .app(bot):
|
||||
botPeer = bot.peer
|
||||
name = bot.shortName
|
||||
imageName = ""
|
||||
if let file = appIcons[.iOSAnimated] {
|
||||
if let file = bot.icons[.iOSAnimated] {
|
||||
animationFile = file
|
||||
} else if let file = appIcons[.iOSStatic] {
|
||||
} else if let file = bot.icons[.iOSStatic] {
|
||||
imageFile = file
|
||||
} else if let file = appIcons[.default] {
|
||||
} else if let file = bot.icons[.default] {
|
||||
imageFile = file
|
||||
}
|
||||
case .standalone:
|
||||
@ -725,6 +725,7 @@ private final class MainButtonNode: HighlightTrackingButtonNode {
|
||||
|
||||
final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
|
||||
private let context: AccountContext
|
||||
private let isScheduledMessages: Bool
|
||||
private var presentationData: PresentationData
|
||||
private var presentationDataDisposable: Disposable?
|
||||
|
||||
@ -775,9 +776,10 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
|
||||
|
||||
var mainButtonPressed: () -> Void = { }
|
||||
|
||||
init(context: AccountContext, chatLocation: ChatLocation?, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, makeEntityInputView: @escaping () -> AttachmentTextInputPanelInputView?) {
|
||||
init(context: AccountContext, chatLocation: ChatLocation?, isScheduledMessages: Bool, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, makeEntityInputView: @escaping () -> AttachmentTextInputPanelInputView?) {
|
||||
self.context = context
|
||||
self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
|
||||
self.isScheduledMessages = isScheduledMessages
|
||||
|
||||
self.makeEntityInputView = makeEntityInputView
|
||||
|
||||
@ -1140,10 +1142,10 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
|
||||
}
|
||||
|
||||
let type = self.buttons[i]
|
||||
if case let .app(peer, _, iconFiles) = type {
|
||||
for (name, file) in iconFiles {
|
||||
if [.default, .iOSAnimated, .placeholder].contains(name) {
|
||||
if self.iconDisposables[file.fileId] == nil, let peer = PeerReference(peer._asPeer()) {
|
||||
if case let .app(bot) = type {
|
||||
for (name, file) in bot.icons {
|
||||
if [.default, .iOSAnimated, .iOSSettingsStatic, .placeholder].contains(name) {
|
||||
if self.iconDisposables[file.fileId] == nil, let peer = PeerReference(bot.peer._asPeer()) {
|
||||
if case .placeholder = name {
|
||||
let account = self.context.account
|
||||
let path = account.postbox.mediaBox.cachedRepresentationCompletePath(file.resource.id, representation: CachedPreparedSvgRepresentation())
|
||||
@ -1212,8 +1214,8 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
|
||||
accessibilityTitle = self.presentationData.strings.Attachment_Poll
|
||||
case .gift:
|
||||
accessibilityTitle = self.presentationData.strings.Attachment_Gift
|
||||
case let .app(_, appName, _):
|
||||
accessibilityTitle = appName
|
||||
case let .app(bot):
|
||||
accessibilityTitle = bot.shortName
|
||||
case .standalone:
|
||||
accessibilityTitle = ""
|
||||
}
|
||||
@ -1248,7 +1250,7 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
|
||||
private func loadTextNodeIfNeeded() {
|
||||
if let _ = self.textInputPanelNode {
|
||||
} else {
|
||||
let textInputPanelNode = AttachmentTextInputPanelNode(context: self.context, presentationInterfaceState: self.presentationInterfaceState, isAttachment: true, presentController: { [weak self] c in
|
||||
let textInputPanelNode = AttachmentTextInputPanelNode(context: self.context, presentationInterfaceState: self.presentationInterfaceState, isAttachment: true, isScheduledMessages: self.isScheduledMessages, presentController: { [weak self] c in
|
||||
if let strongSelf = self {
|
||||
strongSelf.present(c)
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ public final class ChatSendMessageActionSheetController: ViewController {
|
||||
private let context: AccountContext
|
||||
|
||||
private let peerId: EnginePeer.Id?
|
||||
private let isScheduledMessages: Bool
|
||||
private let forwardMessageIds: [EngineMessage.Id]?
|
||||
private let hasEntityKeyboard: Bool
|
||||
|
||||
@ -45,9 +46,10 @@ public final class ChatSendMessageActionSheetController: ViewController {
|
||||
|
||||
public var emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?
|
||||
|
||||
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: EnginePeer.Id?, forwardMessageIds: [EngineMessage.Id]?, hasEntityKeyboard: Bool, gesture: ContextGesture, sourceSendButton: ASDisplayNode, textInputNode: EditableTextNode, attachment: Bool = false, canSendWhenOnline: Bool, completion: @escaping () -> Void, sendMessage: @escaping (SendMode) -> Void, schedule: @escaping () -> Void) {
|
||||
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: EnginePeer.Id?, isScheduledMessages: Bool = false, forwardMessageIds: [EngineMessage.Id]?, hasEntityKeyboard: Bool, gesture: ContextGesture, sourceSendButton: ASDisplayNode, textInputNode: EditableTextNode, attachment: Bool = false, canSendWhenOnline: Bool, completion: @escaping () -> Void, sendMessage: @escaping (SendMode) -> Void, schedule: @escaping () -> Void) {
|
||||
self.context = context
|
||||
self.peerId = peerId
|
||||
self.isScheduledMessages = isScheduledMessages
|
||||
self.forwardMessageIds = forwardMessageIds
|
||||
self.hasEntityKeyboard = hasEntityKeyboard
|
||||
self.gesture = gesture
|
||||
@ -101,6 +103,9 @@ public final class ChatSendMessageActionSheetController: ViewController {
|
||||
isSecret = peerId.namespace == Namespaces.Peer.SecretChat
|
||||
canSchedule = !isSecret
|
||||
}
|
||||
if self.isScheduledMessages {
|
||||
canSchedule = false
|
||||
}
|
||||
|
||||
self.displayNode = ChatSendMessageActionSheetControllerNode(context: self.context, presentationData: self.presentationData, reminders: reminders, gesture: gesture, sourceSendButton: self.sourceSendButton, textInputNode: self.textInputNode, attachment: self.attachment, canSendWhenOnline: self.canSendWhenOnline, forwardedCount: forwardedCount, hasEntityKeyboard: self.hasEntityKeyboard, emojiViewProvider: self.emojiViewProvider, send: { [weak self] in
|
||||
self?.sendMessage(.generic)
|
||||
|
@ -631,7 +631,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
|
||||
func setMessage(_ message: Message, displayInfo: Bool = true, translateToLanguage: String? = nil, peerIsCopyProtected: Bool = false) {
|
||||
self.currentMessage = message
|
||||
|
||||
let canDelete: Bool
|
||||
var canDelete: Bool
|
||||
var canShare = !message.containsSecretMedia
|
||||
|
||||
var canFullscreen = false
|
||||
@ -715,6 +715,10 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
|
||||
canEdit = false
|
||||
}
|
||||
|
||||
if message.containsSecretMedia {
|
||||
canDelete = false
|
||||
}
|
||||
|
||||
var authorNameText: String?
|
||||
if let forwardInfo = message.forwardInfo, forwardInfo.flags.contains(.isImported), let authorSignature = forwardInfo.authorSignature {
|
||||
authorNameText = authorSignature
|
||||
|
@ -1344,7 +1344,9 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
strongSelf.isPlayingPromise.set(false)
|
||||
strongSelf.isPlaying = false
|
||||
if strongSelf.isCentral == true {
|
||||
strongSelf.updateControlsVisibility(true)
|
||||
if !item.isSecret {
|
||||
strongSelf.updateControlsVisibility(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1481,7 +1483,9 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
if strongSelf.actionAtEnd == .stop && strongSelf.isCentral == true {
|
||||
strongSelf.isPlayingPromise.set(false)
|
||||
strongSelf.isPlaying = false
|
||||
strongSelf.updateControlsVisibility(true)
|
||||
if !item.isSecret {
|
||||
strongSelf.updateControlsVisibility(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -60,16 +60,20 @@ private final class SecretMediaPreviewControllerNode: GalleryControllerNode {
|
||||
|
||||
private var validLayout: (ContainerViewLayout, CGFloat)?
|
||||
|
||||
var beginTimeAndTimeout: (Double, Double)? {
|
||||
var beginTimeAndTimeout: (Double, Double, Bool)? {
|
||||
didSet {
|
||||
if let (beginTime, timeout) = self.beginTimeAndTimeout {
|
||||
if let (beginTime, timeout, isOutgoing) = self.beginTimeAndTimeout {
|
||||
var beginTime = beginTime
|
||||
if self.timeoutNode == nil {
|
||||
let timeoutNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.5))
|
||||
self.timeoutNode = timeoutNode
|
||||
let icon: RadialStatusNodeState.SecretTimeoutIcon
|
||||
let timeoutValue = Int32(timeout)
|
||||
if timeoutValue == viewOnceTimeout {
|
||||
|
||||
let state: RadialStatusNodeState
|
||||
if timeoutValue == 0 && isOutgoing {
|
||||
state = .staticTimeout
|
||||
} else if timeoutValue == viewOnceTimeout {
|
||||
beginTime = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970
|
||||
|
||||
if let image = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/ViewOnce"), color: .white) {
|
||||
@ -77,10 +81,11 @@ private final class SecretMediaPreviewControllerNode: GalleryControllerNode {
|
||||
} else {
|
||||
icon = .flame
|
||||
}
|
||||
state = .secretTimeout(color: .white, icon: icon, beginTime: beginTime, timeout: timeout, sparks: isOutgoing ? false : true)
|
||||
} else {
|
||||
icon = .flame
|
||||
state = .secretTimeout(color: .white, icon: .flame, beginTime: beginTime, timeout: timeout, sparks: true)
|
||||
}
|
||||
timeoutNode.transitionToState(.secretTimeout(color: .white, icon: icon, beginTime: beginTime, timeout: timeout, sparks: true), completion: {})
|
||||
timeoutNode.transitionToState(state, completion: {})
|
||||
self.addSubnode(timeoutNode)
|
||||
|
||||
timeoutNode.addTarget(self, action: #selector(self.statusTapGesture), forControlEvents: .touchUpInside)
|
||||
@ -103,6 +108,8 @@ private final class SecretMediaPreviewControllerNode: GalleryControllerNode {
|
||||
}
|
||||
}
|
||||
|
||||
var onDismissTransitionUpdate: (CGFloat) -> Void = { _ in }
|
||||
|
||||
override func animateIn(animateContent: Bool, useSimpleAnimation: Bool) {
|
||||
super.animateIn(animateContent: animateContent, useSimpleAnimation: useSimpleAnimation)
|
||||
|
||||
@ -119,6 +126,7 @@ private final class SecretMediaPreviewControllerNode: GalleryControllerNode {
|
||||
|
||||
override func updateDismissTransition(_ value: CGFloat) {
|
||||
self.timeoutNode?.alpha = value
|
||||
self.onDismissTransitionUpdate(value)
|
||||
}
|
||||
|
||||
override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
@ -157,6 +165,7 @@ public final class SecretMediaPreviewController: ViewController {
|
||||
private var currentNodeMessageId: MessageId?
|
||||
private var currentNodeMessageIsVideo = false
|
||||
private var currentNodeMessageIsViewOnce = false
|
||||
private var currentMessageIsDismissed = false
|
||||
private var tempFile: TempBoxFile?
|
||||
|
||||
private let centralItemAttributesDisposable = DisposableSet();
|
||||
@ -252,6 +261,12 @@ public final class SecretMediaPreviewController: ViewController {
|
||||
}
|
||||
}
|
||||
|
||||
self.controllerNode.onDismissTransitionUpdate = { [weak self] _ in
|
||||
if let self {
|
||||
self.dismissAllTooltips()
|
||||
}
|
||||
}
|
||||
|
||||
self.controllerNode.statusBar = self.statusBar
|
||||
self.controllerNode.navigationBar = self.navigationBar
|
||||
|
||||
@ -270,11 +285,6 @@ public final class SecretMediaPreviewController: ViewController {
|
||||
self.controllerNode.dismiss = { [weak self] in
|
||||
self?._hiddenMedia.set(.single(nil))
|
||||
self?.presentingViewController?.dismiss(animated: false, completion: nil)
|
||||
|
||||
if let tooltipController = self?.tooltipController {
|
||||
self?.tooltipController = nil
|
||||
tooltipController.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
self.controllerNode.beginCustomDismiss = { [weak self] _ in
|
||||
@ -298,7 +308,7 @@ public final class SecretMediaPreviewController: ViewController {
|
||||
var hiddenItem: (MessageId, Media)?
|
||||
if let _ = index {
|
||||
if let message = strongSelf.messageView?.message, let media = mediaForMessage(message: message) {
|
||||
var beginTimeAndTimeout: (Double, Double)?
|
||||
var beginTimeAndTimeout: (Double, Double, Bool)?
|
||||
var videoDuration: Double?
|
||||
for media in message.media {
|
||||
if let file = media as? TelegramMediaFile {
|
||||
@ -306,26 +316,34 @@ public final class SecretMediaPreviewController: ViewController {
|
||||
}
|
||||
}
|
||||
|
||||
var timerStarted = false
|
||||
let isOutgoing = !message.flags.contains(.Incoming)
|
||||
if let attribute = message.autoclearAttribute {
|
||||
strongSelf.currentNodeMessageIsViewOnce = attribute.timeout == viewOnceTimeout
|
||||
|
||||
if let countdownBeginTime = attribute.countdownBeginTime {
|
||||
timerStarted = true
|
||||
if let videoDuration = videoDuration, attribute.timeout != viewOnceTimeout {
|
||||
beginTimeAndTimeout = (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, videoDuration)
|
||||
beginTimeAndTimeout = (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, max(videoDuration, Double(attribute.timeout)), isOutgoing)
|
||||
} else {
|
||||
beginTimeAndTimeout = (Double(countdownBeginTime), Double(attribute.timeout))
|
||||
beginTimeAndTimeout = (Double(countdownBeginTime), Double(attribute.timeout), isOutgoing)
|
||||
}
|
||||
} else if isOutgoing {
|
||||
beginTimeAndTimeout = (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, attribute.timeout != viewOnceTimeout ? 0.0 : Double(viewOnceTimeout), isOutgoing)
|
||||
}
|
||||
} else if let attribute = message.autoremoveAttribute {
|
||||
strongSelf.currentNodeMessageIsViewOnce = attribute.timeout == viewOnceTimeout
|
||||
|
||||
if let countdownBeginTime = attribute.countdownBeginTime {
|
||||
timerStarted = true
|
||||
if let videoDuration = videoDuration, attribute.timeout != viewOnceTimeout {
|
||||
beginTimeAndTimeout = (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, videoDuration)
|
||||
beginTimeAndTimeout = (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, max(videoDuration, Double(attribute.timeout)), isOutgoing)
|
||||
} else {
|
||||
beginTimeAndTimeout = (Double(countdownBeginTime), Double(attribute.timeout))
|
||||
beginTimeAndTimeout = (Double(countdownBeginTime), Double(attribute.timeout), isOutgoing)
|
||||
}
|
||||
}
|
||||
} else if isOutgoing {
|
||||
beginTimeAndTimeout = (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, attribute.timeout != viewOnceTimeout ? 0.0 : Double(viewOnceTimeout), isOutgoing)
|
||||
}
|
||||
}
|
||||
|
||||
if let file = media as? TelegramMediaFile {
|
||||
@ -350,12 +368,12 @@ public final class SecretMediaPreviewController: ViewController {
|
||||
strongSelf.controllerNode.beginTimeAndTimeout = beginTimeAndTimeout
|
||||
}
|
||||
|
||||
if strongSelf.currentNodeMessageIsVideo {
|
||||
if message.flags.contains(.Incoming) || strongSelf.currentNodeMessageIsVideo {
|
||||
if let node = strongSelf.controllerNode.pager.centralItemNode() {
|
||||
strongSelf.footerContentNode.set(node.footerContent())
|
||||
}
|
||||
} else if !message.flags.contains(.Incoming) {
|
||||
if let _ = beginTimeAndTimeout {
|
||||
} else {
|
||||
if timerStarted {
|
||||
strongSelf.controllerNode.updatePresentationState({
|
||||
$0.withUpdatedFooterContentNode(nil)
|
||||
}, transition: .immediate)
|
||||
@ -448,10 +466,7 @@ public final class SecretMediaPreviewController: ViewController {
|
||||
}
|
||||
|
||||
private func dismiss(forceAway: Bool) {
|
||||
if let tooltipController = self.tooltipController {
|
||||
self.tooltipController = nil
|
||||
tooltipController.dismiss()
|
||||
}
|
||||
self.dismissAllTooltips()
|
||||
|
||||
var animatedOutNode = true
|
||||
var animatedOutInterface = false
|
||||
@ -494,6 +509,7 @@ public final class SecretMediaPreviewController: ViewController {
|
||||
if self.currentNodeMessageId != message.id {
|
||||
self.currentNodeMessageId = message.id
|
||||
var tempFilePath: String?
|
||||
var duration: Double = 0.0
|
||||
for media in message.media {
|
||||
if let file = media as? TelegramMediaFile {
|
||||
if let path = self.context.account.postbox.mediaBox.completedResourcePath(file.resource) {
|
||||
@ -502,13 +518,14 @@ public final class SecretMediaPreviewController: ViewController {
|
||||
tempFilePath = tempFile.path
|
||||
self.currentNodeMessageIsVideo = true
|
||||
}
|
||||
duration = file.duration ?? 0.0
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
guard let item = galleryItemForEntry(context: self.context, presentationData: self.presentationData, entry: MessageHistoryEntry(message: message, isRead: false, location: nil, monthLocation: nil, attributes: MutableMessageHistoryEntryAttributes(authorIsContact: false)), streamVideos: false, hideControls: true, isSecret: true, playbackRate: { nil }, tempFilePath: tempFilePath, playbackCompleted: { [weak self] in
|
||||
if let self {
|
||||
if self.currentNodeMessageIsViewOnce {
|
||||
if self.currentNodeMessageIsViewOnce || (duration < 30.0 && !self.currentMessageIsDismissed) {
|
||||
if let node = self.controllerNode.pager.centralItemNode() as? UniversalVideoGalleryItemNode {
|
||||
node.seekToStart()
|
||||
}
|
||||
@ -528,28 +545,34 @@ public final class SecretMediaPreviewController: ViewController {
|
||||
self._ready.set(ready |> map { true })
|
||||
self.markMessageAsConsumedDisposable.set(self.context.engine.messages.markMessageContentAsConsumedInteractively(messageId: message.id).start())
|
||||
} else {
|
||||
var beginTimeAndTimeout: (Double, Double)?
|
||||
var beginTimeAndTimeout: (Double, Double, Bool)?
|
||||
var videoDuration: Double?
|
||||
for media in message.media {
|
||||
if let file = media as? TelegramMediaFile {
|
||||
videoDuration = file.duration
|
||||
}
|
||||
}
|
||||
|
||||
let isOutgoing = !message.flags.contains(.Incoming)
|
||||
if let attribute = message.autoclearAttribute {
|
||||
if let countdownBeginTime = attribute.countdownBeginTime {
|
||||
if let videoDuration = videoDuration, attribute.timeout != viewOnceTimeout {
|
||||
beginTimeAndTimeout = (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, videoDuration)
|
||||
beginTimeAndTimeout = (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, max(videoDuration, Double(attribute.timeout)), isOutgoing)
|
||||
} else {
|
||||
beginTimeAndTimeout = (Double(countdownBeginTime), Double(attribute.timeout))
|
||||
beginTimeAndTimeout = (Double(countdownBeginTime), Double(attribute.timeout), isOutgoing)
|
||||
}
|
||||
} else if isOutgoing {
|
||||
beginTimeAndTimeout = (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, attribute.timeout != viewOnceTimeout ? 0.0 : Double(viewOnceTimeout), isOutgoing)
|
||||
}
|
||||
} else if let attribute = message.autoremoveAttribute {
|
||||
if let countdownBeginTime = attribute.countdownBeginTime {
|
||||
if let videoDuration = videoDuration, attribute.timeout != viewOnceTimeout {
|
||||
beginTimeAndTimeout = (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, videoDuration)
|
||||
beginTimeAndTimeout = (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, max(videoDuration, Double(attribute.timeout)), isOutgoing)
|
||||
} else {
|
||||
beginTimeAndTimeout = (Double(countdownBeginTime), Double(attribute.timeout))
|
||||
beginTimeAndTimeout = (Double(countdownBeginTime), Double(attribute.timeout), isOutgoing)
|
||||
}
|
||||
} else if isOutgoing {
|
||||
beginTimeAndTimeout = (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, attribute.timeout != viewOnceTimeout ? 0.0 : Double(viewOnceTimeout), isOutgoing)
|
||||
}
|
||||
}
|
||||
|
||||
@ -566,6 +589,14 @@ public final class SecretMediaPreviewController: ViewController {
|
||||
if !(self.currentNodeMessageIsVideo || self.currentNodeMessageIsViewOnce) {
|
||||
self.dismiss()
|
||||
}
|
||||
self.currentMessageIsDismissed = true
|
||||
}
|
||||
}
|
||||
|
||||
private func dismissAllTooltips() {
|
||||
if let tooltipController = self.tooltipController {
|
||||
self.tooltipController = nil
|
||||
tooltipController.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
@ -574,10 +605,7 @@ public final class SecretMediaPreviewController: ViewController {
|
||||
return
|
||||
}
|
||||
|
||||
if let tooltipController = self.tooltipController {
|
||||
self.tooltipController = nil
|
||||
tooltipController.dismiss()
|
||||
}
|
||||
self.dismissAllTooltips()
|
||||
|
||||
let absoluteFrame = sourceView.convert(sourceView.bounds, to: nil)
|
||||
let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.maxY + 2.0), size: CGSize())
|
||||
@ -603,6 +631,7 @@ public final class SecretMediaPreviewController: ViewController {
|
||||
location: .point(location, .top),
|
||||
displayDuration: .default,
|
||||
inset: 8.0,
|
||||
cornerRadius: 8.0,
|
||||
shouldDismissOnTouch: { _, _ in
|
||||
return .ignore
|
||||
}
|
||||
|
@ -54,6 +54,8 @@
|
||||
- (void)editorTransitionIn;
|
||||
- (void)editorTransitionOut;
|
||||
|
||||
- (void)onDismiss;
|
||||
|
||||
- (void)setTabBarUserInteractionEnabled:(bool)enabled;
|
||||
|
||||
@end
|
||||
|
@ -36,7 +36,7 @@
|
||||
- (void)setCaption:(NSAttributedString *)caption animated:(bool)animated;
|
||||
- (void)setCaptionPanelHidden:(bool)hidden animated:(bool)animated;
|
||||
|
||||
- (void)setTimeout:(int32_t)timeout;
|
||||
- (void)setTimeout:(int32_t)timeout isVideo:(bool)isVideo;
|
||||
|
||||
- (void)updateLayoutWithFrame:(CGRect)frame edgeInsets:(UIEdgeInsets)edgeInsets animated:(bool)animated;
|
||||
|
||||
|
@ -22,11 +22,11 @@
|
||||
|
||||
@property (nonatomic, readonly) UIView * _Nonnull view;
|
||||
|
||||
- (void)setTimeout:(int32_t)timeout;
|
||||
- (void)setTimeout:(int32_t)timeout isVideo:(bool)isVideo;
|
||||
|
||||
- (NSAttributedString * _Nonnull)caption;
|
||||
- (void)setCaption:(NSAttributedString * _Nullable)caption;
|
||||
- (void)dismissInput;
|
||||
- (bool)dismissInput;
|
||||
|
||||
- (void)animateView:(UIView * _Nonnull)view frame:(CGRect)frame;
|
||||
|
||||
|
@ -384,6 +384,8 @@
|
||||
|
||||
id<TGModernGalleryEditableItem> galleryEditableItem = (id<TGModernGalleryEditableItem>)strongSelf->_currentItem;
|
||||
[strongSelf->_editingContext setTimer:timeout forItem:galleryEditableItem.editableMediaItem];
|
||||
|
||||
[strongSelf->_selectionContext setItem:(id<TGMediaSelectableItem>)galleryEditableItem.editableMediaItem selected:true animated:true sender:nil];
|
||||
};
|
||||
|
||||
_captionMixin.stickersContext = stickersContext;
|
||||
@ -836,7 +838,7 @@
|
||||
id<TGMediaEditAdjustments> adjustments = dict[@"adjustments"];
|
||||
NSNumber *timer = dict[@"timer"];
|
||||
|
||||
[strongSelf->_captionMixin setTimeout:[timer intValue]];
|
||||
[strongSelf->_captionMixin setTimeout:[timer intValue] isVideo:editableMediaItem.isVideo];
|
||||
|
||||
if ([adjustments isKindOfClass:[TGVideoEditAdjustments class]])
|
||||
{
|
||||
@ -1326,8 +1328,13 @@
|
||||
[_captionMixin onAnimateOut];
|
||||
}
|
||||
|
||||
- (void)onDismiss {
|
||||
[_captionMixin onAnimateOut];
|
||||
}
|
||||
|
||||
- (void)setTransitionOutProgress:(CGFloat)transitionOutProgress manual:(bool)manual
|
||||
{
|
||||
[_captionMixin onAnimateOut];
|
||||
if (transitionOutProgress > FLT_EPSILON)
|
||||
[self setAllInterfaceHidden:true delay:0.0 animated:true];
|
||||
else if (!manual)
|
||||
|
@ -141,8 +141,8 @@
|
||||
[_inputPanel setCaption:caption];
|
||||
}
|
||||
|
||||
- (void)setTimeout:(int32_t)timeout {
|
||||
[_inputPanel setTimeout:timeout];
|
||||
- (void)setTimeout:(int32_t)timeout isVideo:(bool)isVideo {
|
||||
[_inputPanel setTimeout:timeout isVideo:isVideo];
|
||||
}
|
||||
|
||||
- (void)setCaptionPanelHidden:(bool)hidden animated:(bool)__unused animated
|
||||
@ -170,13 +170,14 @@
|
||||
if (gestureRecognizer.state != UIGestureRecognizerStateRecognized)
|
||||
return;
|
||||
|
||||
_editing = false;
|
||||
|
||||
[self.inputPanel dismissInput];
|
||||
[_dismissView removeFromSuperview];
|
||||
|
||||
if (self.finishedWithCaption != nil)
|
||||
self.finishedWithCaption([_inputPanel caption]);
|
||||
if ([self.inputPanel dismissInput]) {
|
||||
_editing = false;
|
||||
|
||||
[_dismissView removeFromSuperview];
|
||||
|
||||
if (self.finishedWithCaption != nil)
|
||||
self.finishedWithCaption([_inputPanel caption]);
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Input Panel Delegate
|
||||
|
@ -100,7 +100,7 @@ enum LegacyMediaPickerGallerySource {
|
||||
case selection(item: TGMediaSelectableItem)
|
||||
}
|
||||
|
||||
func presentLegacyMediaPickerGallery(context: AccountContext, peer: EnginePeer?, threadTitle: String?, chatLocation: ChatLocation?, presentationData: PresentationData, source: LegacyMediaPickerGallerySource, immediateThumbnail: UIImage?, selectionContext: TGMediaSelectionContext?, editingContext: TGMediaEditingContext, hasSilentPosting: Bool, hasSchedule: Bool, hasTimer: Bool, updateHiddenMedia: @escaping (String?) -> Void, initialLayout: ContainerViewLayout?, transitionHostView: @escaping () -> UIView?, transitionView: @escaping (String) -> UIView?, completed: @escaping (TGMediaSelectableItem & TGMediaEditableItem, Bool, Int32?, @escaping () -> Void) -> Void, presentSchedulePicker: @escaping (Bool, @escaping (Int32) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, present: @escaping (ViewController, Any?) -> Void, finishedTransitionIn: @escaping () -> Void, willTransitionOut: @escaping () -> Void, dismissAll: @escaping () -> Void) -> TGModernGalleryController {
|
||||
func presentLegacyMediaPickerGallery(context: AccountContext, peer: EnginePeer?, threadTitle: String?, chatLocation: ChatLocation?, isScheduledMessages: Bool, presentationData: PresentationData, source: LegacyMediaPickerGallerySource, immediateThumbnail: UIImage?, selectionContext: TGMediaSelectionContext?, editingContext: TGMediaEditingContext, hasSilentPosting: Bool, hasSchedule: Bool, hasTimer: Bool, updateHiddenMedia: @escaping (String?) -> Void, initialLayout: ContainerViewLayout?, transitionHostView: @escaping () -> UIView?, transitionView: @escaping (String) -> UIView?, completed: @escaping (TGMediaSelectableItem & TGMediaEditableItem, Bool, Int32?, @escaping () -> Void) -> Void, presentSchedulePicker: @escaping (Bool, @escaping (Int32) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, present: @escaping (ViewController, Any?) -> Void, finishedTransitionIn: @escaping () -> Void, willTransitionOut: @escaping () -> Void, dismissAll: @escaping () -> Void) -> TGModernGalleryController {
|
||||
let reminder = peer?.id == context.account.peerId
|
||||
let hasSilentPosting = hasSilentPosting && peer?.id != context.account.peerId
|
||||
|
||||
@ -224,103 +224,107 @@ func presentLegacyMediaPickerGallery(context: AccountContext, peer: EnginePeer?,
|
||||
})
|
||||
}
|
||||
}
|
||||
model.interfaceView.doneLongPressed = { [weak selectionContext, weak editingContext, weak legacyController, weak model] item in
|
||||
if let legacyController = legacyController, let item = item as? TGMediaPickerGalleryItem, let model = model, let selectionContext = selectionContext {
|
||||
var effectiveHasSchedule = hasSchedule
|
||||
|
||||
if let editingContext = editingContext {
|
||||
if let timer = editingContext.timer(for: item.asset)?.intValue, timer > 0 {
|
||||
effectiveHasSchedule = false
|
||||
}
|
||||
for item in selectionContext.selectedItems() {
|
||||
if let editableItem = item as? TGMediaEditableItem, let timer = editingContext.timer(for: editableItem)?.intValue, timer > 0 {
|
||||
if !isScheduledMessages {
|
||||
model.interfaceView.doneLongPressed = { [weak selectionContext, weak editingContext, weak legacyController, weak model] item in
|
||||
if let legacyController = legacyController, let item = item as? TGMediaPickerGalleryItem, let model = model, let selectionContext = selectionContext {
|
||||
var effectiveHasSchedule = hasSchedule
|
||||
|
||||
if let editingContext = editingContext {
|
||||
if let timer = editingContext.timer(for: item.asset)?.intValue, timer > 0 {
|
||||
effectiveHasSchedule = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let sendWhenOnlineAvailable: Signal<Bool, NoError>
|
||||
if let peer {
|
||||
if case .secretChat = peer {
|
||||
effectiveHasSchedule = false
|
||||
}
|
||||
sendWhenOnlineAvailable = context.account.viewTracker.peerView(peer.id)
|
||||
|> take(1)
|
||||
|> map { peerView -> Bool in
|
||||
guard let peer = peerViewMainPeer(peerView) else {
|
||||
return false
|
||||
}
|
||||
var sendWhenOnlineAvailable = false
|
||||
if let presence = peerView.peerPresences[peer.id] as? TelegramUserPresence, case let .present(until) = presence.status {
|
||||
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
|
||||
if currentTime > until {
|
||||
sendWhenOnlineAvailable = true
|
||||
for item in selectionContext.selectedItems() {
|
||||
if let editableItem = item as? TGMediaEditableItem, let timer = editingContext.timer(for: editableItem)?.intValue, timer > 0 {
|
||||
effectiveHasSchedule = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if peer.id.namespace == Namespaces.Peer.CloudUser && peer.id.id._internalGetInt64Value() == 777000 {
|
||||
sendWhenOnlineAvailable = false
|
||||
}
|
||||
|
||||
let sendWhenOnlineAvailable: Signal<Bool, NoError>
|
||||
if let peer {
|
||||
if case .secretChat = peer {
|
||||
effectiveHasSchedule = false
|
||||
}
|
||||
return sendWhenOnlineAvailable
|
||||
}
|
||||
} else {
|
||||
sendWhenOnlineAvailable = .single(false)
|
||||
}
|
||||
|
||||
let _ = (sendWhenOnlineAvailable
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { sendWhenOnlineAvailable in
|
||||
let legacySheetController = LegacyController(presentation: .custom, theme: presentationData.theme, initialLayout: nil)
|
||||
let sheetController = TGMediaPickerSendActionSheetController(context: legacyController.context, isDark: true, sendButtonFrame: model.interfaceView.doneButtonFrame, canSendSilently: hasSilentPosting, canSendWhenOnline: sendWhenOnlineAvailable && effectiveHasSchedule, canSchedule: effectiveHasSchedule, reminder: reminder, hasTimer: false)
|
||||
let dismissImpl = { [weak model] in
|
||||
model?.dismiss(true, false)
|
||||
dismissAll()
|
||||
}
|
||||
sheetController.send = {
|
||||
completed(item.asset, false, nil, {
|
||||
dismissImpl()
|
||||
})
|
||||
}
|
||||
sheetController.sendSilently = {
|
||||
completed(item.asset, true, nil, {
|
||||
dismissImpl()
|
||||
})
|
||||
}
|
||||
sheetController.sendWhenOnline = {
|
||||
completed(item.asset, false, scheduleWhenOnlineTimestamp, {
|
||||
dismissImpl()
|
||||
})
|
||||
}
|
||||
sheetController.schedule = {
|
||||
presentSchedulePicker(true, { time in
|
||||
completed(item.asset, false, time, {
|
||||
dismissImpl()
|
||||
})
|
||||
})
|
||||
}
|
||||
sheetController.sendWithTimer = {
|
||||
presentTimerPicker { time in
|
||||
var items = selectionContext.selectedItems() ?? []
|
||||
items.append(item.asset as Any)
|
||||
|
||||
for case let item as TGMediaEditableItem in items {
|
||||
editingContext?.setTimer(time as NSNumber, for: item)
|
||||
sendWhenOnlineAvailable = context.account.viewTracker.peerView(peer.id)
|
||||
|> take(1)
|
||||
|> map { peerView -> Bool in
|
||||
guard let peer = peerViewMainPeer(peerView) else {
|
||||
return false
|
||||
}
|
||||
|
||||
var sendWhenOnlineAvailable = false
|
||||
if let presence = peerView.peerPresences[peer.id] as? TelegramUserPresence, case let .present(until) = presence.status {
|
||||
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
|
||||
if currentTime > until {
|
||||
sendWhenOnlineAvailable = true
|
||||
}
|
||||
}
|
||||
if peer.id.namespace == Namespaces.Peer.CloudUser && peer.id.id._internalGetInt64Value() == 777000 {
|
||||
sendWhenOnlineAvailable = false
|
||||
}
|
||||
return sendWhenOnlineAvailable
|
||||
}
|
||||
} else {
|
||||
sendWhenOnlineAvailable = .single(false)
|
||||
}
|
||||
|
||||
let _ = (sendWhenOnlineAvailable
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { sendWhenOnlineAvailable in
|
||||
let legacySheetController = LegacyController(presentation: .custom, theme: presentationData.theme, initialLayout: nil)
|
||||
let sheetController = TGMediaPickerSendActionSheetController(context: legacyController.context, isDark: true, sendButtonFrame: model.interfaceView.doneButtonFrame, canSendSilently: hasSilentPosting, canSendWhenOnline: sendWhenOnlineAvailable && effectiveHasSchedule, canSchedule: effectiveHasSchedule, reminder: reminder, hasTimer: false)
|
||||
let dismissImpl = { [weak model] in
|
||||
model?.dismiss(true, false)
|
||||
dismissAll()
|
||||
}
|
||||
sheetController.send = {
|
||||
completed(item.asset, false, nil, {
|
||||
dismissImpl()
|
||||
})
|
||||
}
|
||||
}
|
||||
sheetController.customDismissBlock = { [weak legacySheetController] in
|
||||
legacySheetController?.dismiss()
|
||||
}
|
||||
legacySheetController.bind(controller: sheetController)
|
||||
present(legacySheetController, nil)
|
||||
|
||||
let hapticFeedback = HapticFeedback()
|
||||
hapticFeedback.impact()
|
||||
})
|
||||
sheetController.sendSilently = { [weak model] in
|
||||
model?.interfaceView.onDismiss()
|
||||
|
||||
completed(item.asset, true, nil, {
|
||||
dismissImpl()
|
||||
})
|
||||
}
|
||||
sheetController.sendWhenOnline = {
|
||||
completed(item.asset, false, scheduleWhenOnlineTimestamp, {
|
||||
dismissImpl()
|
||||
})
|
||||
}
|
||||
sheetController.schedule = {
|
||||
presentSchedulePicker(true, { time in
|
||||
completed(item.asset, false, time, {
|
||||
dismissImpl()
|
||||
})
|
||||
})
|
||||
}
|
||||
sheetController.sendWithTimer = {
|
||||
presentTimerPicker { time in
|
||||
var items = selectionContext.selectedItems() ?? []
|
||||
items.append(item.asset as Any)
|
||||
|
||||
for case let item as TGMediaEditableItem in items {
|
||||
editingContext?.setTimer(time as NSNumber, for: item)
|
||||
}
|
||||
|
||||
completed(item.asset, false, nil, {
|
||||
dismissImpl()
|
||||
})
|
||||
}
|
||||
}
|
||||
sheetController.customDismissBlock = { [weak legacySheetController] in
|
||||
legacySheetController?.dismiss()
|
||||
}
|
||||
legacySheetController.bind(controller: sheetController)
|
||||
present(legacySheetController, nil)
|
||||
|
||||
let hapticFeedback = HapticFeedback()
|
||||
hapticFeedback.impact()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
model.interfaceView.setThumbnailSignalForItem { item in
|
||||
|
@ -169,6 +169,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
fileprivate var interaction: MediaPickerInteraction?
|
||||
|
||||
private let peer: EnginePeer?
|
||||
private let isScheduledMessages: Bool
|
||||
private let threadTitle: String?
|
||||
private let chatLocation: ChatLocation?
|
||||
private let bannedSendPhotos: (Int32, Bool)?
|
||||
@ -970,7 +971,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
|
||||
self.openingMedia = true
|
||||
|
||||
self.currentGalleryController = presentLegacyMediaPickerGallery(context: controller.context, peer: controller.peer, threadTitle: controller.threadTitle, chatLocation: controller.chatLocation, presentationData: self.presentationData, source: .fetchResult(fetchResult: fetchResult, index: index, reversed: reversed), immediateThumbnail: immediateThumbnail, selectionContext: interaction.selectionState, editingContext: interaction.editingState, hasSilentPosting: true, hasSchedule: hasSchedule, hasTimer: hasTimer, updateHiddenMedia: { [weak self] id in
|
||||
self.currentGalleryController = presentLegacyMediaPickerGallery(context: controller.context, peer: controller.peer, threadTitle: controller.threadTitle, chatLocation: controller.chatLocation, isScheduledMessages: controller.isScheduledMessages, presentationData: self.presentationData, source: .fetchResult(fetchResult: fetchResult, index: index, reversed: reversed), immediateThumbnail: immediateThumbnail, selectionContext: interaction.selectionState, editingContext: interaction.editingState, hasSilentPosting: true, hasSchedule: hasSchedule, hasTimer: hasTimer, updateHiddenMedia: { [weak self] id in
|
||||
self?.hiddenMediaId.set(.single(id))
|
||||
}, initialLayout: layout, transitionHostView: { [weak self] in
|
||||
return self?.gridNode.view
|
||||
@ -1009,7 +1010,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
}
|
||||
|
||||
self.openingMedia = true
|
||||
self.currentGalleryController = presentLegacyMediaPickerGallery(context: controller.context, peer: controller.peer, threadTitle: controller.threadTitle, chatLocation: controller.chatLocation, presentationData: self.presentationData, source: .selection(item: item), immediateThumbnail: immediateThumbnail, selectionContext: interaction.selectionState, editingContext: interaction.editingState, hasSilentPosting: true, hasSchedule: true, hasTimer: hasTimer, updateHiddenMedia: { [weak self] id in
|
||||
self.currentGalleryController = presentLegacyMediaPickerGallery(context: controller.context, peer: controller.peer, threadTitle: controller.threadTitle, chatLocation: controller.chatLocation, isScheduledMessages: controller.isScheduledMessages, presentationData: self.presentationData, source: .selection(item: item), immediateThumbnail: immediateThumbnail, selectionContext: interaction.selectionState, editingContext: interaction.editingState, hasSilentPosting: true, hasSchedule: true, hasTimer: hasTimer, updateHiddenMedia: { [weak self] id in
|
||||
self?.hiddenMediaId.set(.single(id))
|
||||
}, initialLayout: layout, transitionHostView: { [weak self] in
|
||||
return self?.selectionNode?.view
|
||||
@ -1524,6 +1525,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
peer: EnginePeer?,
|
||||
threadTitle: String?,
|
||||
chatLocation: ChatLocation?,
|
||||
isScheduledMessages: Bool = false,
|
||||
bannedSendPhotos: (Int32, Bool)?,
|
||||
bannedSendVideos: (Int32, Bool)?,
|
||||
subject: Subject,
|
||||
@ -1541,6 +1543,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
self.peer = peer
|
||||
self.threadTitle = threadTitle
|
||||
self.chatLocation = chatLocation
|
||||
self.isScheduledMessages = isScheduledMessages
|
||||
self.bannedSendPhotos = bannedSendPhotos
|
||||
self.bannedSendVideos = bannedSendVideos
|
||||
self.subject = subject
|
||||
@ -2105,7 +2108,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
var updateNavigationStackImpl: ((AttachmentContainable) -> Void)?
|
||||
let groupsController = MediaGroupsScreen(context: self.context, updatedPresentationData: self.updatedPresentationData, mediaAssetsContext: self.controllerNode.mediaAssetsContext, embedded: embedded, openGroup: { [weak self] collection in
|
||||
if let strongSelf = self {
|
||||
let mediaPicker = MediaPickerScreen(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: strongSelf.peer, threadTitle: strongSelf.threadTitle, chatLocation: strongSelf.chatLocation, bannedSendPhotos: strongSelf.bannedSendPhotos, bannedSendVideos: strongSelf.bannedSendVideos, subject: .assets(collection, mode), editingContext: strongSelf.interaction?.editingState, selectionContext: strongSelf.interaction?.selectionState)
|
||||
let mediaPicker = MediaPickerScreen(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: strongSelf.peer, threadTitle: strongSelf.threadTitle, chatLocation: strongSelf.chatLocation, isScheduledMessages: strongSelf.isScheduledMessages, bannedSendPhotos: strongSelf.bannedSendPhotos, bannedSendVideos: strongSelf.bannedSendVideos, subject: .assets(collection, mode), editingContext: strongSelf.interaction?.editingState, selectionContext: strongSelf.interaction?.selectionState)
|
||||
|
||||
mediaPicker.presentSchedulePicker = strongSelf.presentSchedulePicker
|
||||
mediaPicker.presentTimerPicker = strongSelf.presentTimerPicker
|
||||
|
@ -50,6 +50,7 @@ final class MediaPickerTitleView: UIView {
|
||||
transition.updateAlpha(node: self.arrowNode, alpha: self.segmentsHidden ? 1.0 : 0.0)
|
||||
transition.updateAlpha(node: self.segmentedControlNode, alpha: self.segmentsHidden ? 0.0 : 1.0)
|
||||
self.segmentedControlNode.isUserInteractionEnabled = !self.segmentsHidden
|
||||
self.buttonNode.isUserInteractionEnabled = self.isEnabled && self.segmentsHidden
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -581,7 +581,7 @@ public func channelAdminsController(context: AccountContext, updatedPresentation
|
||||
guard let peer = peer, let user = user else {
|
||||
return
|
||||
}
|
||||
presentControllerImpl?(UndoOverlayController(presentationData: context.sharedContext.currentPresentationData.with { $0 }, content: .succeed(text: presentationData.strings.Channel_OwnershipTransfer_TransferCompleted(user.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string), elevatedLayout: false, action: { _ in return false }), nil)
|
||||
presentControllerImpl?(UndoOverlayController(presentationData: context.sharedContext.currentPresentationData.with { $0 }, content: .succeed(text: presentationData.strings.Channel_OwnershipTransfer_TransferCompleted(user.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string, timeout: nil), elevatedLayout: false, action: { _ in return false }), nil)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -21,7 +21,7 @@ private final class RadialStatusIconContentNodeParameters: NSObject {
|
||||
}
|
||||
|
||||
final class RadialStatusIconContentNode: RadialStatusContentNode {
|
||||
private let icon: RadialStatusIcon
|
||||
let icon: RadialStatusIcon
|
||||
|
||||
private var animationNode: FireIconNode?
|
||||
|
||||
@ -35,7 +35,7 @@ final class RadialStatusIconContentNode: RadialStatusContentNode {
|
||||
self.isOpaque = false
|
||||
|
||||
if case .timeout = icon {
|
||||
let animationNode = FireIconNode()
|
||||
let animationNode = FireIconNode(animate: true)
|
||||
self.animationNode = animationNode
|
||||
self.addSubnode(animationNode)
|
||||
}
|
||||
@ -44,7 +44,14 @@ final class RadialStatusIconContentNode: RadialStatusContentNode {
|
||||
override func layout() {
|
||||
super.layout()
|
||||
|
||||
self.animationNode?.frame = CGRect(x: 6.0, y: 2.0, width: 36.0, height: 36.0)
|
||||
var factor: CGFloat = 0.75
|
||||
var offset: CGFloat = 0.0415
|
||||
if self.bounds.width < 30.0 {
|
||||
factor = 1.0
|
||||
offset = 0.0
|
||||
}
|
||||
let size = floorToScreenPixels(self.bounds.width * factor)
|
||||
self.animationNode?.frame = CGRect(x: floorToScreenPixels((self.bounds.width - size) / 2.0), y: ceil(self.bounds.height * offset), width: size, height: size)
|
||||
}
|
||||
|
||||
override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
|
||||
|
@ -224,7 +224,11 @@ public enum RadialStatusNodeState: Equatable {
|
||||
case .staticTimeout:
|
||||
return RadialStatusIconContentNode(icon: .timeout, synchronous: synchronous)
|
||||
case let .secretTimeout(color, icon, beginTime, timeout, sparks):
|
||||
return RadialStatusSecretTimeoutContentNode(color: color, beginTime: beginTime, timeout: timeout, icon: icon, sparks: sparks)
|
||||
var animate = true
|
||||
if let current = current as? RadialStatusIconContentNode, case .timeout = current.icon {
|
||||
animate = false
|
||||
}
|
||||
return RadialStatusSecretTimeoutContentNode(color: color, beginTime: beginTime, timeout: timeout, icon: icon, sparks: sparks, animate: animate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -29,13 +29,15 @@ private final class RadialStatusSecretTimeoutContentNodeParameters: NSObject {
|
||||
let progress: CGFloat
|
||||
let sparks: Bool
|
||||
let particles: [ContentParticle]
|
||||
let alphaProgress: CGFloat
|
||||
|
||||
init(color: UIColor, icon: RadialStatusNodeState.SecretTimeoutIcon, progress: CGFloat, sparks: Bool, particles: [ContentParticle]) {
|
||||
init(color: UIColor, icon: RadialStatusNodeState.SecretTimeoutIcon, progress: CGFloat, sparks: Bool, particles: [ContentParticle], alphaProgress: CGFloat) {
|
||||
self.color = color
|
||||
self.icon = icon
|
||||
self.progress = progress
|
||||
self.sparks = sparks
|
||||
self.particles = particles
|
||||
self.alphaProgress = alphaProgress
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,14 +53,17 @@ final class RadialStatusSecretTimeoutContentNode: RadialStatusContentNode {
|
||||
private let icon: RadialStatusNodeState.SecretTimeoutIcon
|
||||
private let sparks: Bool
|
||||
|
||||
private var animationBeginTime: Double?
|
||||
|
||||
private var progress: CGFloat = 0.0
|
||||
private var alphaProgress: CGFloat = 0.0
|
||||
private var particles: [ContentParticle] = []
|
||||
|
||||
private let animationNode = FireIconNode()
|
||||
private var animationNode: FireIconNode?
|
||||
|
||||
private var displayLink: CADisplayLink?
|
||||
|
||||
init(color: UIColor, beginTime: Double, timeout: Double, icon: RadialStatusNodeState.SecretTimeoutIcon, sparks: Bool) {
|
||||
init(color: UIColor, beginTime: Double, timeout: Double, icon: RadialStatusNodeState.SecretTimeoutIcon, sparks: Bool, animate: Bool = true) {
|
||||
self.color = color
|
||||
self.beginTime = beginTime
|
||||
self.timeout = timeout
|
||||
@ -85,7 +90,13 @@ final class RadialStatusSecretTimeoutContentNode: RadialStatusContentNode {
|
||||
self.displayLink?.add(to: RunLoop.main, forMode: .common)
|
||||
|
||||
if case .flame = icon {
|
||||
self.addSubnode(self.animationNode)
|
||||
if !animate {
|
||||
self.animationBeginTime = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970
|
||||
}
|
||||
|
||||
let animationNode = FireIconNode(animate: animate)
|
||||
self.animationNode = animationNode
|
||||
self.addSubnode(animationNode)
|
||||
}
|
||||
}
|
||||
|
||||
@ -103,7 +114,7 @@ final class RadialStatusSecretTimeoutContentNode: RadialStatusContentNode {
|
||||
offset = 0.08
|
||||
}
|
||||
let size = floorToScreenPixels(self.bounds.width * factor)
|
||||
self.animationNode.frame = CGRect(x: floorToScreenPixels((self.bounds.width - size) / 2.0), y: ceil(self.bounds.height * offset), width: size, height: size)
|
||||
self.animationNode?.frame = CGRect(x: floorToScreenPixels((self.bounds.width - size) / 2.0), y: ceil(self.bounds.height * offset), width: size, height: size)
|
||||
}
|
||||
|
||||
override func animateOut(to: RadialStatusNodeState, completion: @escaping () -> Void) {
|
||||
@ -132,12 +143,23 @@ final class RadialStatusSecretTimeoutContentNode: RadialStatusContentNode {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let absoluteTimestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970
|
||||
|
||||
let alphaProgress: CGFloat
|
||||
if let animationBeginTime = self.animationBeginTime {
|
||||
let fadeInDuration: Double = 0.4
|
||||
alphaProgress = max(0.0, min(1.0, (absoluteTimestamp - animationBeginTime) / fadeInDuration))
|
||||
} else {
|
||||
alphaProgress = 1.0
|
||||
}
|
||||
|
||||
var progress = min(1.0, CGFloat((absoluteTimestamp - self.beginTime) / self.timeout))
|
||||
if self.timeout == 0x7fffffff {
|
||||
progress = 0.0
|
||||
}
|
||||
self.progress = progress
|
||||
self.alphaProgress = alphaProgress
|
||||
|
||||
if self.sparks {
|
||||
let lineWidth: CGFloat = 1.75
|
||||
@ -193,7 +215,7 @@ final class RadialStatusSecretTimeoutContentNode: RadialStatusContentNode {
|
||||
}
|
||||
|
||||
override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
|
||||
return RadialStatusSecretTimeoutContentNodeParameters(color: self.color, icon: self.icon, progress: self.progress, sparks: self.sparks, particles: self.particles)
|
||||
return RadialStatusSecretTimeoutContentNodeParameters(color: self.color, icon: self.icon, progress: self.progress, sparks: self.sparks, particles: self.particles, alphaProgress: self.alphaProgress)
|
||||
}
|
||||
|
||||
@objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
|
||||
@ -240,6 +262,8 @@ final class RadialStatusSecretTimeoutContentNode: RadialStatusContentNode {
|
||||
let endAngle: CGFloat = -CGFloat.pi / 2.0 + 2.0 * CGFloat.pi * parameters.progress
|
||||
|
||||
if drawArc {
|
||||
context.setAlpha(parameters.alphaProgress)
|
||||
|
||||
let path = CGMutablePath()
|
||||
path.addArc(center: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
|
||||
context.addPath(path)
|
||||
@ -248,7 +272,7 @@ final class RadialStatusSecretTimeoutContentNode: RadialStatusContentNode {
|
||||
|
||||
for particle in parameters.particles {
|
||||
let size: CGFloat = 1.3
|
||||
context.setAlpha(particle.alpha)
|
||||
context.setAlpha(particle.alpha * parameters.alphaProgress)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: particle.position.x - size / 2.0, y: particle.position.y - size / 2.0), size: CGSize(width: size, height: size)))
|
||||
}
|
||||
}
|
||||
@ -256,9 +280,13 @@ final class RadialStatusSecretTimeoutContentNode: RadialStatusContentNode {
|
||||
}
|
||||
|
||||
final class FireIconNode: ManagedAnimationNode {
|
||||
init() {
|
||||
init(animate: Bool) {
|
||||
super.init(size: CGSize(width: 100.0, height: 100.0))
|
||||
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("anim_autoremove_on"), frames: .range(startFrame: 0, endFrame: 120), duration: 2.0))
|
||||
if animate {
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("anim_autoremove_on"), frames: .range(startFrame: 0, endFrame: 120), duration: 2.0))
|
||||
} else {
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("anim_autoremove_on"), frames: .range(startFrame: 120, endFrame: 120), duration: 0.001))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -377,7 +377,7 @@ public func dataPrivacyController(context: AccountContext) -> ViewController {
|
||||
text = nil
|
||||
}
|
||||
if let text = text {
|
||||
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .succeed(text: text), elevatedLayout: false, action: { _ in return false }))
|
||||
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .succeed(text: text, timeout: nil), elevatedLayout: false, action: { _ in return false }))
|
||||
}
|
||||
}))
|
||||
}
|
||||
@ -426,7 +426,7 @@ public func dataPrivacyController(context: AccountContext) -> ViewController {
|
||||
return state
|
||||
}
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.Privacy_ContactsReset_ContactsDeleted), elevatedLayout: false, action: { _ in return false }))
|
||||
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.Privacy_ContactsReset_ContactsDeleted, timeout: nil), elevatedLayout: false, action: { _ in return false }))
|
||||
}))
|
||||
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {})]))
|
||||
}
|
||||
@ -476,7 +476,7 @@ public func dataPrivacyController(context: AccountContext) -> ViewController {
|
||||
return state
|
||||
}
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.Privacy_DeleteDrafts_DraftsDeleted), elevatedLayout: false, action: { _ in return false }))
|
||||
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.Privacy_DeleteDrafts_DraftsDeleted, timeout: nil), elevatedLayout: false, action: { _ in return false }))
|
||||
}))
|
||||
}
|
||||
dismissAction()
|
||||
|
@ -32,6 +32,7 @@ swift_library(
|
||||
"//submodules/ItemListPeerItem:ItemListPeerItem",
|
||||
"//submodules/ItemListPeerActionItem:ItemListPeerActionItem",
|
||||
"//submodules/ContextUI:ContextUI",
|
||||
"//submodules/PremiumUI:PremiumUI",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -123,7 +123,7 @@ func mediaContentToUpload(accountPeerId: PeerId, network: Network, postbox: Post
|
||||
}
|
||||
}
|
||||
}
|
||||
return uploadedMediaFileContent(network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, forceReupload: true, isGrouped: isGrouped, passFetchProgress: false, forceNoBigParts: false, peerId: peerId, messageId: messageId, text: text, attributes: attributes, file: file)
|
||||
return uploadedMediaFileContent(network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, forceReupload: true, isGrouped: isGrouped, passFetchProgress: false, forceNoBigParts: false, peerId: peerId, messageId: messageId, text: text, attributes: attributes, autoremoveMessageAttribute: autoremoveMessageAttribute, autoclearMessageAttribute: autoclearMessageAttribute, file: file)
|
||||
} else {
|
||||
if forceReupload {
|
||||
let mediaReference: AnyMediaReference
|
||||
@ -157,7 +157,7 @@ func mediaContentToUpload(accountPeerId: PeerId, network: Network, postbox: Post
|
||||
return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(Api.InputMedia.inputMediaDocument(flags: flags, id: Api.InputDocument.inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())), ttlSeconds: nil, query: emojiSearchQuery), text), reuploadInfo: nil, cacheReferenceKey: nil)))
|
||||
}
|
||||
} else {
|
||||
return uploadedMediaFileContent(network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, forceReupload: forceReupload, isGrouped: isGrouped, passFetchProgress: passFetchProgress, forceNoBigParts: forceNoBigParts, peerId: peerId, messageId: messageId, text: text, attributes: attributes, file: file)
|
||||
return uploadedMediaFileContent(network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, forceReupload: forceReupload, isGrouped: isGrouped, passFetchProgress: passFetchProgress, forceNoBigParts: forceNoBigParts, peerId: peerId, messageId: messageId, text: text, attributes: attributes, autoremoveMessageAttribute: autoremoveMessageAttribute, autoclearMessageAttribute: autoclearMessageAttribute, file: file)
|
||||
}
|
||||
} else if let contact = media as? TelegramMediaContact {
|
||||
let input = Api.InputMedia.inputMediaContact(phoneNumber: contact.phoneNumber, firstName: contact.firstName, lastName: contact.lastName, vcard: contact.vCardData ?? "")
|
||||
@ -291,13 +291,17 @@ private func maybePredownloadedImageResource(postbox: Postbox, peerId: PeerId, r
|
||||
|> switchToLatest
|
||||
}
|
||||
|
||||
private func maybePredownloadedFileResource(postbox: Postbox, auxiliaryMethods: AccountAuxiliaryMethods, peerId: PeerId, resource: MediaResource, forceRefresh: Bool) -> Signal<PredownloadedResource, PendingMessageUploadError> {
|
||||
private func maybePredownloadedFileResource(postbox: Postbox, auxiliaryMethods: AccountAuxiliaryMethods, peerId: PeerId, resource: MediaResource, autoRemove: Bool, forceRefresh: Bool) -> Signal<PredownloadedResource, PendingMessageUploadError> {
|
||||
if peerId.namespace == Namespaces.Peer.SecretChat {
|
||||
return .single(.none)
|
||||
}
|
||||
|
||||
if autoRemove {
|
||||
return .single(.none)
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
if "".isEmpty {
|
||||
if !"".isEmpty {
|
||||
return .single(.none)
|
||||
}
|
||||
#endif
|
||||
@ -507,7 +511,13 @@ if "".isEmpty {
|
||||
if hasSpoiler {
|
||||
flags |= 1 << 1
|
||||
}
|
||||
return maybeCacheUploadedResource(postbox: postbox, key: referenceKey, result: .content(PendingMessageUploadedContentAndReuploadInfo(content: .media(.inputMediaPhoto(flags: flags, id: .inputPhoto(id: id, accessHash: accessHash, fileReference: Buffer(data: fileReference)), ttlSeconds: ttlSeconds), text), reuploadInfo: nil, cacheReferenceKey: nil)), media: mediaImage)
|
||||
|
||||
let result: PendingMessageUploadedContentResult = .content(PendingMessageUploadedContentAndReuploadInfo(content: .media(.inputMediaPhoto(flags: flags, id: .inputPhoto(id: id, accessHash: accessHash, fileReference: Buffer(data: fileReference)), ttlSeconds: ttlSeconds), text), reuploadInfo: nil, cacheReferenceKey: nil))
|
||||
if let _ = ttlSeconds {
|
||||
return .single(result)
|
||||
} else {
|
||||
return maybeCacheUploadedResource(postbox: postbox, key: referenceKey, result: result, media: mediaImage)
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
@ -656,14 +666,20 @@ public func statsCategoryForFileWithAttributes(_ attributes: [TelegramMediaFileA
|
||||
return .file
|
||||
}
|
||||
|
||||
private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxiliaryMethods: AccountAuxiliaryMethods, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, forceReupload: Bool, isGrouped: Bool, passFetchProgress: Bool, forceNoBigParts: Bool, peerId: PeerId, messageId: MessageId?, text: String, attributes: [MessageAttribute], file: TelegramMediaFile) -> Signal<PendingMessageUploadedContentResult, PendingMessageUploadError> {
|
||||
return maybePredownloadedFileResource(postbox: postbox, auxiliaryMethods: auxiliaryMethods, peerId: peerId, resource: file.resource, forceRefresh: forceReupload)
|
||||
private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxiliaryMethods: AccountAuxiliaryMethods, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, forceReupload: Bool, isGrouped: Bool, passFetchProgress: Bool, forceNoBigParts: Bool, peerId: PeerId, messageId: MessageId?, text: String, attributes: [MessageAttribute], autoremoveMessageAttribute: AutoremoveTimeoutMessageAttribute?, autoclearMessageAttribute: AutoclearTimeoutMessageAttribute?, file: TelegramMediaFile) -> Signal<PendingMessageUploadedContentResult, PendingMessageUploadError> {
|
||||
return maybePredownloadedFileResource(postbox: postbox, auxiliaryMethods: auxiliaryMethods, peerId: peerId, resource: file.resource, autoRemove: autoremoveMessageAttribute != nil || autoclearMessageAttribute != nil, forceRefresh: forceReupload)
|
||||
|> mapToSignal { result -> Signal<PendingMessageUploadedContentResult, PendingMessageUploadError> in
|
||||
var referenceKey: CachedSentMediaReferenceKey?
|
||||
switch result {
|
||||
case let .media(media, key):
|
||||
if !forceReupload, let file = media as? TelegramMediaFile, let resource = file.resource as? CloudDocumentMediaResource, let fileReference = resource.fileReference {
|
||||
var flags: Int32 = 0
|
||||
var ttlSeconds: Int32?
|
||||
if let autoclearMessageAttribute = autoclearMessageAttribute {
|
||||
flags |= 1 << 0
|
||||
ttlSeconds = autoclearMessageAttribute.timeout
|
||||
}
|
||||
|
||||
for attribute in attributes {
|
||||
if let _ = attribute as? MediaSpoilerMessageAttribute {
|
||||
flags |= 1 << 2
|
||||
@ -672,7 +688,7 @@ private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxili
|
||||
|
||||
return .single(.progress(1.0))
|
||||
|> then(
|
||||
.single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(Api.InputMedia.inputMediaDocument(flags: flags, id: Api.InputDocument.inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: fileReference)), ttlSeconds: nil, query: nil), text), reuploadInfo: nil, cacheReferenceKey: nil)))
|
||||
.single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(Api.InputMedia.inputMediaDocument(flags: flags, id: Api.InputDocument.inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: fileReference)), ttlSeconds: ttlSeconds, query: nil), text), reuploadInfo: nil, cacheReferenceKey: nil)))
|
||||
)
|
||||
}
|
||||
referenceKey = key
|
||||
@ -885,10 +901,21 @@ private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxili
|
||||
case let .messageMediaDocument(_, document, _, _):
|
||||
if let document = document, let mediaFile = telegramMediaFileFromApiDocument(document), let resource = mediaFile.resource as? CloudDocumentMediaResource, let fileReference = resource.fileReference {
|
||||
var flags: Int32 = 0
|
||||
var ttlSeconds: Int32?
|
||||
if let autoclearMessageAttribute = autoclearMessageAttribute {
|
||||
flags |= 1 << 0
|
||||
ttlSeconds = autoclearMessageAttribute.timeout
|
||||
}
|
||||
if hasSpoiler {
|
||||
flags |= (1 << 2)
|
||||
}
|
||||
return maybeCacheUploadedResource(postbox: postbox, key: referenceKey, result: .content(PendingMessageUploadedContentAndReuploadInfo(content: .media(.inputMediaDocument(flags: flags, id: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: fileReference)), ttlSeconds: nil, query: nil), text), reuploadInfo: nil, cacheReferenceKey: nil)), media: mediaFile)
|
||||
|
||||
let result: PendingMessageUploadedContentResult = .content(PendingMessageUploadedContentAndReuploadInfo(content: .media(.inputMediaDocument(flags: flags, id: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: fileReference)), ttlSeconds: ttlSeconds, query: nil), text), reuploadInfo: nil, cacheReferenceKey: nil))
|
||||
if let _ = ttlSeconds {
|
||||
return .single(result)
|
||||
} else {
|
||||
return maybeCacheUploadedResource(postbox: postbox, key: referenceKey, result: result, media: mediaFile)
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
|
@ -18,7 +18,9 @@ public final class AttachMenuBots: Equatable, Codable {
|
||||
case `default` = 0
|
||||
case iOSStatic
|
||||
case iOSAnimated
|
||||
case iOSSettingsStatic
|
||||
case macOSAnimated
|
||||
case macOSSettingsStatic
|
||||
case placeholder
|
||||
|
||||
init?(string: String) {
|
||||
@ -29,6 +31,10 @@ public final class AttachMenuBots: Equatable, Codable {
|
||||
self = .iOSStatic
|
||||
case "ios_animated":
|
||||
self = .iOSAnimated
|
||||
case "ios_side_menu_static":
|
||||
self = .iOSSettingsStatic
|
||||
case "macos_side_menu_static":
|
||||
self = .macOSSettingsStatic
|
||||
case "macos_animated":
|
||||
self = .macOSAnimated
|
||||
case "placeholder_static":
|
||||
@ -55,6 +61,7 @@ public final class AttachMenuBots: Equatable, Codable {
|
||||
public static let showInAttachMenu = Flags(rawValue: 1 << 2)
|
||||
public static let showInSettings = Flags(rawValue: 1 << 3)
|
||||
public static let showInSettingsDisclaimer = Flags(rawValue: 1 << 4)
|
||||
public static let notActivated = Flags(rawValue: 1 << 5)
|
||||
}
|
||||
|
||||
public struct PeerFlags: OptionSet, Codable {
|
||||
@ -190,6 +197,10 @@ public final class AttachMenuBots: Equatable, Codable {
|
||||
|
||||
try container.encode(Int32(self.flags.rawValue), forKey: .flags)
|
||||
}
|
||||
|
||||
func withUpdatedFlags(_ flags: Flags) -> Bot {
|
||||
return Bot(peerId: self.peerId, name: self.name, icons: self.icons, peerTypes: self.peerTypes, flags: flags)
|
||||
}
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
@ -320,6 +331,9 @@ func managedSynchronizeAttachMenuBots(accountPeerId: PeerId, postbox: Postbox, n
|
||||
}
|
||||
}
|
||||
var flags: AttachMenuBots.Bot.Flags = []
|
||||
if (apiFlags & (1 << 0)) != 0 {
|
||||
flags.insert(.notActivated)
|
||||
}
|
||||
if (apiFlags & (1 << 1)) != 0 {
|
||||
flags.insert(.hasSettings)
|
||||
}
|
||||
@ -412,6 +426,8 @@ func _internal_addBotToAttachMenu(accountPeerId: PeerId, postbox: Postbox, netwo
|
||||
}
|
||||
|
||||
func _internal_removeBotFromAttachMenu(accountPeerId: PeerId, postbox: Postbox, network: Network, botId: PeerId) -> Signal<Bool, NoError> {
|
||||
let _ = removeCachedAttachMenuBot(postbox: postbox, botId: botId).start()
|
||||
|
||||
return postbox.transaction { transaction -> Signal<Bool, NoError> in
|
||||
guard let peer = transaction.getPeer(botId), let inputUser = apiInputUser(peer) else {
|
||||
return .complete()
|
||||
@ -431,13 +447,30 @@ func _internal_removeBotFromAttachMenu(accountPeerId: PeerId, postbox: Postbox,
|
||||
|> afterCompleted {
|
||||
let _ = (managedSynchronizeAttachMenuBots(accountPeerId: accountPeerId, postbox: postbox, network: network, force: true)
|
||||
|> take(1)).start(completed: {
|
||||
let _ = removeCachedAttachMenuBot(postbox: postbox, botId: botId)
|
||||
let _ = removeCachedAttachMenuBot(postbox: postbox, botId: botId).start()
|
||||
})
|
||||
}
|
||||
}
|
||||
|> switchToLatest
|
||||
}
|
||||
|
||||
func _internal_acceptAttachMenuBotDisclaimer(postbox: Postbox, botId: PeerId) -> Signal<Never, NoError> {
|
||||
return postbox.transaction { transaction in
|
||||
if let attachMenuBots = cachedAttachMenuBots(transaction: transaction) {
|
||||
var updatedAttachMenuBots = attachMenuBots
|
||||
if let index = attachMenuBots.bots.firstIndex(where: { $0.peerId == botId }) {
|
||||
var updatedFlags = attachMenuBots.bots[index].flags
|
||||
updatedFlags.remove(.showInSettingsDisclaimer)
|
||||
let updatedBot = attachMenuBots.bots[index].withUpdatedFlags(updatedFlags)
|
||||
var updatedBots = attachMenuBots.bots
|
||||
updatedBots[index] = updatedBot
|
||||
updatedAttachMenuBots = AttachMenuBots(hash: attachMenuBots.hash, bots: updatedBots)
|
||||
}
|
||||
setCachedAttachMenuBots(transaction: transaction, attachMenuBots: updatedAttachMenuBots)
|
||||
}
|
||||
} |> ignoreValues
|
||||
}
|
||||
|
||||
public struct AttachMenuBot {
|
||||
public let peer: EnginePeer
|
||||
public let shortName: String
|
||||
@ -445,7 +478,7 @@ public struct AttachMenuBot {
|
||||
public let peerTypes: AttachMenuBots.Bot.PeerFlags
|
||||
public let flags: AttachMenuBots.Bot.Flags
|
||||
|
||||
init(peer: EnginePeer, shortName: String, icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile], peerTypes: AttachMenuBots.Bot.PeerFlags, flags: AttachMenuBots.Bot.Flags) {
|
||||
public init(peer: EnginePeer, shortName: String, icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile], peerTypes: AttachMenuBots.Bot.PeerFlags, flags: AttachMenuBots.Bot.Flags) {
|
||||
self.peer = peer
|
||||
self.shortName = shortName
|
||||
self.icons = icons
|
||||
@ -590,6 +623,7 @@ public final class BotApp: Equatable, Codable {
|
||||
|
||||
public static let notActivated = Flags(rawValue: 1 << 0)
|
||||
public static let requiresWriteAccess = Flags(rawValue: 1 << 1)
|
||||
public static let hasSettings = Flags(rawValue: 1 << 2)
|
||||
}
|
||||
|
||||
public let id: Int64
|
||||
@ -718,7 +752,7 @@ func _internal_getBotApp(account: Account, reference: BotAppReference) -> Signal
|
||||
}
|
||||
|> mapToSignal { result -> Signal<BotApp, GetBotAppError> in
|
||||
switch result {
|
||||
case let .botApp(botAppFlags, app):
|
||||
case let .botApp(botAppFlags, app):
|
||||
switch app {
|
||||
case let .botApp(flags, id, accessHash, shortName, title, description, photo, document, hash):
|
||||
let _ = flags
|
||||
@ -729,10 +763,13 @@ func _internal_getBotApp(account: Account, reference: BotAppReference) -> Signal
|
||||
if (botAppFlags & (1 << 1)) != 0 {
|
||||
appFlags.insert(.requiresWriteAccess)
|
||||
}
|
||||
if (botAppFlags & (1 << 2)) != 0 {
|
||||
appFlags.insert(.hasSettings)
|
||||
}
|
||||
return .single(BotApp(id: id, accessHash: accessHash, shortName: shortName, title: title, description: description, photo: telegramMediaImageFromApiPhoto(photo), document: document.flatMap(telegramMediaFileFromApiDocument), hash: hash, flags: appFlags))
|
||||
case .botAppNotModified:
|
||||
return .complete()
|
||||
}
|
||||
case .botAppNotModified:
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -528,6 +528,10 @@ public extension TelegramEngine {
|
||||
return _internal_removeBotFromAttachMenu(accountPeerId: self.account.peerId, postbox: self.account.postbox, network: self.account.network, botId: botId)
|
||||
}
|
||||
|
||||
public func acceptAttachMenuBotDisclaimer(botId: PeerId) -> Signal<Never, NoError> {
|
||||
return _internal_acceptAttachMenuBotDisclaimer(postbox: self.account.postbox, botId: botId)
|
||||
}
|
||||
|
||||
public func getAttachMenuBot(botId: PeerId, cached: Bool = false) -> Signal<AttachMenuBot, GetAttachMenuBotError> {
|
||||
return _internal_getAttachMenuBot(accountPeerId: self.account.peerId, postbox: self.account.postbox, network: self.account.network, botId: botId, cached: cached)
|
||||
}
|
||||
|
@ -30,7 +30,6 @@ swift_library(
|
||||
"//submodules/UndoUI:UndoUI",
|
||||
"//submodules/ContextUI:ContextUI",
|
||||
"//submodules/GalleryUI:GalleryUI",
|
||||
"//submodules/AttachmentTextInputPanelNode:AttachmentTextInputPanelNode",
|
||||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||
"//submodules/TelegramNotices:TelegramNotices",
|
||||
"//submodules/StickerPeekUI:StickerPeekUI",
|
||||
@ -40,6 +39,7 @@ swift_library(
|
||||
"//submodules/FeaturedStickersScreen:FeaturedStickersScreen",
|
||||
"//submodules/StickerPackPreviewUI",
|
||||
"//submodules/TelegramUI/Components/EntityKeyboardGifContent:EntityKeyboardGifContent",
|
||||
"//submodules/TelegramUI/Components/LegacyMessageInputPanelInputView:LegacyMessageInputPanelInputView",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -32,6 +32,7 @@ import FeaturedStickersScreen
|
||||
import Pasteboard
|
||||
import StickerPackPreviewUI
|
||||
import EntityKeyboardGifContent
|
||||
import LegacyMessageInputPanelInputView
|
||||
|
||||
public final class EmptyInputView: UIView, UIInputViewAudioFeedback {
|
||||
public var enableInputClicksWhenVisible: Bool {
|
||||
@ -2080,7 +2081,7 @@ private final class ContextControllerContentSourceImpl: ContextControllerContent
|
||||
}
|
||||
}
|
||||
|
||||
public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputView, UIInputViewAudioFeedback {
|
||||
public final class EntityInputView: UIInputView, LegacyMessageInputPanelInputView, AttachmentTextInputPanelInputView, UIInputViewAudioFeedback {
|
||||
private let context: AccountContext
|
||||
|
||||
public var insertText: ((NSAttributedString) -> Void)?
|
||||
|
@ -21,7 +21,9 @@ swift_library(
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/ContextUI",
|
||||
"//submodules/TooltipUI",
|
||||
"//submodules/UndoUI",
|
||||
"//submodules/TelegramUI/Components/MessageInputPanelComponent",
|
||||
"//submodules/TelegramUI/Components/LegacyMessageInputPanelInputView",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -13,13 +13,17 @@ import MessageInputPanelComponent
|
||||
import TelegramPresentationData
|
||||
import ContextUI
|
||||
import TooltipUI
|
||||
import LegacyMessageInputPanelInputView
|
||||
import UndoUI
|
||||
|
||||
public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView {
|
||||
private let context: AccountContext
|
||||
private let chatLocation: ChatLocation
|
||||
private let isScheduledMessages: Bool
|
||||
private let present: (ViewController) -> Void
|
||||
private let presentInGlobalOverlay: (ViewController) -> Void
|
||||
|
||||
private let makeEntityInputView: () -> LegacyMessageInputPanelInputView?
|
||||
|
||||
private let state = ComponentState()
|
||||
private let inputPanelExternalState = MessageInputPanelComponent.ExternalState()
|
||||
private let inputPanel = ComponentView<Empty>()
|
||||
@ -27,21 +31,29 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView {
|
||||
private var currentTimeout: Int32?
|
||||
private var currentIsEditing = false
|
||||
private var currentHeight: CGFloat?
|
||||
private var currentIsVideo = false
|
||||
|
||||
private let hapticFeedback = HapticFeedback()
|
||||
|
||||
private var inputView: LegacyMessageInputPanelInputView?
|
||||
private var isEmojiKeyboardActive = false
|
||||
|
||||
private var validLayout: (width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, keyboardHeight: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, metrics: LayoutMetrics)?
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
chatLocation: ChatLocation,
|
||||
isScheduledMessages: Bool,
|
||||
present: @escaping (ViewController) -> Void,
|
||||
presentInGlobalOverlay: @escaping (ViewController) -> Void
|
||||
presentInGlobalOverlay: @escaping (ViewController) -> Void,
|
||||
makeEntityInputView: @escaping () -> LegacyMessageInputPanelInputView?
|
||||
) {
|
||||
self.context = context
|
||||
self.chatLocation = chatLocation
|
||||
self.isScheduledMessages = isScheduledMessages
|
||||
self.present = present
|
||||
self.presentInGlobalOverlay = presentInGlobalOverlay
|
||||
self.makeEntityInputView = makeEntityInputView
|
||||
|
||||
super.init()
|
||||
|
||||
@ -84,23 +96,34 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView {
|
||||
transition.setFrame(view: view, frame: frame)
|
||||
}
|
||||
|
||||
public func setTimeout(_ timeout: Int32) {
|
||||
public func setTimeout(_ timeout: Int32, isVideo: Bool) {
|
||||
self.dismissTimeoutTooltip()
|
||||
var timeout: Int32? = timeout
|
||||
if timeout == 0 {
|
||||
timeout = nil
|
||||
}
|
||||
self.currentTimeout = timeout
|
||||
self.currentIsVideo = isVideo
|
||||
}
|
||||
|
||||
public func dismissInput() {
|
||||
public func dismissInput() -> Bool {
|
||||
if let view = self.inputPanel.view as? MessageInputPanelComponent.View {
|
||||
view.deactivateInput()
|
||||
if view.canDeactivateInput() {
|
||||
self.isEmojiKeyboardActive = false
|
||||
self.inputView = nil
|
||||
view.deactivateInput(force: true)
|
||||
return true
|
||||
} else {
|
||||
view.animateError()
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public func onAnimateOut() {
|
||||
self.tooltipController?.dismiss()
|
||||
self.dismissTimeoutTooltip()
|
||||
}
|
||||
|
||||
public func baseHeight() -> CGFloat {
|
||||
@ -145,6 +168,8 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView {
|
||||
var maxInputPanelHeight = maxHeight
|
||||
if keyboardHeight.isZero {
|
||||
maxInputPanelHeight = 60.0
|
||||
} else {
|
||||
maxInputPanelHeight = maxHeight - keyboardHeight - 100.0
|
||||
}
|
||||
|
||||
var resetInputContents: MessageInputPanelComponent.SendMessageInput?
|
||||
@ -164,12 +189,16 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView {
|
||||
strings: presentationData.strings,
|
||||
style: .media,
|
||||
placeholder: .plain(presentationData.strings.MediaPicker_AddCaption),
|
||||
maxLength: 1024,
|
||||
maxLength: Int(self.context.userLimits.maxCaptionLength),
|
||||
queryTypes: [.mention],
|
||||
alwaysDarkWhenHasText: false,
|
||||
resetInputContents: resetInputContents,
|
||||
nextInputMode: { _ in
|
||||
return .emoji
|
||||
nextInputMode: { [weak self] _ in
|
||||
if self?.isEmojiKeyboardActive == true {
|
||||
return .text
|
||||
} else {
|
||||
return .emoji
|
||||
}
|
||||
},
|
||||
areVoiceMessagesAvailable: false,
|
||||
presentController: self.present,
|
||||
@ -177,7 +206,7 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView {
|
||||
sendMessageAction: { [weak self] in
|
||||
if let self {
|
||||
self.sendPressed?(self.caption())
|
||||
self.dismissInput()
|
||||
let _ = self.dismissInput()
|
||||
}
|
||||
},
|
||||
sendMessageOptionsAction: nil,
|
||||
@ -190,8 +219,12 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView {
|
||||
myReaction: nil,
|
||||
likeAction: nil,
|
||||
likeOptionsAction: nil,
|
||||
inputModeAction: nil,
|
||||
timeoutAction: self.chatLocation.peerId?.namespace == Namespaces.Peer.CloudUser ? { [weak self] sourceView, gesture in
|
||||
inputModeAction: { [weak self] in
|
||||
if let self {
|
||||
self.toggleInputMode()
|
||||
}
|
||||
},
|
||||
timeoutAction: self.chatLocation.peerId?.namespace == Namespaces.Peer.CloudUser && !self.isScheduledMessages ? { [weak self] sourceView, gesture in
|
||||
if let self {
|
||||
self.presentTimeoutSetup(sourceView: sourceView, gesture: gesture)
|
||||
}
|
||||
@ -214,6 +247,7 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView {
|
||||
bottomInset: 0.0,
|
||||
isFormattingLocked: false,
|
||||
hideKeyboard: false,
|
||||
customInputView: self.inputView,
|
||||
forceIsEditing: false,
|
||||
disabledPlaceholder: nil,
|
||||
isChannel: false,
|
||||
@ -245,6 +279,47 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView {
|
||||
return inputPanelSize.height - 8.0
|
||||
}
|
||||
|
||||
private func toggleInputMode() {
|
||||
self.isEmojiKeyboardActive = !self.isEmojiKeyboardActive
|
||||
|
||||
if self.isEmojiKeyboardActive {
|
||||
let inputView = self.makeEntityInputView()
|
||||
inputView?.insertText = { [weak self] text in
|
||||
if let self {
|
||||
self.inputPanelExternalState.insertText(text)
|
||||
}
|
||||
}
|
||||
inputView?.deleteBackwards = { [weak self] in
|
||||
if let self {
|
||||
self.inputPanelExternalState.deleteBackward()
|
||||
}
|
||||
}
|
||||
inputView?.switchToKeyboard = { [weak self] in
|
||||
if let self {
|
||||
self.isEmojiKeyboardActive = false
|
||||
self.inputView = nil
|
||||
self.update(transition: .immediate)
|
||||
}
|
||||
}
|
||||
inputView?.presentController = { [weak self] c in
|
||||
if let self {
|
||||
if !(c is UndoOverlayController) {
|
||||
self.isEmojiKeyboardActive = false
|
||||
if let view = self.inputPanel.view as? MessageInputPanelComponent.View {
|
||||
view.deactivateInput(force: true)
|
||||
}
|
||||
}
|
||||
self.present(c)
|
||||
}
|
||||
}
|
||||
self.inputView = inputView
|
||||
self.update(transition: .immediate)
|
||||
} else {
|
||||
self.inputView = nil
|
||||
self.update(transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
private func presentTimeoutSetup(sourceView: UIView, gesture: ContextGesture?) {
|
||||
self.hapticFeedback.impact(.light)
|
||||
|
||||
@ -252,10 +327,13 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView {
|
||||
|
||||
let updateTimeout: (Int32?) -> Void = { [weak self] timeout in
|
||||
if let self {
|
||||
let previousTimeout = self.currentTimeout
|
||||
self.currentTimeout = timeout
|
||||
self.timerUpdated?(timeout as? NSNumber)
|
||||
self.update(transition: .immediate)
|
||||
self.presentTimeoutTooltip(sourceView: sourceView, timeout: timeout)
|
||||
if previousTimeout != timeout {
|
||||
self.presentTimeoutTooltip(sourceView: sourceView, timeout: timeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -317,7 +395,7 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView {
|
||||
let absoluteFrame = sourceView.convert(sourceView.bounds, to: nil).offsetBy(dx: -parentFrame.minX, dy: 0.0)
|
||||
let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY - 2.0), size: CGSize())
|
||||
|
||||
let isVideo = !"".isEmpty
|
||||
let isVideo = self.currentIsVideo
|
||||
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
||||
let text: String
|
||||
let iconName: String
|
||||
|
@ -0,0 +1,19 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "LegacyMessageInputPanelInputView",
|
||||
module_name = "LegacyMessageInputPanelInputView",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||
"//submodules/Display",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
@ -0,0 +1,11 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
|
||||
public protocol LegacyMessageInputPanelInputView: UIView {
|
||||
var insertText: ((NSAttributedString) -> Void)? { get set }
|
||||
var deleteBackwards: (() -> Void)? { get set }
|
||||
var switchToKeyboard: (() -> Void)? { get set }
|
||||
var presentController: ((ViewController) -> Void)? { get set }
|
||||
}
|
@ -1214,6 +1214,7 @@ final class MediaEditorScreenComponent: Component {
|
||||
bottomInset: 0.0,
|
||||
isFormattingLocked: !state.isPremium,
|
||||
hideKeyboard: self.currentInputMode == .emoji,
|
||||
customInputView: nil,
|
||||
forceIsEditing: self.currentInputMode == .emoji,
|
||||
disabledPlaceholder: nil,
|
||||
isChannel: false,
|
||||
|
@ -290,6 +290,7 @@ final class StoryPreviewComponent: Component {
|
||||
bottomInset: 0.0,
|
||||
isFormattingLocked: false,
|
||||
hideKeyboard: false,
|
||||
customInputView: nil,
|
||||
forceIsEditing: false,
|
||||
disabledPlaceholder: nil,
|
||||
isChannel: false,
|
||||
|
@ -141,6 +141,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
public let bottomInset: CGFloat
|
||||
public let isFormattingLocked: Bool
|
||||
public let hideKeyboard: Bool
|
||||
public let customInputView: UIView?
|
||||
public let forceIsEditing: Bool
|
||||
public let disabledPlaceholder: String?
|
||||
public let isChannel: Bool
|
||||
@ -193,6 +194,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
bottomInset: CGFloat,
|
||||
isFormattingLocked: Bool,
|
||||
hideKeyboard: Bool,
|
||||
customInputView: UIView?,
|
||||
forceIsEditing: Bool,
|
||||
disabledPlaceholder: String?,
|
||||
isChannel: Bool,
|
||||
@ -244,6 +246,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
self.bottomInset = bottomInset
|
||||
self.isFormattingLocked = isFormattingLocked
|
||||
self.hideKeyboard = hideKeyboard
|
||||
self.customInputView = customInputView
|
||||
self.forceIsEditing = forceIsEditing
|
||||
self.disabledPlaceholder = disabledPlaceholder
|
||||
self.isChannel = isChannel
|
||||
@ -327,6 +330,9 @@ public final class MessageInputPanelComponent: Component {
|
||||
if lhs.hideKeyboard != rhs.hideKeyboard {
|
||||
return false
|
||||
}
|
||||
if lhs.customInputView !== rhs.customInputView {
|
||||
return false
|
||||
}
|
||||
if lhs.forceIsEditing != rhs.forceIsEditing {
|
||||
return false
|
||||
}
|
||||
@ -712,6 +718,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
textColor: UIColor(rgb: 0xffffff),
|
||||
insets: UIEdgeInsets(top: 9.0, left: 8.0, bottom: 10.0, right: 48.0),
|
||||
hideKeyboard: component.hideKeyboard,
|
||||
customInputView: component.customInputView,
|
||||
resetText: component.resetInputContents.flatMap { resetInputContents in
|
||||
switch resetInputContents {
|
||||
case let .text(value):
|
||||
|
@ -2277,7 +2277,7 @@ final class StorageUsageScreenComponent: Component {
|
||||
}
|
||||
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||
controller.present(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.ClearCache_Success("\(dataSizeString(size, formatting: DataSizeStringFormatting(presentationData: presentationData)))", stringForDeviceType()).string), elevatedLayout: false, action: { _ in return false }), in: .current)
|
||||
controller.present(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.ClearCache_Success("\(dataSizeString(size, formatting: DataSizeStringFormatting(presentationData: presentationData)))", stringForDeviceType()).string, timeout: nil), elevatedLayout: false, action: { _ in return false }), in: .current)
|
||||
}
|
||||
|
||||
private func reloadStats(firstTime: Bool, completion: @escaping () -> Void) {
|
||||
|
@ -2651,6 +2651,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
bottomInset: component.inputHeight != 0.0 || inputNodeVisible ? 0.0 : bottomContentInset,
|
||||
isFormattingLocked: false,
|
||||
hideKeyboard: self.sendMessageContext.currentInputMode == .media,
|
||||
customInputView: nil,
|
||||
forceIsEditing: self.sendMessageContext.currentInputMode == .media,
|
||||
disabledPlaceholder: disabledPlaceholder,
|
||||
isChannel: false,
|
||||
|
@ -1401,7 +1401,7 @@ final class StoryItemSetContainerSendMessage {
|
||||
peerType.insert(.sameBot)
|
||||
peerType.remove(.bot)
|
||||
}
|
||||
let button: AttachmentButtonType = .app(bot.peer, bot.shortName, bot.icons)
|
||||
let button: AttachmentButtonType = .app(bot)
|
||||
if !bot.peerTypes.intersection(peerType).isEmpty {
|
||||
buttons.insert(button, at: 1)
|
||||
|
||||
@ -1773,16 +1773,8 @@ final class StoryItemSetContainerSendMessage {
|
||||
}*/
|
||||
//TODO:gift controller
|
||||
break
|
||||
case let .app(bot, botName, _):
|
||||
var payload: String?
|
||||
var fromAttachMenu = true
|
||||
/*if case let .bot(_, botPayload, _) = subject {
|
||||
payload = botPayload
|
||||
fromAttachMenu = false
|
||||
}*/
|
||||
payload = nil
|
||||
fromAttachMenu = true
|
||||
let params = WebAppParameters(peerId: peer.id, botId: bot.id, botName: botName, url: nil, queryId: nil, payload: payload, buttonText: nil, keepAliveSignal: nil, fromMenu: false, fromAttachMenu: fromAttachMenu, isInline: false, isSimple: false)
|
||||
case let .app(bot):
|
||||
let params = WebAppParameters(source: .attachMenu, peerId: peer.id, botId: bot.peer.id, botName: bot.shortName, url: nil, queryId: nil, payload: nil, buttonText: nil, keepAliveSignal: nil, forceHasSettings: false)
|
||||
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)
|
||||
@ -2290,7 +2282,7 @@ final class StoryItemSetContainerSendMessage {
|
||||
return nil
|
||||
}
|
||||
//TODO:self.presentationInterfaceState.customEmojiAvailable
|
||||
return component.context.sharedContext.makeGalleryCaptionPanelView(context: component.context, chatLocation: .peer(id: peer.id), customEmojiAvailable: true, present: { [weak view] c in
|
||||
return component.context.sharedContext.makeGalleryCaptionPanelView(context: component.context, chatLocation: .peer(id: peer.id), isScheduledMessages: false, customEmojiAvailable: true, present: { [weak view] c in
|
||||
guard let view else {
|
||||
return
|
||||
}
|
||||
|
@ -90,6 +90,7 @@ public final class TextFieldComponent: Component {
|
||||
public let textColor: UIColor
|
||||
public let insets: UIEdgeInsets
|
||||
public let hideKeyboard: Bool
|
||||
public let customInputView: UIView?
|
||||
public let resetText: NSAttributedString?
|
||||
public let isOneLineWhenUnfocused: Bool
|
||||
public let formatMenuAvailability: FormatMenuAvailability
|
||||
@ -105,6 +106,7 @@ public final class TextFieldComponent: Component {
|
||||
textColor: UIColor,
|
||||
insets: UIEdgeInsets,
|
||||
hideKeyboard: Bool,
|
||||
customInputView: UIView?,
|
||||
resetText: NSAttributedString?,
|
||||
isOneLineWhenUnfocused: Bool,
|
||||
formatMenuAvailability: FormatMenuAvailability,
|
||||
@ -119,6 +121,7 @@ public final class TextFieldComponent: Component {
|
||||
self.textColor = textColor
|
||||
self.insets = insets
|
||||
self.hideKeyboard = hideKeyboard
|
||||
self.customInputView = customInputView
|
||||
self.resetText = resetText
|
||||
self.isOneLineWhenUnfocused = isOneLineWhenUnfocused
|
||||
self.formatMenuAvailability = formatMenuAvailability
|
||||
@ -146,6 +149,9 @@ public final class TextFieldComponent: Component {
|
||||
if lhs.hideKeyboard != rhs.hideKeyboard {
|
||||
return false
|
||||
}
|
||||
if lhs.customInputView !== rhs.customInputView {
|
||||
return false
|
||||
}
|
||||
if lhs.resetText != rhs.resetText {
|
||||
return false
|
||||
}
|
||||
@ -816,9 +822,14 @@ public final class TextFieldComponent: Component {
|
||||
} while glyphIndexForStringStart < NSMaxRange(glyphRange) && !NSLocationInRange(glyphRange.location, lineRange)
|
||||
|
||||
let padding = self.textView.textContainerInset.left
|
||||
let rightmostX = lineRect.maxX + padding
|
||||
var rightmostX = lineRect.maxX + padding
|
||||
let rightmostY = lineRect.minY + self.textView.textContainerInset.top
|
||||
|
||||
let stringIndex = self.textView.text.index(self.textView.text.startIndex, offsetBy: lineRange.location + lineRange.length - 1)
|
||||
if self.textView.text[stringIndex] == " " {
|
||||
rightmostX -= 3.0
|
||||
}
|
||||
|
||||
return CGPoint(x: rightmostX, y: rightmostY)
|
||||
}
|
||||
|
||||
@ -882,7 +893,14 @@ public final class TextFieldComponent: Component {
|
||||
component.externalState.isEditing = isEditing
|
||||
component.externalState.textLength = self.textStorage.string.count
|
||||
|
||||
if component.hideKeyboard {
|
||||
if let inputView = component.customInputView {
|
||||
if self.textView.inputView == nil {
|
||||
self.textView.inputView = inputView
|
||||
if self.textView.isFirstResponder {
|
||||
self.textView.reloadInputViews()
|
||||
}
|
||||
}
|
||||
} else if component.hideKeyboard {
|
||||
if self.textView.inputView == nil {
|
||||
self.textView.inputView = EmptyInputView()
|
||||
if self.textView.isFirstResponder {
|
||||
@ -916,7 +934,7 @@ public final class TextFieldComponent: Component {
|
||||
view.alpha = 0.0
|
||||
self.textView.addSubview(view)
|
||||
}
|
||||
let ellipsisFrame = CGRect(origin: CGPoint(x: position.x - 11.0, y: position.y), size: ellipsisSize)
|
||||
let ellipsisFrame = CGRect(origin: CGPoint(x: position.x - 8.0, y: position.y), size: ellipsisSize)
|
||||
transition.setFrame(view: view, frame: ellipsisFrame)
|
||||
|
||||
let hasMoreThanOneLine = ellipsisFrame.maxY < self.textView.contentSize.height - 12.0
|
||||
|
@ -4214,7 +4214,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
return state.updatedShowWebView(true).updatedForceInputCommandsHidden(true)
|
||||
}
|
||||
|
||||
let params = WebAppParameters(peerId: peerId, botId: peerId, botName: botName, url: url, queryId: nil, payload: nil, buttonText: buttonText, keepAliveSignal: nil, fromMenu: true, fromAttachMenu: false, isInline: false, isSimple: false)
|
||||
let params = WebAppParameters(source: .menu, peerId: peerId, botId: peerId, botName: botName, url: url, queryId: nil, payload: nil, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false)
|
||||
let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, threadId: strongSelf.chatLocation.threadId, openUrl: { [weak self] url, concealed, commit in
|
||||
self?.openUrl(url, concealed: concealed, forceExternal: true, commit: commit)
|
||||
}, requestSwitchInline: { [weak self] query, chatTypes, completion in
|
||||
@ -4285,7 +4285,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let params = WebAppParameters(peerId: peerId, botId: botId, botName: botName, url: url, queryId: nil, payload: nil, buttonText: buttonText, keepAliveSignal: nil, fromMenu: false, fromAttachMenu: false, isInline: isInline, isSimple: true)
|
||||
let params = WebAppParameters(source: isInline ? .inline : .simple, peerId: peerId, botId: botId, botName: botName, url: url, queryId: nil, payload: nil, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false)
|
||||
let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, threadId: strongSelf.chatLocation.threadId, openUrl: { [weak self] url, concealed, commit in
|
||||
self?.openUrl(url, concealed: concealed, forceExternal: true, commit: commit)
|
||||
}, requestSwitchInline: { [weak self] query, chatTypes, completion in
|
||||
@ -4325,7 +4325,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let params = WebAppParameters(peerId: peerId, botId: peerId, botName: botName, url: result.url, queryId: result.queryId, payload: nil, buttonText: buttonText, keepAliveSignal: result.keepAliveSignal, fromMenu: false, fromAttachMenu: false, isInline: false, isSimple: false)
|
||||
let params = WebAppParameters(source: .generic, peerId: peerId, botId: peerId, botName: botName, url: result.url, queryId: result.queryId, payload: nil, buttonText: buttonText, keepAliveSignal: result.keepAliveSignal, forceHasSettings: false)
|
||||
let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, threadId: strongSelf.chatLocation.threadId, openUrl: { [weak self] url, concealed, commit in
|
||||
self?.openUrl(url, concealed: concealed, forceExternal: true, commit: commit)
|
||||
}, completion: { [weak self] in
|
||||
@ -10318,7 +10318,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
unarchiveAutomaticallyArchivedPeer(account: strongSelf.context.account, peerId: peerId)
|
||||
|
||||
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .succeed(text: strongSelf.presentationData.strings.Conversation_UnarchiveDone), elevatedLayout: false, action: { _ in return false }), in: .current)
|
||||
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .succeed(text: strongSelf.presentationData.strings.Conversation_UnarchiveDone, timeout: nil), elevatedLayout: false, action: { _ in return false }), in: .current)
|
||||
}, scrollToTop: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
@ -12802,7 +12802,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
disposable.set((signal
|
||||
|> deliverOnMainQueue).start(completed: { [weak self] in
|
||||
if let strongSelf = self, let _ = strongSelf.validLayout {
|
||||
strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.ClearCache_Success("\(dataSizeString(selectedSize, formatting: DataSizeStringFormatting(presentationData: presentationData)))", stringForDeviceType()).string), elevatedLayout: false, action: { _ in return false }), in: .current)
|
||||
strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.ClearCache_Success("\(dataSizeString(selectedSize, formatting: DataSizeStringFormatting(presentationData: presentationData)))", stringForDeviceType()).string, timeout: nil), elevatedLayout: false, action: { _ in return false }), in: .current)
|
||||
}
|
||||
}))
|
||||
|
||||
@ -12877,7 +12877,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
|
||||
private func getCaptionPanelView() -> TGCaptionPanelView? {
|
||||
return self.context.sharedContext.makeGalleryCaptionPanelView(context: self.context, chatLocation: self.presentationInterfaceState.chatLocation, customEmojiAvailable: self.presentationInterfaceState.customEmojiAvailable, present: { [weak self] c in
|
||||
var isScheduledMessages = false
|
||||
if case .scheduledMessages = self.presentationInterfaceState.subject {
|
||||
isScheduledMessages = true
|
||||
}
|
||||
return self.context.sharedContext.makeGalleryCaptionPanelView(context: self.context, chatLocation: self.presentationInterfaceState.chatLocation, isScheduledMessages: isScheduledMessages, customEmojiAvailable: self.presentationInterfaceState.customEmojiAvailable, present: { [weak self] c in
|
||||
self?.present(c, in: .window(.root))
|
||||
}, presentInGlobalOverlay: { [weak self] c in
|
||||
guard let self else {
|
||||
@ -13046,7 +13050,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let params = WebAppParameters(peerId: peerId, botId: peerId, botName: botApp.title, url: url, queryId: 0, payload: payload, buttonText: "", keepAliveSignal: nil, fromMenu: false, fromAttachMenu: false, isInline: false, isSimple: false)
|
||||
let params = WebAppParameters(source: .generic, peerId: peerId, botId: peerId, botName: botApp.title, url: url, queryId: 0, payload: payload, buttonText: "", keepAliveSignal: nil, forceHasSettings: botApp.flags.contains(.hasSettings))
|
||||
let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, threadId: strongSelf.chatLocation.threadId, openUrl: { [weak self] url, concealed, commit in
|
||||
self?.openUrl(url, concealed: concealed, forceExternal: true, commit: commit)
|
||||
}, requestSwitchInline: { [weak self] query, chatTypes, completion in
|
||||
@ -13228,7 +13232,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
peerType.insert(.sameBot)
|
||||
peerType.remove(.bot)
|
||||
}
|
||||
let button: AttachmentButtonType = .app(bot.peer, bot.shortName, bot.icons)
|
||||
let button: AttachmentButtonType = .app(bot)
|
||||
if !bot.peerTypes.intersection(peerType).isEmpty {
|
||||
buttons.insert(button, at: 1)
|
||||
|
||||
@ -13269,21 +13273,31 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
if !premiumGiftOptions.isEmpty {
|
||||
buttons.insert(.gift, at: 1)
|
||||
}
|
||||
|
||||
|
||||
guard let initialButton = initialButton else {
|
||||
if case let .bot(botId, botPayload, botJustInstalled) = subject {
|
||||
if let button = allButtons.first(where: { button in
|
||||
if case let .app(botPeer, _, _) = button, botPeer.id == botId {
|
||||
if case let .app(bot) = button, bot.peer.id == botId {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}), case let .app(_, botName, _) = button {
|
||||
strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: botJustInstalled ? strongSelf.presentationData.strings.WebApp_AddToAttachmentSucceeded(botName).string : strongSelf.presentationData.strings.WebApp_AddToAttachmentAlreadyAddedError, timeout: nil), elevatedLayout: false, action: { _ in return false }), in: .current)
|
||||
}), case let .app(bot) = button {
|
||||
let content: UndoOverlayContent
|
||||
if botJustInstalled {
|
||||
if bot.flags.contains(.showInSettings) {
|
||||
content = .succeed(text: strongSelf.presentationData.strings.WebApp_ShortcutsSettingsAdded(bot.shortName).string, timeout: 5.0)
|
||||
} else {
|
||||
content = .succeed(text: strongSelf.presentationData.strings.WebApp_ShortcutsAdded(bot.shortName).string, timeout: 5.0)
|
||||
}
|
||||
} else {
|
||||
content = .info(title: nil, text: strongSelf.presentationData.strings.WebApp_AddToAttachmentAlreadyAddedError, timeout: nil)
|
||||
}
|
||||
strongSelf.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, position: .top, action: { _ in return false }), in: .current)
|
||||
} else {
|
||||
let _ = (context.engine.messages.getAttachMenuBot(botId: botId)
|
||||
|> deliverOnMainQueue).start(next: { bot in
|
||||
let controller = addWebAppToAttachmentController(context: context, peerName: bot.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), icons: bot.icons, requestWriteAccess: bot.flags.contains(.requiresWriteAccess), completion: { allowWrite in
|
||||
let controller = webAppTermsAlertController(context: context, updatedPresentationData: strongSelf.updatedPresentationData, bot: bot, completion: { allowWrite in
|
||||
let _ = (context.engine.messages.addBotToAttachMenu(botId: botId, allowWrite: allowWrite)
|
||||
|> deliverOnMainQueue).start(error: { _ in
|
||||
|
||||
@ -13308,7 +13322,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
|
||||
strongSelf.canReadHistory.set(false)
|
||||
|
||||
let attachmentController = AttachmentController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, chatLocation: strongSelf.chatLocation, buttons: buttons, initialButton: initialButton, makeEntityInputView: { [weak self] in
|
||||
let attachmentController = AttachmentController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, chatLocation: strongSelf.chatLocation, isScheduledMessages: isScheduledMessages, buttons: buttons, initialButton: initialButton, makeEntityInputView: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return nil
|
||||
}
|
||||
@ -13435,7 +13449,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
contactsController.navigationPresentation = .modal
|
||||
completion(contactsController, contactsController.mediaPickerContext)
|
||||
strongSelf.controllerNavigationDisposable.set((contactsController.result
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peers in
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peers in
|
||||
if let strongSelf = self, let (peers, _, silent, scheduleTime, text) = peers {
|
||||
var textEnqueueMessage: EnqueueMessage?
|
||||
if let text = text, text.length > 0 {
|
||||
@ -13607,14 +13621,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
|
||||
let _ = ApplicationSpecificNotice.incrementDismissedPremiumGiftSuggestion(accountManager: context.sharedContext.accountManager, peerId: peer.id).start()
|
||||
}
|
||||
case let .app(bot, botName, _):
|
||||
case let .app(bot):
|
||||
var payload: String?
|
||||
var fromAttachMenu = true
|
||||
if case let .bot(_, botPayload, _) = subject {
|
||||
payload = botPayload
|
||||
fromAttachMenu = false
|
||||
}
|
||||
let params = WebAppParameters(peerId: peer.id, botId: bot.id, botName: botName, url: nil, queryId: nil, payload: payload, buttonText: nil, keepAliveSignal: nil, fromMenu: false, fromAttachMenu: fromAttachMenu, isInline: false, isSimple: false)
|
||||
let params = WebAppParameters(source: fromAttachMenu ? .attachMenu : .generic, peerId: peer.id, botId: bot.peer.id, botName: bot.shortName, url: nil, queryId: nil, payload: payload, buttonText: nil, keepAliveSignal: nil, forceHasSettings: false)
|
||||
let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId
|
||||
let controller = WebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, replyToMessageId: replyMessageId, threadId: strongSelf.chatLocation.threadId)
|
||||
controller.openUrl = { [weak self] url, concealed, commit in
|
||||
@ -13641,6 +13655,26 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
attachmentController.navigationPresentation = .flatModal
|
||||
strongSelf.push(attachmentController)
|
||||
strongSelf.attachmentController = attachmentController
|
||||
|
||||
if case let .bot(botId, _, botJustInstalled) = subject, botJustInstalled {
|
||||
if let button = allButtons.first(where: { button in
|
||||
if case let .app(bot) = button, bot.peer.id == botId {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}), case let .app(bot) = button {
|
||||
Queue.mainQueue().after(0.3) {
|
||||
let content: UndoOverlayContent
|
||||
if bot.flags.contains(.showInSettings) {
|
||||
content = .succeed(text: strongSelf.presentationData.strings.WebApp_ShortcutsSettingsAdded(bot.shortName).string, timeout: 5.0)
|
||||
} else {
|
||||
content = .succeed(text: strongSelf.presentationData.strings.WebApp_ShortcutsAdded(bot.shortName).string, timeout: 5.0)
|
||||
}
|
||||
attachmentController.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, position: .top, action: { _ in return false }), in: .current)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if inputIsActive {
|
||||
@ -14095,7 +14129,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
guard let peer = self.presentationInterfaceState.renderedPeer?.peer else {
|
||||
return
|
||||
}
|
||||
let controller = MediaPickerScreen(context: self.context, updatedPresentationData: self.updatedPresentationData, peer: EnginePeer(peer), threadTitle: self.threadInfo?.title, chatLocation: self.chatLocation, bannedSendPhotos: bannedSendPhotos, bannedSendVideos: bannedSendVideos, subject: subject, saveEditedPhotos: saveEditedPhotos)
|
||||
var isScheduledMessages = false
|
||||
if case .scheduledMessages = self.presentationInterfaceState.subject {
|
||||
isScheduledMessages = true
|
||||
}
|
||||
let controller = MediaPickerScreen(context: self.context, updatedPresentationData: self.updatedPresentationData, peer: EnginePeer(peer), threadTitle: self.threadInfo?.title, chatLocation: self.chatLocation, isScheduledMessages: isScheduledMessages, bannedSendPhotos: bannedSendPhotos, bannedSendVideos: bannedSendVideos, subject: subject, saveEditedPhotos: saveEditedPhotos)
|
||||
let mediaPickerContext = controller.mediaPickerContext
|
||||
controller.openCamera = { [weak self] cameraView in
|
||||
self?.openCamera(cameraView: cameraView)
|
||||
|
@ -1469,7 +1469,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
||||
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuForward, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.actionSheet.primaryTextColor)
|
||||
}, action: { _, f in
|
||||
interfaceInteraction.forwardMessages(selectAll ? messages : [message])
|
||||
interfaceInteraction.forwardMessages(selectAll || isImage ? messages : [message])
|
||||
f(.dismissWithoutContent)
|
||||
})))
|
||||
}
|
||||
|
@ -4192,10 +4192,13 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
case let .message(message, _, _, _, _):
|
||||
for media in message.media {
|
||||
if let action = media as? TelegramMediaAction {
|
||||
if case .phoneCall = action.action { } else {
|
||||
if case .phoneCall = action.action {
|
||||
} else {
|
||||
canHaveSelection = false
|
||||
break
|
||||
}
|
||||
} else if media is TelegramMediaExpiredContent {
|
||||
canHaveSelection = false
|
||||
}
|
||||
}
|
||||
if message.adAttribute != nil {
|
||||
|
@ -558,16 +558,10 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
|
||||
}
|
||||
case let .premiumOffer(reference):
|
||||
dismissInput()
|
||||
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|
||||
|> deliverOnMainQueue).start(next: { peer in
|
||||
let isPremium = peer?.isPremium ?? false
|
||||
if !isPremium {
|
||||
let controller = PremiumIntroScreen(context: context, source: .deeplink(reference))
|
||||
if let navigationController = navigationController {
|
||||
navigationController.pushViewController(controller, animated: true)
|
||||
}
|
||||
}
|
||||
})
|
||||
let controller = PremiumIntroScreen(context: context, source: .deeplink(reference))
|
||||
if let navigationController = navigationController {
|
||||
navigationController.pushViewController(controller, animated: true)
|
||||
}
|
||||
case let .joinVoiceChat(peerId, invite):
|
||||
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|
||||
|> deliverOnMainQueue).start(next: { peer in
|
||||
@ -626,7 +620,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
|
||||
return (chooseTypes?.isEmpty ?? true) ? nil : chooseTypes
|
||||
}
|
||||
|
||||
if let bot = attachMenuBots.first(where: { $0.peer.id == peerId }) {
|
||||
if let bot = attachMenuBots.first(where: { $0.peer.id == peerId }), !bot.flags.contains(.notActivated) {
|
||||
let choose = filterChooseTypes(choose, peerTypes: bot.peerTypes)
|
||||
|
||||
if let choose = choose {
|
||||
@ -675,7 +669,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
|
||||
|> deliverOnMainQueue).start(next: { bot in
|
||||
let choose = filterChooseTypes(choose, peerTypes: bot.peerTypes)
|
||||
|
||||
let controller = addWebAppToAttachmentController(context: context, peerName: bot.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), icons: bot.icons, requestWriteAccess: bot.flags.contains(.requiresWriteAccess), completion: { allowWrite in
|
||||
let controller = webAppTermsAlertController(context: context, updatedPresentationData: updatedPresentationData, bot: bot, completion: { allowWrite in
|
||||
let _ = (context.engine.messages.addBotToAttachMenu(botId: peerId, allowWrite: allowWrite)
|
||||
|> deliverOnMainQueue).start(error: { _ in
|
||||
presentError(presentationData.strings.WebApp_AddToAttachmentUnavailableError)
|
||||
|
@ -1,5 +1,6 @@
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
|
||||
final class PeerInfoScreenDisclosureItem: PeerInfoScreenItem {
|
||||
@ -31,13 +32,15 @@ final class PeerInfoScreenDisclosureItem: PeerInfoScreenItem {
|
||||
let label: Label
|
||||
let text: String
|
||||
let icon: UIImage?
|
||||
let iconSignal: Signal<UIImage?, NoError>?
|
||||
let action: (() -> Void)?
|
||||
|
||||
init(id: AnyHashable, label: Label = .none, text: String, icon: UIImage? = nil, action: (() -> Void)?) {
|
||||
init(id: AnyHashable, label: Label = .none, text: String, icon: UIImage? = nil, iconSignal: Signal<UIImage?, NoError>? = nil, action: (() -> Void)?) {
|
||||
self.id = id
|
||||
self.label = label
|
||||
self.text = text
|
||||
self.icon = icon
|
||||
self.iconSignal = iconSignal
|
||||
self.action = action
|
||||
}
|
||||
|
||||
@ -57,6 +60,8 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode {
|
||||
private let bottomSeparatorNode: ASDisplayNode
|
||||
private let activateArea: AccessibilityAreaNode
|
||||
|
||||
private var iconDisposable: Disposable?
|
||||
|
||||
private var item: PeerInfoScreenDisclosureItem?
|
||||
|
||||
override init() {
|
||||
@ -109,6 +114,10 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode {
|
||||
self.addSubnode(self.activateArea)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.iconDisposable?.dispose()
|
||||
}
|
||||
|
||||
override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
|
||||
guard let item = item as? PeerInfoScreenDisclosureItem else {
|
||||
return 10.0
|
||||
@ -120,9 +129,9 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode {
|
||||
self.selectionNode.pressed = item.action
|
||||
|
||||
let sideInset: CGFloat = 16.0 + safeInsets.left
|
||||
let leftInset = (item.icon == nil ? sideInset : sideInset + 29.0 + 16.0)
|
||||
let leftInset = (item.icon == nil && item.iconSignal == nil ? sideInset : sideInset + 29.0 + 16.0)
|
||||
let rightInset = sideInset + 18.0
|
||||
let separatorInset = item.icon == nil ? sideInset : leftInset - 1.0
|
||||
let separatorInset = item.icon == nil && item.iconSignal == nil ? sideInset : leftInset - 1.0
|
||||
let titleFont = Font.regular(presentationData.listsFontSize.itemListBaseFontSize)
|
||||
|
||||
self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
|
||||
@ -149,12 +158,29 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode {
|
||||
|
||||
let height = textSize.height + 24.0
|
||||
|
||||
if let icon = item.icon {
|
||||
if item.icon != nil || item.iconSignal != nil {
|
||||
if self.iconNode.supernode == nil {
|
||||
self.addSubnode(self.iconNode)
|
||||
}
|
||||
self.iconNode.image = icon
|
||||
let iconFrame = CGRect(origin: CGPoint(x: sideInset, y: floorToScreenPixels((height - icon.size.height) / 2.0)), size: icon.size)
|
||||
let iconSize: CGSize
|
||||
if let icon = item.icon {
|
||||
self.iconNode.image = icon
|
||||
iconSize = icon.size
|
||||
} else if let iconSignal = item.iconSignal {
|
||||
if previousItem?.text != item.text {
|
||||
self.iconNode.image = nil
|
||||
self.iconDisposable = (iconSignal
|
||||
|> deliverOnMainQueue).start(next: { [weak self] icon in
|
||||
if let self {
|
||||
self.iconNode.image = icon
|
||||
}
|
||||
})
|
||||
}
|
||||
iconSize = CGSize(width: 29.0, height: 29.0)
|
||||
} else {
|
||||
iconSize = CGSize(width: 29.0, height: 29.0)
|
||||
}
|
||||
let iconFrame = CGRect(origin: CGPoint(x: sideInset, y: floorToScreenPixels((height - iconSize.height) / 2.0)), size: iconSize)
|
||||
transition.updateFrame(node: self.iconNode, frame: iconFrame)
|
||||
} else if self.iconNode.supernode != nil {
|
||||
self.iconNode.image = nil
|
||||
|
@ -489,6 +489,25 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id,
|
||||
}
|
||||
|> distinctUntilChanged
|
||||
|
||||
let botsKey = ValueBoxKey(length: 8)
|
||||
botsKey.setInt64(0, value: 0)
|
||||
let bots = context.engine.data.subscribe(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: Namespaces.CachedItemCollection.attachMenuBots, id: botsKey))
|
||||
|> mapToSignal { entry -> Signal<[AttachMenuBot], NoError> in
|
||||
let bots: [AttachMenuBots.Bot] = entry?.get(AttachMenuBots.self)?.bots ?? []
|
||||
return context.engine.data.subscribe(
|
||||
EngineDataMap(bots.map(\.peerId).map(TelegramEngine.EngineData.Item.Peer.Peer.init))
|
||||
)
|
||||
|> map { peersMap -> [AttachMenuBot] in
|
||||
var result: [AttachMenuBot] = []
|
||||
for bot in bots {
|
||||
if let maybePeer = peersMap[bot.peerId], let peer = maybePeer {
|
||||
result.append(AttachMenuBot(peer: peer, shortName: bot.name, icons: bot.icons, peerTypes: bot.peerTypes, flags: bot.flags))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
return combineLatest(
|
||||
context.account.viewTracker.peerView(peerId, updateData: true),
|
||||
accountsAndPeers,
|
||||
@ -512,7 +531,7 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id,
|
||||
}
|
||||
|> distinctUntilChanged,
|
||||
hasStories,
|
||||
context.engine.messages.attachMenuBots()
|
||||
bots
|
||||
)
|
||||
|> map { peerView, accountsAndPeers, accountSessions, privacySettings, sharedPreferences, notifications, stickerPacks, hasPassport, hasWatchApp, accountPreferences, suggestions, limits, hasPassword, isPowerSavingEnabled, hasStories, bots -> PeerInfoScreenData in
|
||||
let (notificationExceptions, notificationsAuthorizationStatus, notificationsWarningSuppressed) = notifications
|
||||
|
@ -800,7 +800,20 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p
|
||||
if let settings = data.globalSettings {
|
||||
for bot in settings.bots {
|
||||
if bot.flags.contains(.showInSettings) {
|
||||
items[.apps]!.append(PeerInfoScreenDisclosureItem(id: appIndex, text: bot.peer.compactDisplayTitle, icon: PresentationResourcesSettings.passport, action: {
|
||||
let iconSignal: Signal<UIImage?, NoError>
|
||||
if let peer = PeerReference(bot.peer._asPeer()), let icon = bot.icons[.iOSSettingsStatic] {
|
||||
let fileReference: FileMediaReference = .attachBot(peer: peer, media: icon)
|
||||
iconSignal = instantPageImageFile(account: context.account, userLocation: .other, fileReference: fileReference, fetched: true)
|
||||
|> map { generator -> UIImage? in
|
||||
let size = CGSize(width: 29.0, height: 29.0)
|
||||
let context = generator(TransformImageArguments(corners: ImageCorners(), imageSize: size, boundingSize: size, intrinsicInsets: .zero))
|
||||
return context?.generateImage()
|
||||
}
|
||||
let _ = freeMediaFileInteractiveFetched(account: context.account, userLocation: .other, fileReference: fileReference).start()
|
||||
} else {
|
||||
iconSignal = .single(UIImage(bundleImageName: "Settings/Menu/Websites")!)
|
||||
}
|
||||
items[.apps]!.append(PeerInfoScreenDisclosureItem(id: bot.peer.id.id._internalGetInt64Value(), text: bot.peer.compactDisplayTitle, icon: nil, iconSignal: iconSignal, action: {
|
||||
interaction.openBotApp(bot)
|
||||
}))
|
||||
appIndex += 1
|
||||
@ -808,7 +821,7 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p
|
||||
}
|
||||
}
|
||||
|
||||
items[.apps]!.append(PeerInfoScreenDisclosureItem(id: appIndex, text: presentationData.strings.Settings_MyStories, icon: PresentationResourcesSettings.stories, action: {
|
||||
items[.apps]!.append(PeerInfoScreenDisclosureItem(id: 0, text: presentationData.strings.Settings_MyStories, icon: PresentationResourcesSettings.stories, action: {
|
||||
interaction.openSettings(.stories)
|
||||
}))
|
||||
|
||||
@ -4070,6 +4083,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
if let previousSuggestPasswordConfirmation = previousData?.globalSettings?.suggestPasswordConfirmation, previousSuggestPasswordConfirmation != data.globalSettings?.suggestPasswordConfirmation {
|
||||
infoUpdated = true
|
||||
}
|
||||
if let previousBots = previousData?.globalSettings?.bots, previousBots.count != (data.globalSettings?.bots ?? []).count {
|
||||
infoUpdated = true
|
||||
}
|
||||
}
|
||||
if previousCallsPrivate != currentCallsPrivate || (previousVideoCallsAvailable != currentVideoCallsAvailable && currentVideoCallsAvailable != nil) {
|
||||
infoUpdated = true
|
||||
@ -4604,20 +4620,38 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
guard let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
|
||||
let proceed = { [weak self] in
|
||||
let presentationData = self.presentationData
|
||||
let proceed: (Bool) -> Void = { [weak self] installed in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.openBotAppDisposable.set(((self.context.engine.messages.requestSimpleWebView(botId: bot.peer.id, url: nil, source: .settings, themeParams: generateWebAppThemeParams(self.presentationData.theme))
|
||||
|
||||
let progressSignal = Signal<Never, NoError> { [weak self] subscriber in
|
||||
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil))
|
||||
self?.controller?.present(controller, in: .window(.root))
|
||||
return ActionDisposable { [weak controller] in
|
||||
Queue.mainQueue().async() {
|
||||
controller?.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|> runOn(Queue.mainQueue())
|
||||
|> delay(0.35, queue: Queue.mainQueue())
|
||||
let progressDisposable = progressSignal.start()
|
||||
|
||||
let signal: Signal<String, RequestSimpleWebViewError> = self.context.engine.messages.requestSimpleWebView(botId: bot.peer.id, url: nil, source: .settings, themeParams: generateWebAppThemeParams(self.presentationData.theme))
|
||||
|> afterDisposed {
|
||||
// updateProgress()
|
||||
})
|
||||
Queue.mainQueue().async {
|
||||
progressDisposable.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
self.openBotAppDisposable.set((signal
|
||||
|> deliverOnMainQueue).start(next: { [weak self] url in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let params = WebAppParameters(peerId: self.context.account.peerId, botId: bot.peer.id, botName: bot.peer.compactDisplayTitle, url: url, queryId: nil, payload: nil, buttonText: nil, keepAliveSignal: nil, fromMenu: false, fromAttachMenu: false, isInline: false, isSimple: true)
|
||||
let params = WebAppParameters(source: .settings, peerId: self.context.account.peerId, botId: bot.peer.id, botName: bot.peer.compactDisplayTitle, url: url, queryId: nil, payload: nil, buttonText: nil, keepAliveSignal: nil, forceHasSettings: bot.flags.contains(.hasSettings))
|
||||
let controller = standaloneWebAppController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, params: params, threadId: nil, openUrl: { [weak self] url, concealed, commit in
|
||||
self?.openUrl(url: url, concealed: concealed, external: false, forceExternal: true, commit: commit)
|
||||
}, requestSwitchInline: { _, _, _ in
|
||||
@ -4626,6 +4660,21 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
})
|
||||
controller.navigationPresentation = .flatModal
|
||||
self.controller?.push(controller)
|
||||
|
||||
if installed {
|
||||
Queue.mainQueue().after(0.3, {
|
||||
let text: String
|
||||
if bot.flags.contains(.showInSettings) {
|
||||
text = presentationData.strings.WebApp_ShortcutsSettingsAdded(bot.peer.compactDisplayTitle).string
|
||||
} else {
|
||||
text = presentationData.strings.WebApp_ShortcutsAdded(bot.peer.compactDisplayTitle).string
|
||||
}
|
||||
controller.present(
|
||||
UndoOverlayController(presentationData: presentationData, content: .succeed(text: text, timeout: 5.0), elevatedLayout: false, position: .top, action: { _ in return false }),
|
||||
in: .current
|
||||
)
|
||||
})
|
||||
}
|
||||
}, error: { [weak self] error in
|
||||
if let self {
|
||||
self.controller?.present(textAlertController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, title: nil, text: self.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {
|
||||
@ -4634,13 +4683,28 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
}))
|
||||
}
|
||||
|
||||
if bot.flags.contains(.showInSettingsDisclaimer) {
|
||||
let alertController = webAppTermsAlertController(context: self.context, updatedPresentationData: controller.updatedPresentationData, peer: bot.peer, completion: {
|
||||
proceed()
|
||||
if bot.flags.contains(.notActivated) || bot.flags.contains(.showInSettingsDisclaimer) {
|
||||
let alertController = webAppTermsAlertController(context: self.context, updatedPresentationData: controller.updatedPresentationData, bot: bot, completion: { [weak self] allowWrite in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if bot.flags.contains(.showInSettingsDisclaimer) {
|
||||
let _ = self.context.engine.messages.acceptAttachMenuBotDisclaimer(botId: bot.peer.id).start()
|
||||
}
|
||||
if bot.flags.contains(.notActivated) {
|
||||
let _ = (self.context.engine.messages.addBotToAttachMenu(botId: bot.peer.id, allowWrite: allowWrite)
|
||||
|> deliverOnMainQueue).start(error: { _ in
|
||||
|
||||
}, completed: {
|
||||
proceed(true)
|
||||
})
|
||||
} else {
|
||||
proceed(false)
|
||||
}
|
||||
})
|
||||
controller.present(alertController, in: .window(.root))
|
||||
} else {
|
||||
proceed()
|
||||
proceed(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1624,109 +1624,17 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
return makeAttachmentFileControllerImpl(context: context, updatedPresentationData: updatedPresentationData, bannedSendMedia: bannedSendMedia, presentGallery: presentGallery, presentFiles: presentFiles, send: send)
|
||||
}
|
||||
|
||||
public func makeGalleryCaptionPanelView(context: AccountContext, chatLocation: ChatLocation, customEmojiAvailable: Bool, present: @escaping (ViewController) -> Void, presentInGlobalOverlay: @escaping (ViewController) -> Void) -> NSObject? {
|
||||
// var presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
// presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)
|
||||
//
|
||||
// var presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: .builtin(WallpaperSettings()), theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, limitsConfiguration: context.currentLimitsConfiguration.with { $0 }, fontSize: presentationData.chatFontSize, bubbleCorners: presentationData.chatBubbleCorners, accountPeerId: context.account.peerId, mode: .standard(previewing: false), chatLocation: chatLocation, subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil, threadData: nil, isGeneralThreadClosed: nil)
|
||||
//
|
||||
// var updateChatPresentationInterfaceStateImpl: (((ChatPresentationInterfaceState) -> ChatPresentationInterfaceState) -> Void)?
|
||||
// var ensureFocusedImpl: (() -> Void)?
|
||||
//
|
||||
// let interfaceInteraction = ChatPanelInterfaceInteraction(updateTextInputStateAndMode: { f in
|
||||
// updateChatPresentationInterfaceStateImpl?({
|
||||
// let (updatedState, updatedMode) = f($0.interfaceState.effectiveInputState, $0.inputMode)
|
||||
// return $0.updatedInterfaceState { interfaceState in
|
||||
// return interfaceState.withUpdatedEffectiveInputState(updatedState)
|
||||
// }.updatedInputMode({ _ in updatedMode })
|
||||
// })
|
||||
// }, updateInputModeAndDismissedButtonKeyboardMessageId: { f in
|
||||
// updateChatPresentationInterfaceStateImpl?({
|
||||
// let (updatedInputMode, updatedClosedButtonKeyboardMessageId) = f($0)
|
||||
// return $0.updatedInputMode({ _ in return updatedInputMode }).updatedInterfaceState({
|
||||
// $0.withUpdatedMessageActionsState({ value in
|
||||
// var value = value
|
||||
// value.closedButtonKeyboardMessageId = updatedClosedButtonKeyboardMessageId
|
||||
// return value
|
||||
// })
|
||||
// })
|
||||
// })
|
||||
// }, openLinkEditing: {
|
||||
// var selectionRange: Range<Int>?
|
||||
// var text: NSAttributedString?
|
||||
// var inputMode: ChatInputMode?
|
||||
// updateChatPresentationInterfaceStateImpl?({ state in
|
||||
// selectionRange = state.interfaceState.effectiveInputState.selectionRange
|
||||
// if let selectionRange = selectionRange {
|
||||
// text = state.interfaceState.effectiveInputState.inputText.attributedSubstring(from: NSRange(location: selectionRange.startIndex, length: selectionRange.count))
|
||||
// }
|
||||
// inputMode = state.inputMode
|
||||
// return state
|
||||
// })
|
||||
//
|
||||
// var link: String?
|
||||
// if let text {
|
||||
// text.enumerateAttributes(in: NSMakeRange(0, text.length)) { attributes, _, _ in
|
||||
// if let linkAttribute = attributes[ChatTextInputAttributes.textUrl] as? ChatTextInputTextUrlAttribute {
|
||||
// link = linkAttribute.url
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// let controller = chatTextLinkEditController(sharedContext: context.sharedContext, updatedPresentationData: (presentationData, .never()), account: context.account, text: text?.string ?? "", link: link, apply: { link in
|
||||
// if let inputMode = inputMode, let selectionRange = selectionRange {
|
||||
// if let link = link {
|
||||
// updateChatPresentationInterfaceStateImpl?({
|
||||
// return $0.updatedInterfaceState({
|
||||
// $0.withUpdatedEffectiveInputState(chatTextInputAddLinkAttribute($0.effectiveInputState, selectionRange: selectionRange, url: link))
|
||||
// })
|
||||
// })
|
||||
// }
|
||||
// ensureFocusedImpl?()
|
||||
// updateChatPresentationInterfaceStateImpl?({
|
||||
// return $0.updatedInputMode({ _ in return inputMode }).updatedInterfaceState({
|
||||
// $0.withUpdatedEffectiveInputState(ChatTextInputState(inputText: $0.effectiveInputState.inputText, selectionRange: selectionRange.endIndex ..< selectionRange.endIndex))
|
||||
// })
|
||||
// })
|
||||
// }
|
||||
// })
|
||||
// present(controller)
|
||||
// })
|
||||
//
|
||||
// let inputPanelNode = AttachmentTextInputPanelNode(context: context, presentationInterfaceState: presentationInterfaceState, isCaption: true, presentController: { c in
|
||||
// presentInGlobalOverlay(c)
|
||||
// }, makeEntityInputView: {
|
||||
// return EntityInputView(context: context, isDark: true, areCustomEmojiEnabled: customEmojiAvailable)
|
||||
// })
|
||||
// inputPanelNode.interfaceInteraction = interfaceInteraction
|
||||
// inputPanelNode.effectivePresentationInterfaceState = {
|
||||
// return presentationInterfaceState
|
||||
// }
|
||||
//
|
||||
// updateChatPresentationInterfaceStateImpl = { [weak inputPanelNode] f in
|
||||
// let updatedPresentationInterfaceState = f(presentationInterfaceState)
|
||||
// let updateInputTextState = presentationInterfaceState.interfaceState.effectiveInputState != updatedPresentationInterfaceState.interfaceState.effectiveInputState
|
||||
//
|
||||
// presentationInterfaceState = updatedPresentationInterfaceState
|
||||
//
|
||||
// if let inputPanelNode = inputPanelNode, updateInputTextState {
|
||||
// inputPanelNode.updateInputTextState(updatedPresentationInterfaceState.interfaceState.effectiveInputState, animated: true)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// ensureFocusedImpl = { [weak inputPanelNode] in
|
||||
// inputPanelNode?.ensureFocused()
|
||||
// }
|
||||
//
|
||||
// return inputPanelNode
|
||||
|
||||
public func makeGalleryCaptionPanelView(context: AccountContext, chatLocation: ChatLocation, isScheduledMessages: Bool, customEmojiAvailable: Bool, present: @escaping (ViewController) -> Void, presentInGlobalOverlay: @escaping (ViewController) -> Void) -> NSObject? {
|
||||
let inputPanelNode = LegacyMessageInputPanelNode(
|
||||
context: context,
|
||||
chatLocation: chatLocation,
|
||||
isScheduledMessages: isScheduledMessages,
|
||||
present: present,
|
||||
presentInGlobalOverlay: presentInGlobalOverlay
|
||||
presentInGlobalOverlay: presentInGlobalOverlay,
|
||||
makeEntityInputView: {
|
||||
return EntityInputView(context: context, isDark: true, areCustomEmojiEnabled: customEmojiAvailable)
|
||||
}
|
||||
)
|
||||
|
||||
return inputPanelNode
|
||||
}
|
||||
|
||||
|
@ -11,7 +11,7 @@ public enum UndoOverlayContent {
|
||||
case archivedChat(peerId: Int64, title: String, text: String, undo: Bool)
|
||||
case hidArchive(title: String, text: String, undo: Bool)
|
||||
case revealedArchive(title: String, text: String, undo: Bool)
|
||||
case succeed(text: String)
|
||||
case succeed(text: String, timeout: Double?)
|
||||
case info(title: String?, text: String, timeout: Double?)
|
||||
case emoji(name: String, text: String)
|
||||
case swipeToReply(title: String, text: String)
|
||||
|
@ -191,7 +191,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
if text.contains("](") {
|
||||
isUserInteractionEnabled = true
|
||||
}
|
||||
case let .succeed(text):
|
||||
case let .succeed(text, timeout):
|
||||
self.avatarNode = nil
|
||||
self.iconNode = nil
|
||||
self.iconCheckNode = nil
|
||||
@ -204,7 +204,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
self.textNode.attributedText = attributedText
|
||||
self.textNode.maximumNumberOfLines = 5
|
||||
displayUndo = false
|
||||
self.originalRemainingSeconds = 3
|
||||
self.originalRemainingSeconds = timeout ?? 3
|
||||
case let .info(title, text, timeout):
|
||||
self.avatarNode = nil
|
||||
self.iconNode = nil
|
||||
|
@ -32,6 +32,7 @@ swift_library(
|
||||
"//submodules/InstantPageUI:InstantPageUI",
|
||||
"//submodules/CheckNode:CheckNode",
|
||||
"//submodules/Markdown:Markdown",
|
||||
"//submodules/TextFormat:TextFormat",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -53,9 +53,10 @@ public class WebAppCancelButtonNode: ASDisplayNode {
|
||||
private let strings: PresentationStrings
|
||||
|
||||
public func updateColor(_ color: UIColor?, transition: ContainedViewLayoutTransition) {
|
||||
let previousColor = self.color
|
||||
self.color = color
|
||||
|
||||
if case let .animated(duration, curve) = transition {
|
||||
if case let .animated(duration, curve) = transition, previousColor != color {
|
||||
if let snapshotView = self.view.snapshotContentTree() {
|
||||
snapshotView.frame = self.bounds
|
||||
self.view.addSubview(snapshotView)
|
||||
@ -170,6 +171,24 @@ public class WebAppCancelButtonNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
public struct WebAppParameters {
|
||||
public enum Source {
|
||||
case generic
|
||||
case menu
|
||||
case attachMenu
|
||||
case inline
|
||||
case simple
|
||||
case settings
|
||||
|
||||
var isSimple: Bool {
|
||||
if [.simple, .inline, .settings].contains(self) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let source: Source
|
||||
let peerId: PeerId
|
||||
let botId: PeerId
|
||||
let botName: String
|
||||
@ -178,12 +197,10 @@ public struct WebAppParameters {
|
||||
let payload: String?
|
||||
let buttonText: String?
|
||||
let keepAliveSignal: Signal<Never, KeepWebViewError>?
|
||||
let fromMenu: Bool
|
||||
let fromAttachMenu: Bool
|
||||
let isInline: Bool
|
||||
let isSimple: Bool
|
||||
let forceHasSettings: Bool
|
||||
|
||||
public init(
|
||||
source: Source,
|
||||
peerId: PeerId,
|
||||
botId: PeerId,
|
||||
botName: String,
|
||||
@ -192,11 +209,9 @@ public struct WebAppParameters {
|
||||
payload: String?,
|
||||
buttonText: String?,
|
||||
keepAliveSignal: Signal<Never, KeepWebViewError>?,
|
||||
fromMenu: Bool,
|
||||
fromAttachMenu: Bool,
|
||||
isInline: Bool,
|
||||
isSimple: Bool
|
||||
forceHasSettings: Bool
|
||||
) {
|
||||
self.source = source
|
||||
self.peerId = peerId
|
||||
self.botId = botId
|
||||
self.botName = botName
|
||||
@ -205,10 +220,7 @@ public struct WebAppParameters {
|
||||
self.payload = payload
|
||||
self.buttonText = buttonText
|
||||
self.keepAliveSignal = keepAliveSignal
|
||||
self.fromMenu = fromMenu
|
||||
self.fromAttachMenu = fromAttachMenu
|
||||
self.isInline = isInline
|
||||
self.isSimple = isSimple
|
||||
self.forceHasSettings = forceHasSettings
|
||||
}
|
||||
}
|
||||
|
||||
@ -390,7 +402,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
})
|
||||
})
|
||||
|
||||
if let url = controller.url, !controller.fromMenu {
|
||||
if let url = controller.url, controller.source != .menu {
|
||||
self.queryId = controller.queryId
|
||||
if let parsedUrl = URL(string: url) {
|
||||
self.webView?.load(URLRequest(url: parsedUrl))
|
||||
@ -409,7 +421,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
})
|
||||
}
|
||||
} else {
|
||||
let _ = (context.engine.messages.requestWebView(peerId: controller.peerId, botId: controller.botId, url: controller.url, payload: controller.payload, themeParams: generateWebAppThemeParams(presentationData.theme), fromMenu: controller.fromMenu, replyToMessageId: controller.replyToMessageId, threadId: controller.threadId)
|
||||
let _ = (context.engine.messages.requestWebView(peerId: controller.peerId, botId: controller.botId, url: controller.url, payload: controller.payload, themeParams: generateWebAppThemeParams(presentationData.theme), fromMenu: controller.source == .menu, replyToMessageId: controller.replyToMessageId, threadId: controller.threadId)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] result in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
@ -704,11 +716,11 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
}
|
||||
}
|
||||
case "web_app_data_send":
|
||||
if controller.isSimple, let eventData = body["eventData"] as? String {
|
||||
if controller.source.isSimple, let eventData = body["eventData"] as? String {
|
||||
self.handleSendData(data: eventData)
|
||||
}
|
||||
case "web_app_setup_main_button":
|
||||
if let webView = self.webView, !webView.didTouchOnce && controller.url == nil && controller.fromAttachMenu {
|
||||
if let webView = self.webView, !webView.didTouchOnce && controller.url == nil && controller.source == .attachMenu {
|
||||
self.delayedScriptMessage = message
|
||||
} else if let json = json {
|
||||
if var isVisible = json["is_visible"] as? Bool {
|
||||
@ -1298,6 +1310,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
fileprivate let moreButtonNode: MoreButtonNode
|
||||
|
||||
private let context: AccountContext
|
||||
private let source: WebAppParameters.Source
|
||||
private let peerId: PeerId
|
||||
private let botId: PeerId
|
||||
private let botName: String
|
||||
@ -1305,10 +1318,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
private let queryId: Int64?
|
||||
private let payload: String?
|
||||
private let buttonText: String?
|
||||
private let fromMenu: Bool
|
||||
private let fromAttachMenu: Bool
|
||||
private let isInline: Bool
|
||||
private let isSimple: Bool
|
||||
private let forceHasSettings: Bool
|
||||
private let keepAliveSignal: Signal<Never, KeepWebViewError>?
|
||||
private let replyToMessageId: MessageId?
|
||||
private let threadId: Int64?
|
||||
@ -1324,6 +1334,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
|
||||
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, params: WebAppParameters, replyToMessageId: MessageId?, threadId: Int64?) {
|
||||
self.context = context
|
||||
self.source = params.source
|
||||
self.peerId = params.peerId
|
||||
self.botId = params.botId
|
||||
self.botName = params.botName
|
||||
@ -1331,10 +1342,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
self.queryId = params.queryId
|
||||
self.payload = params.payload
|
||||
self.buttonText = params.buttonText
|
||||
self.fromMenu = params.fromMenu
|
||||
self.fromAttachMenu = params.fromAttachMenu
|
||||
self.isInline = params.isInline
|
||||
self.isSimple = params.isSimple
|
||||
self.forceHasSettings = params.forceHasSettings
|
||||
self.keepAliveSignal = params.keepAliveSignal
|
||||
self.replyToMessageId = replyToMessageId
|
||||
self.threadId = threadId
|
||||
@ -1446,15 +1454,30 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
|
||||
let peerId = self.peerId
|
||||
let botId = self.botId
|
||||
let url = self.url
|
||||
let forceHasSettings = self.forceHasSettings
|
||||
|
||||
let source = self.source
|
||||
|
||||
let items = context.engine.messages.attachMenuBots()
|
||||
|> take(1)
|
||||
|> map { [weak self] attachMenuBots -> ContextController.Items in
|
||||
var items: [ContextMenuItem] = []
|
||||
|
||||
let attachMenuBot = attachMenuBots.first(where: { $0.peer.id == botId})
|
||||
let attachMenuBot = attachMenuBots.first(where: { $0.peer.id == botId && !$0.flags.contains(.notActivated) })
|
||||
|
||||
if self?.url == nil, let attachMenuBot = attachMenuBot, attachMenuBot.flags.contains(.hasSettings) {
|
||||
let hasSettings: Bool
|
||||
if url == nil {
|
||||
if forceHasSettings {
|
||||
hasSettings = true
|
||||
} else {
|
||||
hasSettings = attachMenuBot?.flags.contains(.hasSettings) == true
|
||||
}
|
||||
} else {
|
||||
hasSettings = forceHasSettings
|
||||
}
|
||||
|
||||
if hasSettings {
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.WebApp_Settings, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Settings"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] c, _ in
|
||||
@ -1499,7 +1522,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
self?.controllerNode.webView?.reload()
|
||||
})))
|
||||
|
||||
if let _ = attachMenuBot, self?.url == nil {
|
||||
if let _ = attachMenuBot, [.attachMenu, .settings].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)
|
||||
}, action: { [weak self] c, _ in
|
||||
@ -1654,7 +1677,7 @@ public func standaloneWebAppController(
|
||||
didDismiss: @escaping () -> Void = {},
|
||||
getNavigationController: @escaping () -> NavigationController? = { return nil },
|
||||
getSourceRect: (() -> CGRect?)? = nil) -> ViewController {
|
||||
let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, chatLocation: .peer(id: params.peerId), buttons: [.standalone], initialButton: .standalone, fromMenu: params.fromMenu, hasTextInput: false, makeEntityInputView: {
|
||||
let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, chatLocation: .peer(id: params.peerId), buttons: [.standalone], initialButton: .standalone, fromMenu: params.source == .menu, hasTextInput: false, makeEntityInputView: {
|
||||
return nil
|
||||
})
|
||||
controller.getInputContainerNode = getInputContainerNode
|
||||
|
@ -12,24 +12,27 @@ import AppBundle
|
||||
import AvatarNode
|
||||
import CheckNode
|
||||
import Markdown
|
||||
import TextFormat
|
||||
|
||||
private let textFont = Font.regular(13.0)
|
||||
private let boldTextFont = Font.semibold(13.0)
|
||||
|
||||
private func formattedText(_ text: String, color: UIColor, linkColor: UIColor, textAlignment: NSTextAlignment = .natural) -> NSAttributedString {
|
||||
return parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: color), bold: MarkdownAttributeSet(font: boldTextFont, textColor: color), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { _ in return nil}), textAlignment: textAlignment)
|
||||
private func formattedText(_ text: String, fontSize: CGFloat, color: UIColor, linkColor: UIColor, textAlignment: NSTextAlignment = .natural) -> NSAttributedString {
|
||||
return parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: Font.regular(fontSize), textColor: color), bold: MarkdownAttributeSet(font: Font.semibold(fontSize), textColor: color), link: MarkdownAttributeSet(font: Font.regular(fontSize), textColor: linkColor), linkAttribute: { _ in return (TelegramTextAttributes.URL, "") }), textAlignment: textAlignment)
|
||||
}
|
||||
|
||||
private final class WebAppTermsAlertContentNode: AlertContentNode {
|
||||
private final class WebAppTermsAlertContentNode: AlertContentNode, UIGestureRecognizerDelegate {
|
||||
private let strings: PresentationStrings
|
||||
private let title: String
|
||||
private let text: String
|
||||
private let additionalText: String?
|
||||
|
||||
private let titleNode: ImmediateTextNode
|
||||
private let textNode: ASTextNode
|
||||
private let textNode: ImmediateTextNode
|
||||
private let additionalTextNode: ImmediateTextNode
|
||||
|
||||
private let acceptTermsCheckNode: InteractiveCheckNode
|
||||
private let acceptTermsLabelNode: ASTextNode
|
||||
private let acceptTermsLabelNode: ImmediateTextNode
|
||||
|
||||
private let actionNodesSeparator: ASDisplayNode
|
||||
private let actionNodes: [TextAlertContentActionNode]
|
||||
@ -50,24 +53,34 @@ private final class WebAppTermsAlertContentNode: AlertContentNode {
|
||||
}
|
||||
}
|
||||
|
||||
init(context: AccountContext, theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, title: String, text: String, actions: [TextAlertAction]) {
|
||||
var openTerms: () -> Void = {}
|
||||
|
||||
init(context: AccountContext, theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, title: String, text: String, additionalText: String?, actions: [TextAlertAction]) {
|
||||
self.strings = strings
|
||||
self.title = title
|
||||
self.text = text
|
||||
self.additionalText = additionalText
|
||||
|
||||
self.titleNode = ImmediateTextNode()
|
||||
self.titleNode.displaysAsynchronously = false
|
||||
self.titleNode.maximumNumberOfLines = 1
|
||||
self.titleNode.textAlignment = .center
|
||||
|
||||
self.textNode = ASTextNode()
|
||||
self.textNode.displaysAsynchronously = false
|
||||
self.textNode = ImmediateTextNode()
|
||||
self.textNode.maximumNumberOfLines = 0
|
||||
self.textNode.displaysAsynchronously = false
|
||||
self.textNode.lineSpacing = 0.1
|
||||
self.textNode.textAlignment = .center
|
||||
|
||||
self.additionalTextNode = ImmediateTextNode()
|
||||
self.additionalTextNode.maximumNumberOfLines = 0
|
||||
self.additionalTextNode.displaysAsynchronously = false
|
||||
self.additionalTextNode.lineSpacing = 0.1
|
||||
self.additionalTextNode.textAlignment = .center
|
||||
|
||||
self.acceptTermsCheckNode = InteractiveCheckNode(theme: CheckNodeTheme(backgroundColor: theme.accentColor, strokeColor: theme.contrastColor, borderColor: theme.controlBorderColor, overlayBorder: false, hasInset: false, hasShadow: false))
|
||||
self.acceptTermsLabelNode = ASTextNode()
|
||||
self.acceptTermsLabelNode = ImmediateTextNode()
|
||||
self.acceptTermsLabelNode.maximumNumberOfLines = 4
|
||||
self.acceptTermsLabelNode.isUserInteractionEnabled = true
|
||||
|
||||
self.actionNodesSeparator = ASDisplayNode()
|
||||
self.actionNodesSeparator.isLayerBacked = true
|
||||
@ -90,6 +103,7 @@ private final class WebAppTermsAlertContentNode: AlertContentNode {
|
||||
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.textNode)
|
||||
self.addSubnode(self.additionalTextNode)
|
||||
|
||||
self.addSubnode(self.acceptTermsCheckNode)
|
||||
self.addSubnode(self.acceptTermsLabelNode)
|
||||
@ -103,38 +117,98 @@ private final class WebAppTermsAlertContentNode: AlertContentNode {
|
||||
for separatorNode in self.actionVerticalSeparators {
|
||||
self.addSubnode(separatorNode)
|
||||
}
|
||||
|
||||
if let firstAction = self.actionNodes.first {
|
||||
firstAction.actionEnabled = false
|
||||
}
|
||||
|
||||
|
||||
self.acceptTermsCheckNode.valueChanged = { [weak self] value in
|
||||
if let strongSelf = self {
|
||||
strongSelf.acceptedTerms = !strongSelf.acceptedTerms
|
||||
}
|
||||
}
|
||||
|
||||
self.acceptTermsLabelNode.highlightAttributeAction = { attributes in
|
||||
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
|
||||
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
self.acceptTermsLabelNode.tapAttributeAction = { [weak self] attributes, _ in
|
||||
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
|
||||
self?.openTerms()
|
||||
}
|
||||
}
|
||||
|
||||
self.updateTheme(theme)
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.acceptTermsLabelNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.acceptTap(_:))))
|
||||
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.acceptTap(_:)))
|
||||
tapGesture.delegate = self
|
||||
self.view.addGestureRecognizer(tapGesture)
|
||||
|
||||
if let firstAction = self.actionNodes.first {
|
||||
firstAction.actionEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
let location = gestureRecognizer.location(in: self.acceptTermsLabelNode.view)
|
||||
if self.acceptTermsLabelNode.bounds.contains(location) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if !self.bounds.contains(point) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if let (_, attributes) = self.acceptTermsLabelNode.attributesAtPoint(self.view.convert(point, to: self.acceptTermsLabelNode.view)) {
|
||||
if attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] == nil {
|
||||
return self.view
|
||||
}
|
||||
}
|
||||
|
||||
return super.hitTest(point, with: event)
|
||||
}
|
||||
|
||||
@objc private func acceptTap(_ gestureRecognizer: UITapGestureRecognizer) {
|
||||
let location = gestureRecognizer.location(in: self.acceptTermsLabelNode.view)
|
||||
if self.acceptTermsCheckNode.isUserInteractionEnabled {
|
||||
if let attributes = self.acceptTermsLabelNode.attributesAtPoint(location)?.1 {
|
||||
if attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
self.acceptedTerms = !self.acceptedTerms
|
||||
}
|
||||
}
|
||||
|
||||
override func updateTheme(_ theme: AlertControllerTheme) {
|
||||
self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.semibold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center)
|
||||
self.textNode.attributedText = NSAttributedString(string: self.text, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center)
|
||||
self.textNode.attributedText = formattedText(self.text, fontSize: 13.0, color: theme.primaryColor, linkColor: theme.accentColor, textAlignment: .center)
|
||||
if let additionalText = self.additionalText {
|
||||
self.additionalTextNode.attributedText = formattedText(additionalText, fontSize: 13.0, color: theme.primaryColor, linkColor: theme.accentColor, textAlignment: .center)
|
||||
} else {
|
||||
self.additionalTextNode.attributedText = nil
|
||||
}
|
||||
|
||||
let text = "I agree to the [Terms of Use]()"
|
||||
self.acceptTermsLabelNode.attributedText = formattedText(text, color: theme.primaryColor, linkColor: theme.accentColor)
|
||||
let attributedAgreeText = parseMarkdownIntoAttributedString(
|
||||
self.strings.WebApp_DisclaimerAgree,
|
||||
attributes: MarkdownAttributes(
|
||||
body: MarkdownAttributeSet(font: textFont, textColor: theme.primaryColor),
|
||||
bold: MarkdownAttributeSet(font: boldTextFont, textColor: theme.primaryColor),
|
||||
link: MarkdownAttributeSet(font: textFont, textColor: theme.accentColor),
|
||||
linkAttribute: { contents in
|
||||
return (TelegramTextAttributes.URL, contents)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
self.acceptTermsLabelNode.attributedText = attributedAgreeText
|
||||
self.acceptTermsLabelNode.linkHighlightColor = theme.accentColor.withAlphaComponent(0.2)
|
||||
|
||||
self.actionNodesSeparator.backgroundColor = theme.separatorColor
|
||||
for actionNode in self.actionNodes {
|
||||
@ -163,7 +237,7 @@ private final class WebAppTermsAlertContentNode: AlertContentNode {
|
||||
|
||||
var entriesHeight: CGFloat = 0.0
|
||||
|
||||
let textSize = self.textNode.measure(CGSize(width: size.width - 48.0, height: size.height))
|
||||
let textSize = self.textNode.updateLayout(CGSize(width: size.width - 48.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
|
||||
|
||||
@ -175,7 +249,7 @@ private final class WebAppTermsAlertContentNode: AlertContentNode {
|
||||
let condensedSize = CGSize(width: size.width - 76.0, height: size.height)
|
||||
|
||||
let spacing: CGFloat = 12.0
|
||||
let acceptTermsSize = self.acceptTermsLabelNode.measure(condensedSize)
|
||||
let acceptTermsSize = self.acceptTermsLabelNode.updateLayout(condensedSize)
|
||||
let acceptTermsTotalWidth = checkSize.width + spacing + acceptTermsSize.width
|
||||
let acceptTermsOriginX = floorToScreenPixels((size.width - acceptTermsTotalWidth) / 2.0)
|
||||
|
||||
@ -183,6 +257,14 @@ private final class WebAppTermsAlertContentNode: AlertContentNode {
|
||||
transition.updateFrame(node: self.acceptTermsLabelNode, frame: CGRect(origin: CGPoint(x: acceptTermsOriginX + checkSize.width + spacing, y: origin.y), size: acceptTermsSize))
|
||||
origin.y += acceptTermsSize.height
|
||||
entriesHeight += acceptTermsSize.height
|
||||
origin.y += 21.0
|
||||
}
|
||||
|
||||
let additionalTextSize = self.additionalTextNode.updateLayout(CGSize(width: size.width - 48.0, height: size.height))
|
||||
transition.updateFrame(node: self.additionalTextNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - additionalTextSize.width) / 2.0), y: origin.y), size: additionalTextSize))
|
||||
origin.y += additionalTextSize.height
|
||||
if additionalTextSize.height > 0.0 {
|
||||
entriesHeight += 20.0
|
||||
}
|
||||
|
||||
let actionButtonHeight: CGFloat = 44.0
|
||||
@ -216,7 +298,7 @@ private final class WebAppTermsAlertContentNode: AlertContentNode {
|
||||
actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
|
||||
}
|
||||
|
||||
let resultSize = CGSize(width: contentWidth, height: titleSize.height + textSize.height + entriesHeight + actionsHeight + 3.0 + insets.top + insets.bottom)
|
||||
let resultSize = CGSize(width: contentWidth, height: titleSize.height + textSize.height + additionalTextSize.height + entriesHeight + actionsHeight + 3.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)))
|
||||
|
||||
@ -267,7 +349,12 @@ private final class WebAppTermsAlertContentNode: AlertContentNode {
|
||||
}
|
||||
}
|
||||
|
||||
public func webAppTermsAlertController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, peer: EnginePeer, completion: @escaping () -> Void) -> AlertController {
|
||||
public func webAppTermsAlertController(
|
||||
context: AccountContext,
|
||||
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?,
|
||||
bot: AttachMenuBot,
|
||||
completion: @escaping (Bool) -> Void
|
||||
) -> AlertController {
|
||||
let theme = defaultDarkColorPresentationTheme
|
||||
let presentationData: PresentationData
|
||||
if let updatedPresentationData {
|
||||
@ -278,18 +365,22 @@ public func webAppTermsAlertController(context: AccountContext, updatedPresentat
|
||||
let strings = presentationData.strings
|
||||
|
||||
var dismissImpl: ((Bool) -> Void)?
|
||||
let actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: "Continue", action: {
|
||||
completion()
|
||||
let actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: presentationData.strings.WebApp_DisclaimerContinue, action: {
|
||||
completion(true)
|
||||
dismissImpl?(true)
|
||||
}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
|
||||
dismissImpl?(true)
|
||||
})]
|
||||
|
||||
let title = "Warning"
|
||||
let text = "You are about to use a mini app operated by an independent party not affiliated with Telegram. You must agree to the Terms of Use of mini apps to continue."
|
||||
|
||||
let contentNode = WebAppTermsAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, title: title, text: text, actions: actions)
|
||||
let title = presentationData.strings.WebApp_DisclaimerTitle
|
||||
let text = presentationData.strings.WebApp_DisclaimerText
|
||||
let additionalText: String? = nil
|
||||
|
||||
let contentNode = WebAppTermsAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, title: title, text: text, additionalText: additionalText, actions: actions)
|
||||
contentNode.openTerms = {
|
||||
context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: presentationData.strings.WebApp_Disclaimer_URL, forceExternal: true, presentationData: context.sharedContext.currentPresentationData.with { $0 }, navigationController: nil, dismissInput: {
|
||||
})
|
||||
}
|
||||
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode)
|
||||
dismissImpl = { [weak controller] animated in
|
||||
if animated {
|
||||
|
Loading…
x
Reference in New Issue
Block a user