Various improvements

This commit is contained in:
Ilya Laktyushin 2023-07-03 11:52:02 +02:00
parent f87c2ec00f
commit 5b51f35b36
25 changed files with 1122 additions and 287 deletions

View File

@ -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
}
}
}

View File

@ -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)

View File

@ -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,

View File

@ -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))

View File

@ -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 {
@ -1609,9 +1611,10 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
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)

View File

@ -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

View File

@ -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,33 +6776,10 @@ 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
guard let strongSelf = self, completed, let visibleSearchHeader = strongSelf.visibleSearchHeader else {
return
}
if !strongSelf.isSearchActivated && visibleSearchHeader.superview != strongSelf.scrollView {
strongSelf.scrollView.addSubview(visibleSearchHeader)
strongSelf.mirrorContentScrollView.addSubview(visibleSearchHeader.tintContainerView)
}
})
} else {
transition.setFrame(view: visibleSearchHeader, frame: searchHeaderFrame, completion: { [weak self] completed in
if !useOpaqueTheme {
guard let strongSelf = self, completed, let visibleSearchHeader = strongSelf.visibleSearchHeader else {
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) // Temporary workaround for status selection; use a separate search container (see GIF)
if useOpaqueTheme {
if case let .curve(duration, _) = transition.animation, duration != 0.0 { if case let .curve(duration, _) = transition.animation, duration != 0.0 {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + duration, execute: { [weak self] in DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + duration, execute: { [weak self] in
guard let strongSelf = self, let visibleSearchHeader = strongSelf.visibleSearchHeader else { guard let strongSelf = self, let visibleSearchHeader = strongSelf.visibleSearchHeader else {
@ -6816,8 +6797,6 @@ public final class EmojiPagerContentComponent: Component {
self.mirrorContentScrollView.addSubview(visibleSearchHeader.tintContainerView) self.mirrorContentScrollView.addSubview(visibleSearchHeader.tintContainerView)
} }
} }
}
}
} else { } else {
if let visibleSearchHeader = self.visibleSearchHeader { if let visibleSearchHeader = self.visibleSearchHeader {
self.visibleSearchHeader = nil self.visibleSearchHeader = nil
@ -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))
} }
} }

View File

@ -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: {},

View File

@ -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,6 +914,11 @@ public final class EntityKeyboardComponent: Component {
contentType = .stickers contentType = .stickers
} }
if component.useExternalSearchContainer, let containerNode = component.makeSearchContainerNode(contentType) {
let controller = EntitySearchContainerController(containerNode: containerNode)
self.component?.emojiContent?.inputInteractionHolder.inputInteraction?.pushController(controller)
} else {
self.searchComponent = EntitySearchContentComponent( self.searchComponent = EntitySearchContentComponent(
makeContainerNode: { makeContainerNode: {
return component.makeSearchContainerNode(contentType) return component.makeSearchContainerNode(contentType)
@ -916,6 +927,7 @@ public final class EntityKeyboardComponent: Component {
self?.closeSearch() 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)))
} }

View File

@ -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

View File

@ -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

View File

@ -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",

View File

@ -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
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) 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,8 +1108,12 @@ final class MediaEditorScreenComponent: Component {
default: default:
self.currentInputMode = .emoji self.currentInputMode = .emoji
} }
if self.currentInputMode == .text {
self.activateInput()
} else {
self.state?.updated(transition: .immediate) self.state?.updated(transition: .immediate)
} }
}
}, },
timeoutAction: isEditingStory ? nil : { [weak self] view in timeoutAction: isEditingStory ? nil : { [weak self] view in
guard let self, let controller = self.environment?.controller() as? MediaEditorScreen else { guard let self, let controller = self.environment?.controller() as? MediaEditorScreen else {
@ -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)
@ -1062,18 +1179,11 @@ final class MediaEditorScreenComponent: Component {
} }
} }
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
}

View File

@ -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: {},

View File

@ -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",

View File

@ -8,31 +8,19 @@ import AccountContext
import TelegramPresentationData import TelegramPresentationData
import PeerListItemComponent import PeerListItemComponent
extension ChatPresentationInputQueryResult { final class ContextResultPanelComponent: Component {
enum Results: Equatable {
case mentions([EnginePeer])
case hashtags([String])
var count: Int { var count: Int {
switch self { switch self {
case let .stickers(stickers):
return stickers.count
case let .hashtags(hashtags): case let .hashtags(hashtags):
return hashtags.count return hashtags.count
case let .mentions(peers): case let .mentions(peers):
return peers.count return peers.count
case let .commands(commands):
return commands.count
default:
return 0
} }
} }
}
final class ContextResultPanelComponent: Component {
final class ExternalState {
fileprivate(set) var minimizedHeight: CGFloat = 0.0
fileprivate(set) var effectiveHeight: CGFloat = 0.0
init() {
}
} }
enum ResultAction { enum ResultAction {
@ -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
@ -358,11 +340,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
} }
} }

View File

@ -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 {

View File

@ -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,36 +1196,98 @@ 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()
switch action {
case let .mention(peer):
var mentionQueryRange: NSRange? var mentionQueryRange: NSRange?
inner: for (range, type, _) in textInputStateContextQueryRangeAndType(inputState: inputState) { inner: for (range, type, _) in textInputStateContextQueryRangeAndType(inputState: inputState) {
if type == [.mention] { if type == [.mention] {
@ -1235,6 +1316,9 @@ public final class MessageInputPanelComponent: Component {
textView.updateText(inputText, selectionRange: selectionPosition ..< selectionPosition) textView.updateText(inputText, selectionRange: selectionPosition ..< selectionPosition)
} }
} }
case let .hashtag(hashtag):
let _ = hashtag
}
} }
} }
)), )),
@ -1310,7 +1394,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 {
globalPosition = textView.convert(currentEmojiSuggestion.localPosition, to: self) globalPosition = textView.convert(currentEmojiSuggestion.localPosition, to: self)

View File

@ -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))
}
}

View File

@ -71,6 +71,7 @@ swift_library(
"//submodules/Components/BundleIconComponent", "//submodules/Components/BundleIconComponent",
"//submodules/TinyThumbnail", "//submodules/TinyThumbnail",
"//submodules/ImageBlur", "//submodules/ImageBlur",
"//submodules/StickerPackPreviewUI",
], ],
visibility = [ visibility = [

View File

@ -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

View File

@ -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
@ -381,8 +385,6 @@ public final class StoryItemSetContainerComponent: Component {
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: {},
@ -1647,11 +1666,17 @@ public final class StoryItemSetContainerComponent: Component {
} }
} }
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()
}) })
} }

View File

@ -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,8 +128,12 @@ final class StoryItemSetContainerSendMessage {
switchToTextInput: { [weak self] in switchToTextInput: { [weak self] in
if let self { if let self {
self.currentInputMode = .text self.currentInputMode = .text
if let view = self.view, !hasFirstResponder(view) {
let _ = view.activateInput()
} else {
self.view?.state?.updated(transition: .immediate) 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
if hasFirstResponder(view) {
view.endEditing(true) 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) {

View File

@ -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
} }
} }

View File

@ -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):