mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-10-09 03:20:48 +00:00
Entities keyboard for story replies
This commit is contained in:
parent
c87ba664ca
commit
84bdc0afb8
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
},
|
||||
|
@ -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 {
|
||||
|
@ -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),
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)?
|
||||
|
Loading…
x
Reference in New Issue
Block a user