mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-11-08 01:40:09 +00:00
Various improvements
This commit is contained in:
parent
f87c2ec00f
commit
5b51f35b36
@ -1118,3 +1118,23 @@ public struct StoriesConfiguration {
|
|||||||
//#endif
|
//#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct StickersSearchConfiguration {
|
||||||
|
static var defaultValue: StickersSearchConfiguration {
|
||||||
|
return StickersSearchConfiguration(disableLocalSuggestions: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
public let disableLocalSuggestions: Bool
|
||||||
|
|
||||||
|
fileprivate init(disableLocalSuggestions: Bool) {
|
||||||
|
self.disableLocalSuggestions = disableLocalSuggestions
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func with(appConfiguration: AppConfiguration) -> StickersSearchConfiguration {
|
||||||
|
if let data = appConfiguration.data, let suggestOnlyApi = data["stickers_emoji_suggest_only_api"] as? Bool {
|
||||||
|
return StickersSearchConfiguration(disableLocalSuggestions: suggestOnlyApi)
|
||||||
|
} else {
|
||||||
|
return .defaultValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -184,6 +184,7 @@ open class ViewControllerComponentContainer: ViewController {
|
|||||||
environment: {
|
environment: {
|
||||||
environment
|
environment
|
||||||
},
|
},
|
||||||
|
forceUpdate: self.controller?.forceNextUpdate ?? false,
|
||||||
containerSize: layout.size
|
containerSize: layout.size
|
||||||
)
|
)
|
||||||
transition.setFrame(view: self.hostView, frame: CGRect(origin: CGPoint(), size: layout.size), completion: nil)
|
transition.setFrame(view: self.hostView, frame: CGRect(origin: CGPoint(), size: layout.size), completion: nil)
|
||||||
@ -306,6 +307,13 @@ open class ViewControllerComponentContainer: ViewController {
|
|||||||
super.dismiss(animated: flag, completion: completion)
|
super.dismiss(animated: flag, completion: completion)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fileprivate var forceNextUpdate = false
|
||||||
|
public func requestLayout(forceUpdate: Bool, transition: ContainedViewLayoutTransition) {
|
||||||
|
self.forceNextUpdate = forceUpdate
|
||||||
|
self.requestLayout(transition: transition)
|
||||||
|
self.forceNextUpdate = false
|
||||||
|
}
|
||||||
|
|
||||||
override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||||
super.containerLayoutUpdated(layout, transition: transition)
|
super.containerLayoutUpdated(layout, transition: transition)
|
||||||
|
|
||||||
|
|||||||
@ -301,7 +301,8 @@ private final class StickerSelectionComponent: Component {
|
|||||||
inputHeight: 0.0,
|
inputHeight: 0.0,
|
||||||
displayBottomPanel: true,
|
displayBottomPanel: true,
|
||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
clipContentToTopPanel: false
|
clipContentToTopPanel: false,
|
||||||
|
useExternalSearchContainer: false
|
||||||
)),
|
)),
|
||||||
environment: {},
|
environment: {},
|
||||||
forceUpdate: self.forceUpdate,
|
forceUpdate: self.forceUpdate,
|
||||||
|
|||||||
@ -1272,7 +1272,8 @@ final class AvatarEditorScreenComponent: Component {
|
|||||||
inputHeight: 0.0,
|
inputHeight: 0.0,
|
||||||
displayBottomPanel: false,
|
displayBottomPanel: false,
|
||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
clipContentToTopPanel: false
|
clipContentToTopPanel: false,
|
||||||
|
useExternalSearchContainer: false
|
||||||
)),
|
)),
|
||||||
environment: {},
|
environment: {},
|
||||||
containerSize: CGSize(width: keyboardContainerFrame.size.width, height: keyboardContainerFrame.size.height - 6.0 + (isSearchActive ? 40.0 : 0.0))
|
containerSize: CGSize(width: keyboardContainerFrame.size.width, height: keyboardContainerFrame.size.height - 6.0 + (isSearchActive ? 40.0 : 0.0))
|
||||||
|
|||||||
@ -406,6 +406,8 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var useExternalSearchContainer: Bool = false
|
||||||
|
|
||||||
private final class GifContext {
|
private final class GifContext {
|
||||||
private var componentValue: EntityKeyboardGifContent? {
|
private var componentValue: EntityKeyboardGifContent? {
|
||||||
didSet {
|
didSet {
|
||||||
@ -1608,10 +1610,11 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
|||||||
hideBackground: false,
|
hideBackground: false,
|
||||||
stateContext: nil
|
stateContext: nil
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
self.inputDataDisposable = (combineLatest(queue: .mainQueue(),
|
self.inputDataDisposable = (combineLatest(queue: .mainQueue(),
|
||||||
updatedInputData,
|
updatedInputData,
|
||||||
self.gifComponent.get(),
|
.single(self.currentInputData.gifs) |> then(self.gifComponent.get() |> map(Optional.init)),
|
||||||
self.emojiSearchState.get(),
|
self.emojiSearchState.get(),
|
||||||
self.stickerSearchState.get()
|
self.stickerSearchState.get()
|
||||||
)
|
)
|
||||||
@ -1980,11 +1983,10 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
|||||||
mappedMode = .gif
|
mappedMode = .gif
|
||||||
}
|
}
|
||||||
|
|
||||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
|
||||||
let searchContainerNode = PaneSearchContainerNode(
|
let searchContainerNode = PaneSearchContainerNode(
|
||||||
context: context,
|
context: context,
|
||||||
theme: presentationData.theme,
|
theme: interfaceState.theme,
|
||||||
strings: presentationData.strings,
|
strings: interfaceState.strings,
|
||||||
interaction: interaction,
|
interaction: interaction,
|
||||||
inputNodeInteraction: inputNodeInteraction,
|
inputNodeInteraction: inputNodeInteraction,
|
||||||
mode: mappedMode,
|
mode: mappedMode,
|
||||||
@ -2008,7 +2010,8 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
|||||||
inputHeight: inputHeight,
|
inputHeight: inputHeight,
|
||||||
displayBottomPanel: true,
|
displayBottomPanel: true,
|
||||||
isExpanded: isExpanded && !self.isEmojiSearchActive,
|
isExpanded: isExpanded && !self.isEmojiSearchActive,
|
||||||
clipContentToTopPanel: self.clipContentToTopPanel
|
clipContentToTopPanel: self.clipContentToTopPanel,
|
||||||
|
useExternalSearchContainer: self.useExternalSearchContainer
|
||||||
)),
|
)),
|
||||||
environment: {},
|
environment: {},
|
||||||
containerSize: CGSize(width: width, height: expandedHeight)
|
containerSize: CGSize(width: width, height: expandedHeight)
|
||||||
|
|||||||
@ -191,7 +191,8 @@ public final class EmojiStatusSelectionComponent: Component {
|
|||||||
inputHeight: 0.0,
|
inputHeight: 0.0,
|
||||||
displayBottomPanel: false,
|
displayBottomPanel: false,
|
||||||
isExpanded: false,
|
isExpanded: false,
|
||||||
clipContentToTopPanel: false
|
clipContentToTopPanel: false,
|
||||||
|
useExternalSearchContainer: false
|
||||||
)),
|
)),
|
||||||
environment: {},
|
environment: {},
|
||||||
containerSize: availableSize
|
containerSize: availableSize
|
||||||
|
|||||||
@ -5201,7 +5201,7 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
scrollView.layer.removeAllAnimations()
|
scrollView.layer.removeAllAnimations()
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.isSearchActivated, let component = self.component, component.searchState == .empty(hasResults: false), !component.searchAlwaysActive, let visibleSearchHeader = self.visibleSearchHeader, visibleSearchHeader.currentPresetSearchTerm == nil {
|
if self.isSearchActivated, let component = self.component, component.searchState == .empty(hasResults: true), !component.searchAlwaysActive, let visibleSearchHeader = self.visibleSearchHeader, visibleSearchHeader.currentPresetSearchTerm == nil {
|
||||||
scrollView.isScrollEnabled = false
|
scrollView.isScrollEnabled = false
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
scrollView.isScrollEnabled = true
|
scrollView.isScrollEnabled = true
|
||||||
@ -6176,6 +6176,10 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
if let topVisibleGroupId = topVisibleGroupId {
|
if let topVisibleGroupId = topVisibleGroupId {
|
||||||
self.activeItemUpdated?.invoke((topVisibleGroupId, topVisibleSubgroupId, .immediate))
|
self.activeItemUpdated?.invoke((topVisibleGroupId, topVisibleSubgroupId, .immediate))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let fadingMaskLayer = self.fadingMaskLayer {
|
||||||
|
fadingMaskLayer.internalAlpha = max(0.0, min(1.0, self.scrollView.contentOffset.y / 30.0))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateShimmerIfNeeded() {
|
private func updateShimmerIfNeeded() {
|
||||||
@ -6254,7 +6258,7 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
if self.layer.mask == nil {
|
if self.layer.mask == nil {
|
||||||
self.layer.mask = maskLayer
|
self.layer.mask = maskLayer
|
||||||
}
|
}
|
||||||
maskLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: (topPanelHeight - 34.0) * 0.75), size: backgroundFrame.size)
|
maskLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((topPanelHeight - 34.0) * 0.75)), size: backgroundFrame.size)
|
||||||
} else if component.warpContentsOnEdges {
|
} else if component.warpContentsOnEdges {
|
||||||
self.backgroundView.isHidden = true
|
self.backgroundView.isHidden = true
|
||||||
} else {
|
} else {
|
||||||
@ -6772,10 +6776,13 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
|
|
||||||
let searchHeaderFrame = CGRect(origin: CGPoint(x: itemLayout.searchInsets.left, y: itemLayout.searchInsets.top), size: CGSize(width: itemLayout.width - itemLayout.searchInsets.left - itemLayout.searchInsets.right, height: itemLayout.searchHeight))
|
let searchHeaderFrame = CGRect(origin: CGPoint(x: itemLayout.searchInsets.left, y: itemLayout.searchInsets.top), size: CGSize(width: itemLayout.width - itemLayout.searchInsets.left - itemLayout.searchInsets.right, height: itemLayout.searchHeight))
|
||||||
visibleSearchHeader.update(context: component.context, theme: keyboardChildEnvironment.theme, strings: keyboardChildEnvironment.strings, text: displaySearchWithPlaceholder, useOpaqueTheme: useOpaqueTheme, isActive: self.isSearchActivated, size: searchHeaderFrame.size, canFocus: !component.searchIsPlaceholderOnly, searchCategories: component.searchCategories, searchState: component.searchState, transition: transition)
|
visibleSearchHeader.update(context: component.context, theme: keyboardChildEnvironment.theme, strings: keyboardChildEnvironment.strings, text: displaySearchWithPlaceholder, useOpaqueTheme: useOpaqueTheme, isActive: self.isSearchActivated, size: searchHeaderFrame.size, canFocus: !component.searchIsPlaceholderOnly, searchCategories: component.searchCategories, searchState: component.searchState, transition: transition)
|
||||||
if !useOpaqueTheme {
|
|
||||||
transition.setFrame(view: visibleSearchHeader, frame: searchHeaderFrame)
|
transition.setFrame(view: visibleSearchHeader, frame: searchHeaderFrame)
|
||||||
transition.attachAnimation(view: visibleSearchHeader, id: "search_transition", completion: { [weak self] completed in
|
// Temporary workaround for status selection; use a separate search container (see GIF)
|
||||||
guard let strongSelf = self, completed, let visibleSearchHeader = strongSelf.visibleSearchHeader else {
|
|
||||||
|
if case let .curve(duration, _) = transition.animation, duration != 0.0 {
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + duration, execute: { [weak self] in
|
||||||
|
guard let strongSelf = self, let visibleSearchHeader = strongSelf.visibleSearchHeader else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -6785,37 +6792,9 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
transition.setFrame(view: visibleSearchHeader, frame: searchHeaderFrame, completion: { [weak self] completed in
|
if !self.isSearchActivated && visibleSearchHeader.superview != self.scrollView {
|
||||||
if !useOpaqueTheme {
|
self.scrollView.addSubview(visibleSearchHeader)
|
||||||
guard let strongSelf = self, completed, let visibleSearchHeader = strongSelf.visibleSearchHeader else {
|
self.mirrorContentScrollView.addSubview(visibleSearchHeader.tintContainerView)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strongSelf.isSearchActivated && visibleSearchHeader.superview != strongSelf.scrollView {
|
|
||||||
strongSelf.scrollView.addSubview(visibleSearchHeader)
|
|
||||||
strongSelf.mirrorContentScrollView.addSubview(visibleSearchHeader.tintContainerView)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// Temporary workaround for status selection; use a separate search container (see GIF)
|
|
||||||
if useOpaqueTheme {
|
|
||||||
if case let .curve(duration, _) = transition.animation, duration != 0.0 {
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + duration, execute: { [weak self] in
|
|
||||||
guard let strongSelf = self, let visibleSearchHeader = strongSelf.visibleSearchHeader else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strongSelf.isSearchActivated && visibleSearchHeader.superview != strongSelf.scrollView {
|
|
||||||
strongSelf.scrollView.addSubview(visibleSearchHeader)
|
|
||||||
strongSelf.mirrorContentScrollView.addSubview(visibleSearchHeader.tintContainerView)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
if !self.isSearchActivated && visibleSearchHeader.superview != self.scrollView {
|
|
||||||
self.scrollView.addSubview(visibleSearchHeader)
|
|
||||||
self.mirrorContentScrollView.addSubview(visibleSearchHeader.tintContainerView)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -8553,20 +8532,30 @@ func generateTopicIcon(backgroundColors: [UIColor], strokeColors: [UIColor], tit
|
|||||||
private final class FadingMaskLayer: SimpleLayer {
|
private final class FadingMaskLayer: SimpleLayer {
|
||||||
let gradientLayer = SimpleLayer()
|
let gradientLayer = SimpleLayer()
|
||||||
let fillLayer = SimpleLayer()
|
let fillLayer = SimpleLayer()
|
||||||
|
let gradientFillLayer = SimpleLayer()
|
||||||
|
|
||||||
|
var internalAlpha: CGFloat = 1.0 {
|
||||||
|
didSet {
|
||||||
|
self.gradientFillLayer.opacity = Float(1.0 - self.internalAlpha)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override func layoutSublayers() {
|
override func layoutSublayers() {
|
||||||
let gradientHeight: CGFloat = 66.0
|
let gradientHeight: CGFloat = 66.0
|
||||||
if self.gradientLayer.contents == nil {
|
if self.gradientLayer.contents == nil {
|
||||||
self.addSublayer(self.gradientLayer)
|
self.addSublayer(self.gradientLayer)
|
||||||
self.addSublayer(self.fillLayer)
|
self.addSublayer(self.fillLayer)
|
||||||
|
self.addSublayer(self.gradientFillLayer)
|
||||||
|
|
||||||
let gradientImage = generateGradientImage(size: CGSize(width: 1.0, height: gradientHeight), colors: [UIColor.white.withAlphaComponent(0.0), UIColor.white.withAlphaComponent(0.0), UIColor.white, UIColor.white], locations: [0.0, 0.4, 0.9, 1.0], direction: .vertical)
|
let gradientImage = generateGradientImage(size: CGSize(width: 1.0, height: gradientHeight), colors: [UIColor.white.withAlphaComponent(0.0), UIColor.white.withAlphaComponent(0.0), UIColor.white, UIColor.white], locations: [0.0, 0.4, 0.9, 1.0], direction: .vertical)
|
||||||
self.gradientLayer.contents = gradientImage?.cgImage
|
self.gradientLayer.contents = gradientImage?.cgImage
|
||||||
self.gradientLayer.contentsGravity = .resize
|
self.gradientLayer.contentsGravity = .resize
|
||||||
self.fillLayer.backgroundColor = UIColor.white.cgColor
|
self.fillLayer.backgroundColor = UIColor.white.cgColor
|
||||||
|
self.gradientFillLayer.backgroundColor = UIColor.white.cgColor
|
||||||
}
|
}
|
||||||
|
|
||||||
self.gradientLayer.frame = CGRect(origin: .zero, size: CGSize(width: self.bounds.width, height: gradientHeight))
|
self.gradientLayer.frame = CGRect(origin: .zero, size: CGSize(width: self.bounds.width, height: gradientHeight))
|
||||||
|
self.gradientFillLayer.frame = self.gradientLayer.frame
|
||||||
self.fillLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: gradientHeight), size: CGSize(width: self.bounds.width, height: self.bounds.height - gradientHeight))
|
self.fillLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: gradientHeight), size: CGSize(width: self.bounds.width, height: self.bounds.height - gradientHeight))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -506,6 +506,7 @@ public final class EmojiSearchContent: ASDisplayNode, EntitySearchContainerNode
|
|||||||
displayBottomPanel: false,
|
displayBottomPanel: false,
|
||||||
isExpanded: false,
|
isExpanded: false,
|
||||||
clipContentToTopPanel: false,
|
clipContentToTopPanel: false,
|
||||||
|
useExternalSearchContainer: false,
|
||||||
hidePanels: true
|
hidePanels: true
|
||||||
)),
|
)),
|
||||||
environment: {},
|
environment: {},
|
||||||
|
|||||||
@ -121,6 +121,7 @@ public final class EntityKeyboardComponent: Component {
|
|||||||
public let displayBottomPanel: Bool
|
public let displayBottomPanel: Bool
|
||||||
public let isExpanded: Bool
|
public let isExpanded: Bool
|
||||||
public let clipContentToTopPanel: Bool
|
public let clipContentToTopPanel: Bool
|
||||||
|
public let useExternalSearchContainer: Bool
|
||||||
public let hidePanels: Bool
|
public let hidePanels: Bool
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
@ -153,6 +154,7 @@ public final class EntityKeyboardComponent: Component {
|
|||||||
displayBottomPanel: Bool,
|
displayBottomPanel: Bool,
|
||||||
isExpanded: Bool,
|
isExpanded: Bool,
|
||||||
clipContentToTopPanel: Bool,
|
clipContentToTopPanel: Bool,
|
||||||
|
useExternalSearchContainer: Bool,
|
||||||
hidePanels: Bool = false
|
hidePanels: Bool = false
|
||||||
) {
|
) {
|
||||||
self.theme = theme
|
self.theme = theme
|
||||||
@ -184,6 +186,7 @@ public final class EntityKeyboardComponent: Component {
|
|||||||
self.displayBottomPanel = displayBottomPanel
|
self.displayBottomPanel = displayBottomPanel
|
||||||
self.isExpanded = isExpanded
|
self.isExpanded = isExpanded
|
||||||
self.clipContentToTopPanel = clipContentToTopPanel
|
self.clipContentToTopPanel = clipContentToTopPanel
|
||||||
|
self.useExternalSearchContainer = useExternalSearchContainer
|
||||||
self.hidePanels = hidePanels
|
self.hidePanels = hidePanels
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -248,6 +251,9 @@ public final class EntityKeyboardComponent: Component {
|
|||||||
if lhs.clipContentToTopPanel != rhs.clipContentToTopPanel {
|
if lhs.clipContentToTopPanel != rhs.clipContentToTopPanel {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.useExternalSearchContainer != rhs.useExternalSearchContainer {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -908,14 +914,20 @@ public final class EntityKeyboardComponent: Component {
|
|||||||
contentType = .stickers
|
contentType = .stickers
|
||||||
}
|
}
|
||||||
|
|
||||||
self.searchComponent = EntitySearchContentComponent(
|
if component.useExternalSearchContainer, let containerNode = component.makeSearchContainerNode(contentType) {
|
||||||
makeContainerNode: {
|
let controller = EntitySearchContainerController(containerNode: containerNode)
|
||||||
return component.makeSearchContainerNode(contentType)
|
|
||||||
},
|
self.component?.emojiContent?.inputInteractionHolder.inputInteraction?.pushController(controller)
|
||||||
dismissSearch: { [weak self] in
|
} else {
|
||||||
self?.closeSearch()
|
self.searchComponent = EntitySearchContentComponent(
|
||||||
}
|
makeContainerNode: {
|
||||||
)
|
return component.makeSearchContainerNode(contentType)
|
||||||
|
},
|
||||||
|
dismissSearch: { [weak self] in
|
||||||
|
self?.closeSearch()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
//self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring)))
|
//self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring)))
|
||||||
component.hideInputUpdated(true, true, Transition(animation: .curve(duration: 0.3, curve: .spring)))
|
component.hideInputUpdated(true, true, Transition(animation: .curve(duration: 0.3, curve: .spring)))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,6 +18,57 @@ public protocol EntitySearchContainerNode: ASDisplayNode {
|
|||||||
func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, inputHeight: CGFloat, deviceMetrics: DeviceMetrics, transition: ContainedViewLayoutTransition)
|
func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, inputHeight: CGFloat, deviceMetrics: DeviceMetrics, transition: ContainedViewLayoutTransition)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public final class EntitySearchContainerController: ViewController {
|
||||||
|
private var node: Node {
|
||||||
|
return self.displayNode as! Node
|
||||||
|
}
|
||||||
|
|
||||||
|
private let containerNode: EntitySearchContainerNode
|
||||||
|
|
||||||
|
public init(containerNode: EntitySearchContainerNode) {
|
||||||
|
self.containerNode = containerNode
|
||||||
|
|
||||||
|
super.init(navigationBarPresentationData: nil)
|
||||||
|
|
||||||
|
self.navigationPresentation = .modal
|
||||||
|
}
|
||||||
|
|
||||||
|
required init(coder aDecoder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override public func loadDisplayNode() {
|
||||||
|
self.displayNode = Node(containerNode: self.containerNode, controller: self)
|
||||||
|
self.displayNodeDidLoad()
|
||||||
|
}
|
||||||
|
|
||||||
|
public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||||
|
super.containerLayoutUpdated(layout, transition: transition)
|
||||||
|
|
||||||
|
self.node.containerLayoutUpdated(layout, transition: transition)
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Node: ViewControllerTracingNode, UIScrollViewDelegate {
|
||||||
|
private weak var controller: EntitySearchContainerController?
|
||||||
|
|
||||||
|
private let containerNode: EntitySearchContainerNode
|
||||||
|
|
||||||
|
init(containerNode: EntitySearchContainerNode, controller: EntitySearchContainerController) {
|
||||||
|
self.containerNode = containerNode
|
||||||
|
self.controller = controller
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
self.addSubnode(containerNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||||
|
self.containerNode.updateLayout(size: layout.size, leftInset: 0.0, rightInset: 0.0, bottomInset: layout.intrinsicInsets.bottom, inputHeight: layout.inputHeight ?? 0.0, deviceMetrics: layout.deviceMetrics, transition: transition)
|
||||||
|
transition.updateFrame(node: self.containerNode, frame: CGRect(origin: .zero, size: layout.size))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final class EntitySearchContentEnvironment: Equatable {
|
final class EntitySearchContentEnvironment: Equatable {
|
||||||
let context: AccountContext
|
let context: AccountContext
|
||||||
let theme: PresentationTheme
|
let theme: PresentationTheme
|
||||||
|
|||||||
@ -364,7 +364,8 @@ private final class TopicIconSelectionComponent: Component {
|
|||||||
inputHeight: 0.0,
|
inputHeight: 0.0,
|
||||||
displayBottomPanel: false,
|
displayBottomPanel: false,
|
||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
clipContentToTopPanel: false
|
clipContentToTopPanel: false,
|
||||||
|
useExternalSearchContainer: false
|
||||||
)),
|
)),
|
||||||
environment: {},
|
environment: {},
|
||||||
containerSize: availableSize
|
containerSize: availableSize
|
||||||
|
|||||||
@ -32,6 +32,7 @@ swift_library(
|
|||||||
"//submodules/Components/LottieAnimationComponent:LottieAnimationComponent",
|
"//submodules/Components/LottieAnimationComponent:LottieAnimationComponent",
|
||||||
"//submodules/Components/BundleIconComponent:BundleIconComponent",
|
"//submodules/Components/BundleIconComponent:BundleIconComponent",
|
||||||
"//submodules/TelegramUI/Components/MessageInputPanelComponent",
|
"//submodules/TelegramUI/Components/MessageInputPanelComponent",
|
||||||
|
"//submodules/TelegramUI/Components/TextFieldComponent",
|
||||||
"//submodules/TelegramUI/Components/ChatInputNode",
|
"//submodules/TelegramUI/Components/ChatInputNode",
|
||||||
"//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode",
|
"//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode",
|
||||||
"//submodules/TooltipUI",
|
"//submodules/TooltipUI",
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import MediaEditor
|
|||||||
import Photos
|
import Photos
|
||||||
import LottieAnimationComponent
|
import LottieAnimationComponent
|
||||||
import MessageInputPanelComponent
|
import MessageInputPanelComponent
|
||||||
|
import TextFieldComponent
|
||||||
import EntityKeyboard
|
import EntityKeyboard
|
||||||
import TooltipUI
|
import TooltipUI
|
||||||
import BlurredBackgroundComponent
|
import BlurredBackgroundComponent
|
||||||
@ -268,7 +269,7 @@ final class MediaEditorScreenComponent: Component {
|
|||||||
self.backgroundColor = .clear
|
self.backgroundColor = .clear
|
||||||
|
|
||||||
self.fadeView.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.4)
|
self.fadeView.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.4)
|
||||||
self.fadeView.addTarget(self, action: #selector(self.fadePressed), for: .touchUpInside)
|
self.fadeView.addTarget(self, action: #selector(self.deactivateInput), for: .touchUpInside)
|
||||||
self.fadeView.alpha = 0.0
|
self.fadeView.alpha = 0.0
|
||||||
|
|
||||||
self.addSubview(self.fadeView)
|
self.addSubview(self.fadeView)
|
||||||
@ -305,7 +306,7 @@ final class MediaEditorScreenComponent: Component {
|
|||||||
context: context,
|
context: context,
|
||||||
chatPeerId: nil,
|
chatPeerId: nil,
|
||||||
areCustomEmojiEnabled: true,
|
areCustomEmojiEnabled: true,
|
||||||
hasSearch: false,
|
hasSearch: true,
|
||||||
hideBackground: true,
|
hideBackground: true,
|
||||||
sendGif: nil
|
sendGif: nil
|
||||||
) |> map { inputData -> ChatEntityKeyboardInputNode.InputData in
|
) |> map { inputData -> ChatEntityKeyboardInputNode.InputData in
|
||||||
@ -336,8 +337,7 @@ final class MediaEditorScreenComponent: Component {
|
|||||||
updateChoosingSticker: { _ in },
|
updateChoosingSticker: { _ in },
|
||||||
switchToTextInput: { [weak self] in
|
switchToTextInput: { [weak self] in
|
||||||
if let self {
|
if let self {
|
||||||
self.currentInputMode = .text
|
self.activateInput()
|
||||||
self.state?.updated(transition: .immediate)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
dismissTextInput: {
|
dismissTextInput: {
|
||||||
@ -366,7 +366,7 @@ final class MediaEditorScreenComponent: Component {
|
|||||||
getNavigationController: { return nil },
|
getNavigationController: { return nil },
|
||||||
requestLayout: { [weak self] transition in
|
requestLayout: { [weak self] transition in
|
||||||
if let self {
|
if let self {
|
||||||
self.environment?.controller()?.requestLayout(transition: transition)
|
(self.environment?.controller() as? MediaEditorScreen)?.node.requestLayout(forceUpdate: true, transition: Transition(transition))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -374,9 +374,24 @@ final class MediaEditorScreenComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func fadePressed() {
|
private func activateInput() {
|
||||||
self.currentInputMode = .text
|
self.currentInputMode = .text
|
||||||
self.endEditing(true)
|
if !hasFirstResponder(self) {
|
||||||
|
if let view = self.inputPanel.view as? MessageInputPanelComponent.View {
|
||||||
|
view.activateInput()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.state?.updated(transition: .immediate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func deactivateInput() {
|
||||||
|
self.currentInputMode = .text
|
||||||
|
if hasFirstResponder(self) {
|
||||||
|
self.endEditing(true)
|
||||||
|
} else {
|
||||||
|
self.state?.updated(transition: .spring(duration: 0.4).withUserData(TextFieldComponent.AnimationHint(kind: .textFocusChanged)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var animatingButtons = false
|
private var animatingButtons = false
|
||||||
@ -739,7 +754,7 @@ final class MediaEditorScreenComponent: Component {
|
|||||||
let buttonsAvailableWidth: CGFloat
|
let buttonsAvailableWidth: CGFloat
|
||||||
let buttonsLeftOffset: CGFloat
|
let buttonsLeftOffset: CGFloat
|
||||||
if isTablet {
|
if isTablet {
|
||||||
buttonsAvailableWidth = previewSize.width + 260.0
|
buttonsAvailableWidth = previewSize.width + 180.0
|
||||||
buttonsLeftOffset = floorToScreenPixels((availableSize.width - buttonsAvailableWidth) / 2.0)
|
buttonsLeftOffset = floorToScreenPixels((availableSize.width - buttonsAvailableWidth) / 2.0)
|
||||||
} else {
|
} else {
|
||||||
buttonsAvailableWidth = floor(availableSize.width - cancelButtonSize.width * 0.66 - (doneButtonSize.width - cancelButtonSize.width * 0.33) - buttonSideInset * 2.0)
|
buttonsAvailableWidth = floor(availableSize.width - cancelButtonSize.width * 0.66 - (doneButtonSize.width - cancelButtonSize.width * 0.33) - buttonSideInset * 2.0)
|
||||||
@ -950,6 +965,92 @@ final class MediaEditorScreenComponent: Component {
|
|||||||
inputPanelAvailableHeight = 200.0
|
inputPanelAvailableHeight = 200.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var inputHeight = environment.inputHeight
|
||||||
|
var keyboardHeight = environment.deviceMetrics.standardInputHeight(inLandscape: false)
|
||||||
|
let keyboardWasHidden = self.inputPanelExternalState.isKeyboardHidden
|
||||||
|
|
||||||
|
if case .emoji = self.currentInputMode, let inputData = self.inputMediaNodeData {
|
||||||
|
let inputMediaNode: ChatEntityKeyboardInputNode
|
||||||
|
if let current = self.inputMediaNode {
|
||||||
|
inputMediaNode = current
|
||||||
|
} else {
|
||||||
|
inputMediaNode = ChatEntityKeyboardInputNode(
|
||||||
|
context: component.context,
|
||||||
|
currentInputData: inputData,
|
||||||
|
updatedInputData: self.inputMediaNodeDataPromise.get(),
|
||||||
|
defaultToEmojiTab: true,
|
||||||
|
opaqueTopPanelBackground: false,
|
||||||
|
interaction: self.inputMediaInteraction,
|
||||||
|
chatPeerId: nil,
|
||||||
|
stateContext: self.inputMediaNodeStateContext
|
||||||
|
)
|
||||||
|
inputMediaNode.externalTopPanelContainerImpl = nil
|
||||||
|
if let inputPanelView = self.inputPanel.view, inputMediaNode.view.superview == nil {
|
||||||
|
self.insertSubview(inputMediaNode.view, belowSubview: inputPanelView)
|
||||||
|
}
|
||||||
|
self.inputMediaNode = inputMediaNode
|
||||||
|
}
|
||||||
|
|
||||||
|
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme)
|
||||||
|
let presentationInterfaceState = ChatPresentationInterfaceState(
|
||||||
|
chatWallpaper: .builtin(WallpaperSettings()),
|
||||||
|
theme: presentationData.theme,
|
||||||
|
strings: presentationData.strings,
|
||||||
|
dateTimeFormat: presentationData.dateTimeFormat,
|
||||||
|
nameDisplayOrder: presentationData.nameDisplayOrder,
|
||||||
|
limitsConfiguration: component.context.currentLimitsConfiguration.with { $0 },
|
||||||
|
fontSize: presentationData.chatFontSize,
|
||||||
|
bubbleCorners: presentationData.chatBubbleCorners,
|
||||||
|
accountPeerId: component.context.account.peerId,
|
||||||
|
mode: .standard(previewing: false),
|
||||||
|
chatLocation: .peer(id: component.context.account.peerId),
|
||||||
|
subject: nil,
|
||||||
|
peerNearbyData: nil,
|
||||||
|
greetingData: nil,
|
||||||
|
pendingUnpinnedAllMessages: false,
|
||||||
|
activeGroupCallInfo: nil,
|
||||||
|
hasActiveGroupCall: false,
|
||||||
|
importState: nil,
|
||||||
|
threadData: nil,
|
||||||
|
isGeneralThreadClosed: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
let heightAndOverflow = inputMediaNode.updateLayout(width: availableSize.width, leftInset: 0.0, rightInset: 0.0, bottomInset: component.bottomSafeInset, standardInputHeight: environment.deviceMetrics.standardInputHeight(inLandscape: false), inputHeight: environment.inputHeight, maximumHeight: availableSize.height, inputPanelHeight: 0.0, transition: .immediate, interfaceState: presentationInterfaceState, layoutMetrics: environment.metrics, deviceMetrics: environment.deviceMetrics, isVisible: true, isExpanded: false)
|
||||||
|
let inputNodeHeight = heightAndOverflow.0
|
||||||
|
let inputNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - inputNodeHeight), size: CGSize(width: availableSize.width, height: inputNodeHeight))
|
||||||
|
transition.setFrame(layer: inputMediaNode.layer, frame: inputNodeFrame)
|
||||||
|
|
||||||
|
inputHeight = heightAndOverflow.0
|
||||||
|
keyboardHeight = max(keyboardHeight, heightAndOverflow.0)
|
||||||
|
} else if let inputMediaNode = self.inputMediaNode {
|
||||||
|
self.inputMediaNode = nil
|
||||||
|
|
||||||
|
var dismissingInputHeight = environment.inputHeight
|
||||||
|
if self.currentInputMode == .emoji || (dismissingInputHeight.isZero && keyboardWasHidden) {
|
||||||
|
dismissingInputHeight = max(inputHeight, environment.deviceMetrics.standardInputHeight(inLandscape: false))
|
||||||
|
}
|
||||||
|
|
||||||
|
if let animationHint = transition.userData(TextFieldComponent.AnimationHint.self), case .textFocusChanged = animationHint.kind {
|
||||||
|
dismissingInputHeight = 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetFrame = inputMediaNode.frame
|
||||||
|
if dismissingInputHeight > 0.0 {
|
||||||
|
targetFrame.origin.y = availableSize.height - dismissingInputHeight
|
||||||
|
} else {
|
||||||
|
targetFrame.origin.y = availableSize.height
|
||||||
|
}
|
||||||
|
transition.setFrame(view: inputMediaNode.view, frame: targetFrame, completion: { [weak inputMediaNode] _ in
|
||||||
|
if let inputMediaNode {
|
||||||
|
Queue.mainQueue().after(0.2) {
|
||||||
|
inputMediaNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak inputMediaNode] _ in
|
||||||
|
inputMediaNode?.view.removeFromSuperview()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
let nextInputMode: MessageInputPanelComponent.InputMode
|
let nextInputMode: MessageInputPanelComponent.InputMode
|
||||||
switch self.currentInputMode {
|
switch self.currentInputMode {
|
||||||
case .text:
|
case .text:
|
||||||
@ -960,7 +1061,6 @@ final class MediaEditorScreenComponent: Component {
|
|||||||
nextInputMode = .emoji
|
nextInputMode = .emoji
|
||||||
}
|
}
|
||||||
|
|
||||||
let keyboardWasHidden = self.inputPanelExternalState.isKeyboardHidden
|
|
||||||
self.inputPanel.parentState = state
|
self.inputPanel.parentState = state
|
||||||
let inputPanelSize = self.inputPanel.update(
|
let inputPanelSize = self.inputPanel.update(
|
||||||
transition: transition,
|
transition: transition,
|
||||||
@ -980,13 +1080,19 @@ final class MediaEditorScreenComponent: Component {
|
|||||||
}
|
}
|
||||||
controller.present(c, in: .window(.root))
|
controller.present(c, in: .window(.root))
|
||||||
},
|
},
|
||||||
|
presentInGlobalOverlay: {[weak self] c in
|
||||||
|
guard let self, let _ = self.component, let environment = self.environment, let controller = environment.controller() as? MediaEditorScreen else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
controller.presentInGlobalOverlay(c)
|
||||||
|
},
|
||||||
sendMessageAction: { [weak self] in
|
sendMessageAction: { [weak self] in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.currentInputMode = .text
|
self.deactivateInput()
|
||||||
self.endEditing(true)
|
|
||||||
},
|
},
|
||||||
|
sendStickerAction: { _ in },
|
||||||
setMediaRecordingActive: nil,
|
setMediaRecordingActive: nil,
|
||||||
lockMediaRecording: nil,
|
lockMediaRecording: nil,
|
||||||
stopAndPreviewMediaRecording: nil,
|
stopAndPreviewMediaRecording: nil,
|
||||||
@ -1002,7 +1108,11 @@ final class MediaEditorScreenComponent: Component {
|
|||||||
default:
|
default:
|
||||||
self.currentInputMode = .emoji
|
self.currentInputMode = .emoji
|
||||||
}
|
}
|
||||||
self.state?.updated(transition: .immediate)
|
if self.currentInputMode == .text {
|
||||||
|
self.activateInput()
|
||||||
|
} else {
|
||||||
|
self.state?.updated(transition: .immediate)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
timeoutAction: isEditingStory ? nil : { [weak self] view in
|
timeoutAction: isEditingStory ? nil : { [weak self] view in
|
||||||
@ -1034,12 +1144,19 @@ final class MediaEditorScreenComponent: Component {
|
|||||||
displayGradient: false,
|
displayGradient: false,
|
||||||
bottomInset: 0.0,
|
bottomInset: 0.0,
|
||||||
hideKeyboard: self.currentInputMode == .emoji,
|
hideKeyboard: self.currentInputMode == .emoji,
|
||||||
|
forceIsEditing: self.currentInputMode == .emoji,
|
||||||
disabledPlaceholder: nil
|
disabledPlaceholder: nil
|
||||||
)),
|
)),
|
||||||
environment: {},
|
environment: {},
|
||||||
containerSize: CGSize(width: inputPanelAvailableWidth, height: inputPanelAvailableHeight)
|
containerSize: CGSize(width: inputPanelAvailableWidth, height: inputPanelAvailableHeight)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if self.inputPanelExternalState.isEditing {
|
||||||
|
if self.currentInputMode == .emoji || (inputHeight.isZero && keyboardWasHidden) {
|
||||||
|
inputHeight = max(inputHeight, environment.deviceMetrics.standardInputHeight(inLandscape: false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let fadeTransition = Transition(animation: .curve(duration: 0.3, curve: .easeInOut))
|
let fadeTransition = Transition(animation: .curve(duration: 0.3, curve: .easeInOut))
|
||||||
if self.inputPanelExternalState.isEditing {
|
if self.inputPanelExternalState.isEditing {
|
||||||
fadeTransition.setAlpha(view: self.fadeView, alpha: 1.0)
|
fadeTransition.setAlpha(view: self.fadeView, alpha: 1.0)
|
||||||
@ -1061,19 +1178,12 @@ final class MediaEditorScreenComponent: Component {
|
|||||||
mediaEditor?.play()
|
mediaEditor?.play()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var inputHeight = environment.inputHeight
|
|
||||||
if self.inputPanelExternalState.isEditing {
|
|
||||||
if self.currentInputMode == .emoji || (inputHeight.isZero && keyboardWasHidden) {
|
|
||||||
inputHeight = environment.deviceMetrics.standardInputHeight(inLandscape: false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let inputPanelBackgroundSize = self.inputPanelBackground.update(
|
let inputPanelBackgroundSize = self.inputPanelBackground.update(
|
||||||
transition: transition,
|
transition: transition,
|
||||||
component: AnyComponent(BlurredGradientComponent(position: .bottom, tag: nil)),
|
component: AnyComponent(BlurredGradientComponent(position: .bottom, tag: nil)),
|
||||||
environment: {},
|
environment: {},
|
||||||
containerSize: CGSize(width: availableSize.width, height: environment.deviceMetrics.standardInputHeight(inLandscape: false) + 100.0)
|
containerSize: CGSize(width: availableSize.width, height: keyboardHeight + 100.0)
|
||||||
)
|
)
|
||||||
if let inputPanelBackgroundView = self.inputPanelBackground.view {
|
if let inputPanelBackgroundView = self.inputPanelBackground.view {
|
||||||
if inputPanelBackgroundView.superview == nil {
|
if inputPanelBackgroundView.superview == nil {
|
||||||
@ -1414,76 +1524,6 @@ final class MediaEditorScreenComponent: Component {
|
|||||||
transition.setAlpha(view: textSizeView, alpha: sizeSliderVisible && !component.isInteractingWithEntities ? 1.0 : 0.0)
|
transition.setAlpha(view: textSizeView, alpha: sizeSliderVisible && !component.isInteractingWithEntities ? 1.0 : 0.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
if case .emoji = self.currentInputMode, let inputData = self.inputMediaNodeData {
|
|
||||||
let inputMediaNode: ChatEntityKeyboardInputNode
|
|
||||||
if let current = self.inputMediaNode {
|
|
||||||
inputMediaNode = current
|
|
||||||
} else {
|
|
||||||
inputMediaNode = ChatEntityKeyboardInputNode(
|
|
||||||
context: component.context,
|
|
||||||
currentInputData: inputData,
|
|
||||||
updatedInputData: self.inputMediaNodeDataPromise.get(),
|
|
||||||
defaultToEmojiTab: true,
|
|
||||||
opaqueTopPanelBackground: false,
|
|
||||||
interaction: self.inputMediaInteraction,
|
|
||||||
chatPeerId: nil,
|
|
||||||
stateContext: self.inputMediaNodeStateContext
|
|
||||||
)
|
|
||||||
inputMediaNode.externalTopPanelContainerImpl = nil
|
|
||||||
if let inputPanelView = self.inputPanel.view, inputMediaNode.view.superview == nil {
|
|
||||||
self.insertSubview(inputMediaNode.view, belowSubview: inputPanelView)
|
|
||||||
}
|
|
||||||
self.inputMediaNode = inputMediaNode
|
|
||||||
}
|
|
||||||
|
|
||||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme)
|
|
||||||
let presentationInterfaceState = ChatPresentationInterfaceState(
|
|
||||||
chatWallpaper: .builtin(WallpaperSettings()),
|
|
||||||
theme: presentationData.theme,
|
|
||||||
strings: presentationData.strings,
|
|
||||||
dateTimeFormat: presentationData.dateTimeFormat,
|
|
||||||
nameDisplayOrder: presentationData.nameDisplayOrder,
|
|
||||||
limitsConfiguration: component.context.currentLimitsConfiguration.with { $0 },
|
|
||||||
fontSize: presentationData.chatFontSize,
|
|
||||||
bubbleCorners: presentationData.chatBubbleCorners,
|
|
||||||
accountPeerId: component.context.account.peerId,
|
|
||||||
mode: .standard(previewing: false),
|
|
||||||
chatLocation: .peer(id: component.context.account.peerId),
|
|
||||||
subject: nil,
|
|
||||||
peerNearbyData: nil,
|
|
||||||
greetingData: nil,
|
|
||||||
pendingUnpinnedAllMessages: false,
|
|
||||||
activeGroupCallInfo: nil,
|
|
||||||
hasActiveGroupCall: false,
|
|
||||||
importState: nil,
|
|
||||||
threadData: nil,
|
|
||||||
isGeneralThreadClosed: nil
|
|
||||||
)
|
|
||||||
|
|
||||||
let heightAndOverflow = inputMediaNode.updateLayout(width: availableSize.width, leftInset: 0.0, rightInset: 0.0, bottomInset: component.bottomSafeInset, standardInputHeight: environment.deviceMetrics.standardInputHeight(inLandscape: false), inputHeight: environment.inputHeight, maximumHeight: availableSize.height, inputPanelHeight: 0.0, transition: .immediate, interfaceState: presentationInterfaceState, layoutMetrics: environment.metrics, deviceMetrics: environment.deviceMetrics, isVisible: true, isExpanded: false)
|
|
||||||
let inputNodeHeight = heightAndOverflow.0
|
|
||||||
let inputNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - inputNodeHeight), size: CGSize(width: availableSize.width, height: inputNodeHeight))
|
|
||||||
transition.setFrame(layer: inputMediaNode.layer, frame: inputNodeFrame)
|
|
||||||
} else if let inputMediaNode = self.inputMediaNode {
|
|
||||||
self.inputMediaNode = nil
|
|
||||||
|
|
||||||
var targetFrame = inputMediaNode.frame
|
|
||||||
if inputHeight > 0.0 {
|
|
||||||
targetFrame.origin.y = availableSize.height - inputHeight
|
|
||||||
} else {
|
|
||||||
targetFrame.origin.y = availableSize.height
|
|
||||||
}
|
|
||||||
transition.setFrame(view: inputMediaNode.view, frame: targetFrame, completion: { [weak inputMediaNode] _ in
|
|
||||||
if let inputMediaNode {
|
|
||||||
Queue.mainQueue().after(0.3) {
|
|
||||||
inputMediaNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.35, removeOnCompletion: false, completion: { [weak inputMediaNode] _ in
|
|
||||||
inputMediaNode?.view.removeFromSuperview()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
component.externalState.derivedInputHeight = inputHeight
|
component.externalState.derivedInputHeight = inputHeight
|
||||||
|
|
||||||
return availableSize
|
return availableSize
|
||||||
@ -1690,7 +1730,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||||||
isStatusSelection: false,
|
isStatusSelection: false,
|
||||||
isReactionSelection: false,
|
isReactionSelection: false,
|
||||||
isEmojiSelection: true,
|
isEmojiSelection: true,
|
||||||
hasTrending: false,
|
hasTrending: true,
|
||||||
topReactionItems: [],
|
topReactionItems: [],
|
||||||
areUnicodeEmojiEnabled: true,
|
areUnicodeEmojiEnabled: true,
|
||||||
areCustomEmojiEnabled: true,
|
areCustomEmojiEnabled: true,
|
||||||
@ -2644,6 +2684,13 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||||||
private var drawingScreen: DrawingScreen?
|
private var drawingScreen: DrawingScreen?
|
||||||
private var stickerScreen: StickerPickerScreen?
|
private var stickerScreen: StickerPickerScreen?
|
||||||
|
|
||||||
|
func requestLayout(forceUpdate: Bool, transition: Transition) {
|
||||||
|
guard let layout = self.validLayout else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.containerLayoutUpdated(layout: layout, forceUpdate: forceUpdate, hasAppeared: self.hasAppeared, transition: transition)
|
||||||
|
}
|
||||||
|
|
||||||
func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, hasAppeared: Bool = false, transition: Transition) {
|
func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, hasAppeared: Bool = false, transition: Transition) {
|
||||||
guard let controller = self.controller, !self.isDismissed else {
|
guard let controller = self.controller, !self.isDismissed else {
|
||||||
return
|
return
|
||||||
@ -4293,3 +4340,15 @@ public final class BlurredGradientComponent: Component {
|
|||||||
func draftPath() -> String {
|
func draftPath() -> String {
|
||||||
return NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] + "/storyDrafts"
|
return NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] + "/storyDrafts"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func hasFirstResponder(_ view: UIView) -> Bool {
|
||||||
|
if view.isFirstResponder {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for subview in view.subviews {
|
||||||
|
if hasFirstResponder(subview) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@ -253,10 +253,10 @@ final class StoryPreviewComponent: Component {
|
|||||||
alwaysDarkWhenHasText: false,
|
alwaysDarkWhenHasText: false,
|
||||||
nextInputMode: { _ in return .stickers },
|
nextInputMode: { _ in return .stickers },
|
||||||
areVoiceMessagesAvailable: false,
|
areVoiceMessagesAvailable: false,
|
||||||
presentController: { _ in
|
presentController: { _ in },
|
||||||
},
|
presentInGlobalOverlay: { _ in },
|
||||||
sendMessageAction: {
|
sendMessageAction: { },
|
||||||
},
|
sendStickerAction: { _ in },
|
||||||
setMediaRecordingActive: { _, _, _ in },
|
setMediaRecordingActive: { _, _, _ in },
|
||||||
lockMediaRecording: nil,
|
lockMediaRecording: nil,
|
||||||
stopAndPreviewMediaRecording: nil,
|
stopAndPreviewMediaRecording: nil,
|
||||||
@ -277,6 +277,7 @@ final class StoryPreviewComponent: Component {
|
|||||||
displayGradient: false,
|
displayGradient: false,
|
||||||
bottomInset: 0.0,
|
bottomInset: 0.0,
|
||||||
hideKeyboard: false,
|
hideKeyboard: false,
|
||||||
|
forceIsEditing: false,
|
||||||
disabledPlaceholder: nil
|
disabledPlaceholder: nil
|
||||||
)),
|
)),
|
||||||
environment: {},
|
environment: {},
|
||||||
|
|||||||
@ -30,6 +30,8 @@ swift_library(
|
|||||||
"//submodules/TelegramUI/Components/Stories/PeerListItemComponent",
|
"//submodules/TelegramUI/Components/Stories/PeerListItemComponent",
|
||||||
"//submodules/TelegramUI/Components/MoreHeaderButton",
|
"//submodules/TelegramUI/Components/MoreHeaderButton",
|
||||||
"//submodules/TelegramUI/Components/EmojiSuggestionsComponent",
|
"//submodules/TelegramUI/Components/EmojiSuggestionsComponent",
|
||||||
|
"//submodules/TelegramUI/Components/EmojiTextAttachmentView",
|
||||||
|
"//submodules/StickerPeekUI",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
|||||||
@ -8,30 +8,18 @@ import AccountContext
|
|||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
import PeerListItemComponent
|
import PeerListItemComponent
|
||||||
|
|
||||||
extension ChatPresentationInputQueryResult {
|
|
||||||
var count: Int {
|
|
||||||
switch self {
|
|
||||||
case let .stickers(stickers):
|
|
||||||
return stickers.count
|
|
||||||
case let .hashtags(hashtags):
|
|
||||||
return hashtags.count
|
|
||||||
case let .mentions(peers):
|
|
||||||
return peers.count
|
|
||||||
case let .commands(commands):
|
|
||||||
return commands.count
|
|
||||||
default:
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
final class ContextResultPanelComponent: Component {
|
final class ContextResultPanelComponent: Component {
|
||||||
final class ExternalState {
|
enum Results: Equatable {
|
||||||
fileprivate(set) var minimizedHeight: CGFloat = 0.0
|
case mentions([EnginePeer])
|
||||||
fileprivate(set) var effectiveHeight: CGFloat = 0.0
|
case hashtags([String])
|
||||||
|
|
||||||
init() {
|
var count: Int {
|
||||||
|
switch self {
|
||||||
|
case let .hashtags(hashtags):
|
||||||
|
return hashtags.count
|
||||||
|
case let .mentions(peers):
|
||||||
|
return peers.count
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,22 +28,19 @@ final class ContextResultPanelComponent: Component {
|
|||||||
case hashtag(String)
|
case hashtag(String)
|
||||||
}
|
}
|
||||||
|
|
||||||
let externalState: ExternalState
|
|
||||||
let context: AccountContext
|
let context: AccountContext
|
||||||
let theme: PresentationTheme
|
let theme: PresentationTheme
|
||||||
let strings: PresentationStrings
|
let strings: PresentationStrings
|
||||||
let results: ChatPresentationInputQueryResult
|
let results: Results
|
||||||
let action: (ResultAction) -> Void
|
let action: (ResultAction) -> Void
|
||||||
|
|
||||||
init(
|
init(
|
||||||
externalState: ExternalState,
|
|
||||||
context: AccountContext,
|
context: AccountContext,
|
||||||
theme: PresentationTheme,
|
theme: PresentationTheme,
|
||||||
strings: PresentationStrings,
|
strings: PresentationStrings,
|
||||||
results: ChatPresentationInputQueryResult,
|
results: Results,
|
||||||
action: @escaping (ResultAction) -> Void
|
action: @escaping (ResultAction) -> Void
|
||||||
) {
|
) {
|
||||||
self.externalState = externalState
|
|
||||||
self.context = context
|
self.context = context
|
||||||
self.theme = theme
|
self.theme = theme
|
||||||
self.strings = strings
|
self.strings = strings
|
||||||
@ -64,9 +49,6 @@ final class ContextResultPanelComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static func ==(lhs: ContextResultPanelComponent, rhs: ContextResultPanelComponent) -> Bool {
|
static func ==(lhs: ContextResultPanelComponent, rhs: ContextResultPanelComponent) -> Bool {
|
||||||
if lhs.externalState !== rhs.externalState {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if lhs.context !== rhs.context {
|
if lhs.context !== rhs.context {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -87,27 +69,27 @@ final class ContextResultPanelComponent: Component {
|
|||||||
var bottomInset: CGFloat
|
var bottomInset: CGFloat
|
||||||
var topInset: CGFloat
|
var topInset: CGFloat
|
||||||
var sideInset: CGFloat
|
var sideInset: CGFloat
|
||||||
var itemHeight: CGFloat
|
var itemSize: CGSize
|
||||||
var itemCount: Int
|
var itemCount: Int
|
||||||
|
|
||||||
var contentSize: CGSize
|
var contentSize: CGSize
|
||||||
|
|
||||||
init(containerSize: CGSize, bottomInset: CGFloat, topInset: CGFloat, sideInset: CGFloat, itemHeight: CGFloat, itemCount: Int) {
|
init(containerSize: CGSize, bottomInset: CGFloat, topInset: CGFloat, sideInset: CGFloat, itemSize: CGSize, itemCount: Int) {
|
||||||
self.containerSize = containerSize
|
self.containerSize = containerSize
|
||||||
self.bottomInset = bottomInset
|
self.bottomInset = bottomInset
|
||||||
self.topInset = topInset
|
self.topInset = topInset
|
||||||
self.sideInset = sideInset
|
self.sideInset = sideInset
|
||||||
self.itemHeight = itemHeight
|
self.itemSize = itemSize
|
||||||
self.itemCount = itemCount
|
self.itemCount = itemCount
|
||||||
|
|
||||||
self.contentSize = CGSize(width: containerSize.width, height: topInset + CGFloat(itemCount) * itemHeight + bottomInset)
|
self.contentSize = CGSize(width: containerSize.width, height: topInset + CGFloat(itemCount) * itemSize.height + bottomInset)
|
||||||
}
|
}
|
||||||
|
|
||||||
func visibleItems(for rect: CGRect) -> Range<Int>? {
|
func visibleItems(for rect: CGRect) -> Range<Int>? {
|
||||||
let offsetRect = rect.offsetBy(dx: 0.0, dy: -self.topInset)
|
let offsetRect = rect.offsetBy(dx: 0.0, dy: -self.topInset)
|
||||||
var minVisibleRow = Int(floor((offsetRect.minY) / (self.itemHeight)))
|
var minVisibleRow = Int(floor((offsetRect.minY) / (self.itemSize.height)))
|
||||||
minVisibleRow = max(0, minVisibleRow)
|
minVisibleRow = max(0, minVisibleRow)
|
||||||
let maxVisibleRow = Int(ceil((offsetRect.maxY) / (self.itemHeight)))
|
let maxVisibleRow = Int(ceil((offsetRect.maxY) / (self.itemSize.height)))
|
||||||
|
|
||||||
let minVisibleIndex = minVisibleRow
|
let minVisibleIndex = minVisibleRow
|
||||||
let maxVisibleIndex = maxVisibleRow
|
let maxVisibleIndex = maxVisibleRow
|
||||||
@ -120,7 +102,7 @@ final class ContextResultPanelComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func itemFrame(for index: Int) -> CGRect {
|
func itemFrame(for index: Int) -> CGRect {
|
||||||
return CGRect(origin: CGPoint(x: 0.0, y: self.topInset + CGFloat(index) * self.itemHeight), size: CGSize(width: self.containerSize.width, height: self.itemHeight))
|
return CGRect(origin: CGPoint(x: 0.0, y: self.topInset + CGFloat(index) * self.itemSize.height), size: CGSize(width: self.containerSize.width, height: self.itemSize.height))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,7 +141,7 @@ final class ContextResultPanelComponent: Component {
|
|||||||
self.scrollView = ScrollView()
|
self.scrollView = ScrollView()
|
||||||
self.scrollView.canCancelContentTouches = true
|
self.scrollView.canCancelContentTouches = true
|
||||||
self.scrollView.delaysContentTouches = false
|
self.scrollView.delaysContentTouches = false
|
||||||
self.scrollView.showsVerticalScrollIndicator = true
|
self.scrollView.showsVerticalScrollIndicator = false
|
||||||
self.scrollView.contentInsetAdjustmentBehavior = .never
|
self.scrollView.contentInsetAdjustmentBehavior = .never
|
||||||
self.scrollView.alwaysBounceVertical = true
|
self.scrollView.alwaysBounceVertical = true
|
||||||
self.scrollView.indicatorStyle = .white
|
self.scrollView.indicatorStyle = .white
|
||||||
@ -329,7 +311,7 @@ final class ContextResultPanelComponent: Component {
|
|||||||
bottomInset: 0.0,
|
bottomInset: 0.0,
|
||||||
topInset: 0.0,
|
topInset: 0.0,
|
||||||
sideInset: sideInset,
|
sideInset: sideInset,
|
||||||
itemHeight: measureItemSize.height,
|
itemSize: measureItemSize,
|
||||||
itemCount: component.results.count
|
itemCount: component.results.count
|
||||||
)
|
)
|
||||||
self.itemLayout = itemLayout
|
self.itemLayout = itemLayout
|
||||||
@ -357,11 +339,6 @@ final class ContextResultPanelComponent: Component {
|
|||||||
|
|
||||||
self.ignoreScrolling = false
|
self.ignoreScrolling = false
|
||||||
self.updateScrolling(transition: transition)
|
self.updateScrolling(transition: transition)
|
||||||
|
|
||||||
// component.externalState.minimizedHeight = minimizedHeight
|
|
||||||
|
|
||||||
// let effectiveHeight: CGFloat = minimizedHeight * dismissFraction + (1.0 - dismissFraction) * (60.0 + component.safeInsets.bottom + 1.0)
|
|
||||||
// component.externalState.effectiveHeight = min(minimizedHeight, max(0.0, effectiveHeight))
|
|
||||||
|
|
||||||
return availableSize
|
return availableSize
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import TelegramCore
|
|||||||
import TextFieldComponent
|
import TextFieldComponent
|
||||||
import ChatContextQuery
|
import ChatContextQuery
|
||||||
import AccountContext
|
import AccountContext
|
||||||
|
import TelegramUIPreferences
|
||||||
|
|
||||||
func textInputStateContextQueryRangeAndType(inputState: TextFieldComponent.InputState) -> [(NSRange, PossibleContextQueryTypes, NSRange?)] {
|
func textInputStateContextQueryRangeAndType(inputState: TextFieldComponent.InputState) -> [(NSRange, PossibleContextQueryTypes, NSRange?)] {
|
||||||
return textInputStateContextQueryRangeAndType(inputText: inputState.inputText, selectionRange: inputState.selectionRange)
|
return textInputStateContextQueryRangeAndType(inputText: inputState.inputText, selectionRange: inputState.selectionRange)
|
||||||
@ -40,7 +41,7 @@ func inputContextQueries(_ inputState: TextFieldComponent.InputState) -> [ChatPr
|
|||||||
func contextQueryResultState(context: AccountContext, inputState: TextFieldComponent.InputState, currentQueryStates: inout [ChatPresentationInputQueryKind: (ChatPresentationInputQuery, Disposable)]) -> [ChatPresentationInputQueryKind: ChatContextQueryUpdate] {
|
func contextQueryResultState(context: AccountContext, inputState: TextFieldComponent.InputState, currentQueryStates: inout [ChatPresentationInputQueryKind: (ChatPresentationInputQuery, Disposable)]) -> [ChatPresentationInputQueryKind: ChatContextQueryUpdate] {
|
||||||
let inputQueries = inputContextQueries(inputState).filter({ query in
|
let inputQueries = inputContextQueries(inputState).filter({ query in
|
||||||
switch query {
|
switch query {
|
||||||
case .contextRequest, .command, .emoji:
|
case .contextRequest, .command:
|
||||||
return false
|
return false
|
||||||
default:
|
default:
|
||||||
return true
|
return true
|
||||||
@ -75,6 +76,57 @@ func contextQueryResultState(context: AccountContext, inputState: TextFieldCompo
|
|||||||
|
|
||||||
private func updatedContextQueryResultStateForQuery(context: AccountContext, inputQuery: ChatPresentationInputQuery, previousQuery: ChatPresentationInputQuery?) -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> {
|
private func updatedContextQueryResultStateForQuery(context: AccountContext, inputQuery: ChatPresentationInputQuery, previousQuery: ChatPresentationInputQuery?) -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> {
|
||||||
switch inputQuery {
|
switch inputQuery {
|
||||||
|
case let .emoji(query):
|
||||||
|
var signal: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> = .complete()
|
||||||
|
if let previousQuery = previousQuery {
|
||||||
|
switch previousQuery {
|
||||||
|
case .emoji:
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
signal = .single({ _ in return .stickers([]) })
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
signal = .single({ _ in return .stickers([]) })
|
||||||
|
}
|
||||||
|
|
||||||
|
let stickerConfiguration = context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration])
|
||||||
|
|> map { preferencesView -> StickersSearchConfiguration in
|
||||||
|
let appConfiguration: AppConfiguration = preferencesView.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) ?? .defaultValue
|
||||||
|
return StickersSearchConfiguration.with(appConfiguration: appConfiguration)
|
||||||
|
}
|
||||||
|
let stickerSettings = context.sharedContext.accountManager.transaction { transaction -> StickerSettings in
|
||||||
|
let stickerSettings: StickerSettings = transaction.getSharedData(ApplicationSpecificSharedDataKeys.stickerSettings)?.get(StickerSettings.self) ?? .defaultSettings
|
||||||
|
return stickerSettings
|
||||||
|
}
|
||||||
|
|
||||||
|
let stickers: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> = combineLatest(stickerConfiguration, stickerSettings)
|
||||||
|
|> castError(ChatContextQueryError.self)
|
||||||
|
|> mapToSignal { stickerConfiguration, stickerSettings -> Signal<[FoundStickerItem], ChatContextQueryError> in
|
||||||
|
let scope: SearchStickersScope
|
||||||
|
switch stickerSettings.emojiStickerSuggestionMode {
|
||||||
|
case .none:
|
||||||
|
scope = []
|
||||||
|
case .all:
|
||||||
|
if stickerConfiguration.disableLocalSuggestions {
|
||||||
|
scope = [.remote]
|
||||||
|
} else {
|
||||||
|
scope = [.installed, .remote]
|
||||||
|
}
|
||||||
|
case .installed:
|
||||||
|
scope = [.installed]
|
||||||
|
}
|
||||||
|
return context.engine.stickers.searchStickers(query: [query.basicEmoji.0], scope: scope)
|
||||||
|
|> map { items -> [FoundStickerItem] in
|
||||||
|
return items.items
|
||||||
|
}
|
||||||
|
|> castError(ChatContextQueryError.self)
|
||||||
|
}
|
||||||
|
|> map { stickers -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in
|
||||||
|
return { _ in
|
||||||
|
return .stickers(stickers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return signal |> then(stickers)
|
||||||
case let .hashtag(query):
|
case let .hashtag(query):
|
||||||
var signal: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> = .complete()
|
var signal: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> = .complete()
|
||||||
if let previousQuery = previousQuery {
|
if let previousQuery = previousQuery {
|
||||||
|
|||||||
@ -52,7 +52,9 @@ public final class MessageInputPanelComponent: Component {
|
|||||||
public let nextInputMode: (Bool) -> InputMode?
|
public let nextInputMode: (Bool) -> InputMode?
|
||||||
public let areVoiceMessagesAvailable: Bool
|
public let areVoiceMessagesAvailable: Bool
|
||||||
public let presentController: (ViewController) -> Void
|
public let presentController: (ViewController) -> Void
|
||||||
|
public let presentInGlobalOverlay: (ViewController) -> Void
|
||||||
public let sendMessageAction: () -> Void
|
public let sendMessageAction: () -> Void
|
||||||
|
public let sendStickerAction: (TelegramMediaFile) -> Void
|
||||||
public let setMediaRecordingActive: ((Bool, Bool, Bool) -> Void)?
|
public let setMediaRecordingActive: ((Bool, Bool, Bool) -> Void)?
|
||||||
public let lockMediaRecording: (() -> Void)?
|
public let lockMediaRecording: (() -> Void)?
|
||||||
public let stopAndPreviewMediaRecording: (() -> Void)?
|
public let stopAndPreviewMediaRecording: (() -> Void)?
|
||||||
@ -73,6 +75,7 @@ public final class MessageInputPanelComponent: Component {
|
|||||||
public let displayGradient: Bool
|
public let displayGradient: Bool
|
||||||
public let bottomInset: CGFloat
|
public let bottomInset: CGFloat
|
||||||
public let hideKeyboard: Bool
|
public let hideKeyboard: Bool
|
||||||
|
public let forceIsEditing: Bool
|
||||||
public let disabledPlaceholder: String?
|
public let disabledPlaceholder: String?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
@ -86,7 +89,9 @@ public final class MessageInputPanelComponent: Component {
|
|||||||
nextInputMode: @escaping (Bool) -> InputMode?,
|
nextInputMode: @escaping (Bool) -> InputMode?,
|
||||||
areVoiceMessagesAvailable: Bool,
|
areVoiceMessagesAvailable: Bool,
|
||||||
presentController: @escaping (ViewController) -> Void,
|
presentController: @escaping (ViewController) -> Void,
|
||||||
|
presentInGlobalOverlay: @escaping (ViewController) -> Void,
|
||||||
sendMessageAction: @escaping () -> Void,
|
sendMessageAction: @escaping () -> Void,
|
||||||
|
sendStickerAction: @escaping (TelegramMediaFile) -> Void,
|
||||||
setMediaRecordingActive: ((Bool, Bool, Bool) -> Void)?,
|
setMediaRecordingActive: ((Bool, Bool, Bool) -> Void)?,
|
||||||
lockMediaRecording: (() -> Void)?,
|
lockMediaRecording: (() -> Void)?,
|
||||||
stopAndPreviewMediaRecording: (() -> Void)?,
|
stopAndPreviewMediaRecording: (() -> Void)?,
|
||||||
@ -107,6 +112,7 @@ public final class MessageInputPanelComponent: Component {
|
|||||||
displayGradient: Bool,
|
displayGradient: Bool,
|
||||||
bottomInset: CGFloat,
|
bottomInset: CGFloat,
|
||||||
hideKeyboard: Bool,
|
hideKeyboard: Bool,
|
||||||
|
forceIsEditing: Bool,
|
||||||
disabledPlaceholder: String?
|
disabledPlaceholder: String?
|
||||||
) {
|
) {
|
||||||
self.externalState = externalState
|
self.externalState = externalState
|
||||||
@ -119,7 +125,9 @@ public final class MessageInputPanelComponent: Component {
|
|||||||
self.alwaysDarkWhenHasText = alwaysDarkWhenHasText
|
self.alwaysDarkWhenHasText = alwaysDarkWhenHasText
|
||||||
self.areVoiceMessagesAvailable = areVoiceMessagesAvailable
|
self.areVoiceMessagesAvailable = areVoiceMessagesAvailable
|
||||||
self.presentController = presentController
|
self.presentController = presentController
|
||||||
|
self.presentInGlobalOverlay = presentInGlobalOverlay
|
||||||
self.sendMessageAction = sendMessageAction
|
self.sendMessageAction = sendMessageAction
|
||||||
|
self.sendStickerAction = sendStickerAction
|
||||||
self.setMediaRecordingActive = setMediaRecordingActive
|
self.setMediaRecordingActive = setMediaRecordingActive
|
||||||
self.lockMediaRecording = lockMediaRecording
|
self.lockMediaRecording = lockMediaRecording
|
||||||
self.stopAndPreviewMediaRecording = stopAndPreviewMediaRecording
|
self.stopAndPreviewMediaRecording = stopAndPreviewMediaRecording
|
||||||
@ -140,6 +148,7 @@ public final class MessageInputPanelComponent: Component {
|
|||||||
self.displayGradient = displayGradient
|
self.displayGradient = displayGradient
|
||||||
self.bottomInset = bottomInset
|
self.bottomInset = bottomInset
|
||||||
self.hideKeyboard = hideKeyboard
|
self.hideKeyboard = hideKeyboard
|
||||||
|
self.forceIsEditing = forceIsEditing
|
||||||
self.disabledPlaceholder = disabledPlaceholder
|
self.disabledPlaceholder = disabledPlaceholder
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -204,6 +213,9 @@ public final class MessageInputPanelComponent: Component {
|
|||||||
if lhs.hideKeyboard != rhs.hideKeyboard {
|
if lhs.hideKeyboard != rhs.hideKeyboard {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.forceIsEditing != rhs.forceIsEditing {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if lhs.disabledPlaceholder != rhs.disabledPlaceholder {
|
if lhs.disabledPlaceholder != rhs.disabledPlaceholder {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -248,7 +260,8 @@ public final class MessageInputPanelComponent: Component {
|
|||||||
private var contextQueryStates: [ChatPresentationInputQueryKind: (ChatPresentationInputQuery, Disposable)] = [:]
|
private var contextQueryStates: [ChatPresentationInputQueryKind: (ChatPresentationInputQuery, Disposable)] = [:]
|
||||||
private var contextQueryResults: [ChatPresentationInputQueryKind: ChatPresentationInputQueryResult] = [:]
|
private var contextQueryResults: [ChatPresentationInputQueryKind: ChatPresentationInputQueryResult] = [:]
|
||||||
private var contextQueryResultPanel: ComponentView<Empty>?
|
private var contextQueryResultPanel: ComponentView<Empty>?
|
||||||
private var contextQueryResultPanelExternalState: ContextResultPanelComponent.ExternalState?
|
|
||||||
|
private var stickersResultPanel: ComponentView<Empty>?
|
||||||
|
|
||||||
private var viewForOverlayContent: ViewForOverlayContent?
|
private var viewForOverlayContent: ViewForOverlayContent?
|
||||||
private var currentEmojiSuggestionView: ComponentHostView<Empty>?
|
private var currentEmojiSuggestionView: ComponentHostView<Empty>?
|
||||||
@ -389,6 +402,10 @@ public final class MessageInputPanelComponent: Component {
|
|||||||
self.state?.updated()
|
self.state?.updated()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if result == nil, let stickersResultPanel = self.stickersResultPanel?.view, let panelResult = stickersResultPanel.hitTest(self.convert(point, to: stickersResultPanel), with: event), panelResult !== stickersResultPanel {
|
||||||
|
return panelResult
|
||||||
|
}
|
||||||
|
|
||||||
if result == nil, let contextQueryResultPanel = self.contextQueryResultPanel?.view, let panelResult = contextQueryResultPanel.hitTest(self.convert(point, to: contextQueryResultPanel), with: event), panelResult !== contextQueryResultPanel {
|
if result == nil, let contextQueryResultPanel = self.contextQueryResultPanel?.view, let panelResult = contextQueryResultPanel.hitTest(self.convert(point, to: contextQueryResultPanel), with: event), panelResult !== contextQueryResultPanel {
|
||||||
return panelResult
|
return panelResult
|
||||||
}
|
}
|
||||||
@ -471,6 +488,8 @@ public final class MessageInputPanelComponent: Component {
|
|||||||
environment: {},
|
environment: {},
|
||||||
containerSize: availableTextFieldSize
|
containerSize: availableTextFieldSize
|
||||||
)
|
)
|
||||||
|
let isEditing = self.textFieldExternalState.isEditing || component.forceIsEditing
|
||||||
|
|
||||||
|
|
||||||
let placeholderSize = self.placeholder.update(
|
let placeholderSize = self.placeholder.update(
|
||||||
transition: .immediate,
|
transition: .immediate,
|
||||||
@ -493,7 +512,7 @@ public final class MessageInputPanelComponent: Component {
|
|||||||
environment: {},
|
environment: {},
|
||||||
containerSize: availableTextFieldSize
|
containerSize: availableTextFieldSize
|
||||||
)
|
)
|
||||||
if !self.textFieldExternalState.isEditing && component.setMediaRecordingActive == nil {
|
if !isEditing && component.setMediaRecordingActive == nil {
|
||||||
insets.right = insets.left
|
insets.right = insets.left
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -518,7 +537,7 @@ public final class MessageInputPanelComponent: Component {
|
|||||||
transition.setAlpha(view: self.bottomGradientView, alpha: component.displayGradient ? 1.0 : 0.0)
|
transition.setAlpha(view: self.bottomGradientView, alpha: component.displayGradient ? 1.0 : 0.0)
|
||||||
|
|
||||||
let placeholderOriginX: CGFloat
|
let placeholderOriginX: CGFloat
|
||||||
if self.textFieldExternalState.isEditing || component.style == .story {
|
if isEditing || component.style == .story {
|
||||||
placeholderOriginX = 16.0
|
placeholderOriginX = 16.0
|
||||||
} else {
|
} else {
|
||||||
placeholderOriginX = floorToScreenPixels((availableSize.width - placeholderSize.width) / 2.0)
|
placeholderOriginX = floorToScreenPixels((availableSize.width - placeholderSize.width) / 2.0)
|
||||||
@ -729,14 +748,14 @@ public final class MessageInputPanelComponent: Component {
|
|||||||
|
|
||||||
let inputActionButtonMode: MessageInputActionButtonComponent.Mode
|
let inputActionButtonMode: MessageInputActionButtonComponent.Mode
|
||||||
if case .editor = component.style {
|
if case .editor = component.style {
|
||||||
inputActionButtonMode = self.textFieldExternalState.isEditing ? .apply : .none
|
inputActionButtonMode = isEditing ? .apply : .none
|
||||||
} else {
|
} else {
|
||||||
if hasMediaEditing {
|
if hasMediaEditing {
|
||||||
inputActionButtonMode = .send
|
inputActionButtonMode = .send
|
||||||
} else {
|
} else {
|
||||||
if self.textFieldExternalState.hasText {
|
if self.textFieldExternalState.hasText {
|
||||||
inputActionButtonMode = .send
|
inputActionButtonMode = .send
|
||||||
} else if !self.textFieldExternalState.isEditing && component.forwardAction != nil {
|
} else if !isEditing && component.forwardAction != nil {
|
||||||
inputActionButtonMode = .forward
|
inputActionButtonMode = .forward
|
||||||
} else {
|
} else {
|
||||||
if component.areVoiceMessagesAvailable {
|
if component.areVoiceMessagesAvailable {
|
||||||
@ -831,7 +850,7 @@ public final class MessageInputPanelComponent: Component {
|
|||||||
self.addSubview(inputActionButtonView)
|
self.addSubview(inputActionButtonView)
|
||||||
}
|
}
|
||||||
let inputActionButtonOriginX: CGFloat
|
let inputActionButtonOriginX: CGFloat
|
||||||
if component.setMediaRecordingActive != nil || self.textFieldExternalState.isEditing {
|
if component.setMediaRecordingActive != nil || isEditing {
|
||||||
inputActionButtonOriginX = size.width - insets.right + floorToScreenPixels((insets.right - inputActionButtonSize.width) * 0.5)
|
inputActionButtonOriginX = size.width - insets.right + floorToScreenPixels((insets.right - inputActionButtonSize.width) * 0.5)
|
||||||
} else {
|
} else {
|
||||||
inputActionButtonOriginX = size.width
|
inputActionButtonOriginX = size.width
|
||||||
@ -845,7 +864,7 @@ public final class MessageInputPanelComponent: Component {
|
|||||||
var fieldIconNextX = fieldBackgroundFrame.maxX - 4.0
|
var fieldIconNextX = fieldBackgroundFrame.maxX - 4.0
|
||||||
|
|
||||||
var inputModeVisible = false
|
var inputModeVisible = false
|
||||||
if component.style == .story || self.textFieldExternalState.isEditing {
|
if component.style == .story || isEditing {
|
||||||
inputModeVisible = true
|
inputModeVisible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -996,15 +1015,15 @@ public final class MessageInputPanelComponent: Component {
|
|||||||
transition.setPosition(view: timeoutButtonView, position: timeoutIconFrame.center)
|
transition.setPosition(view: timeoutButtonView, position: timeoutIconFrame.center)
|
||||||
transition.setBounds(view: timeoutButtonView, bounds: CGRect(origin: CGPoint(), size: timeoutIconFrame.size))
|
transition.setBounds(view: timeoutButtonView, bounds: CGRect(origin: CGPoint(), size: timeoutIconFrame.size))
|
||||||
|
|
||||||
transition.setAlpha(view: timeoutButtonView, alpha: self.textFieldExternalState.isEditing ? 0.0 : 1.0)
|
transition.setAlpha(view: timeoutButtonView, alpha: isEditing ? 0.0 : 1.0)
|
||||||
transition.setScale(view: timeoutButtonView, scale: self.textFieldExternalState.isEditing ? 0.1 : 1.0)
|
transition.setScale(view: timeoutButtonView, scale: isEditing ? 0.1 : 1.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var fieldBackgroundIsDark = false
|
var fieldBackgroundIsDark = false
|
||||||
if self.textFieldExternalState.hasText && component.alwaysDarkWhenHasText {
|
if self.textFieldExternalState.hasText && component.alwaysDarkWhenHasText {
|
||||||
fieldBackgroundIsDark = true
|
fieldBackgroundIsDark = true
|
||||||
} else if self.textFieldExternalState.isEditing || component.style == .editor {
|
} else if isEditing || component.style == .editor {
|
||||||
fieldBackgroundIsDark = true
|
fieldBackgroundIsDark = true
|
||||||
}
|
}
|
||||||
self.fieldBackgroundView.updateColor(color: fieldBackgroundIsDark ? UIColor(white: 0.0, alpha: 0.5) : UIColor(white: 1.0, alpha: 0.09), transition: transition.containedViewLayoutTransition)
|
self.fieldBackgroundView.updateColor(color: fieldBackgroundIsDark ? UIColor(white: 0.0, alpha: 0.5) : UIColor(white: 1.0, alpha: 0.09), transition: transition.containedViewLayoutTransition)
|
||||||
@ -1013,7 +1032,7 @@ public final class MessageInputPanelComponent: Component {
|
|||||||
vibrancyPlaceholderView.isHidden = placeholder.isHidden
|
vibrancyPlaceholderView.isHidden = placeholder.isHidden
|
||||||
}
|
}
|
||||||
|
|
||||||
component.externalState.isEditing = self.textFieldExternalState.isEditing
|
component.externalState.isEditing = isEditing
|
||||||
component.externalState.hasText = self.textFieldExternalState.hasText
|
component.externalState.hasText = self.textFieldExternalState.hasText
|
||||||
component.externalState.insertText = { [weak self] text in
|
component.externalState.insertText = { [weak self] text in
|
||||||
if let self, let view = self.textField.view as? TextFieldComponent.View {
|
if let self, let view = self.textField.view as? TextFieldComponent.View {
|
||||||
@ -1177,63 +1196,128 @@ public final class MessageInputPanelComponent: Component {
|
|||||||
let panelLeftInset: CGFloat = max(insets.left, 7.0)
|
let panelLeftInset: CGFloat = max(insets.left, 7.0)
|
||||||
let panelRightInset: CGFloat = max(insets.right, 41.0)
|
let panelRightInset: CGFloat = max(insets.right, 41.0)
|
||||||
|
|
||||||
if let result = self.contextQueryResults[.mention], result.count > 0 && self.textFieldExternalState.isEditing {
|
var contextResults: ContextResultPanelComponent.Results?
|
||||||
|
if let result = self.contextQueryResults[.mention], case let .mentions(mentions) = result, !mentions.isEmpty {
|
||||||
|
contextResults = .mentions(mentions)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let result = self.contextQueryResults[.emoji], case let .stickers(stickers) = result, !stickers.isEmpty {
|
||||||
let availablePanelHeight: CGFloat = 413.0
|
let availablePanelHeight: CGFloat = 413.0
|
||||||
|
|
||||||
var animateIn = false
|
var animateIn = false
|
||||||
let panel: ComponentView<Empty>
|
let panel: ComponentView<Empty>
|
||||||
let externalState: ContextResultPanelComponent.ExternalState
|
|
||||||
var transition = transition
|
var transition = transition
|
||||||
if let current = self.contextQueryResultPanel, let currentState = self.contextQueryResultPanelExternalState {
|
if let current = self.stickersResultPanel {
|
||||||
|
panel = current
|
||||||
|
} else {
|
||||||
|
panel = ComponentView<Empty>()
|
||||||
|
self.stickersResultPanel = panel
|
||||||
|
animateIn = true
|
||||||
|
transition = .immediate
|
||||||
|
}
|
||||||
|
let panelSize = panel.update(
|
||||||
|
transition: transition,
|
||||||
|
component: AnyComponent(StickersResultPanelComponent(
|
||||||
|
context: component.context,
|
||||||
|
theme: component.theme,
|
||||||
|
strings: component.strings,
|
||||||
|
files: stickers.map { $0.file },
|
||||||
|
action: { [weak self] sticker in
|
||||||
|
if let self, let textView = self.textField.view as? TextFieldComponent.View {
|
||||||
|
textView.updateText(NSAttributedString(), selectionRange: 0 ..< 0)
|
||||||
|
self.component?.sendStickerAction(sticker)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
present: { [weak self] c in
|
||||||
|
if let self, let component = self.component {
|
||||||
|
component.presentController(c)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
presentInGlobalOverlay: { [weak self] c in
|
||||||
|
if let self, let component = self.component {
|
||||||
|
component.presentInGlobalOverlay(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: availableSize.width, height: availablePanelHeight)
|
||||||
|
)
|
||||||
|
|
||||||
|
let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: -panelSize.height + 60.0), size: panelSize)
|
||||||
|
if let panelView = panel.view as? StickersResultPanelComponent.View {
|
||||||
|
if panelView.superview == nil {
|
||||||
|
self.insertSubview(panelView, at: 0)
|
||||||
|
}
|
||||||
|
transition.setFrame(view: panelView, frame: panelFrame)
|
||||||
|
|
||||||
|
if animateIn {
|
||||||
|
panelView.animateIn(transition: .spring(duration: 0.4))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if let stickersResultPanel = self.stickersResultPanel?.view as? StickersResultPanelComponent.View {
|
||||||
|
self.stickersResultPanel = nil
|
||||||
|
stickersResultPanel.animateOut(transition: .spring(duration: 0.4), completion: { [weak stickersResultPanel] in
|
||||||
|
stickersResultPanel?.removeFromSuperview()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if let contextResults, isEditing {
|
||||||
|
let availablePanelHeight: CGFloat = 413.0
|
||||||
|
|
||||||
|
var animateIn = false
|
||||||
|
let panel: ComponentView<Empty>
|
||||||
|
var transition = transition
|
||||||
|
if let current = self.contextQueryResultPanel {
|
||||||
panel = current
|
panel = current
|
||||||
externalState = currentState
|
|
||||||
} else {
|
} else {
|
||||||
panel = ComponentView<Empty>()
|
panel = ComponentView<Empty>()
|
||||||
externalState = ContextResultPanelComponent.ExternalState()
|
|
||||||
self.contextQueryResultPanel = panel
|
self.contextQueryResultPanel = panel
|
||||||
self.contextQueryResultPanelExternalState = externalState
|
|
||||||
animateIn = true
|
animateIn = true
|
||||||
transition = .immediate
|
transition = .immediate
|
||||||
}
|
}
|
||||||
let panelSize = panel.update(
|
let panelSize = panel.update(
|
||||||
transition: transition,
|
transition: transition,
|
||||||
component: AnyComponent(ContextResultPanelComponent(
|
component: AnyComponent(ContextResultPanelComponent(
|
||||||
externalState: externalState,
|
|
||||||
context: component.context,
|
context: component.context,
|
||||||
theme: component.theme,
|
theme: component.theme,
|
||||||
strings: component.strings,
|
strings: component.strings,
|
||||||
results: result,
|
results: contextResults,
|
||||||
action: { [weak self] action in
|
action: { [weak self] action in
|
||||||
if let self, case let .mention(peer) = action, let textView = self.textField.view as? TextFieldComponent.View {
|
if let self, let textView = self.textField.view as? TextFieldComponent.View {
|
||||||
let inputState = textView.getInputState()
|
let inputState = textView.getInputState()
|
||||||
|
|
||||||
var mentionQueryRange: NSRange?
|
switch action {
|
||||||
inner: for (range, type, _) in textInputStateContextQueryRangeAndType(inputState: inputState) {
|
case let .mention(peer):
|
||||||
if type == [.mention] {
|
var mentionQueryRange: NSRange?
|
||||||
mentionQueryRange = range
|
inner: for (range, type, _) in textInputStateContextQueryRangeAndType(inputState: inputState) {
|
||||||
break inner
|
if type == [.mention] {
|
||||||
|
mentionQueryRange = range
|
||||||
|
break inner
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
if let range = mentionQueryRange {
|
||||||
if let range = mentionQueryRange {
|
let inputText = NSMutableAttributedString(attributedString: inputState.inputText)
|
||||||
let inputText = NSMutableAttributedString(attributedString: inputState.inputText)
|
if let addressName = peer.addressName, !addressName.isEmpty {
|
||||||
if let addressName = peer.addressName, !addressName.isEmpty {
|
let replacementText = addressName + " "
|
||||||
let replacementText = addressName + " "
|
inputText.replaceCharacters(in: range, with: replacementText)
|
||||||
inputText.replaceCharacters(in: range, with: replacementText)
|
|
||||||
|
let selectionPosition = range.lowerBound + (replacementText as NSString).length
|
||||||
let selectionPosition = range.lowerBound + (replacementText as NSString).length
|
textView.updateText(inputText, selectionRange: selectionPosition ..< selectionPosition)
|
||||||
textView.updateText(inputText, selectionRange: selectionPosition ..< selectionPosition)
|
} else if !peer.compactDisplayTitle.isEmpty {
|
||||||
} else if !peer.compactDisplayTitle.isEmpty {
|
let replacementText = NSMutableAttributedString()
|
||||||
let replacementText = NSMutableAttributedString()
|
replacementText.append(NSAttributedString(string: peer.compactDisplayTitle, attributes: [ChatTextInputAttributes.textMention: ChatTextInputTextMentionAttribute(peerId: peer.id)]))
|
||||||
replacementText.append(NSAttributedString(string: peer.compactDisplayTitle, attributes: [ChatTextInputAttributes.textMention: ChatTextInputTextMentionAttribute(peerId: peer.id)]))
|
replacementText.append(NSAttributedString(string: " "))
|
||||||
replacementText.append(NSAttributedString(string: " "))
|
|
||||||
|
let updatedRange = NSRange(location: range.location - 1, length: range.length + 1)
|
||||||
let updatedRange = NSRange(location: range.location - 1, length: range.length + 1)
|
inputText.replaceCharacters(in: updatedRange, with: replacementText)
|
||||||
inputText.replaceCharacters(in: updatedRange, with: replacementText)
|
|
||||||
|
let selectionPosition = updatedRange.lowerBound + replacementText.length
|
||||||
let selectionPosition = updatedRange.lowerBound + replacementText.length
|
textView.updateText(inputText, selectionRange: selectionPosition ..< selectionPosition)
|
||||||
textView.updateText(inputText, selectionRange: selectionPosition ..< selectionPosition)
|
}
|
||||||
}
|
}
|
||||||
|
case let .hashtag(hashtag):
|
||||||
|
let _ = hashtag
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1309,7 +1393,6 @@ public final class MessageInputPanelComponent: Component {
|
|||||||
|
|
||||||
//self.installEmojiSuggestionPreviewGesture(hostView: currentEmojiSuggestionView)
|
//self.installEmojiSuggestionPreviewGesture(hostView: currentEmojiSuggestionView)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
let globalPosition: CGPoint
|
let globalPosition: CGPoint
|
||||||
if let textView = self.textField.view {
|
if let textView = self.textField.view {
|
||||||
|
|||||||
@ -0,0 +1,528 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Display
|
||||||
|
import ComponentFlow
|
||||||
|
import ComponentDisplayAdapters
|
||||||
|
import SwiftSignalKit
|
||||||
|
import TelegramCore
|
||||||
|
import AccountContext
|
||||||
|
import TelegramPresentationData
|
||||||
|
import PeerListItemComponent
|
||||||
|
import EmojiTextAttachmentView
|
||||||
|
import TextFormat
|
||||||
|
import ContextUI
|
||||||
|
import StickerPeekUI
|
||||||
|
import UndoUI
|
||||||
|
|
||||||
|
final class StickersResultPanelComponent: Component {
|
||||||
|
let context: AccountContext
|
||||||
|
let theme: PresentationTheme
|
||||||
|
let strings: PresentationStrings
|
||||||
|
let files: [TelegramMediaFile]
|
||||||
|
let action: (TelegramMediaFile) -> Void
|
||||||
|
let present: (ViewController) -> Void
|
||||||
|
let presentInGlobalOverlay: (ViewController) -> Void
|
||||||
|
|
||||||
|
init(
|
||||||
|
context: AccountContext,
|
||||||
|
theme: PresentationTheme,
|
||||||
|
strings: PresentationStrings,
|
||||||
|
files: [TelegramMediaFile],
|
||||||
|
action: @escaping (TelegramMediaFile) -> Void,
|
||||||
|
present: @escaping (ViewController) -> Void,
|
||||||
|
presentInGlobalOverlay: @escaping (ViewController) -> Void
|
||||||
|
) {
|
||||||
|
self.context = context
|
||||||
|
self.theme = theme
|
||||||
|
self.strings = strings
|
||||||
|
self.files = files
|
||||||
|
self.action = action
|
||||||
|
self.present = present
|
||||||
|
self.presentInGlobalOverlay = presentInGlobalOverlay
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: StickersResultPanelComponent, rhs: StickersResultPanelComponent) -> Bool {
|
||||||
|
if lhs.context !== rhs.context {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.theme !== rhs.theme {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.strings !== rhs.strings {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.files != rhs.files {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct ItemLayout: Equatable {
|
||||||
|
var containerSize: CGSize
|
||||||
|
var bottomInset: CGFloat
|
||||||
|
var topInset: CGFloat
|
||||||
|
var sideInset: CGFloat
|
||||||
|
var itemSize: CGSize
|
||||||
|
var itemSpacing: CGFloat
|
||||||
|
var itemsPerRow: Int
|
||||||
|
var itemCount: Int
|
||||||
|
|
||||||
|
var contentSize: CGSize
|
||||||
|
|
||||||
|
init(containerSize: CGSize, bottomInset: CGFloat, topInset: CGFloat, sideInset: CGFloat, itemSize: CGSize, itemSpacing: CGFloat, itemsPerRow: Int, itemCount: Int) {
|
||||||
|
self.containerSize = containerSize
|
||||||
|
self.bottomInset = bottomInset
|
||||||
|
self.topInset = topInset
|
||||||
|
self.sideInset = sideInset
|
||||||
|
self.itemSize = itemSize
|
||||||
|
self.itemSpacing = itemSpacing
|
||||||
|
self.itemsPerRow = itemsPerRow
|
||||||
|
self.itemCount = itemCount
|
||||||
|
|
||||||
|
self.contentSize = CGSize(width: containerSize.width, height: topInset + CGFloat(itemCount) * itemSize.height + bottomInset)
|
||||||
|
}
|
||||||
|
|
||||||
|
func visibleItems(for rect: CGRect) -> Range<Int>? {
|
||||||
|
let offsetRect = rect.offsetBy(dx: 0.0, dy: -self.topInset)
|
||||||
|
var minVisibleRow = Int(floor((offsetRect.minY) / (self.itemSize.height + self.itemSpacing)))
|
||||||
|
minVisibleRow = max(0, minVisibleRow)
|
||||||
|
let maxVisibleRow = Int(ceil((offsetRect.maxY) / (self.itemSize.height + self.itemSpacing)))
|
||||||
|
|
||||||
|
let minVisibleIndex = minVisibleRow * self.itemsPerRow
|
||||||
|
let maxVisibleIndex = maxVisibleRow * self.itemsPerRow + self.itemsPerRow
|
||||||
|
|
||||||
|
if maxVisibleIndex >= minVisibleIndex {
|
||||||
|
return minVisibleIndex ..< (maxVisibleIndex + 1)
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func itemFrame(for index: Int) -> CGRect {
|
||||||
|
let rowIndex = Int(floor(CGFloat(index) / CGFloat(self.itemsPerRow)))
|
||||||
|
let columnIndex = index % self.itemsPerRow
|
||||||
|
|
||||||
|
return CGRect(origin: CGPoint(x: self.sideInset + CGFloat(columnIndex) * (self.itemSize.width + self.itemSpacing), y: self.topInset + CGFloat(rowIndex) * (self.itemSize.height + self.itemSpacing)), size: self.itemSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class ScrollView: UIScrollView {
|
||||||
|
override func touchesShouldCancel(in view: UIView) -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class View: UIView, UIScrollViewDelegate, UIGestureRecognizerDelegate {
|
||||||
|
private let backgroundView: BlurredBackgroundView
|
||||||
|
private let containerView: UIView
|
||||||
|
private let scrollView: UIScrollView
|
||||||
|
|
||||||
|
private var itemLayout: ItemLayout?
|
||||||
|
|
||||||
|
private var visibleLayers: [EngineMedia.Id: InlineStickerItemLayer] = [:]
|
||||||
|
private var fadingMaskLayer: FadingMaskLayer?
|
||||||
|
|
||||||
|
private var ignoreScrolling = false
|
||||||
|
|
||||||
|
private var component: StickersResultPanelComponent?
|
||||||
|
private weak var state: EmptyComponentState?
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true)
|
||||||
|
self.backgroundView.isUserInteractionEnabled = false
|
||||||
|
|
||||||
|
self.containerView = UIView()
|
||||||
|
|
||||||
|
self.scrollView = ScrollView()
|
||||||
|
self.scrollView.canCancelContentTouches = true
|
||||||
|
self.scrollView.delaysContentTouches = false
|
||||||
|
self.scrollView.showsVerticalScrollIndicator = false
|
||||||
|
self.scrollView.contentInsetAdjustmentBehavior = .never
|
||||||
|
self.scrollView.alwaysBounceVertical = true
|
||||||
|
self.scrollView.indicatorStyle = .white
|
||||||
|
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.clipsToBounds = true
|
||||||
|
self.scrollView.delegate = self
|
||||||
|
|
||||||
|
self.addSubview(self.backgroundView)
|
||||||
|
self.addSubview(self.containerView)
|
||||||
|
self.containerView.addSubview(self.scrollView)
|
||||||
|
|
||||||
|
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
||||||
|
|
||||||
|
let peekRecognizer = PeekControllerGestureRecognizer(contentAtPoint: { [weak self] point in
|
||||||
|
if let self, let component = self.component {
|
||||||
|
let presentationData = component.strings
|
||||||
|
|
||||||
|
let convertedPoint = self.scrollView.convert(point, from: self)
|
||||||
|
guard self.scrollView.bounds.contains(convertedPoint) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var selectedLayer: InlineStickerItemLayer?
|
||||||
|
for (_, layer) in self.visibleLayers {
|
||||||
|
if layer.frame.contains(convertedPoint) {
|
||||||
|
selectedLayer = layer
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let selectedLayer, let file = selectedLayer.file {
|
||||||
|
return component.context.engine.stickers.isStickerSaved(id: file.fileId)
|
||||||
|
|> deliverOnMainQueue
|
||||||
|
|> map { [weak self] isStarred -> (UIView, CGRect, PeekControllerContent)? in
|
||||||
|
if let self, let component = self.component {
|
||||||
|
let menuItems: [ContextMenuItem] = []
|
||||||
|
let _ = menuItems
|
||||||
|
let _ = presentationData
|
||||||
|
// if strongSelf.peerId != strongSelf.context.account.peerId && strongSelf.peerId?.namespace != Namespaces.Peer.SecretChat {
|
||||||
|
// menuItems.append(.action(ContextMenuActionItem(text: strongSelf.strings.Conversation_SendMessage_SendSilently, icon: { theme in
|
||||||
|
// return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/SilentIcon"), color: theme.actionSheet.primaryTextColor)
|
||||||
|
// }, action: { _, f in
|
||||||
|
// if let strongSelf = self, let peekController = strongSelf.peekController {
|
||||||
|
// if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode {
|
||||||
|
// let _ = controllerInteraction.sendSticker(.standalone(media: item.file), true, false, nil, true, animationNode.view, animationNode.bounds, nil, [])
|
||||||
|
// } else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode {
|
||||||
|
// let _ = controllerInteraction.sendSticker(.standalone(media: item.file), true, false, nil, true, imageNode.view, imageNode.bounds, nil, [])
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// f(.default)
|
||||||
|
// })))
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// menuItems.append(.action(ContextMenuActionItem(text: strongSelf.strings.Conversation_SendMessage_ScheduleMessage, icon: { theme in
|
||||||
|
// return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/ScheduleIcon"), color: theme.actionSheet.primaryTextColor)
|
||||||
|
// }, action: { _, f in
|
||||||
|
// if let strongSelf = self, let peekController = strongSelf.peekController {
|
||||||
|
// if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode {
|
||||||
|
// let _ = controllerInteraction.sendSticker(.standalone(media: item.file), false, true, nil, true, animationNode.view, animationNode.bounds, nil, [])
|
||||||
|
// } else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode {
|
||||||
|
// let _ = controllerInteraction.sendSticker(.standalone(media: item.file), false, true, nil, true, imageNode.view, imageNode.bounds, nil, [])
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// f(.default)
|
||||||
|
// })))
|
||||||
|
|
||||||
|
// menuItems.append(
|
||||||
|
// .action(ContextMenuActionItem(text: isStarred ? presentationData.strings.Stickers_RemoveFromFavorites : presentationData.strings.Stickers_AddToFavorites, icon: { theme in generateTintedImage(image: isStarred ? UIImage(bundleImageName: "Chat/Context Menu/Unfave") : UIImage(bundleImageName: "Chat/Context Menu/Fave"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
|
||||||
|
// f(.default)
|
||||||
|
//
|
||||||
|
// if let self, let component = self.component {
|
||||||
|
// let _ = (component.context.engine.stickers.toggleStickerSaved(file: file, saved: !isStarred)
|
||||||
|
// |> deliverOnMainQueue).start(next: { [weak self] result in
|
||||||
|
// guard let self, let component = self.component else {
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// switch result {
|
||||||
|
// case .generic:
|
||||||
|
// let controller = UndoOverlayController(presentationData: presentationData, content: .sticker(context: component.context, file: file, loop: true, title: nil, text: !isStarred ? presentationData.strings.Conversation_StickerAddedToFavorites : presentationData.strings.Conversation_StickerRemovedFromFavorites, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false })
|
||||||
|
// component.presentInGlobalOverlay(controller)
|
||||||
|
// case let .limitExceeded(limit, premiumLimit):
|
||||||
|
// let premiumConfiguration = PremiumConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 })
|
||||||
|
// let text: String
|
||||||
|
// if limit == premiumLimit || premiumConfiguration.isPremiumDisabled {
|
||||||
|
// text = presentationData.strings.Premium_MaxFavedStickersFinalText
|
||||||
|
// } else {
|
||||||
|
// text = presentationData.strings.Premium_MaxFavedStickersText("\(premiumLimit)").string
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// let controller = UndoOverlayController(presentationData: presentationData, content: .sticker(context: component.context, file: file, loop: true, title: presentationData.strings.Premium_MaxFavedStickersTitle("\(limit)").string, text: text, undoText: nil, customAction: nil), elevatedLayout: false, action: { [weak self] action in
|
||||||
|
// if let self, let component = self.component {
|
||||||
|
// if case .info = action {
|
||||||
|
// let controller = component.context.sharedContext.makePremiumIntroController(context: component.context, source: .savedStickers)
|
||||||
|
// // strongSelf.getControllerInteraction?()?.navigationController()?.pushViewController(controller)
|
||||||
|
// return true
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return false
|
||||||
|
// })
|
||||||
|
// component.presentInGlobalOverlay(controller)
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// }))
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// menuItems.append(
|
||||||
|
// .action(ContextMenuActionItem(text: presentationData.strings.StickerPack_ViewPack, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Sticker"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
|
||||||
|
// f(.default)
|
||||||
|
//
|
||||||
|
// if let self, let component = self.component {
|
||||||
|
// loop: for attribute in file.attributes {
|
||||||
|
// switch attribute {
|
||||||
|
// case let .Sticker(_, packReference, _):
|
||||||
|
// if let packReference = packReference {
|
||||||
|
// let controller = component.context.sharedContext.makeStickerPackScreen(context: component.context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: nil, sendSticker: { [weak self] file, sourceNode, sourceRect in
|
||||||
|
// if let self, let component = self.component {
|
||||||
|
// component.action(file)
|
||||||
|
// return true
|
||||||
|
// } else {
|
||||||
|
// return false
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// component.present(controller)
|
||||||
|
// }
|
||||||
|
// break loop
|
||||||
|
// default:
|
||||||
|
// break
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }))
|
||||||
|
// )
|
||||||
|
return (self, selectedLayer.frame, StickerPreviewPeekContent(context: component.context, theme: component.theme, strings: component.strings, item: .pack(file), menu: menuItems, openPremiumIntro: { [weak self] in
|
||||||
|
guard let self, let component = self.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let controller = component.context.sharedContext.makePremiumIntroController(context: component.context, source: .stickers)
|
||||||
|
component.present(controller)
|
||||||
|
// let controller = PremiumIntroScreen(context: component.context, source: .stickers)
|
||||||
|
// controllerInteraction.navigationController()?.pushViewController(controller)
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, present: { [weak self] content, sourceView, sourceRect in
|
||||||
|
if let self, let component = self.component {
|
||||||
|
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||||
|
let controller = PeekController(presentationData: presentationData, content: content, sourceView: {
|
||||||
|
return (sourceView, sourceRect)
|
||||||
|
})
|
||||||
|
// controller.visibilityUpdated = { [weak self] visible in
|
||||||
|
// self?.previewingStickersPromise.set(visible)
|
||||||
|
// }
|
||||||
|
component.presentInGlobalOverlay(controller)
|
||||||
|
// strongSelf.peekController = controller
|
||||||
|
// strongSelf.getControllerInteraction?()?.presentGlobalOverlayController(controller, nil)
|
||||||
|
return controller
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, updateContent: { [weak self] content in
|
||||||
|
if let self {
|
||||||
|
var item: TelegramMediaFile?
|
||||||
|
if let content = content as? StickerPreviewPeekContent, case let .pack(contentItem) = content.item {
|
||||||
|
item = contentItem
|
||||||
|
}
|
||||||
|
let _ = item
|
||||||
|
let _ = self
|
||||||
|
//strongSelf.updatePreviewingItem(file: item, animated: true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
self.addGestureRecognizer(peekRecognizer)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||||
|
if case .ended = recognizer.state {
|
||||||
|
let location = recognizer.location(in: self.scrollView)
|
||||||
|
if self.scrollView.bounds.contains(location) {
|
||||||
|
var closestFile: (file: TelegramMediaFile, distance: CGFloat)?
|
||||||
|
for (_, itemLayer) in self.visibleLayers {
|
||||||
|
guard let file = itemLayer.file else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if itemLayer.frame.contains(location) {
|
||||||
|
closestFile = (file, 0.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let (file, _) = closestFile {
|
||||||
|
self.component?.action(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func animateIn(transition: Transition) {
|
||||||
|
let offset = self.scrollView.contentOffset.y * -1.0 + 10.0
|
||||||
|
Transition.immediate.setBoundsOrigin(view: self, origin: CGPoint(x: 0.0, y: -offset))
|
||||||
|
transition.setBoundsOrigin(view: self, origin: CGPoint(x: 0.0, y: 0.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
func animateOut(transition: Transition, completion: @escaping () -> Void) {
|
||||||
|
let offset = self.scrollView.contentOffset.y * -1.0 + 10.0
|
||||||
|
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
|
||||||
|
transition.setBoundsOrigin(view: self, origin: CGPoint(x: 0.0, y: -offset), completion: { _ in
|
||||||
|
completion()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||||
|
if !self.ignoreScrolling {
|
||||||
|
self.updateScrolling(transition: .immediate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateScrolling(transition: Transition) {
|
||||||
|
guard let component = self.component, let itemLayout = self.itemLayout else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let visibleBounds = self.scrollView.bounds.insetBy(dx: 0.0, dy: -200.0)
|
||||||
|
|
||||||
|
var synchronousLoad = false
|
||||||
|
if let hint = transition.userData(PeerListItemComponent.TransitionHint.self) {
|
||||||
|
synchronousLoad = hint.synchronousLoad
|
||||||
|
}
|
||||||
|
|
||||||
|
var visibleIds = Set<EngineMedia.Id>()
|
||||||
|
if let range = itemLayout.visibleItems(for: visibleBounds) {
|
||||||
|
for index in range.lowerBound ..< range.upperBound {
|
||||||
|
guard index < component.files.count else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let itemFrame = itemLayout.itemFrame(for: index)
|
||||||
|
|
||||||
|
let item = component.files[index]
|
||||||
|
visibleIds.insert(item.fileId)
|
||||||
|
|
||||||
|
let itemLayer: InlineStickerItemLayer
|
||||||
|
if let current = self.visibleLayers[item.fileId] {
|
||||||
|
itemLayer = current
|
||||||
|
itemLayer.dynamicColor = .white
|
||||||
|
} else {
|
||||||
|
itemLayer = InlineStickerItemLayer(
|
||||||
|
context: component.context,
|
||||||
|
userLocation: .other,
|
||||||
|
attemptSynchronousLoad: synchronousLoad,
|
||||||
|
emoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: item.fileId.id, file: item),
|
||||||
|
file: item,
|
||||||
|
cache: component.context.animationCache,
|
||||||
|
renderer: component.context.animationRenderer,
|
||||||
|
placeholderColor: UIColor(rgb: 0xffffff).mixedWith(UIColor(rgb: 0x1c1c1d), alpha: 0.9),
|
||||||
|
pointSize: itemFrame.size,
|
||||||
|
dynamicColor: .white
|
||||||
|
)
|
||||||
|
self.visibleLayers[item.fileId] = itemLayer
|
||||||
|
self.scrollView.layer.addSublayer(itemLayer)
|
||||||
|
}
|
||||||
|
|
||||||
|
itemLayer.frame = itemFrame
|
||||||
|
|
||||||
|
itemLayer.isVisibleForAnimations = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var removedIds: [EngineMedia.Id] = []
|
||||||
|
for (id, itemLayer) in self.visibleLayers {
|
||||||
|
if !visibleIds.contains(id) {
|
||||||
|
itemLayer.removeFromSuperlayer()
|
||||||
|
removedIds.append(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for id in removedIds {
|
||||||
|
self.visibleLayers.removeValue(forKey: id)
|
||||||
|
}
|
||||||
|
|
||||||
|
let backgroundSize = CGSize(width: self.scrollView.frame.width, height: self.scrollView.frame.height + 20.0)
|
||||||
|
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: 0.0, y: max(0.0, self.scrollView.contentOffset.y * -1.0)), size: backgroundSize))
|
||||||
|
self.backgroundView.update(size: backgroundSize, cornerRadius: 11.0, transition: transition.containedViewLayoutTransition)
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(component: StickersResultPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||||
|
//let itemUpdated = self.component?.results != component.results
|
||||||
|
|
||||||
|
self.component = component
|
||||||
|
self.state = state
|
||||||
|
|
||||||
|
let minimizedHeight = min(availableSize.height, 500.0)
|
||||||
|
|
||||||
|
self.backgroundView.updateColor(color: UIColor(white: 0.0, alpha: 0.7), transition: transition.containedViewLayoutTransition)
|
||||||
|
|
||||||
|
let itemsPerRow = min(8, max(5, Int(availableSize.width / 80)))
|
||||||
|
let sideInset: CGFloat = 2.0
|
||||||
|
let itemSpacing: CGFloat = 2.0
|
||||||
|
let itemSize = floor((availableSize.width - sideInset * 2.0 - itemSpacing * (CGFloat(itemsPerRow) - 1.0)) / CGFloat(itemsPerRow))
|
||||||
|
|
||||||
|
let itemLayout = ItemLayout(
|
||||||
|
containerSize: CGSize(width: availableSize.width, height: minimizedHeight),
|
||||||
|
bottomInset: 9.0,
|
||||||
|
topInset: 9.0,
|
||||||
|
sideInset: sideInset,
|
||||||
|
itemSize: CGSize(width: itemSize, height: itemSize),
|
||||||
|
itemSpacing: itemSpacing,
|
||||||
|
itemsPerRow: itemsPerRow,
|
||||||
|
itemCount: component.files.count
|
||||||
|
)
|
||||||
|
self.itemLayout = itemLayout
|
||||||
|
|
||||||
|
let scrollContentSize = itemLayout.contentSize
|
||||||
|
|
||||||
|
self.ignoreScrolling = true
|
||||||
|
|
||||||
|
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: minimizedHeight)))
|
||||||
|
|
||||||
|
let visibleTopContentHeight = min(scrollContentSize.height, itemSize * 3.0 + 19.0)
|
||||||
|
let topInset = availableSize.height - visibleTopContentHeight
|
||||||
|
|
||||||
|
let scrollContentInsets = UIEdgeInsets(top: topInset, left: 0.0, bottom: 19.0, right: 0.0)
|
||||||
|
let scrollIndicatorInsets = UIEdgeInsets(top: topInset + 17.0, left: 0.0, bottom: 19.0, right: 0.0)
|
||||||
|
if self.scrollView.contentInset != scrollContentInsets {
|
||||||
|
self.scrollView.contentInset = scrollContentInsets
|
||||||
|
}
|
||||||
|
if self.scrollView.scrollIndicatorInsets != scrollIndicatorInsets {
|
||||||
|
self.scrollView.scrollIndicatorInsets = scrollIndicatorInsets
|
||||||
|
}
|
||||||
|
if self.scrollView.contentSize != scrollContentSize {
|
||||||
|
self.scrollView.contentSize = scrollContentSize
|
||||||
|
}
|
||||||
|
|
||||||
|
let maskLayer: FadingMaskLayer
|
||||||
|
if let current = self.fadingMaskLayer {
|
||||||
|
maskLayer = current
|
||||||
|
} else {
|
||||||
|
maskLayer = FadingMaskLayer()
|
||||||
|
self.fadingMaskLayer = maskLayer
|
||||||
|
}
|
||||||
|
if self.containerView.layer.mask == nil {
|
||||||
|
self.containerView.layer.mask = maskLayer
|
||||||
|
}
|
||||||
|
maskLayer.frame = CGRect(origin: .zero, size: self.scrollView.frame.size)
|
||||||
|
|
||||||
|
self.containerView.frame = CGRect(origin: .zero, size: availableSize)
|
||||||
|
|
||||||
|
self.ignoreScrolling = false
|
||||||
|
self.updateScrolling(transition: transition)
|
||||||
|
|
||||||
|
return availableSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeView() -> View {
|
||||||
|
return View(frame: CGRect())
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||||
|
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class FadingMaskLayer: SimpleLayer {
|
||||||
|
let gradientLayer = SimpleLayer()
|
||||||
|
let fillLayer = SimpleLayer()
|
||||||
|
|
||||||
|
override func layoutSublayers() {
|
||||||
|
let gradientHeight: CGFloat = 110.0
|
||||||
|
if self.gradientLayer.contents == nil {
|
||||||
|
self.addSublayer(self.gradientLayer)
|
||||||
|
self.addSublayer(self.fillLayer)
|
||||||
|
|
||||||
|
let gradientImage = generateGradientImage(size: CGSize(width: 1.0, height: gradientHeight), colors: [UIColor.white, UIColor.white, UIColor.white.withAlphaComponent(0.0), UIColor.white.withAlphaComponent(0.0)], locations: [0.0, 0.4, 0.9, 1.0], direction: .vertical)
|
||||||
|
self.gradientLayer.contents = gradientImage?.cgImage
|
||||||
|
self.gradientLayer.contentsGravity = .resize
|
||||||
|
self.fillLayer.backgroundColor = UIColor.white.cgColor
|
||||||
|
}
|
||||||
|
|
||||||
|
self.fillLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: self.bounds.width, height: self.bounds.height - gradientHeight))
|
||||||
|
self.gradientLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: self.bounds.height - gradientHeight), size: CGSize(width: self.bounds.width, height: gradientHeight))
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -71,6 +71,7 @@ swift_library(
|
|||||||
"//submodules/Components/BundleIconComponent",
|
"//submodules/Components/BundleIconComponent",
|
||||||
"//submodules/TinyThumbnail",
|
"//submodules/TinyThumbnail",
|
||||||
"//submodules/ImageBlur",
|
"//submodules/ImageBlur",
|
||||||
|
"//submodules/StickerPackPreviewUI",
|
||||||
|
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
|
|||||||
@ -688,7 +688,7 @@ private final class StoryContainerScreenComponent: Component {
|
|||||||
chatPeerId: nil,
|
chatPeerId: nil,
|
||||||
areCustomEmojiEnabled: true,
|
areCustomEmojiEnabled: true,
|
||||||
hasTrending: false,
|
hasTrending: false,
|
||||||
hasSearch: false,
|
hasSearch: true,
|
||||||
hideBackground: true,
|
hideBackground: true,
|
||||||
sendGif: nil
|
sendGif: nil
|
||||||
)
|
)
|
||||||
@ -847,7 +847,7 @@ private final class StoryContainerScreenComponent: Component {
|
|||||||
var itemSetContainerSafeInsets = environment.safeInsets
|
var itemSetContainerSafeInsets = environment.safeInsets
|
||||||
if case .regular = environment.metrics.widthClass {
|
if case .regular = environment.metrics.widthClass {
|
||||||
let availableHeight = min(1080.0, availableSize.height - max(45.0, environment.safeInsets.bottom) * 2.0)
|
let availableHeight = min(1080.0, availableSize.height - max(45.0, environment.safeInsets.bottom) * 2.0)
|
||||||
let mediaHeight = availableHeight - 40.0
|
let mediaHeight = availableHeight - 60.0
|
||||||
let mediaWidth = floor(mediaHeight * 0.5625)
|
let mediaWidth = floor(mediaHeight * 0.5625)
|
||||||
itemSetContainerSize = CGSize(width: mediaWidth, height: availableHeight)
|
itemSetContainerSize = CGSize(width: mediaWidth, height: availableHeight)
|
||||||
itemSetContainerInsets.top = 0.0
|
itemSetContainerInsets.top = 0.0
|
||||||
@ -886,6 +886,12 @@ private final class StoryContainerScreenComponent: Component {
|
|||||||
environment.controller()?.present(c, in: .window(.root), with: a)
|
environment.controller()?.present(c, in: .window(.root), with: a)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
presentInGlobalOverlay: { [weak self] c, a in
|
||||||
|
guard let self, let environment = self.environment else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
environment.controller()?.presentInGlobalOverlay(c, with: a)
|
||||||
|
},
|
||||||
close: { [weak self] in
|
close: { [weak self] in
|
||||||
guard let self, let environment = self.environment else {
|
guard let self, let environment = self.environment else {
|
||||||
return
|
return
|
||||||
|
|||||||
@ -91,6 +91,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
public let verticalPanFraction: CGFloat
|
public let verticalPanFraction: CGFloat
|
||||||
public let pinchState: PinchState?
|
public let pinchState: PinchState?
|
||||||
public let presentController: (ViewController, Any?) -> Void
|
public let presentController: (ViewController, Any?) -> Void
|
||||||
|
public let presentInGlobalOverlay: (ViewController, Any?) -> Void
|
||||||
public let close: () -> Void
|
public let close: () -> Void
|
||||||
public let navigate: (NavigationDirection) -> Void
|
public let navigate: (NavigationDirection) -> Void
|
||||||
public let delete: () -> Void
|
public let delete: () -> Void
|
||||||
@ -120,6 +121,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
verticalPanFraction: CGFloat,
|
verticalPanFraction: CGFloat,
|
||||||
pinchState: PinchState?,
|
pinchState: PinchState?,
|
||||||
presentController: @escaping (ViewController, Any?) -> Void,
|
presentController: @escaping (ViewController, Any?) -> Void,
|
||||||
|
presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void,
|
||||||
close: @escaping () -> Void,
|
close: @escaping () -> Void,
|
||||||
navigate: @escaping (NavigationDirection) -> Void,
|
navigate: @escaping (NavigationDirection) -> Void,
|
||||||
delete: @escaping () -> Void,
|
delete: @escaping () -> Void,
|
||||||
@ -148,6 +150,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
self.verticalPanFraction = verticalPanFraction
|
self.verticalPanFraction = verticalPanFraction
|
||||||
self.pinchState = pinchState
|
self.pinchState = pinchState
|
||||||
self.presentController = presentController
|
self.presentController = presentController
|
||||||
|
self.presentInGlobalOverlay = presentInGlobalOverlay
|
||||||
self.close = close
|
self.close = close
|
||||||
self.navigate = navigate
|
self.navigate = navigate
|
||||||
self.delete = delete
|
self.delete = delete
|
||||||
@ -355,6 +358,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
self.sendMessageContext = StoryItemSetContainerSendMessage()
|
self.sendMessageContext = StoryItemSetContainerSendMessage()
|
||||||
|
|
||||||
self.itemsContainerView = UIView()
|
self.itemsContainerView = UIView()
|
||||||
|
//self.itemsContainerView.clipsToBounds = true
|
||||||
|
|
||||||
self.scroller = Scroller()
|
self.scroller = Scroller()
|
||||||
self.scroller.alwaysBounceHorizontal = true
|
self.scroller.alwaysBounceHorizontal = true
|
||||||
@ -380,9 +384,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
self.transitionCloneContainerView = UIView()
|
self.transitionCloneContainerView = UIView()
|
||||||
|
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
|
|
||||||
self.clipsToBounds = true
|
|
||||||
|
|
||||||
self.itemsContainerView.addSubview(self.scroller)
|
self.itemsContainerView.addSubview(self.scroller)
|
||||||
self.scroller.delegate = self
|
self.scroller.delegate = self
|
||||||
self.itemsContainerView.addGestureRecognizer(self.scroller.panGestureRecognizer)
|
self.itemsContainerView.addGestureRecognizer(self.scroller.panGestureRecognizer)
|
||||||
@ -581,6 +583,9 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
if hasFirstResponder(self) {
|
if hasFirstResponder(self) {
|
||||||
self.sendMessageContext.currentInputMode = .text
|
self.sendMessageContext.currentInputMode = .text
|
||||||
self.endEditing(true)
|
self.endEditing(true)
|
||||||
|
} else if case .media = self.sendMessageContext.currentInputMode {
|
||||||
|
self.sendMessageContext.currentInputMode = .text
|
||||||
|
self.state?.updated(transition: .spring(duration: 0.4))
|
||||||
} else if self.displayViewList {
|
} else if self.displayViewList {
|
||||||
let point = recognizer.location(in: self)
|
let point = recognizer.location(in: self)
|
||||||
|
|
||||||
@ -1425,9 +1430,9 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
func update(component: StoryItemSetContainerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
func update(component: StoryItemSetContainerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||||
let isFirstTime = self.component == nil
|
let isFirstTime = self.component == nil
|
||||||
|
|
||||||
if let hint = transition.userData(TextFieldComponent.AnimationHint.self), case .textFocusChanged = hint.kind, !hasFirstResponder(self) {
|
// if let hint = transition.userData(TextFieldComponent.AnimationHint.self), case .textFocusChanged = hint.kind, !hasFirstResponder(self) {
|
||||||
self.sendMessageContext.currentInputMode = .text
|
// self.sendMessageContext.currentInputMode = .text
|
||||||
}
|
// }
|
||||||
|
|
||||||
if self.component == nil {
|
if self.component == nil {
|
||||||
self.sendMessageContext.setup(context: component.context, view: self, inputPanelExternalState: self.inputPanelExternalState, keyboardInputData: component.keyboardInputData)
|
self.sendMessageContext.setup(context: component.context, view: self, inputPanelExternalState: self.inputPanelExternalState, keyboardInputData: component.keyboardInputData)
|
||||||
@ -1517,6 +1522,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
disabledPlaceholder = "You can't reply to this story"
|
disabledPlaceholder = "You can't reply to this story"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var keyboardHeight = component.deviceMetrics.standardInputHeight(inLandscape: false)
|
||||||
let keyboardWasHidden = self.inputPanelExternalState.isKeyboardHidden
|
let keyboardWasHidden = self.inputPanelExternalState.isKeyboardHidden
|
||||||
let inputNodeVisible = self.sendMessageContext.currentInputMode == .media || hasFirstResponder(self)
|
let inputNodeVisible = self.sendMessageContext.currentInputMode == .media || hasFirstResponder(self)
|
||||||
self.inputPanel.parentState = state
|
self.inputPanel.parentState = state
|
||||||
@ -1544,12 +1550,24 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
}
|
}
|
||||||
component.presentController(c, nil)
|
component.presentController(c, nil)
|
||||||
},
|
},
|
||||||
|
presentInGlobalOverlay: { [weak self] c in
|
||||||
|
guard let self, let component = self.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
component.presentInGlobalOverlay(c, nil)
|
||||||
|
},
|
||||||
sendMessageAction: { [weak self] in
|
sendMessageAction: { [weak self] in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.sendMessageContext.performSendMessageAction(view: self)
|
self.sendMessageContext.performSendMessageAction(view: self)
|
||||||
},
|
},
|
||||||
|
sendStickerAction: { [weak self] sticker in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.sendMessageContext.performSendStickerAction(view: self, fileReference: .standalone(media: sticker))
|
||||||
|
},
|
||||||
setMediaRecordingActive: { [weak self] isActive, isVideo, sendAction in
|
setMediaRecordingActive: { [weak self] isActive, isVideo, sendAction in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
@ -1634,6 +1652,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
displayGradient: false, //(component.inputHeight != 0.0 || inputNodeVisible) && component.metrics.widthClass != .regular,
|
displayGradient: false, //(component.inputHeight != 0.0 || inputNodeVisible) && component.metrics.widthClass != .regular,
|
||||||
bottomInset: component.inputHeight != 0.0 || inputNodeVisible ? 0.0 : bottomContentInset,
|
bottomInset: component.inputHeight != 0.0 || inputNodeVisible ? 0.0 : bottomContentInset,
|
||||||
hideKeyboard: self.sendMessageContext.currentInputMode == .media,
|
hideKeyboard: self.sendMessageContext.currentInputMode == .media,
|
||||||
|
forceIsEditing: self.sendMessageContext.currentInputMode == .media,
|
||||||
disabledPlaceholder: disabledPlaceholder
|
disabledPlaceholder: disabledPlaceholder
|
||||||
)),
|
)),
|
||||||
environment: {},
|
environment: {},
|
||||||
@ -1646,12 +1665,18 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
inputHeight = component.deviceMetrics.standardInputHeight(inLandscape: false)
|
inputHeight = component.deviceMetrics.standardInputHeight(inLandscape: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let inputMediaNodeHeight = self.sendMessageContext.updateInputMediaNode(inputPanel: self.inputPanel, availableSize: availableSize, bottomInset: component.safeInsets.bottom, inputHeight: component.inputHeight, effectiveInputHeight: inputHeight, metrics: component.metrics, deviceMetrics: component.deviceMetrics, transition: transition)
|
||||||
|
if inputMediaNodeHeight > 0.0 {
|
||||||
|
inputHeight = inputMediaNodeHeight
|
||||||
|
}
|
||||||
|
keyboardHeight = max(keyboardHeight, inputMediaNodeHeight)
|
||||||
|
|
||||||
let inputPanelBackgroundSize = self.inputPanelBackground.update(
|
let inputPanelBackgroundSize = self.inputPanelBackground.update(
|
||||||
transition: transition,
|
transition: transition,
|
||||||
component: AnyComponent(BlurredGradientComponent(position: .bottom, dark: true, tag: nil)),
|
component: AnyComponent(BlurredGradientComponent(position: .bottom, dark: true, tag: nil)),
|
||||||
environment: {},
|
environment: {},
|
||||||
containerSize: CGSize(width: availableSize.width, height: component.deviceMetrics.standardInputHeight(inLandscape: false) + 100.0)
|
containerSize: CGSize(width: availableSize.width, height: keyboardHeight + 100.0)
|
||||||
)
|
)
|
||||||
if let inputPanelBackgroundView = self.inputPanelBackground.view {
|
if let inputPanelBackgroundView = self.inputPanelBackground.view {
|
||||||
if inputPanelBackgroundView.superview == nil {
|
if inputPanelBackgroundView.superview == nil {
|
||||||
@ -1662,8 +1687,6 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
transition.setAlpha(view: inputPanelBackgroundView, alpha: isVisible ? 1.0 : 0.0, delay: isVisible ? 0.0 : 0.4)
|
transition.setAlpha(view: inputPanelBackgroundView, alpha: isVisible ? 1.0 : 0.0, delay: isVisible ? 0.0 : 0.4)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.sendMessageContext.updateInputMediaNode(inputPanel: self.inputPanel, availableSize: availableSize, bottomInset: component.safeInsets.bottom, inputHeight: component.inputHeight, effectiveInputHeight: inputHeight, metrics: component.metrics, deviceMetrics: component.deviceMetrics, transition: transition)
|
|
||||||
|
|
||||||
var viewListInset: CGFloat = 0.0
|
var viewListInset: CGFloat = 0.0
|
||||||
|
|
||||||
var inputPanelBottomInset: CGFloat
|
var inputPanelBottomInset: CGFloat
|
||||||
@ -2295,9 +2318,9 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
if self.voiceMessagesRestrictedTooltipController != nil {
|
if self.voiceMessagesRestrictedTooltipController != nil {
|
||||||
effectiveDisplayReactions = false
|
effectiveDisplayReactions = false
|
||||||
}
|
}
|
||||||
// if self.sendMessageContext.currentInputMode != .text {
|
if self.sendMessageContext.currentInputMode != .text {
|
||||||
// effectiveDisplayReactions = false
|
effectiveDisplayReactions = false
|
||||||
// }
|
}
|
||||||
|
|
||||||
if let reactionContextNode = self.reactionContextNode, reactionContextNode.isReactionSearchActive {
|
if let reactionContextNode = self.reactionContextNode, reactionContextNode.isReactionSearchActive {
|
||||||
effectiveDisplayReactions = true
|
effectiveDisplayReactions = true
|
||||||
@ -2530,7 +2553,8 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
reactionContextNode.animateOut(to: reactionsAnchorRect, animatingOutToReaction: true)
|
reactionContextNode.animateOut(to: reactionsAnchorRect, animatingOutToReaction: true)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
transition.setAlpha(view: reactionContextNode.view, alpha: 0.0, completion: { [weak reactionContextNode] _ in
|
let reactionTransition = Transition.easeInOut(duration: 0.25)
|
||||||
|
reactionTransition.setAlpha(view: reactionContextNode.view, alpha: 0.0, completion: { [weak reactionContextNode] _ in
|
||||||
reactionContextNode?.view.removeFromSuperview()
|
reactionContextNode?.view.removeFromSuperview()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,6 +35,7 @@ import Postbox
|
|||||||
import OverlayStatusController
|
import OverlayStatusController
|
||||||
import PresentationDataUtils
|
import PresentationDataUtils
|
||||||
import TextFieldComponent
|
import TextFieldComponent
|
||||||
|
import StickerPackPreviewUI
|
||||||
|
|
||||||
final class StoryItemSetContainerSendMessage {
|
final class StoryItemSetContainerSendMessage {
|
||||||
enum InputMode {
|
enum InputMode {
|
||||||
@ -127,7 +128,11 @@ final class StoryItemSetContainerSendMessage {
|
|||||||
switchToTextInput: { [weak self] in
|
switchToTextInput: { [weak self] in
|
||||||
if let self {
|
if let self {
|
||||||
self.currentInputMode = .text
|
self.currentInputMode = .text
|
||||||
self.view?.state?.updated(transition: .immediate)
|
if let view = self.view, !hasFirstResponder(view) {
|
||||||
|
let _ = view.activateInput()
|
||||||
|
} else {
|
||||||
|
self.view?.state?.updated(transition: .immediate)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
dismissTextInput: {
|
dismissTextInput: {
|
||||||
@ -156,9 +161,9 @@ final class StoryItemSetContainerSendMessage {
|
|||||||
getNavigationController: {
|
getNavigationController: {
|
||||||
return self.view?.component?.controller()?.navigationController as? NavigationController
|
return self.view?.component?.controller()?.navigationController as? NavigationController
|
||||||
},
|
},
|
||||||
requestLayout: { [weak self] _ in
|
requestLayout: { [weak self] transition in
|
||||||
if let self {
|
if let self {
|
||||||
self.view?.state?.updated()
|
self.view?.state?.updated(transition: Transition(transition))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -179,11 +184,12 @@ final class StoryItemSetContainerSendMessage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateInputMediaNode(inputPanel: ComponentView<Empty>, availableSize: CGSize, bottomInset: CGFloat, inputHeight: CGFloat, effectiveInputHeight: CGFloat, metrics: LayoutMetrics, deviceMetrics: DeviceMetrics, transition: Transition) {
|
func updateInputMediaNode(inputPanel: ComponentView<Empty>, availableSize: CGSize, bottomInset: CGFloat, inputHeight: CGFloat, effectiveInputHeight: CGFloat, metrics: LayoutMetrics, deviceMetrics: DeviceMetrics, transition: Transition) -> CGFloat {
|
||||||
guard let context = self.context, let inputPanelView = inputPanel.view as? MessageInputPanelComponent.View else {
|
guard let context = self.context, let inputPanelView = inputPanel.view as? MessageInputPanelComponent.View else {
|
||||||
return
|
return 0.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var height: CGFloat = 0.0
|
||||||
if let component = self.view?.component, case .media = self.currentInputMode, let inputData = self.inputMediaNodeData {
|
if let component = self.view?.component, case .media = self.currentInputMode, let inputData = self.inputMediaNodeData {
|
||||||
let inputMediaNode: ChatEntityKeyboardInputNode
|
let inputMediaNode: ChatEntityKeyboardInputNode
|
||||||
if let current = self.inputMediaNode {
|
if let current = self.inputMediaNode {
|
||||||
@ -200,10 +206,10 @@ final class StoryItemSetContainerSendMessage {
|
|||||||
stateContext: self.inputMediaNodeStateContext
|
stateContext: self.inputMediaNodeStateContext
|
||||||
)
|
)
|
||||||
inputMediaNode.externalTopPanelContainerImpl = nil
|
inputMediaNode.externalTopPanelContainerImpl = nil
|
||||||
|
inputMediaNode.useExternalSearchContainer = true
|
||||||
if inputMediaNode.view.superview == nil {
|
if inputMediaNode.view.superview == nil {
|
||||||
self.inputMediaNodeBackground.removeAllAnimations()
|
self.inputMediaNodeBackground.removeAllAnimations()
|
||||||
self.inputMediaNodeBackground.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.7).cgColor
|
self.inputMediaNodeBackground.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.7).cgColor
|
||||||
// inputPanelView.superview?.layer.insertSublayer(self.inputMediaNodeBackground, below: inputPanelView.layer)
|
|
||||||
inputPanelView.superview?.insertSubview(inputMediaNode.view, belowSubview: inputPanelView)
|
inputPanelView.superview?.insertSubview(inputMediaNode.view, belowSubview: inputPanelView)
|
||||||
}
|
}
|
||||||
self.inputMediaNode = inputMediaNode
|
self.inputMediaNode = inputMediaNode
|
||||||
@ -241,6 +247,8 @@ final class StoryItemSetContainerSendMessage {
|
|||||||
}
|
}
|
||||||
transition.setFrame(layer: inputMediaNode.layer, frame: inputNodeFrame)
|
transition.setFrame(layer: inputMediaNode.layer, frame: inputNodeFrame)
|
||||||
transition.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeFrame)
|
transition.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeFrame)
|
||||||
|
|
||||||
|
height = heightAndOverflow.0
|
||||||
} else if let inputMediaNode = self.inputMediaNode {
|
} else if let inputMediaNode = self.inputMediaNode {
|
||||||
self.inputMediaNode = nil
|
self.inputMediaNode = nil
|
||||||
|
|
||||||
@ -279,6 +287,8 @@ final class StoryItemSetContainerSendMessage {
|
|||||||
inputPanelView.activateInput()
|
inputPanelView.activateInput()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return height
|
||||||
}
|
}
|
||||||
|
|
||||||
func animateOut(bounds: CGRect) {
|
func animateOut(bounds: CGRect) {
|
||||||
@ -317,7 +327,25 @@ final class StoryItemSetContainerSendMessage {
|
|||||||
let peer = component.slice.peer
|
let peer = component.slice.peer
|
||||||
|
|
||||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||||
let controller = component.controller()
|
let controller = component.controller() as? StoryContainerScreen
|
||||||
|
|
||||||
|
if let navigationController = controller?.navigationController as? NavigationController {
|
||||||
|
var controllers = navigationController.viewControllers
|
||||||
|
for controller in controllers.reversed() {
|
||||||
|
if !(controller is StoryContainerScreen) {
|
||||||
|
controllers.removeLast()
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
navigationController.setViewControllers(controllers, animated: true)
|
||||||
|
|
||||||
|
controller?.window?.forEachController({ controller in
|
||||||
|
if let controller = controller as? StickerPackScreenImpl {
|
||||||
|
controller.dismiss()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
let _ = (component.context.engine.messages.enqueueOutgoingMessage(
|
let _ = (component.context.engine.messages.enqueueOutgoingMessage(
|
||||||
to: peerId,
|
to: peerId,
|
||||||
@ -344,7 +372,12 @@ final class StoryItemSetContainerSendMessage {
|
|||||||
})
|
})
|
||||||
|
|
||||||
self.currentInputMode = .text
|
self.currentInputMode = .text
|
||||||
view.endEditing(true)
|
if hasFirstResponder(view) {
|
||||||
|
view.endEditing(true)
|
||||||
|
} else {
|
||||||
|
view.state?.updated(transition: .spring(duration: 0.3))
|
||||||
|
controller?.requestLayout(forceUpdate: true, transition: .animated(duration: 0.3, curve: .spring))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func performSendGifAction(view: StoryItemSetContainerComponent.View, fileReference: FileMediaReference) {
|
func performSendGifAction(view: StoryItemSetContainerComponent.View, fileReference: FileMediaReference) {
|
||||||
|
|||||||
@ -59,7 +59,7 @@ public final class TextFieldComponent: Component {
|
|||||||
|
|
||||||
public let kind: Kind
|
public let kind: Kind
|
||||||
|
|
||||||
fileprivate init(kind: Kind) {
|
public init(kind: Kind) {
|
||||||
self.kind = kind
|
self.kind = kind
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -57,26 +57,6 @@ func contextQueryResultStateForChatInterfacePresentationState(_ chatPresentation
|
|||||||
return updates
|
return updates
|
||||||
}
|
}
|
||||||
|
|
||||||
struct StickersSearchConfiguration {
|
|
||||||
static var defaultValue: StickersSearchConfiguration {
|
|
||||||
return StickersSearchConfiguration(disableLocalSuggestions: false)
|
|
||||||
}
|
|
||||||
|
|
||||||
public let disableLocalSuggestions: Bool
|
|
||||||
|
|
||||||
fileprivate init(disableLocalSuggestions: Bool) {
|
|
||||||
self.disableLocalSuggestions = disableLocalSuggestions
|
|
||||||
}
|
|
||||||
|
|
||||||
static func with(appConfiguration: AppConfiguration) -> StickersSearchConfiguration {
|
|
||||||
if let data = appConfiguration.data, let suggestOnlyApi = data["stickers_emoji_suggest_only_api"] as? Bool {
|
|
||||||
return StickersSearchConfiguration(disableLocalSuggestions: suggestOnlyApi)
|
|
||||||
} else {
|
|
||||||
return .defaultValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updatedContextQueryResultStateForQuery(context: AccountContext, peer: Peer, chatLocation: ChatLocation, inputQuery: ChatPresentationInputQuery, previousQuery: ChatPresentationInputQuery?, requestBotLocationStatus: @escaping (PeerId) -> Void) -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> {
|
private func updatedContextQueryResultStateForQuery(context: AccountContext, peer: Peer, chatLocation: ChatLocation, inputQuery: ChatPresentationInputQuery, previousQuery: ChatPresentationInputQuery?, requestBotLocationStatus: @escaping (PeerId) -> Void) -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> {
|
||||||
switch inputQuery {
|
switch inputQuery {
|
||||||
case let .emoji(query):
|
case let .emoji(query):
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user