mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Merge commit '9426bd5060b18f9074f312104c6399518116145f'
This commit is contained in:
commit
6bad4bba96
@ -17,7 +17,7 @@ public enum AttachmentButtonType: Equatable {
|
|||||||
case location
|
case location
|
||||||
case contact
|
case contact
|
||||||
case poll
|
case poll
|
||||||
case app(PeerId, String, TelegramMediaFile)
|
case app(PeerId, String, [AttachMenuBots.Bot.IconName: TelegramMediaFile])
|
||||||
case standalone
|
case standalone
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,6 +63,11 @@ public protocol AttachmentMediaPickerContext {
|
|||||||
var selectionCount: Signal<Int, NoError> { get }
|
var selectionCount: Signal<Int, NoError> { get }
|
||||||
var caption: Signal<NSAttributedString?, NoError> { get }
|
var caption: Signal<NSAttributedString?, NoError> { get }
|
||||||
|
|
||||||
|
var loadingProgress: Signal<CGFloat?, NoError> { get }
|
||||||
|
var mainButtonState: Signal<AttachmentMainButtonState?, NoError> { get }
|
||||||
|
|
||||||
|
func mainButtonAction()
|
||||||
|
|
||||||
func setCaption(_ caption: NSAttributedString)
|
func setCaption(_ caption: NSAttributedString)
|
||||||
func send(silently: Bool, mode: AttachmentMediaPickerSendMode)
|
func send(silently: Bool, mode: AttachmentMediaPickerSendMode)
|
||||||
func schedule()
|
func schedule()
|
||||||
@ -139,6 +144,9 @@ public class AttachmentController: ViewController {
|
|||||||
private let captionDisposable = MetaDisposable()
|
private let captionDisposable = MetaDisposable()
|
||||||
private let mediaSelectionCountDisposable = MetaDisposable()
|
private let mediaSelectionCountDisposable = MetaDisposable()
|
||||||
|
|
||||||
|
private let loadingProgressDisposable = MetaDisposable()
|
||||||
|
private let mainButtonStateDisposable = MetaDisposable()
|
||||||
|
|
||||||
private var selectionCount: Int = 0
|
private var selectionCount: Int = 0
|
||||||
|
|
||||||
fileprivate var mediaPickerContext: AttachmentMediaPickerContext? {
|
fileprivate var mediaPickerContext: AttachmentMediaPickerContext? {
|
||||||
@ -156,9 +164,30 @@ public class AttachmentController: ViewController {
|
|||||||
strongSelf.updateSelectionCount(count)
|
strongSelf.updateSelectionCount(count)
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
self.loadingProgressDisposable.set((mediaPickerContext.loadingProgress
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] progress in
|
||||||
|
if let strongSelf = self {
|
||||||
|
strongSelf.panel.updateLoadingProgress(progress)
|
||||||
|
if let layout = strongSelf.validLayout {
|
||||||
|
print(progress ?? 0)
|
||||||
|
strongSelf.containerLayoutUpdated(layout, transition: .immediate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
self.mainButtonStateDisposable.set((mediaPickerContext.mainButtonState
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] mainButtonState in
|
||||||
|
if let strongSelf = self {
|
||||||
|
strongSelf.panel.updateMainButtonState(mainButtonState)
|
||||||
|
if let layout = strongSelf.validLayout {
|
||||||
|
strongSelf.containerLayoutUpdated(layout, transition: .animated(duration: 0.4, curve: .spring))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
} else {
|
} else {
|
||||||
self.updateSelectionCount(0)
|
self.updateSelectionCount(0)
|
||||||
self.mediaSelectionCountDisposable.set(nil)
|
self.mediaSelectionCountDisposable.set(nil)
|
||||||
|
self.loadingProgressDisposable.set(nil)
|
||||||
|
self.mainButtonStateDisposable.set(nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -254,6 +283,12 @@ public class AttachmentController: ViewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.panel.mainButtonPressed = { [weak self] in
|
||||||
|
if let strongSelf = self {
|
||||||
|
strongSelf.mediaPickerContext?.mainButtonAction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.panel.requestLayout = { [weak self] in
|
self.panel.requestLayout = { [weak self] in
|
||||||
if let strongSelf = self, let layout = strongSelf.validLayout {
|
if let strongSelf = self, let layout = strongSelf.validLayout {
|
||||||
strongSelf.containerLayoutUpdated(layout, transition: .animated(duration: 0.2, curve: .easeInOut))
|
strongSelf.containerLayoutUpdated(layout, transition: .animated(duration: 0.2, curve: .easeInOut))
|
||||||
@ -553,13 +588,28 @@ public class AttachmentController: ViewController {
|
|||||||
self.wrapperNode.view.mask = nil
|
self.wrapperNode.view.mask = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var containerInsets = containerLayout.intrinsicInsets
|
||||||
|
var hasPanel = false
|
||||||
|
let hasButton = self.panel.isButtonVisible
|
||||||
|
if let controller = self.controller, controller.buttons.count > 1 {
|
||||||
|
hasPanel = true
|
||||||
|
}
|
||||||
|
|
||||||
let isEffecitvelyCollapsedUpdated = (self.selectionCount > 0) != (self.panel.isSelecting)
|
let isEffecitvelyCollapsedUpdated = (self.selectionCount > 0) != (self.panel.isSelecting)
|
||||||
let panelHeight = self.panel.update(layout: containerLayout, buttons: self.controller?.buttons ?? [], isSelecting: self.selectionCount > 0, transition: transition)
|
let panelHeight = self.panel.update(layout: containerLayout, buttons: self.controller?.buttons ?? [], isSelecting: self.selectionCount > 0, transition: transition)
|
||||||
|
if hasPanel || hasButton {
|
||||||
|
containerInsets.bottom = panelHeight
|
||||||
|
}
|
||||||
|
|
||||||
var panelTransition = transition
|
var panelTransition = transition
|
||||||
if isEffecitvelyCollapsedUpdated {
|
if isEffecitvelyCollapsedUpdated {
|
||||||
panelTransition = .animated(duration: 0.25, curve: .easeInOut)
|
panelTransition = .animated(duration: 0.25, curve: .easeInOut)
|
||||||
}
|
}
|
||||||
panelTransition.updateFrame(node: self.panel, frame: CGRect(origin: CGPoint(x: 0.0, y: containerRect.height - panelHeight), size: CGSize(width: containerRect.width, height: panelHeight)))
|
var panelY = containerRect.height - panelHeight
|
||||||
|
if !hasPanel && !hasButton {
|
||||||
|
panelY = containerRect.height
|
||||||
|
}
|
||||||
|
panelTransition.updateFrame(node: self.panel, frame: CGRect(origin: CGPoint(x: 0.0, y: panelY), size: CGSize(width: containerRect.width, height: panelHeight)))
|
||||||
|
|
||||||
var shadowFrame = containerRect.insetBy(dx: -60.0, dy: -60.0)
|
var shadowFrame = containerRect.insetBy(dx: -60.0, dy: -60.0)
|
||||||
shadowFrame.size.height -= 12.0
|
shadowFrame.size.height -= 12.0
|
||||||
@ -579,22 +629,13 @@ public class AttachmentController: ViewController {
|
|||||||
let controllers = self.currentControllers
|
let controllers = self.currentControllers
|
||||||
containerTransition.updateFrame(node: self.container, frame: CGRect(origin: CGPoint(), size: containerRect.size))
|
containerTransition.updateFrame(node: self.container, frame: CGRect(origin: CGPoint(), size: containerRect.size))
|
||||||
|
|
||||||
var containerInsets = containerLayout.intrinsicInsets
|
|
||||||
var hasPanel = false
|
|
||||||
if let controller = self.controller, controller.buttons.count > 1 {
|
|
||||||
hasPanel = true
|
|
||||||
containerInsets.bottom = panelHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
let containerLayout = containerLayout.withUpdatedIntrinsicInsets(containerInsets)
|
let containerLayout = containerLayout.withUpdatedIntrinsicInsets(containerInsets)
|
||||||
|
|
||||||
self.container.update(layout: containerLayout, controllers: controllers, coveredByModalTransition: 0.0, transition: self.switchingController ? .immediate : transition)
|
self.container.update(layout: containerLayout, controllers: controllers, coveredByModalTransition: 0.0, transition: self.switchingController ? .immediate : transition)
|
||||||
|
|
||||||
if self.container.supernode == nil, !controllers.isEmpty && self.container.isReady {
|
if self.container.supernode == nil, !controllers.isEmpty && self.container.isReady {
|
||||||
self.wrapperNode.addSubnode(self.container)
|
self.wrapperNode.addSubnode(self.container)
|
||||||
if hasPanel {
|
|
||||||
self.container.addSubnode(self.panel)
|
self.container.addSubnode(self.panel)
|
||||||
}
|
|
||||||
|
|
||||||
self.animateIn()
|
self.animateIn()
|
||||||
}
|
}
|
||||||
|
@ -167,6 +167,7 @@ private final class AttachButtonComponent: CombinedComponent {
|
|||||||
let name: String
|
let name: String
|
||||||
let imageName: String
|
let imageName: String
|
||||||
var imageFile: TelegramMediaFile?
|
var imageFile: TelegramMediaFile?
|
||||||
|
var animationFile: TelegramMediaFile?
|
||||||
|
|
||||||
let component = context.component
|
let component = context.component
|
||||||
let strings = component.strings
|
let strings = component.strings
|
||||||
@ -187,17 +188,22 @@ private final class AttachButtonComponent: CombinedComponent {
|
|||||||
case .poll:
|
case .poll:
|
||||||
name = strings.Attachment_Poll
|
name = strings.Attachment_Poll
|
||||||
imageName = "Chat/Attach Menu/Poll"
|
imageName = "Chat/Attach Menu/Poll"
|
||||||
case let .app(_, appName, appIcon):
|
case let .app(_, appName, appIcons):
|
||||||
name = appName
|
name = appName
|
||||||
imageName = ""
|
imageName = ""
|
||||||
imageFile = appIcon
|
if let file = appIcons[.iOSAnimated] {
|
||||||
|
animationFile = file
|
||||||
|
} else if let file = appIcons[.iOSStatic] {
|
||||||
|
imageFile = file
|
||||||
|
} else if let file = appIcons[.default] {
|
||||||
|
imageFile = file
|
||||||
|
}
|
||||||
case .standalone:
|
case .standalone:
|
||||||
name = ""
|
name = ""
|
||||||
imageName = ""
|
imageName = ""
|
||||||
imageFile = nil
|
imageFile = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
let tintColor = component.isSelected ? component.theme.rootController.tabBar.selectedIconColor : component.theme.rootController.tabBar.iconColor
|
let tintColor = component.isSelected ? component.theme.rootController.tabBar.selectedIconColor : component.theme.rootController.tabBar.iconColor
|
||||||
|
|
||||||
let iconSize = CGSize(width: 30.0, height: 30.0)
|
let iconSize = CGSize(width: 30.0, height: 30.0)
|
||||||
@ -205,12 +211,13 @@ private final class AttachButtonComponent: CombinedComponent {
|
|||||||
let spacing: CGFloat = 15.0 + UIScreenPixel
|
let spacing: CGFloat = 15.0 + UIScreenPixel
|
||||||
|
|
||||||
let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((context.availableSize.width - iconSize.width) / 2.0), y: topInset), size: iconSize)
|
let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((context.availableSize.width - iconSize.width) / 2.0), y: topInset), size: iconSize)
|
||||||
if let imageFile = imageFile, (imageFile.fileName ?? "").lowercased().hasSuffix(".tgs") {
|
if let animationFile = animationFile {
|
||||||
let icon = animatedIcon.update(
|
let icon = animatedIcon.update(
|
||||||
component: AnimatedStickerComponent(
|
component: AnimatedStickerComponent(
|
||||||
account: component.context.account,
|
account: component.context.account,
|
||||||
animation: AnimatedStickerComponent.Animation(
|
animation: AnimatedStickerComponent.Animation(
|
||||||
source: .file(media: imageFile),
|
source: .file(media: animationFile),
|
||||||
|
scale: UIScreenScale,
|
||||||
loop: false,
|
loop: false,
|
||||||
tintColor: tintColor
|
tintColor: tintColor
|
||||||
),
|
),
|
||||||
@ -278,6 +285,151 @@ private final class AttachButtonComponent: CombinedComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final class LoadingProgressNode: ASDisplayNode {
|
||||||
|
var color: UIColor {
|
||||||
|
didSet {
|
||||||
|
self.foregroundNode.backgroundColor = self.color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private let foregroundNode: ASDisplayNode
|
||||||
|
|
||||||
|
init(color: UIColor) {
|
||||||
|
self.color = color
|
||||||
|
|
||||||
|
self.foregroundNode = ASDisplayNode()
|
||||||
|
self.foregroundNode.backgroundColor = color
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
self.addSubnode(self.foregroundNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var _progress: CGFloat = 0.0
|
||||||
|
func updateProgress(_ progress: CGFloat, animated: Bool = false) {
|
||||||
|
if self._progress == progress && animated {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var animated = animated
|
||||||
|
if (progress < self._progress && animated) {
|
||||||
|
animated = false
|
||||||
|
}
|
||||||
|
|
||||||
|
let size = self.bounds.size
|
||||||
|
|
||||||
|
self._progress = progress
|
||||||
|
|
||||||
|
let transition: ContainedViewLayoutTransition
|
||||||
|
if animated && progress > 0.0 {
|
||||||
|
transition = .animated(duration: 0.7, curve: .spring)
|
||||||
|
} else {
|
||||||
|
transition = .immediate
|
||||||
|
}
|
||||||
|
|
||||||
|
let alpaTransition: ContainedViewLayoutTransition
|
||||||
|
if animated {
|
||||||
|
alpaTransition = .animated(duration: 0.3, curve: .easeInOut)
|
||||||
|
} else {
|
||||||
|
alpaTransition = .immediate
|
||||||
|
}
|
||||||
|
|
||||||
|
transition.updateFrame(node: self.foregroundNode, frame: CGRect(x: -2.0, y: 0.0, width: (size.width + 4.0) * progress, height: size.height))
|
||||||
|
|
||||||
|
let alpha: CGFloat = progress < 0.001 || progress > 0.999 ? 0.0 : 1.0
|
||||||
|
alpaTransition.updateAlpha(node: self.foregroundNode, alpha: alpha)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layout() {
|
||||||
|
super.layout()
|
||||||
|
|
||||||
|
self.foregroundNode.cornerRadius = self.frame.height / 2.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct AttachmentMainButtonState {
|
||||||
|
let text: String?
|
||||||
|
let backgroundColor: UIColor
|
||||||
|
let textColor: UIColor
|
||||||
|
let isEnabled: Bool
|
||||||
|
let isVisible: Bool
|
||||||
|
|
||||||
|
public init(
|
||||||
|
text: String?,
|
||||||
|
backgroundColor: UIColor,
|
||||||
|
textColor: UIColor,
|
||||||
|
isEnabled: Bool,
|
||||||
|
isVisible: Bool
|
||||||
|
) {
|
||||||
|
self.text = text
|
||||||
|
self.backgroundColor = backgroundColor
|
||||||
|
self.textColor = textColor
|
||||||
|
self.isEnabled = isEnabled
|
||||||
|
self.isVisible = isVisible
|
||||||
|
}
|
||||||
|
|
||||||
|
static var initial: AttachmentMainButtonState {
|
||||||
|
return AttachmentMainButtonState(text: nil, backgroundColor: .clear, textColor: .clear, isEnabled: false, isVisible: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class MainButtonNode: HighlightTrackingButtonNode {
|
||||||
|
private var state: AttachmentMainButtonState
|
||||||
|
|
||||||
|
private let backgroundNode: ASDisplayNode
|
||||||
|
private let textNode: ImmediateTextNode
|
||||||
|
|
||||||
|
override init(pointerStyle: PointerStyle? = nil) {
|
||||||
|
self.state = AttachmentMainButtonState.initial
|
||||||
|
|
||||||
|
self.backgroundNode = ASDisplayNode()
|
||||||
|
self.backgroundNode.allowsGroupOpacity = true
|
||||||
|
self.backgroundNode.isUserInteractionEnabled = false
|
||||||
|
self.backgroundNode.cornerRadius = 12.0
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
self.backgroundNode.layer.cornerCurve = .continuous
|
||||||
|
}
|
||||||
|
|
||||||
|
self.textNode = ImmediateTextNode()
|
||||||
|
self.textNode.textAlignment = .center
|
||||||
|
|
||||||
|
super.init(pointerStyle: pointerStyle)
|
||||||
|
|
||||||
|
self.addSubnode(self.backgroundNode)
|
||||||
|
self.backgroundNode.addSubnode(self.textNode)
|
||||||
|
|
||||||
|
self.highligthedChanged = { [weak self] highlighted in
|
||||||
|
if let strongSelf = self, strongSelf.state.isEnabled {
|
||||||
|
if highlighted {
|
||||||
|
strongSelf.backgroundNode.layer.removeAnimation(forKey: "opacity")
|
||||||
|
strongSelf.backgroundNode.alpha = 0.65
|
||||||
|
} else {
|
||||||
|
strongSelf.backgroundNode.alpha = 1.0
|
||||||
|
strongSelf.backgroundNode.layer.animateAlpha(from: 0.65, to: 1.0, duration: 0.2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateLayout(size: CGSize, state: AttachmentMainButtonState, transition: ContainedViewLayoutTransition) {
|
||||||
|
self.state = state
|
||||||
|
|
||||||
|
self.isUserInteractionEnabled = state.isVisible
|
||||||
|
self.isEnabled = state.isEnabled
|
||||||
|
transition.updateAlpha(node: self, alpha: state.isEnabled ? 1.0 : 0.4)
|
||||||
|
|
||||||
|
if let text = state.text {
|
||||||
|
self.textNode.attributedText = NSAttributedString(string: text, font: Font.semibold(17.0), textColor: state.textColor)
|
||||||
|
|
||||||
|
let textSize = self.textNode.updateLayout(size)
|
||||||
|
self.textNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: floorToScreenPixels((size.height - textSize.height) / 2.0)), size: textSize)
|
||||||
|
|
||||||
|
self.backgroundNode.backgroundColor = state.backgroundColor
|
||||||
|
}
|
||||||
|
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
|
final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
|
||||||
private let context: AccountContext
|
private let context: AccountContext
|
||||||
private var presentationData: PresentationData
|
private var presentationData: PresentationData
|
||||||
@ -295,10 +447,16 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
private var buttonViews: [Int: ComponentHostView<Empty>] = [:]
|
private var buttonViews: [Int: ComponentHostView<Empty>] = [:]
|
||||||
|
|
||||||
private var textInputPanelNode: AttachmentTextInputPanelNode?
|
private var textInputPanelNode: AttachmentTextInputPanelNode?
|
||||||
|
private var progressNode: LoadingProgressNode?
|
||||||
|
private var mainButtonNode: MainButtonNode
|
||||||
|
|
||||||
|
private var loadingProgress: CGFloat?
|
||||||
|
private var mainButtonState: AttachmentMainButtonState = .initial
|
||||||
|
|
||||||
private var buttons: [AttachmentButtonType] = []
|
private var buttons: [AttachmentButtonType] = []
|
||||||
private var selectedIndex: Int = 0
|
private var selectedIndex: Int = 0
|
||||||
private(set) var isSelecting: Bool = false
|
private(set) var isSelecting: Bool = false
|
||||||
|
private(set) var isButtonVisible: Bool = false
|
||||||
|
|
||||||
private var validLayout: ContainerViewLayout?
|
private var validLayout: ContainerViewLayout?
|
||||||
private var scrollLayout: (width: CGFloat, contentSize: CGSize)?
|
private var scrollLayout: (width: CGFloat, contentSize: CGSize)?
|
||||||
@ -311,6 +469,8 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
var present: (ViewController) -> Void = { _ in }
|
var present: (ViewController) -> Void = { _ in }
|
||||||
var presentInGlobalOverlay: (ViewController) -> Void = { _ in }
|
var presentInGlobalOverlay: (ViewController) -> Void = { _ in }
|
||||||
|
|
||||||
|
var mainButtonPressed: () -> Void = { }
|
||||||
|
|
||||||
init(context: AccountContext, chatLocation: ChatLocation, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?) {
|
init(context: AccountContext, chatLocation: ChatLocation, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
|
self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
|
||||||
@ -326,6 +486,8 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
self.separatorNode = ASDisplayNode()
|
self.separatorNode = ASDisplayNode()
|
||||||
self.separatorNode.backgroundColor = self.presentationData.theme.rootController.tabBar.separatorColor
|
self.separatorNode.backgroundColor = self.presentationData.theme.rootController.tabBar.separatorColor
|
||||||
|
|
||||||
|
self.mainButtonNode = MainButtonNode()
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
self.addSubnode(self.containerNode)
|
self.addSubnode(self.containerNode)
|
||||||
@ -333,6 +495,10 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
self.containerNode.addSubnode(self.separatorNode)
|
self.containerNode.addSubnode(self.separatorNode)
|
||||||
self.containerNode.addSubnode(self.scrollNode)
|
self.containerNode.addSubnode(self.scrollNode)
|
||||||
|
|
||||||
|
self.addSubnode(self.mainButtonNode)
|
||||||
|
|
||||||
|
self.mainButtonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
|
||||||
|
|
||||||
self.interfaceInteraction = ChatPanelInterfaceInteraction(setupReplyMessage: { _, _ in
|
self.interfaceInteraction = ChatPanelInterfaceInteraction(setupReplyMessage: { _, _ in
|
||||||
}, setupEditMessage: { _, _ in
|
}, setupEditMessage: { _, _ in
|
||||||
}, beginMessageSelection: { _, _ in
|
}, beginMessageSelection: { _, _ in
|
||||||
@ -537,6 +703,10 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
self.scrollNode.view.showsVerticalScrollIndicator = false
|
self.scrollNode.view.showsVerticalScrollIndicator = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func buttonPressed() {
|
||||||
|
self.mainButtonPressed()
|
||||||
|
}
|
||||||
|
|
||||||
func updateBackgroundAlpha(_ alpha: CGFloat, transition: ContainedViewLayoutTransition) {
|
func updateBackgroundAlpha(_ alpha: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||||
transition.updateAlpha(node: self.separatorNode, alpha: alpha)
|
transition.updateAlpha(node: self.separatorNode, alpha: alpha)
|
||||||
transition.updateAlpha(node: self.backgroundNode, alpha: alpha)
|
transition.updateAlpha(node: self.backgroundNode, alpha: alpha)
|
||||||
@ -610,9 +780,13 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let type = self.buttons[i]
|
let type = self.buttons[i]
|
||||||
if case let .app(_, _, iconFile) = type {
|
if case let .app(_, _, iconFiles) = type {
|
||||||
if self.iconDisposables[iconFile.fileId] == nil {
|
for (name, file) in iconFiles {
|
||||||
self.iconDisposables[iconFile.fileId] = freeMediaFileInteractiveFetched(account: self.context.account, fileReference: .standalone(media: iconFile)).start()
|
if [.default, .iOSAnimated].contains(name) {
|
||||||
|
if self.iconDisposables[file.fileId] == nil {
|
||||||
|
self.iconDisposables[file.fileId] = freeMediaFileInteractiveFetched(account: self.context.account, fileReference: .standalone(media: file)).start()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let _ = buttonView.update(
|
let _ = buttonView.update(
|
||||||
@ -659,7 +833,7 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
}
|
}
|
||||||
self.scrollLayout = (layout.size.width, contentSize)
|
self.scrollLayout = (layout.size.width, contentSize)
|
||||||
|
|
||||||
transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(x: 0.0, y: self.isSelecting ? -buttonSize.height : 0.0), size: CGSize(width: layout.size.width, height: buttonSize.height)))
|
transition.updateFrameAsPositionAndBounds(node: self.scrollNode, frame: CGRect(origin: CGPoint(x: 0.0, y: self.isSelecting || self.isButtonVisible ? -buttonSize.height : 0.0), size: CGSize(width: layout.size.width, height: buttonSize.height)))
|
||||||
self.scrollNode.view.contentSize = contentSize
|
self.scrollNode.view.contentSize = contentSize
|
||||||
|
|
||||||
return true
|
return true
|
||||||
@ -697,10 +871,25 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateLoadingProgress(_ progress: CGFloat?) {
|
||||||
|
self.loadingProgress = progress
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateMainButtonState(_ mainButtonState: AttachmentMainButtonState?) {
|
||||||
|
var currentButtonState = self.mainButtonState
|
||||||
|
if mainButtonState == nil {
|
||||||
|
currentButtonState = AttachmentMainButtonState(text: currentButtonState.text, backgroundColor: currentButtonState.backgroundColor, textColor: currentButtonState.textColor, isEnabled: currentButtonState.isEnabled, isVisible: false)
|
||||||
|
}
|
||||||
|
self.mainButtonState = mainButtonState ?? currentButtonState
|
||||||
|
}
|
||||||
|
|
||||||
func update(layout: ContainerViewLayout, buttons: [AttachmentButtonType], isSelecting: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
|
func update(layout: ContainerViewLayout, buttons: [AttachmentButtonType], isSelecting: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
|
||||||
self.validLayout = layout
|
self.validLayout = layout
|
||||||
self.buttons = buttons
|
self.buttons = buttons
|
||||||
|
|
||||||
|
let isButtonVisibleUpdated = self.isButtonVisible != self.mainButtonState.isVisible
|
||||||
|
self.isButtonVisible = self.mainButtonState.isVisible
|
||||||
|
|
||||||
let isSelectingUpdated = self.isSelecting != isSelecting
|
let isSelectingUpdated = self.isSelecting != isSelecting
|
||||||
self.isSelecting = isSelecting
|
self.isSelecting = isSelecting
|
||||||
|
|
||||||
@ -713,6 +902,8 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
insets.bottom = layout.intrinsicInsets.bottom
|
insets.bottom = layout.intrinsicInsets.bottom
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let isButtonVisible = self.mainButtonState.isVisible
|
||||||
|
|
||||||
if isSelecting {
|
if isSelecting {
|
||||||
self.loadTextNodeIfNeeded()
|
self.loadTextNodeIfNeeded()
|
||||||
} else {
|
} else {
|
||||||
@ -742,18 +933,21 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
let bounds = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: buttonSize.height + insets.bottom))
|
let bounds = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: buttonSize.height + insets.bottom))
|
||||||
let containerTransition: ContainedViewLayoutTransition
|
let containerTransition: ContainedViewLayoutTransition
|
||||||
let containerFrame: CGRect
|
let containerFrame: CGRect
|
||||||
if isSelecting {
|
if isButtonVisible {
|
||||||
|
containerFrame = CGRect(origin: CGPoint(), size: CGSize(width: bounds.width, height: bounds.height + 9.0))
|
||||||
|
} else if isSelecting {
|
||||||
containerFrame = CGRect(origin: CGPoint(), size: CGSize(width: bounds.width, height: textPanelHeight + insets.bottom))
|
containerFrame = CGRect(origin: CGPoint(), size: CGSize(width: bounds.width, height: textPanelHeight + insets.bottom))
|
||||||
} else {
|
} else {
|
||||||
containerFrame = bounds
|
containerFrame = bounds
|
||||||
}
|
}
|
||||||
let containerBounds = CGRect(origin: CGPoint(), size: containerFrame.size)
|
let containerBounds = CGRect(origin: CGPoint(), size: containerFrame.size)
|
||||||
if isSelectingUpdated {
|
if isSelectingUpdated || isButtonVisibleUpdated {
|
||||||
containerTransition = .animated(duration: 0.25, curve: .easeInOut)
|
containerTransition = .animated(duration: 0.25, curve: .easeInOut)
|
||||||
} else {
|
} else {
|
||||||
containerTransition = transition
|
containerTransition = transition
|
||||||
}
|
}
|
||||||
containerTransition.updateAlpha(node: self.scrollNode, alpha: isSelecting ? 0.0 : 1.0)
|
containerTransition.updateAlpha(node: self.scrollNode, alpha: isSelecting || isButtonVisible ? 0.0 : 1.0)
|
||||||
|
containerTransition.updateTransformScale(node: self.scrollNode, scale: isSelecting || isButtonVisible ? 0.8 : 1.0)
|
||||||
|
|
||||||
if isSelectingUpdated {
|
if isSelectingUpdated {
|
||||||
if isSelecting {
|
if isSelecting {
|
||||||
@ -772,16 +966,40 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
containerTransition.updateFrame(node: self.containerNode, frame: containerFrame)
|
containerTransition.updateFrame(node: self.containerNode, frame: containerFrame)
|
||||||
containerTransition.updateFrame(node: self.backgroundNode, frame: containerBounds)
|
containerTransition.updateFrame(node: self.backgroundNode, frame: containerBounds)
|
||||||
self.backgroundNode.update(size: containerBounds.size, transition: transition)
|
self.backgroundNode.update(size: containerBounds.size, transition: transition)
|
||||||
containerTransition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: bounds.width, height: UIScreenPixel)))
|
containerTransition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: bounds.width, height: UIScreenPixel)))
|
||||||
|
|
||||||
let _ = self.updateScrollLayoutIfNeeded(force: isSelectingUpdated, transition: containerTransition)
|
let _ = self.updateScrollLayoutIfNeeded(force: isSelectingUpdated || isButtonVisibleUpdated, transition: containerTransition)
|
||||||
|
|
||||||
self.updateViews(transition: .immediate)
|
self.updateViews(transition: .immediate)
|
||||||
|
|
||||||
|
if let progress = self.loadingProgress {
|
||||||
|
let loadingProgressNode: LoadingProgressNode
|
||||||
|
if let current = self.progressNode {
|
||||||
|
loadingProgressNode = current
|
||||||
|
} else {
|
||||||
|
loadingProgressNode = LoadingProgressNode(color: self.presentationData.theme.rootController.tabBar.selectedIconColor)
|
||||||
|
self.addSubnode(loadingProgressNode)
|
||||||
|
self.progressNode = loadingProgressNode
|
||||||
|
}
|
||||||
|
let loadingProgressHeight: CGFloat = 2.0
|
||||||
|
transition.updateFrame(node: loadingProgressNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: loadingProgressHeight)))
|
||||||
|
|
||||||
|
loadingProgressNode.updateProgress(progress, animated: true)
|
||||||
|
} else if let progressNode = self.progressNode {
|
||||||
|
self.progressNode = nil
|
||||||
|
progressNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak progressNode] _ in
|
||||||
|
progressNode?.removeFromSupernode()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let sideInset: CGFloat = 16.0
|
||||||
|
let buttonSize = CGSize(width: layout.size.width - (sideInset + safeAreaInsets.left) * 2.0, height: 50.0)
|
||||||
|
self.mainButtonNode.updateLayout(size: buttonSize, state: self.mainButtonState, transition: transition)
|
||||||
|
transition.updateFrame(node: self.mainButtonNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + sideInset, y: isButtonVisible ? 8.0 : containerFrame.height), size: buttonSize))
|
||||||
|
|
||||||
return containerFrame.height
|
return containerFrame.height
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,11 +14,13 @@ public final class AnimatedStickerComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public var source: Source
|
public var source: Source
|
||||||
|
public var scale: CGFloat
|
||||||
public var loop: Bool
|
public var loop: Bool
|
||||||
public var tintColor: UIColor?
|
public var tintColor: UIColor?
|
||||||
|
|
||||||
public init(source: Source, loop: Bool, tintColor: UIColor? = nil) {
|
public init(source: Source, scale: CGFloat = 2.0, loop: Bool, tintColor: UIColor? = nil) {
|
||||||
self.source = source
|
self.source = source
|
||||||
|
self.scale = scale
|
||||||
self.loop = loop
|
self.loop = loop
|
||||||
self.tintColor = tintColor
|
self.tintColor = tintColor
|
||||||
}
|
}
|
||||||
@ -106,7 +108,7 @@ public final class AnimatedStickerComponent: Component {
|
|||||||
} else if component.isAnimating {
|
} else if component.isAnimating {
|
||||||
playbackMode = .once
|
playbackMode = .once
|
||||||
}
|
}
|
||||||
animationNode.setup(source: source, width: Int(component.size.width * 2.0), height: Int(component.size.height * 2.0), playbackMode: playbackMode, mode: .direct(cachePathPrefix: nil))
|
animationNode.setup(source: source, width: Int(component.size.width * component.animation.scale), height: Int(component.size.height * component.animation.scale), playbackMode: playbackMode, mode: .direct(cachePathPrefix: nil))
|
||||||
animationNode.visibility = self.isInHierarchy
|
animationNode.visibility = self.isInHierarchy
|
||||||
|
|
||||||
self.animationNode = animationNode
|
self.animationNode = animationNode
|
||||||
|
@ -96,6 +96,14 @@ public class LegacyAssetPickerContext: AttachmentMediaPickerContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var loadingProgress: Signal<CGFloat?, NoError> {
|
||||||
|
return .single(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
public var mainButtonState: Signal<AttachmentMainButtonState?, NoError> {
|
||||||
|
return .single(nil)
|
||||||
|
}
|
||||||
|
|
||||||
public init(controller: TGMediaAssetsController) {
|
public init(controller: TGMediaAssetsController) {
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
}
|
}
|
||||||
@ -111,6 +119,10 @@ public class LegacyAssetPickerContext: AttachmentMediaPickerContext {
|
|||||||
public func schedule() {
|
public func schedule() {
|
||||||
self.controller?.schedule(false)
|
self.controller?.schedule(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func mainButtonAction() {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func legacyAssetPicker(context: AccountContext, presentationData: PresentationData, editingMedia: Bool, fileMode: Bool, peer: Peer?, saveEditedPhotos: Bool, allowGrouping: Bool, selectionLimit: Int) -> Signal<(LegacyComponentsContext) -> TGMediaAssetsController, Void> {
|
public func legacyAssetPicker(context: AccountContext, presentationData: PresentationData, editingMedia: Bool, fileMode: Bool, peer: Peer?, saveEditedPhotos: Bool, allowGrouping: Bool, selectionLimit: Int) -> Signal<(LegacyComponentsContext) -> TGMediaAssetsController, Void> {
|
||||||
|
@ -1473,6 +1473,14 @@ final class MediaPickerContext: AttachmentMediaPickerContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var loadingProgress: Signal<CGFloat?, NoError> {
|
||||||
|
return .single(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
public var mainButtonState: Signal<AttachmentMainButtonState?, NoError> {
|
||||||
|
return .single(nil)
|
||||||
|
}
|
||||||
|
|
||||||
init(interaction: MediaPickerInteraction) {
|
init(interaction: MediaPickerInteraction) {
|
||||||
self.interaction = interaction
|
self.interaction = interaction
|
||||||
}
|
}
|
||||||
@ -1488,6 +1496,10 @@ final class MediaPickerContext: AttachmentMediaPickerContext {
|
|||||||
func schedule() {
|
func schedule() {
|
||||||
self.interaction?.schedule()
|
self.interaction?.schedule()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mainButtonAction() {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class MediaPickerContextReferenceContentSource: ContextReferenceContentSource {
|
private final class MediaPickerContextReferenceContentSource: ContextReferenceContentSource {
|
||||||
|
@ -619,8 +619,12 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if let adminPeer = adminView.peers[adminView.peerId] as? TelegramUser, adminPeer.botInfo != nil, case .group = channel.info, invite, let channelPeer = channelView.peers[channelView.peerId], canEditAdminRights(accountPeerId: accountPeerId, channelPeer: channelPeer, initialParticipant: initialParticipant) {
|
if let adminPeer = adminView.peers[adminView.peerId] as? TelegramUser, adminPeer.botInfo != nil, case .group = channel.info, invite, let channelPeer = channelView.peers[channelView.peerId], canEditAdminRights(accountPeerId: accountPeerId, channelPeer: channelPeer, initialParticipant: initialParticipant) {
|
||||||
|
if let initialParticipant = initialParticipant, case let .member(_, _, adminInfo, _, _) = initialParticipant, adminInfo != nil {
|
||||||
|
|
||||||
|
} else {
|
||||||
entries.append(.adminRights(presentationData.theme, presentationData.strings.Bot_AddToChat_Add_AdminRights, state.adminRights))
|
entries.append(.adminRights(presentationData.theme, presentationData.strings.Bot_AddToChat_Add_AdminRights, state.adminRights))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !invite || state.adminRights {
|
if !invite || state.adminRights {
|
||||||
entries.append(.rightsTitle(presentationData.theme, presentationData.strings.Channel_EditAdmin_PermissionsHeader))
|
entries.append(.rightsTitle(presentationData.theme, presentationData.strings.Channel_EditAdmin_PermissionsHeader))
|
||||||
@ -686,6 +690,7 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !invite || state.adminRights {
|
||||||
if canTransfer {
|
if canTransfer {
|
||||||
entries.append(.transfer(presentationData.theme, isGroup ? presentationData.strings.Group_EditAdmin_TransferOwnership : presentationData.strings.Channel_EditAdmin_TransferOwnership))
|
entries.append(.transfer(presentationData.theme, isGroup ? presentationData.strings.Group_EditAdmin_TransferOwnership : presentationData.strings.Channel_EditAdmin_TransferOwnership))
|
||||||
}
|
}
|
||||||
@ -707,6 +712,7 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s
|
|||||||
entries.append(.rank(presentationData.theme, presentationData.strings, isCreator ? presentationData.strings.Group_EditAdmin_RankOwnerPlaceholder : presentationData.strings.Group_EditAdmin_RankAdminPlaceholder, currentRank ?? "", rankEnabled))
|
entries.append(.rank(presentationData.theme, presentationData.strings, isCreator ? presentationData.strings.Group_EditAdmin_RankOwnerPlaceholder : presentationData.strings.Group_EditAdmin_RankAdminPlaceholder, currentRank ?? "", rankEnabled))
|
||||||
entries.append(.rankInfo(presentationData.theme, presentationData.strings.Group_EditAdmin_RankInfo(placeholder).string, invite))
|
entries.append(.rankInfo(presentationData.theme, presentationData.strings.Group_EditAdmin_RankInfo(placeholder).string, invite))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if canDismiss {
|
if canDismiss {
|
||||||
entries.append(.dismiss(presentationData.theme, presentationData.strings.Channel_Moderator_AccessLevelRevoke))
|
entries.append(.dismiss(presentationData.theme, presentationData.strings.Channel_Moderator_AccessLevelRevoke))
|
||||||
@ -733,8 +739,12 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s
|
|||||||
entries.append(.rank(presentationData.theme, presentationData.strings, isCreator ? presentationData.strings.Group_EditAdmin_RankOwnerPlaceholder : presentationData.strings.Group_EditAdmin_RankAdminPlaceholder, currentRank ?? "", rankEnabled))
|
entries.append(.rank(presentationData.theme, presentationData.strings, isCreator ? presentationData.strings.Group_EditAdmin_RankOwnerPlaceholder : presentationData.strings.Group_EditAdmin_RankAdminPlaceholder, currentRank ?? "", rankEnabled))
|
||||||
} else {
|
} else {
|
||||||
if let adminPeer = adminView.peers[adminView.peerId] as? TelegramUser, adminPeer.botInfo != nil, invite {
|
if let adminPeer = adminView.peers[adminView.peerId] as? TelegramUser, adminPeer.botInfo != nil, invite {
|
||||||
|
if let initialParticipant = initialParticipant, case let .member(_, _, adminRights, _, _) = initialParticipant, adminRights != nil {
|
||||||
|
|
||||||
|
} else {
|
||||||
entries.append(.adminRights(presentationData.theme, presentationData.strings.Bot_AddToChat_Add_AdminRights, state.adminRights))
|
entries.append(.adminRights(presentationData.theme, presentationData.strings.Bot_AddToChat_Add_AdminRights, state.adminRights))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !invite || state.adminRights {
|
if !invite || state.adminRights {
|
||||||
entries.append(.rightsTitle(presentationData.theme, presentationData.strings.Channel_EditAdmin_PermissionsHeader))
|
entries.append(.rightsTitle(presentationData.theme, presentationData.strings.Channel_EditAdmin_PermissionsHeader))
|
||||||
|
@ -131,7 +131,7 @@ private func filterMessageAttributesForOutgoingMessage(_ attributes: [MessageAtt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func filterMessageAttributesForForwardedMessage(_ attributes: [MessageAttribute]) -> [MessageAttribute] {
|
private func filterMessageAttributesForForwardedMessage(_ attributes: [MessageAttribute], forwardedMessageIds: [MessageId]? = nil) -> [MessageAttribute] {
|
||||||
return attributes.filter { attribute in
|
return attributes.filter { attribute in
|
||||||
switch attribute {
|
switch attribute {
|
||||||
case _ as TextEntitiesMessageAttribute:
|
case _ as TextEntitiesMessageAttribute:
|
||||||
@ -146,8 +146,12 @@ private func filterMessageAttributesForForwardedMessage(_ attributes: [MessageAt
|
|||||||
return true
|
return true
|
||||||
case _ as SendAsMessageAttribute:
|
case _ as SendAsMessageAttribute:
|
||||||
return true
|
return true
|
||||||
case _ as ReplyMessageAttribute:
|
case let attribute as ReplyMessageAttribute:
|
||||||
return true
|
if let forwardedMessageIds = forwardedMessageIds {
|
||||||
|
return forwardedMessageIds.contains(attribute.messageId)
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -281,6 +285,13 @@ public func resendMessages(account: Account, messageIds: [MessageId]) -> Signal<
|
|||||||
}
|
}
|
||||||
|
|
||||||
func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, messages: [(Bool, EnqueueMessage)], disableAutoremove: Bool = false, transformGroupingKeysWithPeerId: Bool = false) -> [MessageId?] {
|
func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, messages: [(Bool, EnqueueMessage)], disableAutoremove: Bool = false, transformGroupingKeysWithPeerId: Bool = false) -> [MessageId?] {
|
||||||
|
var forwardedMessageIds: [MessageId] = []
|
||||||
|
for (_, message) in messages {
|
||||||
|
if case let .forward(sourceId, _, _, _) = message {
|
||||||
|
forwardedMessageIds.append(sourceId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var updatedMessages: [(Bool, EnqueueMessage)] = []
|
var updatedMessages: [(Bool, EnqueueMessage)] = []
|
||||||
outer: for (transformedMedia, message) in messages {
|
outer: for (transformedMedia, message) in messages {
|
||||||
var updatedMessage = message
|
var updatedMessage = message
|
||||||
@ -612,7 +623,7 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId,
|
|||||||
}
|
}
|
||||||
|
|
||||||
attributes.append(contentsOf: filterMessageAttributesForForwardedMessage(requestedAttributes))
|
attributes.append(contentsOf: filterMessageAttributesForForwardedMessage(requestedAttributes))
|
||||||
attributes.append(contentsOf: filterMessageAttributesForForwardedMessage(sourceMessage.attributes))
|
attributes.append(contentsOf: filterMessageAttributesForForwardedMessage(sourceMessage.attributes, forwardedMessageIds: forwardedMessageIds))
|
||||||
|
|
||||||
var sourceReplyMarkup: ReplyMarkupMessageAttribute? = nil
|
var sourceReplyMarkup: ReplyMarkupMessageAttribute? = nil
|
||||||
var sourceSentViaBot = false
|
var sourceSentViaBot = false
|
||||||
|
@ -2940,7 +2940,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
translationSettings = TranslationSettings.defaultSettings
|
translationSettings = TranslationSettings.defaultSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
let (_, language) = canTranslateText(context: context, text: text.string, showTranslate: translationSettings.showTranslate, ignoredLanguages: translationSettings.ignoredLanguages)
|
var showTranslateIfTopical = false
|
||||||
|
if let peer = strongSelf.presentationInterfaceState.renderedPeer?.chatMainPeer as? TelegramChannel, case .broadcast = peer.info, !(peer.addressName ?? "").isEmpty {
|
||||||
|
showTranslateIfTopical = true
|
||||||
|
}
|
||||||
|
|
||||||
|
let (_, language) = canTranslateText(context: context, text: text.string, showTranslate: translationSettings.showTranslate, showTranslateIfTopical: showTranslateIfTopical, ignoredLanguages: translationSettings.ignoredLanguages)
|
||||||
|
|
||||||
let controller = TranslateScreen(context: context, text: text.string, fromLanguage: language)
|
let controller = TranslateScreen(context: context, text: text.string, fromLanguage: language)
|
||||||
controller.pushController = { [weak self] c in
|
controller.pushController = { [weak self] c in
|
||||||
@ -3304,8 +3309,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let botName = EnginePeer(peer).displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)
|
strongSelf.chatDisplayNode.dismissInput()
|
||||||
|
|
||||||
|
let botName = EnginePeer(peer).displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)
|
||||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||||||
return $0.updatedTitlePanelContext {
|
return $0.updatedTitlePanelContext {
|
||||||
if !$0.contains(where: {
|
if !$0.contains(where: {
|
||||||
@ -3359,7 +3365,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peerId: peerId, botId: peerId, botName: botName, url: url, queryId: nil, buttonText: buttonText, keepAliveSignal: nil)
|
let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peerId: peerId, botId: peerId, botName: botName, url: url, queryId: nil, buttonText: buttonText, keepAliveSignal: nil, openUrl: { [weak self] url in
|
||||||
|
self?.openUrl(url, concealed: true)
|
||||||
|
})
|
||||||
strongSelf.present(controller, in: .window(.root))
|
strongSelf.present(controller, in: .window(.root))
|
||||||
// controller.getNavigationController = { [weak self] in
|
// controller.getNavigationController = { [weak self] in
|
||||||
// return self?.effectiveNavigationController
|
// return self?.effectiveNavigationController
|
||||||
@ -3381,7 +3389,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peerId: peerId, botId: peerId, botName: botName, url: result.url, queryId: result.queryId, buttonText: buttonText, keepAliveSignal: result.keepAliveSignal, completion: { [weak self] in
|
let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peerId: peerId, botId: peerId, botName: botName, url: result.url, queryId: result.queryId, buttonText: buttonText, keepAliveSignal: result.keepAliveSignal, openUrl: { [weak self] url in
|
||||||
|
self?.openUrl(url, concealed: true)
|
||||||
|
}, completion: { [weak self] in
|
||||||
self?.chatDisplayNode.historyNode.scrollToEndOfHistory()
|
self?.chatDisplayNode.historyNode.scrollToEndOfHistory()
|
||||||
})
|
})
|
||||||
strongSelf.present(controller, in: .window(.root))
|
strongSelf.present(controller, in: .window(.root))
|
||||||
@ -10576,25 +10586,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
initialButton = .gallery
|
initialButton = .gallery
|
||||||
}
|
}
|
||||||
for bot in attachMenuBots.reversed() {
|
for bot in attachMenuBots.reversed() {
|
||||||
let iconFile: TelegramMediaFile?
|
let button: AttachmentButtonType = .app(bot.peer.id, bot.shortName, bot.icons)
|
||||||
if let file = bot.icons[.iOSAnimated] {
|
|
||||||
iconFile = file
|
|
||||||
} else if let file = bot.icons[.iOSStatic] {
|
|
||||||
iconFile = file
|
|
||||||
} else if let file = bot.icons[.default] {
|
|
||||||
iconFile = file
|
|
||||||
} else {
|
|
||||||
iconFile = nil
|
|
||||||
}
|
|
||||||
if let iconFile = iconFile {
|
|
||||||
let button: AttachmentButtonType = .app(bot.peer.id, bot.shortName, iconFile)
|
|
||||||
buttons.insert(button, at: 1)
|
buttons.insert(button, at: 1)
|
||||||
|
|
||||||
if initialButton == nil && bot.peer.id == botId {
|
if initialButton == nil && bot.peer.id == botId {
|
||||||
initialButton = button
|
initialButton = button
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return (buttons, initialButton)
|
return (buttons, initialButton)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -10893,9 +10891,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
let controller = strongSelf.configurePollCreation()
|
let controller = strongSelf.configurePollCreation()
|
||||||
completion(controller, nil)
|
completion(controller, nil)
|
||||||
strongSelf.controllerNavigationDisposable.set(nil)
|
strongSelf.controllerNavigationDisposable.set(nil)
|
||||||
case let .app(botId, botName, botIcon):
|
case let .app(botId, botName, botIcons):
|
||||||
let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId
|
let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId
|
||||||
let controller = WebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peerId: peer.id, botId: botId, botName: botName, url: nil, queryId: nil, buttonText: nil, keepAliveSignal: nil, replyToMessageId: replyMessageId, iconFile: botIcon)
|
let controller = WebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peerId: peer.id, botId: botId, botName: botName, url: nil, queryId: nil, buttonText: nil, keepAliveSignal: nil, replyToMessageId: replyMessageId, iconFile: botIcons[.default])
|
||||||
|
controller.openUrl = { [weak self] url in
|
||||||
|
self?.openUrl(url, concealed: true)
|
||||||
|
}
|
||||||
controller.getNavigationController = { [weak self] in
|
controller.getNavigationController = { [weak self] in
|
||||||
return self?.effectiveNavigationController
|
return self?.effectiveNavigationController
|
||||||
}
|
}
|
||||||
@ -10907,7 +10908,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory()
|
strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
completion(controller, nil)
|
completion(controller, controller.mediaPickerContext)
|
||||||
strongSelf.controllerNavigationDisposable.set(nil)
|
strongSelf.controllerNavigationDisposable.set(nil)
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
|
@ -857,7 +857,12 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
|||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
let (canTranslate, _) = canTranslateText(context: context, text: messageText, showTranslate: translationSettings.showTranslate, ignoredLanguages: translationSettings.ignoredLanguages)
|
var showTranslateIfTopical = false
|
||||||
|
if let peer = chatPresentationInterfaceState.renderedPeer?.chatMainPeer as? TelegramChannel, case .broadcast = peer.info, !(peer.addressName ?? "").isEmpty {
|
||||||
|
showTranslateIfTopical = true
|
||||||
|
}
|
||||||
|
|
||||||
|
let (canTranslate, _) = canTranslateText(context: context, text: messageText, showTranslate: translationSettings.showTranslate, showTranslateIfTopical: showTranslateIfTopical, ignoredLanguages: translationSettings.ignoredLanguages)
|
||||||
if canTranslate {
|
if canTranslate {
|
||||||
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuTranslate, icon: { theme in
|
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuTranslate, icon: { theme in
|
||||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Translate"), color: theme.actionSheet.primaryTextColor)
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Translate"), color: theme.actionSheet.primaryTextColor)
|
||||||
|
@ -427,6 +427,14 @@ final class ContactsPickerContext: AttachmentMediaPickerContext {
|
|||||||
return .single(nil)
|
return .single(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var loadingProgress: Signal<CGFloat?, NoError> {
|
||||||
|
return .single(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
public var mainButtonState: Signal<AttachmentMainButtonState?, NoError> {
|
||||||
|
return .single(nil)
|
||||||
|
}
|
||||||
|
|
||||||
init(controller: ContactSelectionControllerImpl) {
|
init(controller: ContactSelectionControllerImpl) {
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
}
|
}
|
||||||
@ -444,4 +452,7 @@ final class ContactsPickerContext: AttachmentMediaPickerContext {
|
|||||||
self.controller?.contactsNode.requestMultipleAction?(false, time)
|
self.controller?.contactsNode.requestMultipleAction?(false, time)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mainButtonAction() {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -141,8 +141,8 @@ public var popularTranslationLanguages = [
|
|||||||
@available(iOS 12.0, *)
|
@available(iOS 12.0, *)
|
||||||
private let languageRecognizer = NLLanguageRecognizer()
|
private let languageRecognizer = NLLanguageRecognizer()
|
||||||
|
|
||||||
public func canTranslateText(context: AccountContext, text: String, showTranslate: Bool, ignoredLanguages: [String]?) -> (canTranslate: Bool, language: String?) {
|
public func canTranslateText(context: AccountContext, text: String, showTranslate: Bool, showTranslateIfTopical: Bool = false, ignoredLanguages: [String]?) -> (canTranslate: Bool, language: String?) {
|
||||||
guard showTranslate, text.count > 0 else {
|
guard showTranslate || showTranslateIfTopical, text.count > 0 else {
|
||||||
return (false, nil)
|
return (false, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,6 +159,11 @@ public func canTranslateText(context: AccountContext, text: String, showTranslat
|
|||||||
let hypotheses = languageRecognizer.languageHypotheses(withMaximum: 3)
|
let hypotheses = languageRecognizer.languageHypotheses(withMaximum: 3)
|
||||||
languageRecognizer.reset()
|
languageRecognizer.reset()
|
||||||
|
|
||||||
|
var supportedTranslationLanguages = supportedTranslationLanguages
|
||||||
|
if !showTranslate && showTranslateIfTopical {
|
||||||
|
supportedTranslationLanguages = ["uk", "ru"]
|
||||||
|
}
|
||||||
|
|
||||||
let filteredLanguages = hypotheses.filter { supportedTranslationLanguages.contains($0.key.rawValue) }.sorted(by: { $0.value > $1.value })
|
let filteredLanguages = hypotheses.filter { supportedTranslationLanguages.contains($0.key.rawValue) }.sorted(by: { $0.value > $1.value })
|
||||||
if let language = filteredLanguages.first(where: { supportedTranslationLanguages.contains($0.key.rawValue) }) {
|
if let language = filteredLanguages.first(where: { supportedTranslationLanguages.contains($0.key.rawValue) }) {
|
||||||
return (!dontTranslateLanguages.contains(language.key.rawValue), language.key.rawValue)
|
return (!dontTranslateLanguages.contains(language.key.rawValue), language.key.rawValue)
|
||||||
|
@ -606,6 +606,19 @@ public func isTelegramMeLink(_ url: String) -> Bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func isTelegraPhLink(_ url: String) -> Bool {
|
||||||
|
let schemes = ["http://", "https://", ""]
|
||||||
|
for basePath in baseTelegramMePaths {
|
||||||
|
for scheme in schemes {
|
||||||
|
let basePrefix = scheme + basePath + "/"
|
||||||
|
if url.lowercased().hasPrefix(basePrefix) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
public func parseProxyUrl(_ url: String) -> (host: String, port: Int32, username: String?, password: String?, secret: Data?)? {
|
public func parseProxyUrl(_ url: String) -> (host: String, port: Int32, username: String?, password: String?, secret: Data?)? {
|
||||||
let schemes = ["http://", "https://", ""]
|
let schemes = ["http://", "https://", ""]
|
||||||
for basePath in baseTelegramMePaths {
|
for basePath in baseTelegramMePaths {
|
||||||
|
@ -593,6 +593,14 @@ public class WebSearchPickerContext: AttachmentMediaPickerContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var loadingProgress: Signal<CGFloat?, NoError> {
|
||||||
|
return .single(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
public var mainButtonState: Signal<AttachmentMainButtonState?, NoError> {
|
||||||
|
return .single(nil)
|
||||||
|
}
|
||||||
|
|
||||||
init(interaction: WebSearchControllerInteraction) {
|
init(interaction: WebSearchControllerInteraction) {
|
||||||
self.interaction = interaction
|
self.interaction = interaction
|
||||||
}
|
}
|
||||||
@ -608,4 +616,8 @@ public class WebSearchPickerContext: AttachmentMediaPickerContext {
|
|||||||
public func schedule() {
|
public func schedule() {
|
||||||
self.interaction?.schedule()
|
self.interaction?.schedule()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func mainButtonAction() {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,7 @@ swift_library(
|
|||||||
"//submodules/PhotoResources:PhotoResources",
|
"//submodules/PhotoResources:PhotoResources",
|
||||||
"//submodules/ShimmerEffect:ShimmerEffect",
|
"//submodules/ShimmerEffect:ShimmerEffect",
|
||||||
"//submodules/LegacyComponents:LegacyComponents",
|
"//submodules/LegacyComponents:LegacyComponents",
|
||||||
|
"//submodules/UrlHandling:UrlHandling",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
@ -16,20 +16,7 @@ import HexColor
|
|||||||
import ShimmerEffect
|
import ShimmerEffect
|
||||||
import PhotoResources
|
import PhotoResources
|
||||||
import LegacyComponents
|
import LegacyComponents
|
||||||
|
import UrlHandling
|
||||||
private class WeakGameScriptMessageHandler: NSObject, WKScriptMessageHandler {
|
|
||||||
private let f: (WKScriptMessage) -> ()
|
|
||||||
|
|
||||||
init(_ f: @escaping (WKScriptMessage) -> ()) {
|
|
||||||
self.f = f
|
|
||||||
|
|
||||||
super.init()
|
|
||||||
}
|
|
||||||
|
|
||||||
func userContentController(_ controller: WKUserContentController, didReceive scriptMessage: WKScriptMessage) {
|
|
||||||
self.f(scriptMessage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func generateWebAppThemeParams(_ presentationTheme: PresentationTheme) -> [String: Any] {
|
public func generateWebAppThemeParams(_ presentationTheme: PresentationTheme) -> [String: Any] {
|
||||||
var backgroundColor = presentationTheme.list.plainBackgroundColor.rgb
|
var backgroundColor = presentationTheme.list.plainBackgroundColor.rgb
|
||||||
@ -46,68 +33,6 @@ public func generateWebAppThemeParams(_ presentationTheme: PresentationTheme) ->
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class LoadingProgressNode: ASDisplayNode {
|
|
||||||
var color: UIColor {
|
|
||||||
didSet {
|
|
||||||
self.foregroundNode.backgroundColor = self.color
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private let foregroundNode: ASDisplayNode
|
|
||||||
|
|
||||||
init(color: UIColor) {
|
|
||||||
self.color = color
|
|
||||||
|
|
||||||
self.foregroundNode = ASDisplayNode()
|
|
||||||
self.foregroundNode.backgroundColor = color
|
|
||||||
|
|
||||||
super.init()
|
|
||||||
|
|
||||||
self.addSubnode(self.foregroundNode)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var _progress: CGFloat = 0.0
|
|
||||||
func updateProgress(_ progress: CGFloat, animated: Bool = false) {
|
|
||||||
if self._progress == progress && animated {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var animated = animated
|
|
||||||
if (progress < self._progress && animated) {
|
|
||||||
animated = false
|
|
||||||
}
|
|
||||||
|
|
||||||
let size = self.bounds.size
|
|
||||||
|
|
||||||
self._progress = progress
|
|
||||||
|
|
||||||
let transition: ContainedViewLayoutTransition
|
|
||||||
if animated && progress > 0.0 {
|
|
||||||
transition = .animated(duration: 0.7, curve: .spring)
|
|
||||||
} else {
|
|
||||||
transition = .immediate
|
|
||||||
}
|
|
||||||
|
|
||||||
let alpaTransition: ContainedViewLayoutTransition
|
|
||||||
if animated {
|
|
||||||
alpaTransition = .animated(duration: 0.3, curve: .easeInOut)
|
|
||||||
} else {
|
|
||||||
alpaTransition = .immediate
|
|
||||||
}
|
|
||||||
|
|
||||||
transition.updateFrame(node: self.foregroundNode, frame: CGRect(x: -2.0, y: 0.0, width: (size.width + 4.0) * progress, height: size.height))
|
|
||||||
|
|
||||||
let alpha: CGFloat = progress < 0.001 || progress > 0.999 ? 0.0 : 1.0
|
|
||||||
alpaTransition.updateAlpha(node: self.foregroundNode, alpha: alpha)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func layout() {
|
|
||||||
super.layout()
|
|
||||||
|
|
||||||
self.foregroundNode.cornerRadius = self.frame.height / 2.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public final class WebAppController: ViewController, AttachmentContainable {
|
public final class WebAppController: ViewController, AttachmentContainable {
|
||||||
public var requestAttachmentMenuExpansion: () -> Void = { }
|
public var requestAttachmentMenuExpansion: () -> Void = { }
|
||||||
public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in }
|
public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in }
|
||||||
@ -115,15 +40,15 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
|||||||
public var cancelPanGesture: () -> Void = { }
|
public var cancelPanGesture: () -> Void = { }
|
||||||
public var isContainerPanning: () -> Bool = { return false }
|
public var isContainerPanning: () -> Bool = { return false }
|
||||||
|
|
||||||
private class Node: ViewControllerTracingNode, WKNavigationDelegate, UIScrollViewDelegate {
|
fileprivate class Node: ViewControllerTracingNode, WKNavigationDelegate, WKUIDelegate, UIScrollViewDelegate {
|
||||||
private weak var controller: WebAppController?
|
private weak var controller: WebAppController?
|
||||||
|
|
||||||
fileprivate var webView: WebAppWebView?
|
fileprivate var webView: WebAppWebView?
|
||||||
|
|
||||||
private var placeholderIcon: UIImage?
|
private var placeholderIcon: UIImage?
|
||||||
private var placeholderNode: ShimmerEffectNode?
|
private var placeholderNode: ShimmerEffectNode?
|
||||||
|
|
||||||
private let loadingProgressNode: LoadingProgressNode
|
fileprivate let loadingProgressPromise = Promise<CGFloat?>(nil)
|
||||||
|
fileprivate let mainButtonStatePromise = Promise<AttachmentMainButtonState?>(nil)
|
||||||
|
|
||||||
private let context: AccountContext
|
private let context: AccountContext
|
||||||
var presentationData: PresentationData
|
var presentationData: PresentationData
|
||||||
@ -139,8 +64,6 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
|||||||
self.presentationData = controller.presentationData
|
self.presentationData = controller.presentationData
|
||||||
self.present = present
|
self.present = present
|
||||||
|
|
||||||
self.loadingProgressNode = LoadingProgressNode(color: presentationData.theme.rootController.tabBar.selectedIconColor)
|
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
if self.presentationData.theme.list.plainBackgroundColor.rgb == 0x000000 {
|
if self.presentationData.theme.list.plainBackgroundColor.rgb == 0x000000 {
|
||||||
@ -149,67 +72,21 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
|||||||
self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
|
self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
|
||||||
}
|
}
|
||||||
|
|
||||||
let configuration = WKWebViewConfiguration()
|
let webView = WebAppWebView()
|
||||||
let userController = WKUserContentController()
|
|
||||||
|
|
||||||
let js = "var TelegramWebviewProxyProto = function() {}; " +
|
|
||||||
"TelegramWebviewProxyProto.prototype.postEvent = function(eventName, eventData) { " +
|
|
||||||
"window.webkit.messageHandlers.performAction.postMessage({'eventName': eventName, 'eventData': eventData}); " +
|
|
||||||
"}; " +
|
|
||||||
"var TelegramWebviewProxy = new TelegramWebviewProxyProto();"
|
|
||||||
|
|
||||||
let userScript = WKUserScript(source: js, injectionTime: .atDocumentStart, forMainFrameOnly: false)
|
|
||||||
userController.addUserScript(userScript)
|
|
||||||
userController.add(WeakGameScriptMessageHandler { [weak self] message in
|
|
||||||
if let strongSelf = self {
|
|
||||||
strongSelf.handleScriptMessage(message)
|
|
||||||
}
|
|
||||||
}, name: "performAction")
|
|
||||||
|
|
||||||
let selectionString = "var css = '*{-webkit-touch-callout:none;} :not(input):not(textarea){-webkit-user-select:none;}';"
|
|
||||||
+ " var head = document.head || document.getElementsByTagName('head')[0];"
|
|
||||||
+ " var style = document.createElement('style'); style.type = 'text/css';" +
|
|
||||||
" style.appendChild(document.createTextNode(css)); head.appendChild(style);"
|
|
||||||
let selectionScript: WKUserScript = WKUserScript(source: selectionString, injectionTime: .atDocumentEnd, forMainFrameOnly: true)
|
|
||||||
userController.addUserScript(selectionScript)
|
|
||||||
|
|
||||||
configuration.userContentController = userController
|
|
||||||
|
|
||||||
configuration.allowsInlineMediaPlayback = true
|
|
||||||
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
|
|
||||||
configuration.mediaTypesRequiringUserActionForPlayback = []
|
|
||||||
} else if #available(iOSApplicationExtension 9.0, iOS 9.0, *) {
|
|
||||||
configuration.requiresUserActionForMediaPlayback = false
|
|
||||||
} else {
|
|
||||||
configuration.mediaPlaybackRequiresUserAction = false
|
|
||||||
}
|
|
||||||
|
|
||||||
let webView = WebAppWebView(frame: CGRect(), configuration: configuration)
|
|
||||||
webView.alpha = 0.0
|
webView.alpha = 0.0
|
||||||
webView.isOpaque = false
|
|
||||||
webView.backgroundColor = .clear
|
|
||||||
webView.navigationDelegate = self
|
webView.navigationDelegate = self
|
||||||
|
webView.uiDelegate = self
|
||||||
if #available(iOSApplicationExtension 9.0, iOS 9.0, *) {
|
|
||||||
webView.allowsLinkPreview = false
|
|
||||||
}
|
|
||||||
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
|
|
||||||
webView.scrollView.contentInsetAdjustmentBehavior = .never
|
|
||||||
}
|
|
||||||
webView.interactiveTransitionGestureRecognizerTest = { point -> Bool in
|
|
||||||
return point.x > 30.0
|
|
||||||
}
|
|
||||||
webView.allowsBackForwardNavigationGestures = false
|
|
||||||
webView.scrollView.delegate = self
|
webView.scrollView.delegate = self
|
||||||
webView.scrollView.contentInset = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 1.0, right: 0.0)
|
|
||||||
webView.addObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress), options: [], context: nil)
|
webView.addObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress), options: [], context: nil)
|
||||||
webView.tintColor = self.presentationData.theme.rootController.tabBar.iconColor
|
webView.tintColor = self.presentationData.theme.rootController.tabBar.iconColor
|
||||||
|
webView.handleScriptMessage = { [weak self] message in
|
||||||
|
self?.handleScriptMessage(message)
|
||||||
|
}
|
||||||
self.webView = webView
|
self.webView = webView
|
||||||
|
|
||||||
let placeholderNode = ShimmerEffectNode()
|
let placeholderNode = ShimmerEffectNode()
|
||||||
self.addSubnode(placeholderNode)
|
self.addSubnode(placeholderNode)
|
||||||
self.placeholderNode = placeholderNode
|
self.placeholderNode = placeholderNode
|
||||||
self.addSubnode(self.loadingProgressNode)
|
|
||||||
|
|
||||||
if let iconFile = controller.iconFile {
|
if let iconFile = controller.iconFile {
|
||||||
let _ = freeMediaFileInteractiveFetched(account: self.context.account, fileReference: .standalone(media: iconFile)).start()
|
let _ = freeMediaFileInteractiveFetched(account: self.context.account, fileReference: .standalone(media: iconFile)).start()
|
||||||
@ -286,17 +163,10 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.view.addSubview(webView)
|
self.view.addSubview(webView)
|
||||||
|
}
|
||||||
|
|
||||||
if #available(iOS 11.0, *) {
|
@objc fileprivate func mainButtonPressed() {
|
||||||
let webScrollView = webView.subviews.compactMap { $0 as? UIScrollView }.first
|
self.webView?.sendEvent(name: "main_button_pressed", data: nil)
|
||||||
Queue.mainQueue().after(0.1, {
|
|
||||||
let contentView = webScrollView?.subviews.first(where: { $0.interactions.count > 1 })
|
|
||||||
guard let dragInteraction = (contentView?.interactions.compactMap { $0 as? UIDragInteraction }.first) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
contentView?.removeInteraction(dragInteraction)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updatePlaceholder() {
|
private func updatePlaceholder() {
|
||||||
@ -307,6 +177,26 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
|||||||
self.placeholderNode?.update(backgroundColor: self.backgroundColor ?? .clear, foregroundColor: theme.list.mediaPlaceholderColor, shimmeringColor: theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: [.image(image: image, rect: CGRect(origin: CGPoint(), size: image.size))], horizontal: true, size: image.size)
|
self.placeholderNode?.update(backgroundColor: self.backgroundColor ?? .clear, foregroundColor: theme.list.mediaPlaceholderColor, shimmeringColor: theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: [.image(image: image, rect: CGRect(origin: CGPoint(), size: image.size))], horizontal: true, size: image.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
|
||||||
|
if let url = navigationAction.request.url?.absoluteString {
|
||||||
|
if isTelegramMeLink(url) || isTelegraPhLink(url) {
|
||||||
|
decisionHandler(.cancel)
|
||||||
|
self.controller?.openUrl(url)
|
||||||
|
} else {
|
||||||
|
decisionHandler(.allow)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
decisionHandler(.allow)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
|
||||||
|
if navigationAction.targetFrame == nil, let url = navigationAction.request.url {
|
||||||
|
self.controller?.openUrl(url.absoluteString)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
private var loadCount = 0
|
private var loadCount = 0
|
||||||
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
|
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
|
||||||
self.loadCount += 1
|
self.loadCount += 1
|
||||||
@ -327,6 +217,10 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if let (layout, navigationBarHeight) = self.validLayout {
|
||||||
|
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||||
@ -334,15 +228,17 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
|||||||
self.controller?.navigationBar?.updateBackgroundAlpha(min(30.0, contentOffset) / 30.0, transition: .immediate)
|
self.controller?.navigationBar?.updateBackgroundAlpha(min(30.0, contentOffset) / 30.0, transition: .immediate)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private var validLayout: (ContainerViewLayout, CGFloat)?
|
private var validLayout: (ContainerViewLayout, CGFloat)?
|
||||||
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||||
let previous = self.validLayout?.0
|
let previousLayout = self.validLayout?.0
|
||||||
self.validLayout = (layout, navigationBarHeight)
|
self.validLayout = (layout, navigationBarHeight)
|
||||||
|
|
||||||
if let webView = self.webView, let controller = self.controller {
|
if let webView = self.webView {
|
||||||
let frame = CGRect(origin: CGPoint(x: layout.safeInsets.left, y: navigationBarHeight), size: CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right, height: max(1.0, layout.size.height - navigationBarHeight - layout.intrinsicInsets.bottom - layout.additionalInsets.bottom)))
|
let frame = CGRect(origin: CGPoint(x: layout.safeInsets.left, y: navigationBarHeight), size: CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right, height: max(1.0, layout.size.height - navigationBarHeight - layout.intrinsicInsets.bottom)))
|
||||||
webView.updateFrame(frame: frame, panning: controller.isContainerPanning(), transition: transition)
|
let viewportFrame = CGRect(origin: CGPoint(x: layout.safeInsets.left, y: navigationBarHeight), size: CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right, height: max(1.0, layout.size.height - navigationBarHeight - layout.intrinsicInsets.bottom - layout.additionalInsets.bottom)))
|
||||||
|
transition.updateFrame(view: webView, frame: frame)
|
||||||
|
|
||||||
|
webView.updateFrame(frame: viewportFrame, transition: transition)
|
||||||
}
|
}
|
||||||
|
|
||||||
if let placeholderNode = self.placeholderNode {
|
if let placeholderNode = self.placeholderNode {
|
||||||
@ -358,43 +254,19 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
|||||||
let placeholderFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - iconSize.width) / 2.0), y: floorToScreenPixels((height - iconSize.height) / 2.0)), size: iconSize)
|
let placeholderFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - iconSize.width) / 2.0), y: floorToScreenPixels((height - iconSize.height) / 2.0)), size: iconSize)
|
||||||
transition.updateFrame(node: placeholderNode, frame: placeholderFrame)
|
transition.updateFrame(node: placeholderNode, frame: placeholderFrame)
|
||||||
placeholderNode.updateAbsoluteRect(placeholderFrame, within: layout.size)
|
placeholderNode.updateAbsoluteRect(placeholderFrame, within: layout.size)
|
||||||
|
|
||||||
let loadingProgressHeight: CGFloat = 2.0
|
|
||||||
transition.updateFrame(node: self.loadingProgressNode, frame: CGRect(origin: CGPoint(x: 0.0, y: height - loadingProgressHeight), size: CGSize(width: layout.size.width, height: loadingProgressHeight)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let previous = previous, (previous.inputHeight ?? 0.0).isZero, let inputHeight = layout.inputHeight, inputHeight > 44.0 {
|
if let previousLayout = previousLayout, (previousLayout.inputHeight ?? 0.0).isZero, let inputHeight = layout.inputHeight, inputHeight > 44.0 {
|
||||||
self.controller?.requestAttachmentMenuExpansion()
|
self.controller?.requestAttachmentMenuExpansion()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func isContainerPanningUpdated(_ panning: Bool) {
|
|
||||||
guard let (layout, navigationBarHeight) = self.validLayout else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if let webView = self.webView {
|
|
||||||
let frame = CGRect(origin: CGPoint(x: layout.safeInsets.left, y: navigationBarHeight), size: CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right, height: max(1.0, layout.size.height - navigationBarHeight - layout.intrinsicInsets.bottom - layout.additionalInsets.bottom)))
|
|
||||||
webView.updateFrame(frame: frame, panning: panning, transition: .immediate)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
||||||
if keyPath == "estimatedProgress", let webView = self.webView {
|
if keyPath == "estimatedProgress", let webView = self.webView {
|
||||||
self.loadingProgressNode.updateProgress(webView.estimatedProgress, animated: true)
|
self.loadingProgressPromise.set(.single(CGFloat(webView.estimatedProgress)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func animateIn() {
|
|
||||||
self.layer.animatePosition(from: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), to: self.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
|
|
||||||
}
|
|
||||||
|
|
||||||
func animateOut(completion: (() -> Void)? = nil) {
|
|
||||||
self.layer.animatePosition(from: self.layer.position, to: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { _ in
|
|
||||||
completion?()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private func handleScriptMessage(_ message: WKScriptMessage) {
|
private func handleScriptMessage(_ message: WKScriptMessage) {
|
||||||
guard let body = message.body as? [String: Any] else {
|
guard let body = message.body as? [String: Any] else {
|
||||||
return
|
return
|
||||||
@ -409,6 +281,22 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
|||||||
if let eventData = body["eventData"] as? String {
|
if let eventData = body["eventData"] as? String {
|
||||||
self.handleSendData(data: eventData)
|
self.handleSendData(data: eventData)
|
||||||
}
|
}
|
||||||
|
case "web_app_setup_main_button":
|
||||||
|
if let eventData = (body["eventData"] as? String)?.data(using: .utf8), let json = try? JSONSerialization.jsonObject(with: eventData, options: []) as? [String: Any] {
|
||||||
|
if let backgroundColorString = json["color"] as? String, let backgroundColor = UIColor(hexString: backgroundColorString),
|
||||||
|
let textColorString = json["text_color"] as? String, let textColor = UIColor(hexString: textColorString),
|
||||||
|
let text = json["text"] as? String,
|
||||||
|
let isEnabled = json["is_active"] as? Bool,
|
||||||
|
let isVisible = json["is_visible"] as? Bool
|
||||||
|
{
|
||||||
|
let state = AttachmentMainButtonState(text: text, backgroundColor: backgroundColor, textColor: textColor, isEnabled: isEnabled, isVisible: isVisible)
|
||||||
|
self.mainButtonStatePromise.set(.single(state))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "web_app_request_viewport":
|
||||||
|
if let (layout, navigationBarHeight) = self.validLayout {
|
||||||
|
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
|
||||||
|
}
|
||||||
case "web_app_close":
|
case "web_app_close":
|
||||||
self.controller?.dismiss()
|
self.controller?.dismiss()
|
||||||
default:
|
default:
|
||||||
@ -437,13 +325,6 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendEvent(name: String, data: String) {
|
|
||||||
let script = "window.TelegramGameProxy.receiveEvent(\"\(name)\", \(data))"
|
|
||||||
self.webView?.evaluateJavaScript(script, completionHandler: { _, _ in
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func updatePresentationData(_ presentationData: PresentationData) {
|
func updatePresentationData(_ presentationData: PresentationData) {
|
||||||
self.presentationData = presentationData
|
self.presentationData = presentationData
|
||||||
|
|
||||||
@ -466,11 +347,11 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
themeParamsString.append("}}")
|
themeParamsString.append("}}")
|
||||||
self.sendEvent(name: "theme_changed", data: themeParamsString)
|
self.webView?.sendEvent(name: "theme_changed", data: themeParamsString)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var controllerNode: Node {
|
fileprivate var controllerNode: Node {
|
||||||
return self.displayNode as! Node
|
return self.displayNode as! Node
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -491,8 +372,8 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
|||||||
fileprivate let updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?
|
fileprivate let updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?
|
||||||
private var presentationDataDisposable: Disposable?
|
private var presentationDataDisposable: Disposable?
|
||||||
|
|
||||||
|
public var openUrl: (String) -> Void = { _ in }
|
||||||
public var getNavigationController: () -> NavigationController? = { return nil }
|
public var getNavigationController: () -> NavigationController? = { return nil }
|
||||||
|
|
||||||
public var completion: () -> Void = {}
|
public var completion: () -> Void = {}
|
||||||
|
|
||||||
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: PeerId, botId: PeerId, botName: String, url: String?, queryId: Int64?, buttonText: String?, keepAliveSignal: Signal<Never, KeepWebViewError>?, replyToMessageId: MessageId?, iconFile: TelegramMediaFile?) {
|
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: PeerId, botId: PeerId, botName: String, url: String?, queryId: Int64?, buttonText: String?, keepAliveSignal: Signal<Never, KeepWebViewError>?, replyToMessageId: MessageId?, iconFile: TelegramMediaFile?) {
|
||||||
@ -509,17 +390,10 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
|||||||
self.updatedPresentationData = updatedPresentationData
|
self.updatedPresentationData = updatedPresentationData
|
||||||
self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
|
self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
|
||||||
|
|
||||||
var theme = NavigationBarTheme(rootControllerTheme: self.presentationData.theme)
|
|
||||||
if self.presentationData.theme.list.plainBackgroundColor.rgb == 0x000000 {
|
|
||||||
theme = theme.withUpdatedBackgroundColor(self.presentationData.theme.list.itemBlocksBackgroundColor)
|
|
||||||
} else {
|
|
||||||
theme = theme.withUpdatedBackgroundColor(self.presentationData.theme.list.plainBackgroundColor)
|
|
||||||
}
|
|
||||||
let navigationBarPresentationData = NavigationBarPresentationData(theme: theme, strings: NavigationBarStrings(back: "", close: ""))
|
|
||||||
|
|
||||||
self.moreButtonNode = MoreButtonNode(theme: self.presentationData.theme)
|
self.moreButtonNode = MoreButtonNode(theme: self.presentationData.theme)
|
||||||
self.moreButtonNode.iconNode.enqueueState(.more, animated: false)
|
self.moreButtonNode.iconNode.enqueueState(.more, animated: false)
|
||||||
|
|
||||||
|
let navigationBarPresentationData = NavigationBarPresentationData(theme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme), strings: NavigationBarStrings(back: "", close: ""))
|
||||||
super.init(navigationBarPresentationData: navigationBarPresentationData)
|
super.init(navigationBarPresentationData: navigationBarPresentationData)
|
||||||
|
|
||||||
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
|
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
|
||||||
@ -545,13 +419,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
|||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
strongSelf.presentationData = presentationData
|
strongSelf.presentationData = presentationData
|
||||||
|
|
||||||
var theme = NavigationBarTheme(rootControllerTheme: presentationData.theme)
|
let navigationBarPresentationData = NavigationBarPresentationData(theme: NavigationBarTheme(rootControllerTheme: presentationData.theme), strings: NavigationBarStrings(back: "", close: ""))
|
||||||
if presentationData.theme.list.plainBackgroundColor.rgb == 0x000000 {
|
|
||||||
theme = theme.withUpdatedBackgroundColor(presentationData.theme.list.itemBlocksBackgroundColor)
|
|
||||||
} else {
|
|
||||||
theme = theme.withUpdatedBackgroundColor(presentationData.theme.list.plainBackgroundColor)
|
|
||||||
}
|
|
||||||
let navigationBarPresentationData = NavigationBarPresentationData(theme: theme, strings: NavigationBarStrings(back: "", close: ""))
|
|
||||||
strongSelf.navigationBar?.updatePresentationData(navigationBarPresentationData)
|
strongSelf.navigationBar?.updatePresentationData(navigationBarPresentationData)
|
||||||
strongSelf.titleView?.theme = presentationData.theme
|
strongSelf.titleView?.theme = presentationData.theme
|
||||||
|
|
||||||
@ -634,10 +502,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
|||||||
})
|
})
|
||||||
|
|
||||||
self.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate)
|
self.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate)
|
||||||
}
|
self.updateTabBarAlpha(1.0, .immediate)
|
||||||
|
|
||||||
public func isContainerPanningUpdated(_ panning: Bool) {
|
|
||||||
self.controllerNode.isContainerPanningUpdated(panning)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||||
@ -652,8 +517,54 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
|||||||
} set(value) {
|
} set(value) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var mediaPickerContext: AttachmentMediaPickerContext? {
|
||||||
|
return WebAppPickerContext(controller: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func prepareForReuse() {
|
||||||
|
self.updateTabBarAlpha(1.0, .immediate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class WebAppPickerContext: AttachmentMediaPickerContext {
|
||||||
|
private weak var controller: WebAppController?
|
||||||
|
|
||||||
|
var selectionCount: Signal<Int, NoError> {
|
||||||
|
return .single(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
var caption: Signal<NSAttributedString?, NoError> {
|
||||||
|
return .single(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
public var loadingProgress: Signal<CGFloat?, NoError> {
|
||||||
|
return self.controller?.controllerNode.loadingProgressPromise.get() ?? .single(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
public var mainButtonState: Signal<AttachmentMainButtonState?, NoError> {
|
||||||
|
return self.controller?.controllerNode.mainButtonStatePromise.get() ?? .single(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(controller: WebAppController) {
|
||||||
|
self.controller = controller
|
||||||
|
}
|
||||||
|
|
||||||
|
func setCaption(_ caption: NSAttributedString) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func send(silently: Bool, mode: AttachmentMediaPickerSendMode) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func schedule() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func mainButtonAction() {
|
||||||
|
self.controller?.controllerNode.mainButtonPressed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private final class WebAppContextReferenceContentSource: ContextReferenceContentSource {
|
private final class WebAppContextReferenceContentSource: ContextReferenceContentSource {
|
||||||
private let controller: ViewController
|
private let controller: ViewController
|
||||||
private let sourceNode: ContextReferenceContentNode
|
private let sourceNode: ContextReferenceContentNode
|
||||||
@ -668,12 +579,13 @@ private final class WebAppContextReferenceContentSource: ContextReferenceContent
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func standaloneWebAppController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: PeerId, botId: PeerId, botName: String, url: String, queryId: Int64?, buttonText: String?, keepAliveSignal: Signal<Never, KeepWebViewError>?, completion: @escaping () -> Void = {}) -> ViewController {
|
public func standaloneWebAppController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: PeerId, botId: PeerId, botName: String, url: String, queryId: Int64?, buttonText: String?, keepAliveSignal: Signal<Never, KeepWebViewError>?, openUrl: @escaping (String) -> Void, completion: @escaping () -> Void = {}) -> ViewController {
|
||||||
let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, chatLocation: .peer(id: peerId), buttons: [.standalone], initialButton: .standalone)
|
let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, chatLocation: .peer(id: peerId), buttons: [.standalone], initialButton: .standalone)
|
||||||
controller.requestController = { _, present in
|
controller.requestController = { _, present in
|
||||||
let webAppController = WebAppController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, botId: botId, botName: botName, url: url, queryId: queryId, buttonText: buttonText, keepAliveSignal: keepAliveSignal, replyToMessageId: nil, iconFile: nil)
|
let webAppController = WebAppController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, botId: botId, botName: botName, url: url, queryId: queryId, buttonText: buttonText, keepAliveSignal: keepAliveSignal, replyToMessageId: nil, iconFile: nil)
|
||||||
|
webAppController.openUrl = openUrl
|
||||||
webAppController.completion = completion
|
webAppController.completion = completion
|
||||||
present(webAppController, nil)
|
present(webAppController, webAppController.mediaPickerContext)
|
||||||
}
|
}
|
||||||
return controller
|
return controller
|
||||||
}
|
}
|
||||||
|
@ -4,145 +4,106 @@ import Display
|
|||||||
import WebKit
|
import WebKit
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
|
|
||||||
private let findFixedPositionClasses = """
|
private class WeakGameScriptMessageHandler: NSObject, WKScriptMessageHandler {
|
||||||
function findFixedPositionClasses() {
|
private let f: (WKScriptMessage) -> ()
|
||||||
var elems = document.body.getElementsByTagName("*");
|
|
||||||
var len = elems.length
|
|
||||||
|
|
||||||
var result = []
|
init(_ f: @escaping (WKScriptMessage) -> ()) {
|
||||||
var j = 0;
|
self.f = f
|
||||||
for (var i = 0; i < len; i++) {
|
|
||||||
if ((window.getComputedStyle(elems[i],null).getPropertyValue('position') == 'fixed') && (window.getComputedStyle(elems[i],null).getPropertyValue('bottom') == '0px')) {
|
|
||||||
result[j] = elems[i].className;
|
|
||||||
j++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
findFixedPositionClasses();
|
|
||||||
"""
|
|
||||||
|
|
||||||
private func findFixedPositionViews(webView: WKWebView, classes: [String]) -> [(String, UIView)] {
|
super.init()
|
||||||
if let contentView = webView.scrollView.subviews.first {
|
|
||||||
func recursiveSearch(_ view: UIView) -> [(String, UIView)] {
|
|
||||||
var result: [(String, UIView)] = []
|
|
||||||
|
|
||||||
let description = view.description
|
|
||||||
if description.contains("class='") {
|
|
||||||
for className in classes {
|
|
||||||
if description.contains(className) {
|
|
||||||
result.append((className, view))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for subview in view.subviews {
|
func userContentController(_ controller: WKUserContentController, didReceive scriptMessage: WKScriptMessage) {
|
||||||
result.append(contentsOf: recursiveSearch(subview))
|
self.f(scriptMessage)
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
return recursiveSearch(contentView)
|
|
||||||
} else {
|
|
||||||
return []
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final class WebAppWebView: WKWebView {
|
final class WebAppWebView: WKWebView {
|
||||||
private var fixedPositionClasses: [String] = []
|
var handleScriptMessage: (WKScriptMessage) -> Void = { _ in }
|
||||||
private var currentFixedViews: [(String, UIView, UIView)] = []
|
|
||||||
|
|
||||||
private var timer: SwiftSignalKit.Timer?
|
init() {
|
||||||
|
let configuration = WKWebViewConfiguration()
|
||||||
|
let userController = WKUserContentController()
|
||||||
|
|
||||||
deinit {
|
let js = "var TelegramWebviewProxyProto = function() {}; " +
|
||||||
self.timer?.invalidate()
|
"TelegramWebviewProxyProto.prototype.postEvent = function(eventName, eventData) { " +
|
||||||
|
"window.webkit.messageHandlers.performAction.postMessage({'eventName': eventName, 'eventData': eventData}); " +
|
||||||
|
"}; " +
|
||||||
|
"var TelegramWebviewProxy = new TelegramWebviewProxyProto();"
|
||||||
|
|
||||||
|
var handleScriptMessageImpl: ((WKScriptMessage) -> Void)?
|
||||||
|
let userScript = WKUserScript(source: js, injectionTime: .atDocumentStart, forMainFrameOnly: false)
|
||||||
|
userController.addUserScript(userScript)
|
||||||
|
userController.add(WeakGameScriptMessageHandler { message in
|
||||||
|
handleScriptMessageImpl?(message)
|
||||||
|
}, name: "performAction")
|
||||||
|
|
||||||
|
let selectionString = "var css = '*{-webkit-touch-callout:none;} :not(input):not(textarea){-webkit-user-select:none;}';"
|
||||||
|
+ " var head = document.head || document.getElementsByTagName('head')[0];"
|
||||||
|
+ " var style = document.createElement('style'); style.type = 'text/css';" +
|
||||||
|
" style.appendChild(document.createTextNode(css)); head.appendChild(style);"
|
||||||
|
let selectionScript: WKUserScript = WKUserScript(source: selectionString, injectionTime: .atDocumentEnd, forMainFrameOnly: true)
|
||||||
|
userController.addUserScript(selectionScript)
|
||||||
|
|
||||||
|
configuration.userContentController = userController
|
||||||
|
|
||||||
|
configuration.allowsInlineMediaPlayback = true
|
||||||
|
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
|
||||||
|
configuration.mediaTypesRequiringUserActionForPlayback = []
|
||||||
|
} else if #available(iOSApplicationExtension 9.0, iOS 9.0, *) {
|
||||||
|
configuration.requiresUserActionForMediaPlayback = false
|
||||||
|
} else {
|
||||||
|
configuration.mediaPlaybackRequiresUserAction = false
|
||||||
|
}
|
||||||
|
|
||||||
|
super.init(frame: CGRect(), configuration: configuration)
|
||||||
|
|
||||||
|
self.isOpaque = false
|
||||||
|
self.backgroundColor = .clear
|
||||||
|
if #available(iOSApplicationExtension 9.0, iOS 9.0, *) {
|
||||||
|
self.allowsLinkPreview = false
|
||||||
|
}
|
||||||
|
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
|
||||||
|
self.scrollView.contentInsetAdjustmentBehavior = .never
|
||||||
|
}
|
||||||
|
self.interactiveTransitionGestureRecognizerTest = { point -> Bool in
|
||||||
|
return point.x > 30.0
|
||||||
|
}
|
||||||
|
self.allowsBackForwardNavigationGestures = false
|
||||||
|
|
||||||
|
handleScriptMessageImpl = { [weak self] message in
|
||||||
|
if let strongSelf = self {
|
||||||
|
strongSelf.handleScriptMessage(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
override func didMoveToSuperview() {
|
override func didMoveToSuperview() {
|
||||||
super.didMoveToSuperview()
|
super.didMoveToSuperview()
|
||||||
|
|
||||||
if self.timer == nil {
|
if #available(iOS 11.0, *) {
|
||||||
let timer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { [weak self] in
|
let webScrollView = self.subviews.compactMap { $0 as? UIScrollView }.first
|
||||||
guard let strongSelf = self else {
|
Queue.mainQueue().after(0.1, {
|
||||||
|
let contentView = webScrollView?.subviews.first(where: { $0.interactions.count > 1 })
|
||||||
|
guard let dragInteraction = (contentView?.interactions.compactMap { $0 as? UIDragInteraction }.first) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
contentView?.removeInteraction(dragInteraction)
|
||||||
strongSelf.evaluateJavaScript(findFixedPositionClasses, completionHandler: { [weak self] result, _ in
|
|
||||||
if let result = result {
|
|
||||||
Queue.mainQueue().async {
|
|
||||||
self?.fixedPositionClasses = (result as? [String]) ?? []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}, queue: Queue.mainQueue())
|
|
||||||
timer.start()
|
|
||||||
self.timer = timer
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sendEvent(name: String, data: String?) {
|
||||||
|
let script = "window.TelegramGameProxy.receiveEvent(\"\(name)\", \(data ?? "null"))"
|
||||||
func updateFrame(frame: CGRect, panning: Bool, transition: ContainedViewLayoutTransition) {
|
self.evaluateJavaScript(script, completionHandler: { _, _ in
|
||||||
let reset = { [weak self] in
|
|
||||||
guard let strongSelf = self else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for (_, view, snapshotView) in strongSelf.currentFixedViews {
|
|
||||||
view.isHidden = false
|
|
||||||
snapshotView.removeFromSuperview()
|
|
||||||
}
|
|
||||||
strongSelf.currentFixedViews = []
|
|
||||||
}
|
|
||||||
|
|
||||||
let update = { [weak self] in
|
|
||||||
guard let strongSelf = self else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for (_, view, snapshotView) in strongSelf.currentFixedViews {
|
|
||||||
view.isHidden = true
|
|
||||||
|
|
||||||
var snapshotFrame = view.frame
|
|
||||||
snapshotFrame.origin.y = frame.height - snapshotFrame.height
|
|
||||||
transition.updateFrame(view: snapshotView, frame: snapshotFrame)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if panning {
|
|
||||||
let fixedPositionViews = findFixedPositionViews(webView: self, classes: self.fixedPositionClasses)
|
|
||||||
if fixedPositionViews.count != self.currentFixedViews.count {
|
|
||||||
var existing: [String: (UIView, UIView)] = [:]
|
|
||||||
for (className, originalView, snapshotView) in self.currentFixedViews {
|
|
||||||
existing[className] = (originalView, snapshotView)
|
|
||||||
}
|
|
||||||
|
|
||||||
var updatedFixedViews: [(String, UIView, UIView)] = []
|
|
||||||
for (className, view) in fixedPositionViews {
|
|
||||||
if let (_, existingSnapshotView) = existing[className] {
|
|
||||||
updatedFixedViews.append((className, view, existingSnapshotView))
|
|
||||||
existing[className] = nil
|
|
||||||
} else if let snapshotView = view.snapshotView(afterScreenUpdates: false) {
|
|
||||||
updatedFixedViews.append((className, view, snapshotView))
|
|
||||||
self.addSubview(snapshotView)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (_, originalAndSnapshotView) in existing {
|
|
||||||
originalAndSnapshotView.0.isHidden = false
|
|
||||||
originalAndSnapshotView.1.removeFromSuperview()
|
|
||||||
}
|
|
||||||
|
|
||||||
self.currentFixedViews = updatedFixedViews
|
|
||||||
}
|
|
||||||
transition.updateFrame(view: self, frame: frame)
|
|
||||||
update()
|
|
||||||
} else {
|
|
||||||
update()
|
|
||||||
transition.updateFrame(view: self, frame: frame, completion: { _ in
|
|
||||||
reset()
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateFrame(frame: CGRect, transition: ContainedViewLayoutTransition) {
|
||||||
|
self.sendEvent(name: "viewport_changed", data: "{height:\(frame.height)}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user