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
}
}
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
},
forceUpdate: self.controller?.forceNextUpdate ?? false,
containerSize: layout.size
)
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)
}
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) {
super.containerLayoutUpdated(layout, transition: transition)

View File

@ -301,7 +301,8 @@ private final class StickerSelectionComponent: Component {
inputHeight: 0.0,
displayBottomPanel: true,
isExpanded: true,
clipContentToTopPanel: false
clipContentToTopPanel: false,
useExternalSearchContainer: false
)),
environment: {},
forceUpdate: self.forceUpdate,

View File

@ -1272,7 +1272,8 @@ final class AvatarEditorScreenComponent: Component {
inputHeight: 0.0,
displayBottomPanel: false,
isExpanded: true,
clipContentToTopPanel: false
clipContentToTopPanel: false,
useExternalSearchContainer: false
)),
environment: {},
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
}
public var useExternalSearchContainer: Bool = false
private final class GifContext {
private var componentValue: EntityKeyboardGifContent? {
didSet {
@ -1608,10 +1610,11 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
hideBackground: false,
stateContext: nil
)
self.inputDataDisposable = (combineLatest(queue: .mainQueue(),
updatedInputData,
self.gifComponent.get(),
.single(self.currentInputData.gifs) |> then(self.gifComponent.get() |> map(Optional.init)),
self.emojiSearchState.get(),
self.stickerSearchState.get()
)
@ -1980,11 +1983,10 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
mappedMode = .gif
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let searchContainerNode = PaneSearchContainerNode(
context: context,
theme: presentationData.theme,
strings: presentationData.strings,
theme: interfaceState.theme,
strings: interfaceState.strings,
interaction: interaction,
inputNodeInteraction: inputNodeInteraction,
mode: mappedMode,
@ -2008,7 +2010,8 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
inputHeight: inputHeight,
displayBottomPanel: true,
isExpanded: isExpanded && !self.isEmojiSearchActive,
clipContentToTopPanel: self.clipContentToTopPanel
clipContentToTopPanel: self.clipContentToTopPanel,
useExternalSearchContainer: self.useExternalSearchContainer
)),
environment: {},
containerSize: CGSize(width: width, height: expandedHeight)

View File

@ -191,7 +191,8 @@ public final class EmojiStatusSelectionComponent: Component {
inputHeight: 0.0,
displayBottomPanel: false,
isExpanded: false,
clipContentToTopPanel: false
clipContentToTopPanel: false,
useExternalSearchContainer: false
)),
environment: {},
containerSize: availableSize

View File

@ -5201,7 +5201,7 @@ public final class EmojiPagerContentComponent: Component {
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
DispatchQueue.main.async {
scrollView.isScrollEnabled = true
@ -6176,6 +6176,10 @@ public final class EmojiPagerContentComponent: Component {
if let topVisibleGroupId = topVisibleGroupId {
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() {
@ -6254,7 +6258,7 @@ public final class EmojiPagerContentComponent: Component {
if self.layer.mask == nil {
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 {
self.backgroundView.isHidden = true
} else {
@ -6772,10 +6776,13 @@ public final class EmojiPagerContentComponent: Component {
let searchHeaderFrame = CGRect(origin: CGPoint(x: itemLayout.searchInsets.left, y: itemLayout.searchInsets.top), size: CGSize(width: itemLayout.width - itemLayout.searchInsets.left - itemLayout.searchInsets.right, height: itemLayout.searchHeight))
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.attachAnimation(view: visibleSearchHeader, id: "search_transition", completion: { [weak self] completed in
guard let strongSelf = self, completed, let visibleSearchHeader = strongSelf.visibleSearchHeader else {
transition.setFrame(view: visibleSearchHeader, frame: searchHeaderFrame)
// Temporary workaround for status selection; use a separate search container (see GIF)
if case let .curve(duration, _) = transition.animation, duration != 0.0 {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + duration, execute: { [weak self] in
guard let strongSelf = self, let visibleSearchHeader = strongSelf.visibleSearchHeader else {
return
}
@ -6785,37 +6792,9 @@ public final class EmojiPagerContentComponent: Component {
}
})
} 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)
if useOpaqueTheme {
if case let .curve(duration, _) = transition.animation, duration != 0.0 {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + duration, execute: { [weak self] in
guard let strongSelf = self, let visibleSearchHeader = strongSelf.visibleSearchHeader else {
return
}
if !strongSelf.isSearchActivated && visibleSearchHeader.superview != strongSelf.scrollView {
strongSelf.scrollView.addSubview(visibleSearchHeader)
strongSelf.mirrorContentScrollView.addSubview(visibleSearchHeader.tintContainerView)
}
})
} else {
if !self.isSearchActivated && visibleSearchHeader.superview != self.scrollView {
self.scrollView.addSubview(visibleSearchHeader)
self.mirrorContentScrollView.addSubview(visibleSearchHeader.tintContainerView)
}
}
if !self.isSearchActivated && visibleSearchHeader.superview != self.scrollView {
self.scrollView.addSubview(visibleSearchHeader)
self.mirrorContentScrollView.addSubview(visibleSearchHeader.tintContainerView)
}
}
} else {
@ -8553,20 +8532,30 @@ func generateTopicIcon(backgroundColors: [UIColor], strokeColors: [UIColor], tit
private final class FadingMaskLayer: SimpleLayer {
let gradientLayer = SimpleLayer()
let fillLayer = SimpleLayer()
let gradientFillLayer = SimpleLayer()
var internalAlpha: CGFloat = 1.0 {
didSet {
self.gradientFillLayer.opacity = Float(1.0 - self.internalAlpha)
}
}
override func layoutSublayers() {
let gradientHeight: CGFloat = 66.0
if self.gradientLayer.contents == nil {
self.addSublayer(self.gradientLayer)
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)
self.gradientLayer.contents = gradientImage?.cgImage
self.gradientLayer.contentsGravity = .resize
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.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))
}
}

View File

@ -506,6 +506,7 @@ public final class EmojiSearchContent: ASDisplayNode, EntitySearchContainerNode
displayBottomPanel: false,
isExpanded: false,
clipContentToTopPanel: false,
useExternalSearchContainer: false,
hidePanels: true
)),
environment: {},

View File

@ -121,6 +121,7 @@ public final class EntityKeyboardComponent: Component {
public let displayBottomPanel: Bool
public let isExpanded: Bool
public let clipContentToTopPanel: Bool
public let useExternalSearchContainer: Bool
public let hidePanels: Bool
public init(
@ -153,6 +154,7 @@ public final class EntityKeyboardComponent: Component {
displayBottomPanel: Bool,
isExpanded: Bool,
clipContentToTopPanel: Bool,
useExternalSearchContainer: Bool,
hidePanels: Bool = false
) {
self.theme = theme
@ -184,6 +186,7 @@ public final class EntityKeyboardComponent: Component {
self.displayBottomPanel = displayBottomPanel
self.isExpanded = isExpanded
self.clipContentToTopPanel = clipContentToTopPanel
self.useExternalSearchContainer = useExternalSearchContainer
self.hidePanels = hidePanels
}
@ -248,6 +251,9 @@ public final class EntityKeyboardComponent: Component {
if lhs.clipContentToTopPanel != rhs.clipContentToTopPanel {
return false
}
if lhs.useExternalSearchContainer != rhs.useExternalSearchContainer {
return false
}
return true
}
@ -908,14 +914,20 @@ public final class EntityKeyboardComponent: Component {
contentType = .stickers
}
self.searchComponent = EntitySearchContentComponent(
makeContainerNode: {
return component.makeSearchContainerNode(contentType)
},
dismissSearch: { [weak self] in
self?.closeSearch()
}
)
if component.useExternalSearchContainer, let containerNode = component.makeSearchContainerNode(contentType) {
let controller = EntitySearchContainerController(containerNode: containerNode)
self.component?.emojiContent?.inputInteractionHolder.inputInteraction?.pushController(controller)
} else {
self.searchComponent = EntitySearchContentComponent(
makeContainerNode: {
return component.makeSearchContainerNode(contentType)
},
dismissSearch: { [weak self] in
self?.closeSearch()
}
)
}
//self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring)))
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)
}
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 {
let context: AccountContext
let theme: PresentationTheme

View File

@ -364,7 +364,8 @@ private final class TopicIconSelectionComponent: Component {
inputHeight: 0.0,
displayBottomPanel: false,
isExpanded: true,
clipContentToTopPanel: false
clipContentToTopPanel: false,
useExternalSearchContainer: false
)),
environment: {},
containerSize: availableSize

View File

@ -32,6 +32,7 @@ swift_library(
"//submodules/Components/LottieAnimationComponent:LottieAnimationComponent",
"//submodules/Components/BundleIconComponent:BundleIconComponent",
"//submodules/TelegramUI/Components/MessageInputPanelComponent",
"//submodules/TelegramUI/Components/TextFieldComponent",
"//submodules/TelegramUI/Components/ChatInputNode",
"//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode",
"//submodules/TooltipUI",

View File

@ -16,6 +16,7 @@ import MediaEditor
import Photos
import LottieAnimationComponent
import MessageInputPanelComponent
import TextFieldComponent
import EntityKeyboard
import TooltipUI
import BlurredBackgroundComponent
@ -268,7 +269,7 @@ final class MediaEditorScreenComponent: Component {
self.backgroundColor = .clear
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.addSubview(self.fadeView)
@ -305,7 +306,7 @@ final class MediaEditorScreenComponent: Component {
context: context,
chatPeerId: nil,
areCustomEmojiEnabled: true,
hasSearch: false,
hasSearch: true,
hideBackground: true,
sendGif: nil
) |> map { inputData -> ChatEntityKeyboardInputNode.InputData in
@ -336,8 +337,7 @@ final class MediaEditorScreenComponent: Component {
updateChoosingSticker: { _ in },
switchToTextInput: { [weak self] in
if let self {
self.currentInputMode = .text
self.state?.updated(transition: .immediate)
self.activateInput()
}
},
dismissTextInput: {
@ -366,7 +366,7 @@ final class MediaEditorScreenComponent: Component {
getNavigationController: { return nil },
requestLayout: { [weak self] transition in
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.endEditing(true)
if !hasFirstResponder(self) {
if let view = self.inputPanel.view as? MessageInputPanelComponent.View {
view.activateInput()
}
} else {
self.state?.updated(transition: .immediate)
}
}
@objc private func deactivateInput() {
self.currentInputMode = .text
if hasFirstResponder(self) {
self.endEditing(true)
} else {
self.state?.updated(transition: .spring(duration: 0.4).withUserData(TextFieldComponent.AnimationHint(kind: .textFocusChanged)))
}
}
private var animatingButtons = false
@ -739,7 +754,7 @@ final class MediaEditorScreenComponent: Component {
let buttonsAvailableWidth: CGFloat
let buttonsLeftOffset: CGFloat
if isTablet {
buttonsAvailableWidth = previewSize.width + 260.0
buttonsAvailableWidth = previewSize.width + 180.0
buttonsLeftOffset = floorToScreenPixels((availableSize.width - buttonsAvailableWidth) / 2.0)
} else {
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
}
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
switch self.currentInputMode {
case .text:
@ -960,7 +1061,6 @@ final class MediaEditorScreenComponent: Component {
nextInputMode = .emoji
}
let keyboardWasHidden = self.inputPanelExternalState.isKeyboardHidden
self.inputPanel.parentState = state
let inputPanelSize = self.inputPanel.update(
transition: transition,
@ -980,13 +1080,19 @@ final class MediaEditorScreenComponent: Component {
}
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
guard let self else {
return
}
self.currentInputMode = .text
self.endEditing(true)
self.deactivateInput()
},
sendStickerAction: { _ in },
setMediaRecordingActive: nil,
lockMediaRecording: nil,
stopAndPreviewMediaRecording: nil,
@ -1002,7 +1108,11 @@ final class MediaEditorScreenComponent: Component {
default:
self.currentInputMode = .emoji
}
self.state?.updated(transition: .immediate)
if self.currentInputMode == .text {
self.activateInput()
} else {
self.state?.updated(transition: .immediate)
}
}
},
timeoutAction: isEditingStory ? nil : { [weak self] view in
@ -1034,12 +1144,19 @@ final class MediaEditorScreenComponent: Component {
displayGradient: false,
bottomInset: 0.0,
hideKeyboard: self.currentInputMode == .emoji,
forceIsEditing: self.currentInputMode == .emoji,
disabledPlaceholder: nil
)),
environment: {},
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))
if self.inputPanelExternalState.isEditing {
fadeTransition.setAlpha(view: self.fadeView, alpha: 1.0)
@ -1061,19 +1178,12 @@ final class MediaEditorScreenComponent: Component {
mediaEditor?.play()
}
}
var inputHeight = environment.inputHeight
if self.inputPanelExternalState.isEditing {
if self.currentInputMode == .emoji || (inputHeight.isZero && keyboardWasHidden) {
inputHeight = environment.deviceMetrics.standardInputHeight(inLandscape: false)
}
}
let inputPanelBackgroundSize = self.inputPanelBackground.update(
transition: transition,
component: AnyComponent(BlurredGradientComponent(position: .bottom, tag: nil)),
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 inputPanelBackgroundView.superview == nil {
@ -1414,76 +1524,6 @@ final class MediaEditorScreenComponent: Component {
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
return availableSize
@ -1690,7 +1730,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
isStatusSelection: false,
isReactionSelection: false,
isEmojiSelection: true,
hasTrending: false,
hasTrending: true,
topReactionItems: [],
areUnicodeEmojiEnabled: true,
areCustomEmojiEnabled: true,
@ -2644,6 +2684,13 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
private var drawingScreen: DrawingScreen?
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) {
guard let controller = self.controller, !self.isDismissed else {
return
@ -4293,3 +4340,15 @@ public final class BlurredGradientComponent: Component {
func draftPath() -> String {
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,
nextInputMode: { _ in return .stickers },
areVoiceMessagesAvailable: false,
presentController: { _ in
},
sendMessageAction: {
},
presentController: { _ in },
presentInGlobalOverlay: { _ in },
sendMessageAction: { },
sendStickerAction: { _ in },
setMediaRecordingActive: { _, _, _ in },
lockMediaRecording: nil,
stopAndPreviewMediaRecording: nil,
@ -277,6 +277,7 @@ final class StoryPreviewComponent: Component {
displayGradient: false,
bottomInset: 0.0,
hideKeyboard: false,
forceIsEditing: false,
disabledPlaceholder: nil
)),
environment: {},

View File

@ -30,6 +30,8 @@ swift_library(
"//submodules/TelegramUI/Components/Stories/PeerListItemComponent",
"//submodules/TelegramUI/Components/MoreHeaderButton",
"//submodules/TelegramUI/Components/EmojiSuggestionsComponent",
"//submodules/TelegramUI/Components/EmojiTextAttachmentView",
"//submodules/StickerPeekUI",
],
visibility = [
"//visibility:public",

View File

@ -8,30 +8,18 @@ import AccountContext
import TelegramPresentationData
import PeerListItemComponent
extension ChatPresentationInputQueryResult {
var count: Int {
switch self {
case let .stickers(stickers):
return stickers.count
case let .hashtags(hashtags):
return hashtags.count
case let .mentions(peers):
return peers.count
case let .commands(commands):
return commands.count
default:
return 0
}
}
}
final class ContextResultPanelComponent: Component {
final class ExternalState {
fileprivate(set) var minimizedHeight: CGFloat = 0.0
fileprivate(set) var effectiveHeight: CGFloat = 0.0
init() {
enum Results: Equatable {
case mentions([EnginePeer])
case hashtags([String])
var count: Int {
switch self {
case let .hashtags(hashtags):
return hashtags.count
case let .mentions(peers):
return peers.count
}
}
}
@ -40,22 +28,19 @@ final class ContextResultPanelComponent: Component {
case hashtag(String)
}
let externalState: ExternalState
let context: AccountContext
let theme: PresentationTheme
let strings: PresentationStrings
let results: ChatPresentationInputQueryResult
let results: Results
let action: (ResultAction) -> Void
init(
externalState: ExternalState,
context: AccountContext,
theme: PresentationTheme,
strings: PresentationStrings,
results: ChatPresentationInputQueryResult,
results: Results,
action: @escaping (ResultAction) -> Void
) {
self.externalState = externalState
self.context = context
self.theme = theme
self.strings = strings
@ -64,9 +49,6 @@ final class ContextResultPanelComponent: Component {
}
static func ==(lhs: ContextResultPanelComponent, rhs: ContextResultPanelComponent) -> Bool {
if lhs.externalState !== rhs.externalState {
return false
}
if lhs.context !== rhs.context {
return false
}
@ -87,27 +69,27 @@ final class ContextResultPanelComponent: Component {
var bottomInset: CGFloat
var topInset: CGFloat
var sideInset: CGFloat
var itemHeight: CGFloat
var itemSize: CGSize
var itemCount: Int
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.bottomInset = bottomInset
self.topInset = topInset
self.sideInset = sideInset
self.itemHeight = itemHeight
self.itemSize = itemSize
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>? {
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)
let maxVisibleRow = Int(ceil((offsetRect.maxY) / (self.itemHeight)))
let maxVisibleRow = Int(ceil((offsetRect.maxY) / (self.itemSize.height)))
let minVisibleIndex = minVisibleRow
let maxVisibleIndex = maxVisibleRow
@ -120,7 +102,7 @@ final class ContextResultPanelComponent: Component {
}
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.canCancelContentTouches = true
self.scrollView.delaysContentTouches = false
self.scrollView.showsVerticalScrollIndicator = true
self.scrollView.showsVerticalScrollIndicator = false
self.scrollView.contentInsetAdjustmentBehavior = .never
self.scrollView.alwaysBounceVertical = true
self.scrollView.indicatorStyle = .white
@ -329,7 +311,7 @@ final class ContextResultPanelComponent: Component {
bottomInset: 0.0,
topInset: 0.0,
sideInset: sideInset,
itemHeight: measureItemSize.height,
itemSize: measureItemSize,
itemCount: component.results.count
)
self.itemLayout = itemLayout
@ -357,11 +339,6 @@ final class ContextResultPanelComponent: Component {
self.ignoreScrolling = false
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
}

View File

@ -4,6 +4,7 @@ import TelegramCore
import TextFieldComponent
import ChatContextQuery
import AccountContext
import TelegramUIPreferences
func textInputStateContextQueryRangeAndType(inputState: TextFieldComponent.InputState) -> [(NSRange, PossibleContextQueryTypes, NSRange?)] {
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] {
let inputQueries = inputContextQueries(inputState).filter({ query in
switch query {
case .contextRequest, .command, .emoji:
case .contextRequest, .command:
return false
default:
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> {
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):
var signal: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> = .complete()
if let previousQuery = previousQuery {

View File

@ -52,7 +52,9 @@ public final class MessageInputPanelComponent: Component {
public let nextInputMode: (Bool) -> InputMode?
public let areVoiceMessagesAvailable: Bool
public let presentController: (ViewController) -> Void
public let presentInGlobalOverlay: (ViewController) -> Void
public let sendMessageAction: () -> Void
public let sendStickerAction: (TelegramMediaFile) -> Void
public let setMediaRecordingActive: ((Bool, Bool, Bool) -> Void)?
public let lockMediaRecording: (() -> Void)?
public let stopAndPreviewMediaRecording: (() -> Void)?
@ -73,6 +75,7 @@ public final class MessageInputPanelComponent: Component {
public let displayGradient: Bool
public let bottomInset: CGFloat
public let hideKeyboard: Bool
public let forceIsEditing: Bool
public let disabledPlaceholder: String?
public init(
@ -86,7 +89,9 @@ public final class MessageInputPanelComponent: Component {
nextInputMode: @escaping (Bool) -> InputMode?,
areVoiceMessagesAvailable: Bool,
presentController: @escaping (ViewController) -> Void,
presentInGlobalOverlay: @escaping (ViewController) -> Void,
sendMessageAction: @escaping () -> Void,
sendStickerAction: @escaping (TelegramMediaFile) -> Void,
setMediaRecordingActive: ((Bool, Bool, Bool) -> Void)?,
lockMediaRecording: (() -> Void)?,
stopAndPreviewMediaRecording: (() -> Void)?,
@ -107,6 +112,7 @@ public final class MessageInputPanelComponent: Component {
displayGradient: Bool,
bottomInset: CGFloat,
hideKeyboard: Bool,
forceIsEditing: Bool,
disabledPlaceholder: String?
) {
self.externalState = externalState
@ -119,7 +125,9 @@ public final class MessageInputPanelComponent: Component {
self.alwaysDarkWhenHasText = alwaysDarkWhenHasText
self.areVoiceMessagesAvailable = areVoiceMessagesAvailable
self.presentController = presentController
self.presentInGlobalOverlay = presentInGlobalOverlay
self.sendMessageAction = sendMessageAction
self.sendStickerAction = sendStickerAction
self.setMediaRecordingActive = setMediaRecordingActive
self.lockMediaRecording = lockMediaRecording
self.stopAndPreviewMediaRecording = stopAndPreviewMediaRecording
@ -140,6 +148,7 @@ public final class MessageInputPanelComponent: Component {
self.displayGradient = displayGradient
self.bottomInset = bottomInset
self.hideKeyboard = hideKeyboard
self.forceIsEditing = forceIsEditing
self.disabledPlaceholder = disabledPlaceholder
}
@ -204,6 +213,9 @@ public final class MessageInputPanelComponent: Component {
if lhs.hideKeyboard != rhs.hideKeyboard {
return false
}
if lhs.forceIsEditing != rhs.forceIsEditing {
return false
}
if lhs.disabledPlaceholder != rhs.disabledPlaceholder {
return false
}
@ -248,7 +260,8 @@ public final class MessageInputPanelComponent: Component {
private var contextQueryStates: [ChatPresentationInputQueryKind: (ChatPresentationInputQuery, Disposable)] = [:]
private var contextQueryResults: [ChatPresentationInputQueryKind: ChatPresentationInputQueryResult] = [:]
private var contextQueryResultPanel: ComponentView<Empty>?
private var contextQueryResultPanelExternalState: ContextResultPanelComponent.ExternalState?
private var stickersResultPanel: ComponentView<Empty>?
private var viewForOverlayContent: ViewForOverlayContent?
private var currentEmojiSuggestionView: ComponentHostView<Empty>?
@ -389,6 +402,10 @@ public final class MessageInputPanelComponent: Component {
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 {
return panelResult
}
@ -471,6 +488,8 @@ public final class MessageInputPanelComponent: Component {
environment: {},
containerSize: availableTextFieldSize
)
let isEditing = self.textFieldExternalState.isEditing || component.forceIsEditing
let placeholderSize = self.placeholder.update(
transition: .immediate,
@ -493,7 +512,7 @@ public final class MessageInputPanelComponent: Component {
environment: {},
containerSize: availableTextFieldSize
)
if !self.textFieldExternalState.isEditing && component.setMediaRecordingActive == nil {
if !isEditing && component.setMediaRecordingActive == nil {
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)
let placeholderOriginX: CGFloat
if self.textFieldExternalState.isEditing || component.style == .story {
if isEditing || component.style == .story {
placeholderOriginX = 16.0
} else {
placeholderOriginX = floorToScreenPixels((availableSize.width - placeholderSize.width) / 2.0)
@ -729,14 +748,14 @@ public final class MessageInputPanelComponent: Component {
let inputActionButtonMode: MessageInputActionButtonComponent.Mode
if case .editor = component.style {
inputActionButtonMode = self.textFieldExternalState.isEditing ? .apply : .none
inputActionButtonMode = isEditing ? .apply : .none
} else {
if hasMediaEditing {
inputActionButtonMode = .send
} else {
if self.textFieldExternalState.hasText {
inputActionButtonMode = .send
} else if !self.textFieldExternalState.isEditing && component.forwardAction != nil {
} else if !isEditing && component.forwardAction != nil {
inputActionButtonMode = .forward
} else {
if component.areVoiceMessagesAvailable {
@ -831,7 +850,7 @@ public final class MessageInputPanelComponent: Component {
self.addSubview(inputActionButtonView)
}
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)
} else {
inputActionButtonOriginX = size.width
@ -845,7 +864,7 @@ public final class MessageInputPanelComponent: Component {
var fieldIconNextX = fieldBackgroundFrame.maxX - 4.0
var inputModeVisible = false
if component.style == .story || self.textFieldExternalState.isEditing {
if component.style == .story || isEditing {
inputModeVisible = true
}
@ -996,15 +1015,15 @@ public final class MessageInputPanelComponent: Component {
transition.setPosition(view: timeoutButtonView, position: timeoutIconFrame.center)
transition.setBounds(view: timeoutButtonView, bounds: CGRect(origin: CGPoint(), size: timeoutIconFrame.size))
transition.setAlpha(view: timeoutButtonView, alpha: self.textFieldExternalState.isEditing ? 0.0 : 1.0)
transition.setScale(view: timeoutButtonView, scale: self.textFieldExternalState.isEditing ? 0.1 : 1.0)
transition.setAlpha(view: timeoutButtonView, alpha: isEditing ? 0.0 : 1.0)
transition.setScale(view: timeoutButtonView, scale: isEditing ? 0.1 : 1.0)
}
}
var fieldBackgroundIsDark = false
if self.textFieldExternalState.hasText && component.alwaysDarkWhenHasText {
fieldBackgroundIsDark = true
} else if self.textFieldExternalState.isEditing || component.style == .editor {
} else if isEditing || component.style == .editor {
fieldBackgroundIsDark = true
}
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
}
component.externalState.isEditing = self.textFieldExternalState.isEditing
component.externalState.isEditing = isEditing
component.externalState.hasText = self.textFieldExternalState.hasText
component.externalState.insertText = { [weak self] text in
if let self, let view = self.textField.view as? TextFieldComponent.View {
@ -1177,63 +1196,128 @@ public final class MessageInputPanelComponent: Component {
let panelLeftInset: CGFloat = max(insets.left, 7.0)
let 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
var animateIn = false
let panel: ComponentView<Empty>
let externalState: ContextResultPanelComponent.ExternalState
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
externalState = currentState
} else {
panel = ComponentView<Empty>()
externalState = ContextResultPanelComponent.ExternalState()
self.contextQueryResultPanel = panel
self.contextQueryResultPanelExternalState = externalState
animateIn = true
transition = .immediate
}
let panelSize = panel.update(
transition: transition,
component: AnyComponent(ContextResultPanelComponent(
externalState: externalState,
context: component.context,
theme: component.theme,
strings: component.strings,
results: result,
results: contextResults,
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()
var mentionQueryRange: NSRange?
inner: for (range, type, _) in textInputStateContextQueryRangeAndType(inputState: inputState) {
if type == [.mention] {
mentionQueryRange = range
break inner
switch action {
case let .mention(peer):
var mentionQueryRange: NSRange?
inner: for (range, type, _) in textInputStateContextQueryRangeAndType(inputState: inputState) {
if type == [.mention] {
mentionQueryRange = range
break inner
}
}
}
if let range = mentionQueryRange {
let inputText = NSMutableAttributedString(attributedString: inputState.inputText)
if let addressName = peer.addressName, !addressName.isEmpty {
let replacementText = addressName + " "
inputText.replaceCharacters(in: range, with: replacementText)
let selectionPosition = range.lowerBound + (replacementText as NSString).length
textView.updateText(inputText, selectionRange: selectionPosition ..< selectionPosition)
} else if !peer.compactDisplayTitle.isEmpty {
let replacementText = NSMutableAttributedString()
replacementText.append(NSAttributedString(string: peer.compactDisplayTitle, attributes: [ChatTextInputAttributes.textMention: ChatTextInputTextMentionAttribute(peerId: peer.id)]))
replacementText.append(NSAttributedString(string: " "))
let updatedRange = NSRange(location: range.location - 1, length: range.length + 1)
inputText.replaceCharacters(in: updatedRange, with: replacementText)
let selectionPosition = updatedRange.lowerBound + replacementText.length
textView.updateText(inputText, selectionRange: selectionPosition ..< selectionPosition)
if let range = mentionQueryRange {
let inputText = NSMutableAttributedString(attributedString: inputState.inputText)
if let addressName = peer.addressName, !addressName.isEmpty {
let replacementText = addressName + " "
inputText.replaceCharacters(in: range, with: replacementText)
let selectionPosition = range.lowerBound + (replacementText as NSString).length
textView.updateText(inputText, selectionRange: selectionPosition ..< selectionPosition)
} else if !peer.compactDisplayTitle.isEmpty {
let replacementText = NSMutableAttributedString()
replacementText.append(NSAttributedString(string: peer.compactDisplayTitle, attributes: [ChatTextInputAttributes.textMention: ChatTextInputTextMentionAttribute(peerId: peer.id)]))
replacementText.append(NSAttributedString(string: " "))
let updatedRange = NSRange(location: range.location - 1, length: range.length + 1)
inputText.replaceCharacters(in: updatedRange, with: replacementText)
let selectionPosition = updatedRange.lowerBound + replacementText.length
textView.updateText(inputText, selectionRange: selectionPosition ..< selectionPosition)
}
}
case let .hashtag(hashtag):
let _ = hashtag
}
}
}
@ -1309,7 +1393,6 @@ public final class MessageInputPanelComponent: Component {
//self.installEmojiSuggestionPreviewGesture(hostView: currentEmojiSuggestionView)
}
let globalPosition: CGPoint
if let textView = self.textField.view {

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/TinyThumbnail",
"//submodules/ImageBlur",
"//submodules/StickerPackPreviewUI",
],
visibility = [

View File

@ -688,7 +688,7 @@ private final class StoryContainerScreenComponent: Component {
chatPeerId: nil,
areCustomEmojiEnabled: true,
hasTrending: false,
hasSearch: false,
hasSearch: true,
hideBackground: true,
sendGif: nil
)
@ -847,7 +847,7 @@ private final class StoryContainerScreenComponent: Component {
var itemSetContainerSafeInsets = environment.safeInsets
if case .regular = environment.metrics.widthClass {
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)
itemSetContainerSize = CGSize(width: mediaWidth, height: availableHeight)
itemSetContainerInsets.top = 0.0
@ -886,6 +886,12 @@ private final class StoryContainerScreenComponent: Component {
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
guard let self, let environment = self.environment else {
return

View File

@ -91,6 +91,7 @@ public final class StoryItemSetContainerComponent: Component {
public let verticalPanFraction: CGFloat
public let pinchState: PinchState?
public let presentController: (ViewController, Any?) -> Void
public let presentInGlobalOverlay: (ViewController, Any?) -> Void
public let close: () -> Void
public let navigate: (NavigationDirection) -> Void
public let delete: () -> Void
@ -120,6 +121,7 @@ public final class StoryItemSetContainerComponent: Component {
verticalPanFraction: CGFloat,
pinchState: PinchState?,
presentController: @escaping (ViewController, Any?) -> Void,
presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void,
close: @escaping () -> Void,
navigate: @escaping (NavigationDirection) -> Void,
delete: @escaping () -> Void,
@ -148,6 +150,7 @@ public final class StoryItemSetContainerComponent: Component {
self.verticalPanFraction = verticalPanFraction
self.pinchState = pinchState
self.presentController = presentController
self.presentInGlobalOverlay = presentInGlobalOverlay
self.close = close
self.navigate = navigate
self.delete = delete
@ -355,6 +358,7 @@ public final class StoryItemSetContainerComponent: Component {
self.sendMessageContext = StoryItemSetContainerSendMessage()
self.itemsContainerView = UIView()
//self.itemsContainerView.clipsToBounds = true
self.scroller = Scroller()
self.scroller.alwaysBounceHorizontal = true
@ -380,9 +384,7 @@ public final class StoryItemSetContainerComponent: Component {
self.transitionCloneContainerView = UIView()
super.init(frame: frame)
self.clipsToBounds = true
self.itemsContainerView.addSubview(self.scroller)
self.scroller.delegate = self
self.itemsContainerView.addGestureRecognizer(self.scroller.panGestureRecognizer)
@ -581,6 +583,9 @@ public final class StoryItemSetContainerComponent: Component {
if hasFirstResponder(self) {
self.sendMessageContext.currentInputMode = .text
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 {
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 {
let isFirstTime = self.component == nil
if let hint = transition.userData(TextFieldComponent.AnimationHint.self), case .textFocusChanged = hint.kind, !hasFirstResponder(self) {
self.sendMessageContext.currentInputMode = .text
}
// if let hint = transition.userData(TextFieldComponent.AnimationHint.self), case .textFocusChanged = hint.kind, !hasFirstResponder(self) {
// self.sendMessageContext.currentInputMode = .text
// }
if self.component == nil {
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"
}
var keyboardHeight = component.deviceMetrics.standardInputHeight(inLandscape: false)
let keyboardWasHidden = self.inputPanelExternalState.isKeyboardHidden
let inputNodeVisible = self.sendMessageContext.currentInputMode == .media || hasFirstResponder(self)
self.inputPanel.parentState = state
@ -1544,12 +1550,24 @@ public final class StoryItemSetContainerComponent: Component {
}
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
guard let self else {
return
}
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
guard let self else {
return
@ -1634,6 +1652,7 @@ public final class StoryItemSetContainerComponent: Component {
displayGradient: false, //(component.inputHeight != 0.0 || inputNodeVisible) && component.metrics.widthClass != .regular,
bottomInset: component.inputHeight != 0.0 || inputNodeVisible ? 0.0 : bottomContentInset,
hideKeyboard: self.sendMessageContext.currentInputMode == .media,
forceIsEditing: self.sendMessageContext.currentInputMode == .media,
disabledPlaceholder: disabledPlaceholder
)),
environment: {},
@ -1646,12 +1665,18 @@ public final class StoryItemSetContainerComponent: Component {
inputHeight = component.deviceMetrics.standardInputHeight(inLandscape: false)
}
}
let inputMediaNodeHeight = self.sendMessageContext.updateInputMediaNode(inputPanel: self.inputPanel, availableSize: availableSize, bottomInset: component.safeInsets.bottom, inputHeight: component.inputHeight, effectiveInputHeight: inputHeight, metrics: component.metrics, deviceMetrics: component.deviceMetrics, transition: transition)
if inputMediaNodeHeight > 0.0 {
inputHeight = inputMediaNodeHeight
}
keyboardHeight = max(keyboardHeight, inputMediaNodeHeight)
let inputPanelBackgroundSize = self.inputPanelBackground.update(
transition: transition,
component: AnyComponent(BlurredGradientComponent(position: .bottom, dark: true, tag: nil)),
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 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)
}
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 inputPanelBottomInset: CGFloat
@ -2295,9 +2318,9 @@ public final class StoryItemSetContainerComponent: Component {
if self.voiceMessagesRestrictedTooltipController != nil {
effectiveDisplayReactions = false
}
// if self.sendMessageContext.currentInputMode != .text {
// effectiveDisplayReactions = false
// }
if self.sendMessageContext.currentInputMode != .text {
effectiveDisplayReactions = false
}
if let reactionContextNode = self.reactionContextNode, reactionContextNode.isReactionSearchActive {
effectiveDisplayReactions = true
@ -2530,7 +2553,8 @@ public final class StoryItemSetContainerComponent: Component {
reactionContextNode.animateOut(to: reactionsAnchorRect, animatingOutToReaction: true)
}
} 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()
})
}

View File

@ -35,6 +35,7 @@ import Postbox
import OverlayStatusController
import PresentationDataUtils
import TextFieldComponent
import StickerPackPreviewUI
final class StoryItemSetContainerSendMessage {
enum InputMode {
@ -127,7 +128,11 @@ final class StoryItemSetContainerSendMessage {
switchToTextInput: { [weak self] in
if let self {
self.currentInputMode = .text
self.view?.state?.updated(transition: .immediate)
if let view = self.view, !hasFirstResponder(view) {
let _ = view.activateInput()
} else {
self.view?.state?.updated(transition: .immediate)
}
}
},
dismissTextInput: {
@ -156,9 +161,9 @@ final class StoryItemSetContainerSendMessage {
getNavigationController: {
return self.view?.component?.controller()?.navigationController as? NavigationController
},
requestLayout: { [weak self] _ in
requestLayout: { [weak self] transition in
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 {
return
return 0.0
}
var height: CGFloat = 0.0
if let component = self.view?.component, case .media = self.currentInputMode, let inputData = self.inputMediaNodeData {
let inputMediaNode: ChatEntityKeyboardInputNode
if let current = self.inputMediaNode {
@ -200,10 +206,10 @@ final class StoryItemSetContainerSendMessage {
stateContext: self.inputMediaNodeStateContext
)
inputMediaNode.externalTopPanelContainerImpl = nil
inputMediaNode.useExternalSearchContainer = true
if inputMediaNode.view.superview == nil {
self.inputMediaNodeBackground.removeAllAnimations()
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)
}
self.inputMediaNode = inputMediaNode
@ -241,6 +247,8 @@ final class StoryItemSetContainerSendMessage {
}
transition.setFrame(layer: inputMediaNode.layer, frame: inputNodeFrame)
transition.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeFrame)
height = heightAndOverflow.0
} else if let inputMediaNode = self.inputMediaNode {
self.inputMediaNode = nil
@ -279,6 +287,8 @@ final class StoryItemSetContainerSendMessage {
inputPanelView.activateInput()
}
}
return height
}
func animateOut(bounds: CGRect) {
@ -317,7 +327,25 @@ final class StoryItemSetContainerSendMessage {
let peer = component.slice.peer
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(
to: peerId,
@ -344,7 +372,12 @@ final class StoryItemSetContainerSendMessage {
})
self.currentInputMode = .text
view.endEditing(true)
if hasFirstResponder(view) {
view.endEditing(true)
} else {
view.state?.updated(transition: .spring(duration: 0.3))
controller?.requestLayout(forceUpdate: true, transition: .animated(duration: 0.3, curve: .spring))
}
}
func performSendGifAction(view: StoryItemSetContainerComponent.View, fileReference: FileMediaReference) {

View File

@ -59,7 +59,7 @@ public final class TextFieldComponent: Component {
public let kind: Kind
fileprivate init(kind: Kind) {
public init(kind: Kind) {
self.kind = kind
}
}

View File

@ -57,26 +57,6 @@ func contextQueryResultStateForChatInterfacePresentationState(_ chatPresentation
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> {
switch inputQuery {
case let .emoji(query):