Entities keyboard for story replies

This commit is contained in:
Ilya Laktyushin 2023-06-23 15:58:26 +04:00
parent c87ba664ca
commit 84bdc0afb8
11 changed files with 581 additions and 88 deletions

View File

@ -1074,3 +1074,42 @@ public struct AntiSpamBotConfiguration {
}
}
}
public struct StoriesConfiguration {
public enum PostingAvailability {
case enabled
case premium
case disabled
}
static var defaultValue: StoriesConfiguration {
return StoriesConfiguration(posting: .disabled)
}
public let posting: PostingAvailability
fileprivate init(posting: PostingAvailability) {
self.posting = posting
}
public static func with(appConfiguration: AppConfiguration) -> StoriesConfiguration {
#if DEBUG
return StoriesConfiguration(posting: .premium)
#else
if let data = appConfiguration.data, let postingString = data["stories_posting"] as? String {
let posting: PostingAvailability
switch postingString {
case "enabled":
posting = .enabled
case "premium":
posting = .premium
default:
posting = .disabled
}
return StoriesConfiguration(posting: posting)
} else {
return .defaultValue
}
#endif
}
}

View File

@ -159,6 +159,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
private var clearUnseenDownloadsTimer: SwiftSignalKit.Timer?
private(set) var isPremium: Bool = false
private(set) var storyPostingAvailability: StoriesConfiguration.PostingAvailability = .disabled
private var storiesPostingAvailabilityDisposable: Disposable?
private var didSetupTabs = false
@ -697,6 +699,23 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
self.reloadFilters()
}
self.storiesPostingAvailabilityDisposable = (self.context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration])
|> map { view -> AppConfiguration in
let appConfiguration: AppConfiguration = view.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) ?? AppConfiguration.defaultValue
return appConfiguration
}
|> distinctUntilChanged
|> map { appConfiguration -> StoriesConfiguration.PostingAvailability in
let storiesConfiguration = StoriesConfiguration.with(appConfiguration: appConfiguration)
return storiesConfiguration.posting
}
|> deliverOnMainQueue
).start(next: { [weak self] postingAvailability in
if let self {
self.storyPostingAvailability = postingAvailability
}
})
self.updateNavigationMetadata()
}
@ -725,6 +744,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
self.storySubscriptionsDisposable?.dispose()
self.preloadStorySubscriptionsDisposable?.dispose()
self.storyProgressDisposable?.dispose()
self.storiesPostingAvailabilityDisposable?.dispose()
}
private func updateNavigationMetadata() {
@ -2341,19 +2361,27 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
fileprivate func openStoryCamera() {
// guard self.isPremium else {
// let context = self.context
// var replaceImpl: ((ViewController) -> Void)?
// let controller = context.sharedContext.makePremiumDemoController(context: self.context, subject: .stories, action: {
// let controller = context.sharedContext.makePremiumIntroController(context: context, source: .stories)
// replaceImpl?(controller)
// })
// replaceImpl = { [weak controller] c in
// controller?.replace(with: c)
// }
// self.push(controller)
// return
// }
switch self.storyPostingAvailability {
case .premium:
guard self.isPremium else {
let context = self.context
var replaceImpl: ((ViewController) -> Void)?
let controller = context.sharedContext.makePremiumDemoController(context: self.context, subject: .stories, action: {
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .stories)
replaceImpl?(controller)
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
}
self.push(controller)
return
}
case .disabled:
return
default:
break
}
var cameraTransitionIn: StoryCameraTransitionIn?
if let componentView = self.chatListHeaderView() {
if let (transitionView, _) = componentView.storyPeerListView()?.transitionViewForItem(peerId: self.context.account.peerId) {
@ -4754,7 +4782,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
let (accountPeer, limits, _) = result
let isPremium = accountPeer?.isPremium ?? false
let _ = strongSelf.context.engine.peers.markChatListFeaturedFiltersAsSeen().start()
let (_, filterItems) = filterItemsAndTotalCount
@ -4907,6 +4934,17 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
self.storyCameraTransitionInCoordinator = nil
}
}
var isStoryPostingAvailable: Bool {
switch self.storyPostingAvailability {
case .enabled:
return true
case .premium:
return self.isPremium
case .disabled:
return false
}
}
}
private final class ChatListTabBarContextExtractedContentSource: ContextExtractedContentSource {

View File

@ -1214,7 +1214,7 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele
return bandingStart + (1.0 - (1.0 / ((bandedOffset * coefficient / range) + 1.0))) * range
}
if case .compact = layout.metrics.widthClass, self.controller?.isPremium == true {
if case .compact = layout.metrics.widthClass, self.controller?.isStoryPostingAvailable == true {
let cameraIsAlreadyOpened = self.controller?.hasStoryCameraTransition ?? false
if selectedIndex <= 0 && translation.x > 0.0 {
transitionFraction = 0.0

View File

@ -183,18 +183,18 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
return hasPremium
}
public static func inputData(context: AccountContext, chatPeerId: PeerId?, areCustomEmojiEnabled: Bool, sendGif: ((FileMediaReference, UIView, CGRect, Bool, Bool) -> Bool)?) -> Signal<InputData, NoError> {
public static func inputData(context: AccountContext, chatPeerId: PeerId?, areCustomEmojiEnabled: Bool, hasSearch: Bool = true, hideBackground: Bool = false, sendGif: ((FileMediaReference, UIView, CGRect, Bool, Bool) -> Bool)?) -> Signal<InputData, NoError> {
let animationCache = context.animationCache
let animationRenderer = context.animationRenderer
let emojiItems = EmojiPagerContentComponent.emojiInputData(context: context, animationCache: animationCache, animationRenderer: animationRenderer, isStandalone: false, isStatusSelection: false, isReactionSelection: false, isEmojiSelection: true, hasTrending: true, topReactionItems: [], areUnicodeEmojiEnabled: true, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: chatPeerId)
let emojiItems = EmojiPagerContentComponent.emojiInputData(context: context, animationCache: animationCache, animationRenderer: animationRenderer, isStandalone: false, isStatusSelection: false, isReactionSelection: false, isEmojiSelection: true, hasTrending: true, topReactionItems: [], areUnicodeEmojiEnabled: true, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: chatPeerId, hasSearch: hasSearch, hideBackground: hideBackground)
let stickerNamespaces: [ItemCollectionId.Namespace] = [Namespaces.ItemCollection.CloudStickerPacks]
let stickerOrderedItemListCollectionIds: [Int32] = [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudAllPremiumStickers]
let strings = context.sharedContext.currentPresentationData.with({ $0 }).strings
let stickerItems = EmojiPagerContentComponent.stickerInputData(context: context, animationCache: animationCache, animationRenderer: animationRenderer, stickerNamespaces: stickerNamespaces, stickerOrderedItemListCollectionIds: stickerOrderedItemListCollectionIds, chatPeerId: chatPeerId, hasSearch: true, hasTrending: true, forceHasPremium: false)
let stickerItems = EmojiPagerContentComponent.stickerInputData(context: context, animationCache: animationCache, animationRenderer: animationRenderer, stickerNamespaces: stickerNamespaces, stickerOrderedItemListCollectionIds: stickerOrderedItemListCollectionIds, chatPeerId: chatPeerId, hasSearch: hasSearch, hasTrending: true, forceHasPremium: false, hideBackground: hideBackground)
let reactions: Signal<[String], NoError> = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.App())
|> map { appConfiguration -> [String] in
@ -240,7 +240,9 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
openSearch: {
},
updateSearchQuery: { _ in
}
},
hideBackground: hideBackground,
hasSearch: hasSearch
)
// We are going to subscribe to the actual data when the view is loaded
@ -256,7 +258,8 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
displaySearchWithPlaceholder: nil,
searchCategories: nil,
searchInitiallyHidden: true,
searchState: .empty(hasResults: false)
searchState: .empty(hasResults: false),
hideBackground: hideBackground
)
))
@ -429,6 +432,8 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
self.subject = subject
self.gifInputInteraction = gifInputInteraction
let hideBackground = gifInputInteraction.hideBackground
let hasRecentGifs = context.engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: Namespaces.OrderedItemList.CloudRecentGifs))
|> map { savedGifs -> Bool in
return !savedGifs.isEmpty
@ -461,10 +466,11 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
items: items,
isLoading: false,
loadMoreToken: nil,
displaySearchWithPlaceholder: presentationData.strings.Common_Search,
displaySearchWithPlaceholder: gifInputInteraction.hasSearch ? presentationData.strings.Common_Search : nil,
searchCategories: searchCategories,
searchInitiallyHidden: true,
searchState: .empty(hasResults: false)
searchState: .empty(hasResults: false),
hideBackground: hideBackground
)
)
}
@ -494,10 +500,11 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
items: items,
isLoading: isLoading,
loadMoreToken: nil,
displaySearchWithPlaceholder: presentationData.strings.Common_Search,
displaySearchWithPlaceholder: gifInputInteraction.hasSearch ? presentationData.strings.Common_Search : nil,
searchCategories: searchCategories,
searchInitiallyHidden: true,
searchState: .empty(hasResults: false)
searchState: .empty(hasResults: false),
hideBackground: hideBackground
)
)
}
@ -533,10 +540,11 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
items: items,
isLoading: isLoading,
loadMoreToken: loadMoreToken,
displaySearchWithPlaceholder: presentationData.strings.Common_Search,
displaySearchWithPlaceholder: gifInputInteraction.hasSearch ? presentationData.strings.Common_Search : nil,
searchCategories: searchCategories,
searchInitiallyHidden: true,
searchState: .active
searchState: .active,
hideBackground: gifInputInteraction.hideBackground
)
)
}
@ -619,10 +627,11 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
items: items,
isLoading: isLoading,
loadMoreToken: loadMoreToken,
displaySearchWithPlaceholder: presentationData.strings.Common_Search,
displaySearchWithPlaceholder: gifInputInteraction.hasSearch ? presentationData.strings.Common_Search : nil,
searchCategories: searchCategories,
searchInitiallyHidden: true,
searchState: .active
searchState: .active,
hideBackground: gifInputInteraction.hideBackground
)
)
}
@ -1738,7 +1747,9 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
} else {
self.gifMode = .recent
}
}
},
hideBackground: currentInputData.gifs?.component.hideBackground ?? false,
hasSearch: currentInputData.gifs?.component.inputInteraction.hasSearch ?? false
)
self.switchToTextInput = { [weak self] in

View File

@ -148,19 +148,25 @@ public final class GifPagerContentComponent: Component {
public let loadMore: (String) -> Void
public let openSearch: () -> Void
public let updateSearchQuery: ([String]?) -> Void
public let hideBackground: Bool
public let hasSearch: Bool
public init(
performItemAction: @escaping (Item, UIView, CGRect) -> Void,
openGifContextMenu: @escaping (Item, UIView, CGRect, ContextGesture, Bool) -> Void,
loadMore: @escaping (String) -> Void,
openSearch: @escaping () -> Void,
updateSearchQuery: @escaping ([String]?) -> Void
updateSearchQuery: @escaping ([String]?) -> Void,
hideBackground: Bool,
hasSearch: Bool
) {
self.performItemAction = performItemAction
self.openGifContextMenu = openGifContextMenu
self.loadMore = loadMore
self.openSearch = openSearch
self.updateSearchQuery = updateSearchQuery
self.hideBackground = hideBackground
self.hasSearch = hasSearch
}
}
@ -198,6 +204,7 @@ public final class GifPagerContentComponent: Component {
public let searchCategories: EmojiSearchCategories?
public let searchInitiallyHidden: Bool
public let searchState: EmojiPagerContentComponent.SearchState
public let hideBackground: Bool
public init(
context: AccountContext,
@ -209,7 +216,8 @@ public final class GifPagerContentComponent: Component {
displaySearchWithPlaceholder: String?,
searchCategories: EmojiSearchCategories?,
searchInitiallyHidden: Bool,
searchState: EmojiPagerContentComponent.SearchState
searchState: EmojiPagerContentComponent.SearchState,
hideBackground: Bool
) {
self.context = context
self.inputInteraction = inputInteraction
@ -221,6 +229,7 @@ public final class GifPagerContentComponent: Component {
self.searchCategories = searchCategories
self.searchInitiallyHidden = searchInitiallyHidden
self.searchState = searchState
self.hideBackground = hideBackground
}
public static func ==(lhs: GifPagerContentComponent, rhs: GifPagerContentComponent) -> Bool {
@ -254,6 +263,9 @@ public final class GifPagerContentComponent: Component {
if lhs.searchState != rhs.searchState {
return false
}
if lhs.hideBackground != rhs.hideBackground {
return false
}
return true
}
@ -1103,6 +1115,8 @@ public final class GifPagerContentComponent: Component {
transition.setPosition(view: self.scrollClippingView, position: clippingFrame.center)
transition.setBounds(view: self.scrollClippingView, bounds: clippingFrame)
self.backgroundView.isHidden = component.hideBackground
return availableSize
}
}

View File

@ -242,7 +242,7 @@ final class MediaEditorScreenComponent: Component {
private var isDismissed = false
private var isEditingCaption = false
private var currentInputMode: MessageInputPanelComponent.InputMode = .keyboard
private var currentInputMode: MessageInputPanelComponent.InputMode = .text
private var didInitializeInputMediaNodeDataPromise = false
private var inputMediaNodeData: ChatEntityKeyboardInputNode.InputData?
@ -337,7 +337,7 @@ final class MediaEditorScreenComponent: Component {
updateChoosingSticker: { _ in },
switchToTextInput: { [weak self] in
if let self {
self.currentInputMode = .keyboard
self.currentInputMode = .text
self.state?.updated(transition: .immediate)
}
},
@ -373,7 +373,7 @@ final class MediaEditorScreenComponent: Component {
}
@objc private func fadePressed() {
self.currentInputMode = .keyboard
self.currentInputMode = .text
self.endEditing(true)
}
@ -900,10 +900,10 @@ final class MediaEditorScreenComponent: Component {
let nextInputMode: MessageInputPanelComponent.InputMode
switch self.currentInputMode {
case .keyboard:
case .text:
nextInputMode = .emoji
case .emoji:
nextInputMode = .keyboard
nextInputMode = .text
default:
nextInputMode = .emoji
}
@ -918,7 +918,7 @@ final class MediaEditorScreenComponent: Component {
style: .editor,
placeholder: "Add a caption...",
alwaysDarkWhenHasText: false,
nextInputMode: nextInputMode,
nextInputMode: { _ in return nextInputMode },
areVoiceMessagesAvailable: false,
presentController: { [weak self] c in
guard let self, let _ = self.component else {
@ -930,7 +930,7 @@ final class MediaEditorScreenComponent: Component {
guard let self else {
return
}
self.currentInputMode = .keyboard
self.currentInputMode = .text
self.endEditing(true)
},
setMediaRecordingActive: nil,
@ -941,10 +941,10 @@ final class MediaEditorScreenComponent: Component {
inputModeAction: { [weak self] in
if let self {
switch self.currentInputMode {
case .keyboard:
case .text:
self.currentInputMode = .emoji
case .emoji:
self.currentInputMode = .keyboard
self.currentInputMode = .text
default:
self.currentInputMode = .emoji
}
@ -1028,8 +1028,7 @@ final class MediaEditorScreenComponent: Component {
transition.setFrame(view: inputPanelBackgroundView, frame: CGRect(origin: CGPoint(x: 0.0, y: isVisible ? availableSize.height - inputPanelBackgroundSize.height : availableSize.height), size: inputPanelBackgroundSize))
transition.setAlpha(view: inputPanelBackgroundView, alpha: isVisible ? 1.0 : 0.0, delay: isVisible ? 0.0 : 0.4)
}
var isEditingTextEntity = false
var sizeSliderVisible = false
var sizeValue: CGFloat?
@ -1385,7 +1384,7 @@ final class MediaEditorScreenComponent: Component {
stateContext: self.inputMediaNodeStateContext
)
inputMediaNode.externalTopPanelContainerImpl = nil
if let inputPanelView = self.inputPanel.view {
if let inputPanelView = self.inputPanel.view, inputMediaNode.view.superview == nil {
self.insertSubview(inputMediaNode.view, belowSubview: inputPanelView)
}
self.inputMediaNode = inputMediaNode
@ -3833,13 +3832,16 @@ public final class BlurredGradientComponent: Component {
}
let position: Position
let dark: Bool
let tag: AnyObject?
public init(
position: Position,
dark: Bool = false,
tag: AnyObject?
) {
self.position = position
self.dark = dark
self.tag = tag
}
@ -3847,6 +3849,9 @@ public final class BlurredGradientComponent: Component {
if lhs.position != rhs.position {
return false
}
if lhs.dark != rhs.dark {
return false
}
return true
}
@ -3882,7 +3887,12 @@ public final class BlurredGradientComponent: Component {
direction: .vertical
)
self.gradientForeground.colors = [UIColor(rgb: 0x000000, alpha: 0.35).cgColor, UIColor(rgb: 0x000000, alpha: 0.0).cgColor]
if component.dark {
self.gradientForeground.colors = [UIColor(rgb: 0x000000, alpha: 0.6).cgColor, UIColor(rgb: 0x000000, alpha: 0.6).cgColor, UIColor(rgb: 0x000000, alpha: 0.0).cgColor]
self.gradientForeground.locations = [0.0, 0.8, 1.0]
} else {
self.gradientForeground.colors = [UIColor(rgb: 0x000000, alpha: 0.35).cgColor, UIColor(rgb: 0x000000, alpha: 0.0).cgColor]
}
self.gradientForeground.startPoint = CGPoint(x: 0.5, y: component.position == .top ? 0.0 : 1.0)
self.gradientForeground.endPoint = CGPoint(x: 0.5, y: component.position == .top ? 1.0 : 0.0)

View File

@ -251,7 +251,7 @@ final class StoryPreviewComponent: Component {
style: .story,
placeholder: "Reply Privately...",
alwaysDarkWhenHasText: false,
nextInputMode: nil,
nextInputMode: { _ in return nil },
areVoiceMessagesAvailable: false,
presentController: { _ in
},

View File

@ -20,7 +20,7 @@ public final class MessageInputPanelComponent: Component {
}
public enum InputMode: Hashable {
case keyboard
case text
case stickers
case emoji
}
@ -28,6 +28,7 @@ public final class MessageInputPanelComponent: Component {
public final class ExternalState {
public fileprivate(set) var isEditing: Bool = false
public fileprivate(set) var hasText: Bool = false
public fileprivate(set) var isKeyboardHidden: Bool = false
public fileprivate(set) var insertText: (NSAttributedString) -> Void = { _ in }
public fileprivate(set) var deleteBackward: () -> Void = { }
@ -43,7 +44,7 @@ public final class MessageInputPanelComponent: Component {
public let style: Style
public let placeholder: String
public let alwaysDarkWhenHasText: Bool
public let nextInputMode: InputMode?
public let nextInputMode: (Bool) -> InputMode?
public let areVoiceMessagesAvailable: Bool
public let presentController: (ViewController) -> Void
public let sendMessageAction: () -> Void
@ -76,7 +77,7 @@ public final class MessageInputPanelComponent: Component {
style: Style,
placeholder: String,
alwaysDarkWhenHasText: Bool,
nextInputMode: InputMode?,
nextInputMode: @escaping (Bool) -> InputMode?,
areVoiceMessagesAvailable: Bool,
presentController: @escaping (ViewController) -> Void,
sendMessageAction: @escaping () -> Void,
@ -150,9 +151,6 @@ public final class MessageInputPanelComponent: Component {
if lhs.style != rhs.style {
return false
}
if lhs.nextInputMode != rhs.nextInputMode {
return false
}
if lhs.placeholder != rhs.placeholder {
return false
}
@ -239,6 +237,8 @@ public final class MessageInputPanelComponent: Component {
private var contextQueryResultPanel: ComponentView<Empty>?
private var contextQueryResultPanelExternalState: ContextResultPanelComponent.ExternalState?
private var currentInputMode: InputMode?
private var component: MessageInputPanelComponent?
private weak var state: EmptyComponentState?
@ -363,7 +363,7 @@ public final class MessageInputPanelComponent: Component {
let baseFieldHeight: CGFloat = 40.0
let previousComponent = self.component
self.component = component
self.state = state
@ -759,10 +759,15 @@ public final class MessageInputPanelComponent: Component {
let animationName: String
var animationPlay = false
if let inputMode = component.nextInputMode {
let previousInputMode = self.currentInputMode
let inputMode = component.nextInputMode(self.textFieldExternalState.hasText)
self.currentInputMode = inputMode
if let inputMode {
self.currentInputMode = inputMode
switch inputMode {
case .keyboard:
if let previousInputMode = previousComponent?.nextInputMode {
case .text:
if let previousInputMode {
if case .stickers = previousInputMode {
animationName = "input_anim_stickerToKey"
animationPlay = true
@ -776,8 +781,8 @@ public final class MessageInputPanelComponent: Component {
animationName = "input_anim_stickerToKey"
}
case .stickers:
if let previousInputMode = previousComponent?.nextInputMode {
if case .keyboard = previousInputMode {
if let previousInputMode {
if case .text = previousInputMode {
animationName = "input_anim_keyToSticker"
animationPlay = true
} else if case .emoji = previousInputMode {
@ -790,8 +795,8 @@ public final class MessageInputPanelComponent: Component {
animationName = "input_anim_keyToSticker"
}
case .emoji:
if let previousInputMode = previousComponent?.nextInputMode {
if case .keyboard = previousInputMode {
if let previousInputMode {
if case .text = previousInputMode {
animationName = "input_anim_keyToSmile"
animationPlay = true
} else if case .stickers = previousInputMode {
@ -927,6 +932,7 @@ public final class MessageInputPanelComponent: Component {
view.deleteBackward()
}
}
component.externalState.isKeyboardHidden = component.hideKeyboard
if hasMediaRecording {
if let dismissingMediaRecordingPanel = self.dismissingMediaRecordingPanel {

View File

@ -778,6 +778,7 @@ private final class StoryContainerScreenComponent: Component {
safeInsets: itemSetContainerSafeInsets,
inputHeight: environment.inputHeight,
metrics: environment.metrics,
deviceMetrics: environment.deviceMetrics,
isProgressPaused: isProgressPaused || i != focusedIndex,
hideUI: (i == focusedIndex && (self.itemSetPanState?.didBegin == false || self.itemSetPinchState != nil)),
visibilityFraction: 1.0 - abs(panFraction + cubeAdditionalRotationFraction),

View File

@ -23,6 +23,8 @@ import PlainButtonComponent
import TooltipUI
import PresentationDataUtils
import PeerReportScreen
import ChatEntityKeyboardInputNode
import TextFieldComponent
public final class StoryItemSetContainerComponent: Component {
public final class ExternalState {
@ -60,6 +62,7 @@ public final class StoryItemSetContainerComponent: Component {
public let safeInsets: UIEdgeInsets
public let inputHeight: CGFloat
public let metrics: LayoutMetrics
public let deviceMetrics: DeviceMetrics
public let isProgressPaused: Bool
public let hideUI: Bool
public let visibilityFraction: CGFloat
@ -84,6 +87,7 @@ public final class StoryItemSetContainerComponent: Component {
safeInsets: UIEdgeInsets,
inputHeight: CGFloat,
metrics: LayoutMetrics,
deviceMetrics: DeviceMetrics,
isProgressPaused: Bool,
hideUI: Bool,
visibilityFraction: CGFloat,
@ -107,6 +111,7 @@ public final class StoryItemSetContainerComponent: Component {
self.safeInsets = safeInsets
self.inputHeight = inputHeight
self.metrics = metrics
self.deviceMetrics = deviceMetrics
self.isProgressPaused = isProgressPaused
self.hideUI = hideUI
self.visibilityFraction = visibilityFraction
@ -146,6 +151,9 @@ public final class StoryItemSetContainerComponent: Component {
if lhs.metrics != rhs.metrics {
return false
}
if lhs.deviceMetrics != rhs.deviceMetrics {
return false
}
if lhs.isProgressPaused != rhs.isProgressPaused {
return false
}
@ -245,6 +253,7 @@ public final class StoryItemSetContainerComponent: Component {
let inputBackground = ComponentView<Empty>()
let inputPanel = ComponentView<Empty>()
let inputPanelExternalState = MessageInputPanelComponent.ExternalState()
private let inputPanelBackground = ComponentView<Empty>()
var displayViewList: Bool = false
var viewList: ViewList?
@ -494,6 +503,7 @@ public final class StoryItemSetContainerComponent: Component {
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state, let component = self.component, let itemLayout = self.itemLayout {
if hasFirstResponder(self) {
self.sendMessageContext.currentInputMode = .text
self.endEditing(true)
} else if self.displayViewList {
self.displayViewList = false
@ -567,6 +577,11 @@ public final class StoryItemSetContainerComponent: Component {
if self.sendMessageContext.shareController != nil {
return true
}
if let navigationController = component.controller()?.navigationController as? NavigationController {
if !(navigationController.topViewController is StoryContainerScreen) {
return true
}
}
if let captionItem = self.captionItem, captionItem.externalState.isExpanded {
return true
}
@ -765,6 +780,8 @@ public final class StoryItemSetContainerComponent: Component {
func animateOut(transitionOut: StoryContainerScreen.TransitionOut, transitionCloneMasterView: UIView, completion: @escaping () -> Void) {
var cleanups: [() -> Void] = []
self.sendMessageContext.animateOut(bounds: self.bounds)
if let inputPanelView = self.inputPanel.view {
inputPanelView.layer.animatePosition(
from: CGPoint(),
@ -776,6 +793,17 @@ public final class StoryItemSetContainerComponent: Component {
)
inputPanelView.layer.animateAlpha(from: inputPanelView.alpha, to: 0.0, duration: 0.3, removeOnCompletion: false)
}
if let inputPanelBackground = self.inputPanelBackground.view {
inputPanelBackground.layer.animatePosition(
from: CGPoint(),
to: CGPoint(x: 0.0, y: self.bounds.height - inputPanelBackground.frame.minY),
duration: 0.3,
timingFunction: kCAMediaTimingFunctionSpring,
removeOnCompletion: false,
additive: true
)
inputPanelBackground.layer.animateAlpha(from: inputPanelBackground.alpha, to: 0.0, duration: 0.3, removeOnCompletion: false)
}
if let viewListView = self.viewList?.view.view {
viewListView.layer.animatePosition(
from: CGPoint(),
@ -1007,7 +1035,13 @@ 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 self.component == nil {
self.sendMessageContext.setup(context: component.context, view: self, inputPanelExternalState: self.inputPanelExternalState)
let _ = (allowedStoryReactions(context: component.context)
|> deliverOnMainQueue).start(next: { [weak self] reactionItems in
guard let self, let component = self.component else {
@ -1095,14 +1129,9 @@ public final class StoryItemSetContainerComponent: Component {
}
}
}
let nextInputMode: MessageInputPanelComponent.InputMode
if self.inputPanelExternalState.hasText {
nextInputMode = .emoji
} else {
nextInputMode = .stickers
}
let keyboardWasHidden = self.inputPanelExternalState.isKeyboardHidden
let inputNodeVisible = self.sendMessageContext.currentInputMode == .media || hasFirstResponder(self)
self.inputPanel.parentState = state
let inputPanelSize = self.inputPanel.update(
transition: inputPanelTransition,
@ -1114,7 +1143,13 @@ public final class StoryItemSetContainerComponent: Component {
style: .story,
placeholder: "Reply Privately...",
alwaysDarkWhenHasText: component.metrics.widthClass == .regular,
nextInputMode: nextInputMode,
nextInputMode: { [weak self] hasText in
if case .media = self?.sendMessageContext.currentInputMode {
return .text
} else {
return hasText ? .emoji : .stickers
}
},
areVoiceMessagesAvailable: component.slice.additionalPeerData.areVoiceMessagesAvailable,
presentController: { [weak self] c in
guard let self, let component = self.component else {
@ -1164,7 +1199,7 @@ public final class StoryItemSetContainerComponent: Component {
return
}
self.sendMessageContext.toggleInputMode()
self.state?.updated(transition: Transition(animation: .curve(duration: 0.5, curve: .spring)))
self.state?.updated(transition: .immediate)
},
timeoutAction: nil,
forwardAction: component.slice.item.storyItem.isPublic ? { [weak self] in
@ -1292,20 +1327,44 @@ public final class StoryItemSetContainerComponent: Component {
wasRecordingDismissed: self.sendMessageContext.wasRecordingDismissed,
timeoutValue: nil,
timeoutSelected: false,
displayGradient: component.inputHeight != 0.0 && component.metrics.widthClass != .regular,
bottomInset: component.inputHeight != 0.0 ? 0.0 : bottomContentInset,
hideKeyboard: false
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
)),
environment: {},
containerSize: CGSize(width: inputPanelAvailableWidth, height: 200.0)
)
var inputHeight = component.inputHeight
if self.inputPanelExternalState.isEditing {
if self.sendMessageContext.currentInputMode == .media || (inputHeight.isZero && keyboardWasHidden) {
inputHeight = component.deviceMetrics.standardInputHeight(inLandscape: false)
}
}
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)
)
if let inputPanelBackgroundView = self.inputPanelBackground.view {
if inputPanelBackgroundView.superview == nil {
self.addSubview(inputPanelBackgroundView)
}
let isVisible = inputHeight > 44.0
transition.setFrame(view: inputPanelBackgroundView, frame: CGRect(origin: CGPoint(x: 0.0, y: isVisible ? availableSize.height - inputPanelBackgroundSize.height : availableSize.height), size: inputPanelBackgroundSize))
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)
let bottomContentInsetWithoutInput = bottomContentInset
var viewListInset: CGFloat = 0.0
var inputPanelBottomInset: CGFloat
let inputPanelIsOverlay: Bool
if component.inputHeight == 0.0 {
if inputHeight == 0.0 {
inputPanelBottomInset = bottomContentInset
if case .regular = component.metrics.widthClass {
bottomContentInset += 60.0
@ -1315,7 +1374,7 @@ public final class StoryItemSetContainerComponent: Component {
inputPanelIsOverlay = false
} else {
bottomContentInset += 44.0
inputPanelBottomInset = component.inputHeight - component.containerInsets.bottom
inputPanelBottomInset = inputHeight - component.containerInsets.bottom
inputPanelIsOverlay = true
}
@ -1883,6 +1942,9 @@ public final class StoryItemSetContainerComponent: Component {
if self.voiceMessagesRestrictedTooltipController != nil {
effectiveDisplayReactions = false
}
// if self.sendMessageContext.currentInputMode != .text {
// effectiveDisplayReactions = false
// }
if let reactionContextNode = self.reactionContextNode, reactionContextNode.isReactionSearchActive {
effectiveDisplayReactions = true
@ -1992,6 +2054,7 @@ public final class StoryItemSetContainerComponent: Component {
if hasFirstResponder(self) {
self.sendMessageContext.currentInputMode = .text
self.endEditing(true)
}
self.state?.updated(transition: Transition(animation: .curve(duration: 0.25, curve: .easeInOut)))
@ -2102,7 +2165,7 @@ public final class StoryItemSetContainerComponent: Component {
}
let bottomGradientHeight = inputPanelSize.height + 32.0
transition.setFrame(layer: self.bottomContentGradientLayer, frame: CGRect(origin: CGPoint(x: contentFrame.minX, y: availableSize.height - component.inputHeight - bottomGradientHeight), size: CGSize(width: contentFrame.width, height: bottomGradientHeight)))
transition.setFrame(layer: self.bottomContentGradientLayer, frame: CGRect(origin: CGPoint(x: contentFrame.minX, y: availableSize.height - inputHeight - bottomGradientHeight), size: CGSize(width: contentFrame.width, height: bottomGradientHeight)))
//transition.setAlpha(layer: self.bottomContentGradientLayer, alpha: inputPanelIsOverlay ? 1.0 : 0.0)
transition.setAlpha(layer: self.bottomContentGradientLayer, alpha: 0.0)

View File

@ -34,23 +34,36 @@ import ChatPresentationInterfaceState
import Postbox
import OverlayStatusController
import PresentationDataUtils
import TextFieldComponent
final class StoryItemSetContainerSendMessage {
enum InputMode {
case text
case emoji
case sticker
case media
}
private var context: AccountContext?
private weak var view: StoryItemSetContainerComponent.View?
private var inputPanelExternalState: MessageInputPanelComponent.ExternalState?
weak var attachmentController: AttachmentController?
weak var shareController: ShareController?
var inputMode: InputMode = .text
var currentInputMode: InputMode = .text
private var needsInputActivation = false
var audioRecorderValue: ManagedAudioRecorder?
var audioRecorder = Promise<ManagedAudioRecorder?>()
var recordedAudioPreview: ChatRecordedMediaPreview?
var inputMediaNodeData: ChatEntityKeyboardInputNode.InputData?
var inputMediaNodeDataPromise = Promise<ChatEntityKeyboardInputNode.InputData>()
var inputMediaNodeDataDisposable: Disposable?
var inputMediaNodeStateContext = ChatEntityKeyboardInputNode.StateContext()
var inputMediaInteraction: ChatEntityKeyboardInputNode.Interaction?
var inputMediaNode: ChatEntityKeyboardInputNode?
var inputMediaNodeBackground = SimpleLayer()
var videoRecorderValue: InstantVideoController?
var tempVideoRecorderValue: InstantVideoController?
var videoRecorder = Promise<InstantVideoController?>()
@ -62,14 +75,308 @@ final class StoryItemSetContainerSendMessage {
private(set) var isMediaRecordingLocked: Bool = false
var wasRecordingDismissed: Bool = false
init() {
self.inputMediaNodeDataDisposable = (self.inputMediaNodeDataPromise.get()
|> deliverOnMainQueue).start(next: { [weak self] value in
guard let self else {
return
}
self.inputMediaNodeData = value
})
}
deinit {
self.controllerNavigationDisposable.dispose()
self.enqueueMediaMessageDisposable.dispose()
self.navigationActionDisposable.dispose()
self.resolvePeerByNameDisposable.dispose()
self.inputMediaNodeDataDisposable?.dispose()
}
func setup(context: AccountContext, view: StoryItemSetContainerComponent.View, inputPanelExternalState: MessageInputPanelComponent.ExternalState) {
self.context = context
self.inputPanelExternalState = inputPanelExternalState
self.view = view
self.inputMediaNodeDataPromise.set(
ChatEntityKeyboardInputNode.inputData(
context: context,
chatPeerId: nil,
areCustomEmojiEnabled: true,
hasSearch: false,
hideBackground: true,
sendGif: nil
)
)
self.inputMediaInteraction = ChatEntityKeyboardInputNode.Interaction(
sendSticker: { [weak self] fileReference, _, _, _, _, _, _, _, _ in
if let view = self?.view {
self?.performSendStickerAction(view: view, fileReference: fileReference)
}
return false
},
sendEmoji: { [weak self] text, attribute, bool1 in
if let self {
let _ = self
}
},
sendGif: { [weak self] fileReference, _, _, _, _ in
if let view = self?.view {
self?.performSendGifAction(view: view, fileReference: fileReference)
}
return false
},
sendBotContextResultAsGif: { _, _, _, _, _, _ in
return false
},
updateChoosingSticker: { _ in },
switchToTextInput: { [weak self] in
if let self {
self.currentInputMode = .text
self.view?.state?.updated(transition: .immediate)
}
},
dismissTextInput: {
},
insertText: { [weak self] text in
if let self {
self.inputPanelExternalState?.insertText(text)
}
},
backwardsDeleteText: { [weak self] in
if let self {
self.inputPanelExternalState?.deleteBackward()
}
},
presentController: { [weak self] c, a in
if let self {
self.view?.component?.controller()?.present(c, in: .window(.root), with: a)
}
},
presentGlobalOverlayController: { [weak self] c, a in
if let self {
self.view?.component?.controller()?.presentInGlobalOverlay(c, with: a)
}
},
getNavigationController: {
return self.view?.component?.controller()?.navigationController as? NavigationController
},
requestLayout: { [weak self] _ in
if let self {
self.view?.state?.updated()
}
}
)
}
func toggleInputMode() {
guard let view = self.view else {
return
}
if case .text = self.currentInputMode {
if !hasFirstResponder(view) {
self.needsInputActivation = true
}
self.currentInputMode = .media
} else {
self.currentInputMode = .text
}
}
func updateInputMediaNode(inputPanel: ComponentView<Empty>, availableSize: CGSize, bottomInset: CGFloat, inputHeight: CGFloat, effectiveInputHeight: CGFloat, metrics: LayoutMetrics, deviceMetrics: DeviceMetrics, transition: Transition) {
guard let context = self.context, let inputPanelView = inputPanel.view as? MessageInputPanelComponent.View else {
return
}
if case .media = self.currentInputMode, let inputData = self.inputMediaNodeData {
let inputMediaNode: ChatEntityKeyboardInputNode
if let current = self.inputMediaNode {
inputMediaNode = current
} else {
inputMediaNode = ChatEntityKeyboardInputNode(
context: context,
currentInputData: inputData,
updatedInputData: self.inputMediaNodeDataPromise.get(),
defaultToEmojiTab: self.inputPanelExternalState?.hasText ?? false,
opaqueTopPanelBackground: false,
interaction: self.inputMediaInteraction,
chatPeerId: nil,
stateContext: self.inputMediaNodeStateContext
)
inputMediaNode.externalTopPanelContainerImpl = nil
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
}
let presentationData = 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: context.currentLimitsConfiguration.with { $0 },
fontSize: presentationData.chatFontSize,
bubbleCorners: presentationData.chatBubbleCorners,
accountPeerId: context.account.peerId,
mode: .standard(previewing: false),
chatLocation: .peer(id: 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: bottomInset, standardInputHeight: deviceMetrics.standardInputHeight(inLandscape: false), inputHeight: inputHeight, maximumHeight: availableSize.height, inputPanelHeight: 0.0, transition: .immediate, interfaceState: presentationInterfaceState, layoutMetrics: metrics, deviceMetrics: deviceMetrics, isVisible: true, isExpanded: false)
let inputNodeHeight = heightAndOverflow.0
var inputNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - inputNodeHeight), size: CGSize(width: availableSize.width, height: inputNodeHeight))
if self.needsInputActivation {
inputNodeFrame = inputNodeFrame.offsetBy(dx: 0.0, dy: inputNodeHeight)
}
transition.setFrame(layer: inputMediaNode.layer, frame: inputNodeFrame)
transition.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeFrame)
} else if let inputMediaNode = self.inputMediaNode {
self.inputMediaNode = nil
var targetFrame = inputMediaNode.frame
if effectiveInputHeight > 0.0 {
targetFrame.origin.y = availableSize.height - effectiveInputHeight
} 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()
})
}
}
})
transition.setFrame(layer: self.inputMediaNodeBackground, frame: targetFrame, completion: { _ in
Queue.mainQueue().after(0.3) {
if self.currentInputMode == .text {
self.inputMediaNodeBackground.animateAlpha(from: 1.0, to: 0.0, duration: 0.35, removeOnCompletion: false, completion: { finished in
if finished {
self.inputMediaNodeBackground.removeFromSuperlayer()
}
self.inputMediaNodeBackground.removeAllAnimations()
})
}
}
})
}
if self.needsInputActivation {
self.needsInputActivation = false
Queue.mainQueue().justDispatch {
inputPanelView.activateInput()
}
}
}
func animateOut(bounds: CGRect) {
if let inputMediaNode = self.inputMediaNode {
inputMediaNode.layer.animatePosition(
from: CGPoint(),
to: CGPoint(x: 0.0, y: bounds.height - inputMediaNode.frame.minY),
duration: 0.3,
timingFunction: kCAMediaTimingFunctionSpring,
removeOnCompletion: false,
additive: true
)
inputMediaNode.layer.animateAlpha(from: inputMediaNode.alpha, to: 0.0, duration: 0.3, removeOnCompletion: false)
self.inputMediaNodeBackground.animatePosition(
from: CGPoint(),
to: CGPoint(x: 0.0, y: bounds.height - self.inputMediaNodeBackground.frame.minY),
duration: 0.3,
timingFunction: kCAMediaTimingFunctionSpring,
removeOnCompletion: false,
additive: true
)
self.inputMediaNodeBackground.animateAlpha(from: CGFloat(self.inputMediaNodeBackground.opacity), to: 0.0, duration: 0.3, removeOnCompletion: false)
}
}
func performSendStickerAction(view: StoryItemSetContainerComponent.View, fileReference: FileMediaReference) {
guard let component = view.component else {
return
}
let focusedItem = component.slice.item
guard let peerId = focusedItem.peerId else {
return
}
let focusedStoryId = StoryId(peerId: peerId, id: focusedItem.storyItem.id)
component.context.engine.messages.enqueueOutgoingMessage(
to: peerId,
replyTo: nil,
storyId: focusedStoryId,
content: .file(fileReference)
)
self.currentInputMode = .text
view.endEditing(true)
Queue.mainQueue().after(0.66) {
if let controller = component.controller() {
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
controller.present(UndoOverlayController(
presentationData: presentationData,
content: .succeed(text: "Message Sent"),
elevatedLayout: false,
animateInAsReplacement: false,
action: { _ in return false }
), in: .current)
}
}
}
func performSendGifAction(view: StoryItemSetContainerComponent.View, fileReference: FileMediaReference) {
guard let component = view.component else {
return
}
let focusedItem = component.slice.item
guard let peerId = focusedItem.peerId else {
return
}
let focusedStoryId = StoryId(peerId: peerId, id: focusedItem.storyItem.id)
component.context.engine.messages.enqueueOutgoingMessage(
to: peerId,
replyTo: nil,
storyId: focusedStoryId,
content: .file(fileReference)
)
self.currentInputMode = .text
view.endEditing(true)
Queue.mainQueue().after(0.66) {
if let controller = component.controller() {
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
controller.present(UndoOverlayController(
presentationData: presentationData,
content: .succeed(text: "Message Sent"),
elevatedLayout: false,
animateInAsReplacement: false,
action: { _ in return false }
), in: .current)
}
}
}
func performSendMessageAction(
@ -105,21 +412,24 @@ final class StoryItemSetContainerSendMessage {
component.context.engine.messages.enqueueOutgoingMessage(
to: peerId,
replyTo: nil,
storyId: StoryId(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id),
storyId: focusedStoryId,
content: .text(text.string, entities)
)
inputPanelView.clearSendMessageInput()
self.currentInputMode = .text
view.endEditing(true)
if let controller = component.controller() {
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
controller.present(UndoOverlayController(
presentationData: presentationData,
content: .succeed(text: "Message Sent"),
elevatedLayout: false,
animateInAsReplacement: false,
action: { _ in return false }
), in: .current)
Queue.mainQueue().after(0.66) {
if let controller = component.controller() {
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
controller.present(UndoOverlayController(
presentationData: presentationData,
content: .succeed(text: "Message Sent"),
elevatedLayout: false,
animateInAsReplacement: false,
action: { _ in return false }
), in: .current)
}
}
}
}
@ -448,6 +758,7 @@ final class StoryItemSetContainerSendMessage {
let inputIsActive = !"".isEmpty
self.currentInputMode = .text
view.endEditing(true)
var banSendText: (Int32, Bool)?