mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Various improvements
This commit is contained in:
parent
cd7fc1cf9a
commit
a30ab38ce4
@ -30,6 +30,8 @@ swift_library(
|
|||||||
"//submodules/Markdown",
|
"//submodules/Markdown",
|
||||||
"//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode",
|
"//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode",
|
||||||
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
|
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
|
||||||
|
"//submodules/TelegramUI/Components/TextNodeWithEntities",
|
||||||
|
"//submodules/InvisibleInkDustNode",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
@ -20,6 +20,8 @@ import ShimmerEffect
|
|||||||
import Markdown
|
import Markdown
|
||||||
import ChatMessageBubbleContentNode
|
import ChatMessageBubbleContentNode
|
||||||
import ChatMessageItemCommon
|
import ChatMessageItemCommon
|
||||||
|
import TextNodeWithEntities
|
||||||
|
import InvisibleInkDustNode
|
||||||
|
|
||||||
private func attributedServiceMessageString(theme: ChatPresentationThemeData, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: EngineMessage, accountPeerId: EnginePeer.Id) -> NSAttributedString? {
|
private func attributedServiceMessageString(theme: ChatPresentationThemeData, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: EngineMessage, accountPeerId: EnginePeer.Id) -> NSAttributedString? {
|
||||||
return universalServiceMessageString(presentationData: (theme.theme, theme.wallpaper), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: false, forForumOverview: false)
|
return universalServiceMessageString(presentationData: (theme.theme, theme.wallpaper), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: false, forForumOverview: false)
|
||||||
@ -34,7 +36,8 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
private let mediaBackgroundMaskNode: ASImageNode
|
private let mediaBackgroundMaskNode: ASImageNode
|
||||||
private var mediaBackgroundContent: WallpaperBubbleBackgroundNode?
|
private var mediaBackgroundContent: WallpaperBubbleBackgroundNode?
|
||||||
private let titleNode: TextNode
|
private let titleNode: TextNode
|
||||||
private let subtitleNode: TextNode
|
private let subtitleNode: TextNodeWithEntities
|
||||||
|
private var dustNode: InvisibleInkDustNode?
|
||||||
private let placeholderNode: StickerShimmerEffectNode
|
private let placeholderNode: StickerShimmerEffectNode
|
||||||
private let animationNode: AnimatedStickerNode
|
private let animationNode: AnimatedStickerNode
|
||||||
|
|
||||||
@ -60,6 +63,16 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
|
|
||||||
if wasVisible != isVisible {
|
if wasVisible != isVisible {
|
||||||
self.visibilityStatus = isVisible
|
self.visibilityStatus = isVisible
|
||||||
|
|
||||||
|
switch self.visibility {
|
||||||
|
case .none:
|
||||||
|
self.subtitleNode.visibilityRect = nil
|
||||||
|
case let .visible(_, subRect):
|
||||||
|
var subRect = subRect
|
||||||
|
subRect.origin.x = 0.0
|
||||||
|
subRect.size.width = 10000.0
|
||||||
|
self.subtitleNode.visibilityRect = subRect
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -88,9 +101,9 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
self.titleNode.isUserInteractionEnabled = false
|
self.titleNode.isUserInteractionEnabled = false
|
||||||
self.titleNode.displaysAsynchronously = false
|
self.titleNode.displaysAsynchronously = false
|
||||||
|
|
||||||
self.subtitleNode = TextNode()
|
self.subtitleNode = TextNodeWithEntities()
|
||||||
self.subtitleNode.isUserInteractionEnabled = false
|
self.subtitleNode.textNode.isUserInteractionEnabled = false
|
||||||
self.subtitleNode.displaysAsynchronously = false
|
self.subtitleNode.textNode.displaysAsynchronously = false
|
||||||
|
|
||||||
self.buttonNode = HighlightTrackingButtonNode()
|
self.buttonNode = HighlightTrackingButtonNode()
|
||||||
self.buttonNode.clipsToBounds = true
|
self.buttonNode.clipsToBounds = true
|
||||||
@ -120,8 +133,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
self.addSubnode(self.labelNode)
|
self.addSubnode(self.labelNode)
|
||||||
|
|
||||||
self.addSubnode(self.titleNode)
|
self.addSubnode(self.titleNode)
|
||||||
self.addSubnode(self.subtitleNode)
|
self.addSubnode(self.subtitleNode.textNode)
|
||||||
self.addSubnode(self.subtitleNode)
|
|
||||||
self.addSubnode(self.placeholderNode)
|
self.addSubnode(self.placeholderNode)
|
||||||
self.addSubnode(self.animationNode)
|
self.addSubnode(self.animationNode)
|
||||||
|
|
||||||
@ -236,7 +248,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, unboundSize: CGSize?, maxWidth: CGFloat, layout: (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) {
|
override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, unboundSize: CGSize?, maxWidth: CGFloat, layout: (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) {
|
||||||
let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
|
let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
|
||||||
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
||||||
let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode)
|
let makeSubtitleLayout = TextNodeWithEntities.asyncLayout(self.subtitleNode)
|
||||||
let makeButtonTitleLayout = TextNode.asyncLayout(self.buttonTitleNode)
|
let makeButtonTitleLayout = TextNode.asyncLayout(self.buttonTitleNode)
|
||||||
let makeRibbonTextLayout = TextNode.asyncLayout(self.ribbonTextNode)
|
let makeRibbonTextLayout = TextNode.asyncLayout(self.ribbonTextNode)
|
||||||
|
|
||||||
@ -259,6 +271,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
var animationFile: TelegramMediaFile?
|
var animationFile: TelegramMediaFile?
|
||||||
var title = item.presentationData.strings.Notification_PremiumGift_Title
|
var title = item.presentationData.strings.Notification_PremiumGift_Title
|
||||||
var text = ""
|
var text = ""
|
||||||
|
var entities: [MessageTextEntity] = []
|
||||||
var buttonTitle = item.presentationData.strings.Notification_PremiumGift_View
|
var buttonTitle = item.presentationData.strings.Notification_PremiumGift_View
|
||||||
var ribbonTitle = ""
|
var ribbonTitle = ""
|
||||||
var hasServiceMessage = true
|
var hasServiceMessage = true
|
||||||
@ -329,8 +342,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
buttonTitle = item.presentationData.strings.Notification_PremiumPrize_View
|
buttonTitle = item.presentationData.strings.Notification_PremiumPrize_View
|
||||||
hasServiceMessage = false
|
hasServiceMessage = false
|
||||||
}
|
}
|
||||||
case let .starGift(gift, convertStars, giftText, entities, nameHidden, savedToProfile, converted):
|
case let .starGift(gift, convertStars, giftText, giftEntities, _, savedToProfile, converted):
|
||||||
let _ = nameHidden
|
|
||||||
//TODO:localize
|
//TODO:localize
|
||||||
if !incoming {
|
if !incoming {
|
||||||
buttonTitle = ""
|
buttonTitle = ""
|
||||||
@ -339,7 +351,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
title = "Gift from \(authorName)"
|
title = "Gift from \(authorName)"
|
||||||
if let giftText, !giftText.isEmpty {
|
if let giftText, !giftText.isEmpty {
|
||||||
text = giftText
|
text = giftText
|
||||||
let _ = entities
|
entities = giftEntities ?? []
|
||||||
} else {
|
} else {
|
||||||
if incoming {
|
if incoming {
|
||||||
if converted {
|
if converted {
|
||||||
@ -386,15 +398,20 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
|
|
||||||
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: Font.semibold(15.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
|
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: Font.semibold(15.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
|
||||||
|
|
||||||
let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(
|
let attributedText: NSAttributedString
|
||||||
body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: primaryTextColor),
|
if let _ = animationFile {
|
||||||
bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: primaryTextColor),
|
attributedText = stringWithAppliedEntities(text, entities: entities, baseColor: primaryTextColor, linkColor: primaryTextColor, baseFont: Font.regular(13.0), linkFont: Font.regular(13.0), boldFont: Font.semibold(13.0), italicFont: Font.italic(13.0), boldItalicFont: Font.semiboldItalic(13.0), fixedFont: Font.monospace(13.0), blockQuoteFont: Font.regular(13.0), message: nil)
|
||||||
link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: primaryTextColor),
|
} else {
|
||||||
linkAttribute: { url in
|
attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(
|
||||||
return ("URL", url)
|
body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: primaryTextColor),
|
||||||
}
|
bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: primaryTextColor),
|
||||||
), textAlignment: .center)
|
link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: primaryTextColor),
|
||||||
|
linkAttribute: { url in
|
||||||
|
return ("URL", url)
|
||||||
|
}
|
||||||
|
), textAlignment: .center)
|
||||||
|
}
|
||||||
|
|
||||||
let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
|
let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
|
||||||
|
|
||||||
let (buttonTitleLayout, buttonTitleApply) = makeButtonTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: buttonTitle, font: Font.semibold(15.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
|
let (buttonTitleLayout, buttonTitleApply) = makeButtonTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: buttonTitle, font: Font.semibold(15.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
|
||||||
@ -511,7 +528,13 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
|
|
||||||
let _ = labelApply()
|
let _ = labelApply()
|
||||||
let _ = titleApply()
|
let _ = titleApply()
|
||||||
let _ = subtitleApply()
|
let _ = subtitleApply(TextNodeWithEntities.Arguments(
|
||||||
|
context: item.context,
|
||||||
|
cache: item.controllerInteraction.presentationContext.animationCache,
|
||||||
|
renderer: item.controllerInteraction.presentationContext.animationRenderer,
|
||||||
|
placeholderColor: item.presentationData.theme.theme.chat.message.freeform.withWallpaper.reactionInactiveBackground,
|
||||||
|
attemptSynchronous: synchronousLoads
|
||||||
|
))
|
||||||
let _ = buttonTitleApply()
|
let _ = buttonTitleApply()
|
||||||
let _ = ribbonTextApply()
|
let _ = ribbonTextApply()
|
||||||
|
|
||||||
@ -522,7 +545,26 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
strongSelf.titleNode.frame = titleFrame
|
strongSelf.titleNode.frame = titleFrame
|
||||||
|
|
||||||
let subtitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - subtitleLayout.size.width) / 2.0) , y: titleFrame.maxY + textSpacing), size: subtitleLayout.size)
|
let subtitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - subtitleLayout.size.width) / 2.0) , y: titleFrame.maxY + textSpacing), size: subtitleLayout.size)
|
||||||
strongSelf.subtitleNode.frame = subtitleFrame
|
strongSelf.subtitleNode.textNode.frame = subtitleFrame
|
||||||
|
|
||||||
|
if !subtitleLayout.spoilers.isEmpty {
|
||||||
|
let dustColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper).primaryText
|
||||||
|
|
||||||
|
let dustNode: InvisibleInkDustNode
|
||||||
|
if let current = strongSelf.dustNode {
|
||||||
|
dustNode = current
|
||||||
|
} else {
|
||||||
|
dustNode = InvisibleInkDustNode(textNode: nil, enableAnimations: item.context.sharedContext.energyUsageSettings.fullTranslucency)
|
||||||
|
dustNode.isUserInteractionEnabled = false
|
||||||
|
strongSelf.dustNode = dustNode
|
||||||
|
strongSelf.insertSubnode(dustNode, aboveSubnode: strongSelf.subtitleNode.textNode)
|
||||||
|
}
|
||||||
|
dustNode.frame = subtitleFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 1.0)
|
||||||
|
dustNode.update(size: dustNode.frame.size, color: dustColor, textColor: dustColor, rects: subtitleLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }, wordRects: subtitleLayout.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) })
|
||||||
|
} else if let dustNode = strongSelf.dustNode {
|
||||||
|
dustNode.removeFromSupernode()
|
||||||
|
strongSelf.dustNode = nil
|
||||||
|
}
|
||||||
|
|
||||||
let buttonTitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonTitleLayout.size.width) / 2.0), y: subtitleFrame.maxY + 18.0), size: buttonTitleLayout.size)
|
let buttonTitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonTitleLayout.size.width) / 2.0), y: subtitleFrame.maxY + 18.0), size: buttonTitleLayout.size)
|
||||||
strongSelf.buttonTitleNode.frame = buttonTitleFrame
|
strongSelf.buttonTitleNode.frame = buttonTitleFrame
|
||||||
@ -616,6 +658,16 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
if let (rect, size) = strongSelf.absoluteRect {
|
if let (rect, size) = strongSelf.absoluteRect {
|
||||||
strongSelf.updateAbsoluteRect(rect, within: size)
|
strongSelf.updateAbsoluteRect(rect, within: size)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch strongSelf.visibility {
|
||||||
|
case .none:
|
||||||
|
strongSelf.subtitleNode.visibilityRect = nil
|
||||||
|
case let .visible(_, subRect):
|
||||||
|
var subRect = subRect
|
||||||
|
subRect.origin.x = 0.0
|
||||||
|
subRect.size.width = 10000.0
|
||||||
|
strongSelf.subtitleNode.visibilityRect = subRect
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -742,6 +794,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
self.updateVisibility()
|
self.updateVisibility()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var internalPlayedOnce = false
|
||||||
private func updateVisibility() {
|
private func updateVisibility() {
|
||||||
guard let item = self.item else {
|
guard let item = self.item else {
|
||||||
return
|
return
|
||||||
@ -772,9 +825,10 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !item.controllerInteraction.seenOneTimeAnimatedMedia.contains(item.message.id) {
|
if !item.controllerInteraction.seenOneTimeAnimatedMedia.contains(item.message.id) && !self.internalPlayedOnce {
|
||||||
item.controllerInteraction.seenOneTimeAnimatedMedia.insert(item.message.id)
|
item.controllerInteraction.seenOneTimeAnimatedMedia.insert(item.message.id)
|
||||||
self.animationNode.playOnce()
|
self.animationNode.playOnce()
|
||||||
|
self.internalPlayedOnce = true
|
||||||
|
|
||||||
Queue.mainQueue().after(0.05) {
|
Queue.mainQueue().after(0.05) {
|
||||||
if let itemNode = self.itemNode, let supernode = itemNode.supernode {
|
if let itemNode = self.itemNode, let supernode = itemNode.supernode {
|
||||||
|
@ -34,10 +34,13 @@ swift_library(
|
|||||||
"//submodules/TelegramUI/Components/ButtonComponent",
|
"//submodules/TelegramUI/Components/ButtonComponent",
|
||||||
"//submodules/AppBundle",
|
"//submodules/AppBundle",
|
||||||
"//submodules/WallpaperBackgroundNode",
|
"//submodules/WallpaperBackgroundNode",
|
||||||
|
"//submodules/TextFormat",
|
||||||
"//submodules/ChatPresentationInterfaceState",
|
"//submodules/ChatPresentationInterfaceState",
|
||||||
"//submodules/TelegramUI/Components/TextFieldComponent",
|
"//submodules/TelegramUI/Components/TextFieldComponent",
|
||||||
"//submodules/TelegramUI/Components/ListItemComponentAdaptor",
|
"//submodules/TelegramUI/Components/ListItemComponentAdaptor",
|
||||||
"//submodules/BotPaymentsUI",
|
"//submodules/BotPaymentsUI",
|
||||||
|
"//submodules/TelegramUI/Components/EmojiSuggestionsComponent",
|
||||||
|
"//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
@ -28,6 +28,7 @@ final class ChatGiftPreviewItem: ListViewItem, ItemListItem, ListItemComponentAd
|
|||||||
let accountPeer: EnginePeer?
|
let accountPeer: EnginePeer?
|
||||||
let gift: StarGift
|
let gift: StarGift
|
||||||
let text: String
|
let text: String
|
||||||
|
let entities: [MessageTextEntity]
|
||||||
|
|
||||||
init(
|
init(
|
||||||
context: AccountContext,
|
context: AccountContext,
|
||||||
@ -42,7 +43,8 @@ final class ChatGiftPreviewItem: ListViewItem, ItemListItem, ListItemComponentAd
|
|||||||
nameDisplayOrder: PresentationPersonNameOrder,
|
nameDisplayOrder: PresentationPersonNameOrder,
|
||||||
accountPeer: EnginePeer?,
|
accountPeer: EnginePeer?,
|
||||||
gift: StarGift,
|
gift: StarGift,
|
||||||
text: String
|
text: String,
|
||||||
|
entities: [MessageTextEntity]
|
||||||
) {
|
) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.theme = theme
|
self.theme = theme
|
||||||
@ -57,6 +59,7 @@ final class ChatGiftPreviewItem: ListViewItem, ItemListItem, ListItemComponentAd
|
|||||||
self.accountPeer = accountPeer
|
self.accountPeer = accountPeer
|
||||||
self.gift = gift
|
self.gift = gift
|
||||||
self.text = text
|
self.text = text
|
||||||
|
self.entities = entities
|
||||||
}
|
}
|
||||||
|
|
||||||
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||||
@ -130,6 +133,9 @@ final class ChatGiftPreviewItem: ListViewItem, ItemListItem, ListItemComponentAd
|
|||||||
if lhs.text != rhs.text {
|
if lhs.text != rhs.text {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.entities != rhs.entities {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -201,7 +207,7 @@ final class ChatGiftPreviewItemNode: ListViewItemNode {
|
|||||||
peers[authorPeerId] = item.accountPeer?._asPeer()
|
peers[authorPeerId] = item.accountPeer?._asPeer()
|
||||||
|
|
||||||
let media: [Media] = [
|
let media: [Media] = [
|
||||||
TelegramMediaAction(action: .starGift(gift: item.gift, convertStars: item.gift.convertStars, text: item.text, entities: [], nameHidden: false, savedToProfile: false, converted: false))
|
TelegramMediaAction(action: .starGift(gift: item.gift, convertStars: item.gift.convertStars, text: item.text, entities: item.entities, nameHidden: false, savedToProfile: false, converted: false))
|
||||||
]
|
]
|
||||||
let message = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[authorPeerId], text: "", attributes: [], media: media, peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
|
let message = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[authorPeerId], text: "", attributes: [], media: media, peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
|
||||||
items.append(item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [message], theme: item.componentTheme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true, isStandalone: false))
|
items.append(item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [message], theme: item.componentTheme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true, isStandalone: false))
|
||||||
@ -221,21 +227,21 @@ final class ChatGiftPreviewItemNode: ListViewItemNode {
|
|||||||
itemNode.insets = layout.insets
|
itemNode.insets = layout.insets
|
||||||
itemNode.frame = nodeFrame
|
itemNode.frame = nodeFrame
|
||||||
itemNode.isUserInteractionEnabled = false
|
itemNode.isUserInteractionEnabled = false
|
||||||
|
itemNode.visibility = .visible(1.0, .infinite)
|
||||||
|
|
||||||
Queue.mainQueue().after(0.01) {
|
apply(ListViewItemApply(isOnScreen: true))
|
||||||
apply(ListViewItemApply(isOnScreen: true))
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var messageNodes: [ListViewItemNode] = []
|
var messageNodes: [ListViewItemNode] = []
|
||||||
for i in 0 ..< items.count {
|
for i in 0 ..< items.count {
|
||||||
var itemNode: ListViewItemNode?
|
var itemNode: ListViewItemNode?
|
||||||
items[i].nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: false, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], completion: { node, apply in
|
items[i].nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: true, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], completion: { node, apply in
|
||||||
itemNode = node
|
itemNode = node
|
||||||
apply().1(ListViewItemApply(isOnScreen: true))
|
apply().1(ListViewItemApply(isOnScreen: true))
|
||||||
})
|
})
|
||||||
itemNode!.isUserInteractionEnabled = false
|
itemNode!.isUserInteractionEnabled = false
|
||||||
|
itemNode?.visibility = .visible(1.0, .infinite)
|
||||||
messageNodes.append(itemNode!)
|
messageNodes.append(itemNode!)
|
||||||
|
|
||||||
self.initialBubbleHeight = itemNode?.frame.height
|
self.initialBubbleHeight = itemNode?.frame.height
|
||||||
|
@ -22,6 +22,11 @@ import LottieComponent
|
|||||||
import TextFieldComponent
|
import TextFieldComponent
|
||||||
import ButtonComponent
|
import ButtonComponent
|
||||||
import BotPaymentsUI
|
import BotPaymentsUI
|
||||||
|
import ChatEntityKeyboardInputNode
|
||||||
|
import EmojiSuggestionsComponent
|
||||||
|
import ChatPresentationInterfaceState
|
||||||
|
import AudioToolbox
|
||||||
|
import TextFormat
|
||||||
|
|
||||||
final class GiftSetupScreenComponent: Component {
|
final class GiftSetupScreenComponent: Component {
|
||||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||||
@ -81,9 +86,23 @@ final class GiftSetupScreenComponent: Component {
|
|||||||
private let textInputTag = NSObject()
|
private let textInputTag = NSObject()
|
||||||
private var resetText: String?
|
private var resetText: String?
|
||||||
|
|
||||||
|
private var currentInputMode: ListMultilineTextFieldItemComponent.InputMode = .keyboard
|
||||||
|
|
||||||
|
private var inputMediaNodeData: ChatEntityKeyboardInputNode.InputData?
|
||||||
|
private var inputMediaNodeDataDisposable: Disposable?
|
||||||
|
private var inputMediaNodeStateContext = ChatEntityKeyboardInputNode.StateContext()
|
||||||
|
private var inputMediaInteraction: ChatEntityKeyboardInputNode.Interaction?
|
||||||
|
private var inputMediaNode: ChatEntityKeyboardInputNode?
|
||||||
|
private var inputMediaNodeBackground = SimpleLayer()
|
||||||
|
private var inputMediaNodeTargetTag: AnyObject?
|
||||||
|
private let inputMediaNodeDataPromise = Promise<ChatEntityKeyboardInputNode.InputData>()
|
||||||
|
|
||||||
|
private var currentEmojiSuggestionView: ComponentHostView<Empty>?
|
||||||
|
|
||||||
private var hideName = false
|
private var hideName = false
|
||||||
|
|
||||||
private var previousHadInputHeight: Bool = false
|
private var previousHadInputHeight: Bool = false
|
||||||
|
private var previousInputHeight: CGFloat?
|
||||||
private var recenterOnTag: NSObject?
|
private var recenterOnTag: NSObject?
|
||||||
|
|
||||||
private var peerMap: [EnginePeer.Id: EnginePeer] = [:]
|
private var peerMap: [EnginePeer.Id: EnginePeer] = [:]
|
||||||
@ -175,7 +194,8 @@ final class GiftSetupScreenComponent: Component {
|
|||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let source: BotPaymentInvoiceSource = .starGift(hideName: self.hideName, peerId: component.peerId, giftId: component.gift.id, text: self.textInputState.text.string, entities: [])
|
let entities = generateChatInputTextEntities(self.textInputState.text)
|
||||||
|
let source: BotPaymentInvoiceSource = .starGift(hideName: self.hideName, peerId: component.peerId, giftId: component.gift.id, text: self.textInputState.text.string, entities: entities)
|
||||||
let inputData = BotCheckoutController.InputData.fetch(context: component.context, source: source)
|
let inputData = BotCheckoutController.InputData.fetch(context: component.context, source: source)
|
||||||
|> map(Optional.init)
|
|> map(Optional.init)
|
||||||
|> `catch` { _ -> Signal<BotCheckoutController.InputData?, NoError> in
|
|> `catch` { _ -> Signal<BotCheckoutController.InputData?, NoError> in
|
||||||
@ -264,6 +284,108 @@ final class GiftSetupScreenComponent: Component {
|
|||||||
|
|
||||||
self.state?.updated()
|
self.state?.updated()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
self.inputMediaNodeDataPromise.set(
|
||||||
|
ChatEntityKeyboardInputNode.inputData(
|
||||||
|
context: component.context,
|
||||||
|
chatPeerId: nil,
|
||||||
|
areCustomEmojiEnabled: true,
|
||||||
|
hasTrending: false,
|
||||||
|
hasSearch: true,
|
||||||
|
hasStickers: false,
|
||||||
|
hasGifs: false,
|
||||||
|
hideBackground: true,
|
||||||
|
sendGif: nil
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.inputMediaNodeDataDisposable = (self.inputMediaNodeDataPromise.get()
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] value in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.inputMediaNodeData = value
|
||||||
|
})
|
||||||
|
|
||||||
|
self.inputMediaInteraction = ChatEntityKeyboardInputNode.Interaction(
|
||||||
|
sendSticker: { _, _, _, _, _, _, _, _, _ in
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
sendEmoji: { _, _, _ in
|
||||||
|
let _ = self
|
||||||
|
},
|
||||||
|
sendGif: { _, _, _, _, _ in
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
sendBotContextResultAsGif: { _, _ , _, _, _, _ in
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
updateChoosingSticker: { _ in
|
||||||
|
},
|
||||||
|
switchToTextInput: { [weak self] in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.currentInputMode = .keyboard
|
||||||
|
self.state?.updated(transition: .spring(duration: 0.4))
|
||||||
|
},
|
||||||
|
dismissTextInput: {
|
||||||
|
},
|
||||||
|
insertText: { [weak self] text in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if let textInputView = self.introSection.findTaggedView(tag: self.textInputTag) as? ListMultilineTextFieldItemComponent.View {
|
||||||
|
if self.textInputState.isEditing {
|
||||||
|
textInputView.insertText(text: text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
backwardsDeleteText: { [weak self] in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if let textInputView = self.introSection.findTaggedView(tag: self.textInputTag) as? ListMultilineTextFieldItemComponent.View {
|
||||||
|
if self.textInputState.isEditing {
|
||||||
|
textInputView.backwardsDeleteText()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
openStickerEditor: {
|
||||||
|
},
|
||||||
|
presentController: { [weak self] c, a in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.environment?.controller()?.present(c, in: .window(.root), with: a)
|
||||||
|
},
|
||||||
|
presentGlobalOverlayController: { [weak self] c, a in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.environment?.controller()?.presentInGlobalOverlay(c, with: a)
|
||||||
|
},
|
||||||
|
getNavigationController: { [weak self] () -> NavigationController? in
|
||||||
|
guard let self else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard let controller = self.environment?.controller() as? GiftSetupScreen else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if let navigationController = controller.navigationController as? NavigationController {
|
||||||
|
return navigationController
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
requestLayout: { [weak self] transition in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !self.isUpdating {
|
||||||
|
self.state?.updated(transition: ComponentTransition(transition))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
let environment = environment[EnvironmentType.self].value
|
let environment = environment[EnvironmentType.self].value
|
||||||
@ -316,16 +438,7 @@ final class GiftSetupScreenComponent: Component {
|
|||||||
|
|
||||||
contentHeight += environment.navigationHeight
|
contentHeight += environment.navigationHeight
|
||||||
contentHeight += 26.0
|
contentHeight += 26.0
|
||||||
|
|
||||||
self.recenterOnTag = nil
|
|
||||||
if let hint = transition.userData(TextFieldComponent.AnimationHint.self), let targetView = hint.view {
|
|
||||||
if let textView = self.introSection.findTaggedView(tag: self.textInputTag) {
|
|
||||||
if targetView.isDescendant(of: textView) {
|
|
||||||
self.recenterOnTag = self.textInputTag
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var introSectionItems: [AnyComponentWithIdentity<Empty>] = []
|
var introSectionItems: [AnyComponentWithIdentity<Empty>] = []
|
||||||
introSectionItems.append(AnyComponentWithIdentity(id: introSectionItems.count, component: AnyComponent(Rectangle(color: .clear, height: 346.0, tag: self.introPlaceholderTag))))
|
introSectionItems.append(AnyComponentWithIdentity(id: introSectionItems.count, component: AnyComponent(Rectangle(color: .clear, height: 346.0, tag: self.introPlaceholderTag))))
|
||||||
introSectionItems.append(AnyComponentWithIdentity(id: introSectionItems.count, component: AnyComponent(ListMultilineTextFieldItemComponent(
|
introSectionItems.append(AnyComponentWithIdentity(id: introSectionItems.count, component: AnyComponent(ListMultilineTextFieldItemComponent(
|
||||||
@ -337,13 +450,14 @@ final class GiftSetupScreenComponent: Component {
|
|||||||
resetText: self.resetText.flatMap {
|
resetText: self.resetText.flatMap {
|
||||||
return ListMultilineTextFieldItemComponent.ResetText(value: $0)
|
return ListMultilineTextFieldItemComponent.ResetText(value: $0)
|
||||||
},
|
},
|
||||||
placeholder: environment.strings.Business_Intro_IntroTextPlaceholder,
|
placeholder: "Enter Message",
|
||||||
autocapitalizationType: .none,
|
autocapitalizationType: .none,
|
||||||
autocorrectionType: .no,
|
autocorrectionType: .no,
|
||||||
returnKeyType: .done,
|
returnKeyType: .done,
|
||||||
characterLimit: 255,
|
characterLimit: 255,
|
||||||
displayCharacterLimit: true,
|
displayCharacterLimit: true,
|
||||||
emptyLineHandling: .notAllowed,
|
emptyLineHandling: .notAllowed,
|
||||||
|
formatMenuAvailability: .available([.bold, .italic, .underline, .strikethrough, .spoiler]),
|
||||||
updated: { _ in
|
updated: { _ in
|
||||||
},
|
},
|
||||||
returnKeyAction: { [weak self] in
|
returnKeyAction: { [weak self] in
|
||||||
@ -355,10 +469,173 @@ final class GiftSetupScreenComponent: Component {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
textUpdateTransition: .spring(duration: 0.4),
|
textUpdateTransition: .spring(duration: 0.4),
|
||||||
|
inputMode: self.currentInputMode,
|
||||||
|
toggleInputMode: { [weak self] in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch self.currentInputMode {
|
||||||
|
case .keyboard:
|
||||||
|
self.currentInputMode = .emoji
|
||||||
|
case .emoji:
|
||||||
|
self.currentInputMode = .keyboard
|
||||||
|
}
|
||||||
|
self.state?.updated(transition: .spring(duration: 0.4))
|
||||||
|
},
|
||||||
tag: self.textInputTag
|
tag: self.textInputTag
|
||||||
))))
|
))))
|
||||||
self.resetText = nil
|
self.resetText = nil
|
||||||
|
|
||||||
|
|
||||||
|
var inputHeight: CGFloat = 0.0
|
||||||
|
inputHeight += self.updateInputMediaNode(
|
||||||
|
component: component,
|
||||||
|
availableSize: availableSize,
|
||||||
|
bottomInset: environment.safeInsets.bottom,
|
||||||
|
inputHeight: 0.0,
|
||||||
|
effectiveInputHeight: environment.deviceMetrics.standardInputHeight(inLandscape: false),
|
||||||
|
metrics: environment.metrics,
|
||||||
|
deviceMetrics: environment.deviceMetrics,
|
||||||
|
transition: transition
|
||||||
|
)
|
||||||
|
if self.inputMediaNode == nil {
|
||||||
|
if environment.inputHeight.isZero && self.textInputState.isEditing, let previousInputHeight = self.previousInputHeight {
|
||||||
|
inputHeight = previousInputHeight
|
||||||
|
} else {
|
||||||
|
inputHeight = environment.inputHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.textInputState.isEditing, let emojiSuggestion = self.textInputState.currentEmojiSuggestion, emojiSuggestion.disposable == nil {
|
||||||
|
emojiSuggestion.disposable = (EmojiSuggestionsComponent.suggestionData(context: component.context, isSavedMessages: false, query: emojiSuggestion.position.value)
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self, weak emojiSuggestion] result in
|
||||||
|
guard let self, self.textInputState.currentEmojiSuggestion === emojiSuggestion else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
emojiSuggestion?.value = result
|
||||||
|
self.state?.updated()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasTrackingView = self.textInputState.hasTrackingView
|
||||||
|
if let currentEmojiSuggestion = self.textInputState.currentEmojiSuggestion, let value = currentEmojiSuggestion.value as? [TelegramMediaFile], value.isEmpty {
|
||||||
|
hasTrackingView = false
|
||||||
|
}
|
||||||
|
if !self.textInputState.isEditing {
|
||||||
|
hasTrackingView = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasTrackingView {
|
||||||
|
if let currentEmojiSuggestion = self.textInputState.currentEmojiSuggestion {
|
||||||
|
self.textInputState.currentEmojiSuggestion = nil
|
||||||
|
currentEmojiSuggestion.disposable?.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
if let currentEmojiSuggestionView = self.currentEmojiSuggestionView {
|
||||||
|
self.currentEmojiSuggestionView = nil
|
||||||
|
|
||||||
|
currentEmojiSuggestionView.alpha = 0.0
|
||||||
|
currentEmojiSuggestionView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak currentEmojiSuggestionView] _ in
|
||||||
|
currentEmojiSuggestionView?.removeFromSuperview()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.textInputState.isEditing, let emojiSuggestion = self.textInputState.currentEmojiSuggestion, let value = emojiSuggestion.value as? [TelegramMediaFile] {
|
||||||
|
let currentEmojiSuggestionView: ComponentHostView<Empty>
|
||||||
|
if let current = self.currentEmojiSuggestionView {
|
||||||
|
currentEmojiSuggestionView = current
|
||||||
|
} else {
|
||||||
|
currentEmojiSuggestionView = ComponentHostView<Empty>()
|
||||||
|
self.currentEmojiSuggestionView = currentEmojiSuggestionView
|
||||||
|
self.addSubview(currentEmojiSuggestionView)
|
||||||
|
|
||||||
|
currentEmojiSuggestionView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
||||||
|
}
|
||||||
|
|
||||||
|
let globalPosition: CGPoint
|
||||||
|
if let textView = (self.introSection.findTaggedView(tag: self.textInputTag) as? ListMultilineTextFieldItemComponent.View)?.textFieldView {
|
||||||
|
globalPosition = textView.convert(emojiSuggestion.localPosition, to: self)
|
||||||
|
} else {
|
||||||
|
globalPosition = .zero
|
||||||
|
}
|
||||||
|
|
||||||
|
let sideInset: CGFloat = 7.0
|
||||||
|
|
||||||
|
let viewSize = currentEmojiSuggestionView.update(
|
||||||
|
transition: .immediate,
|
||||||
|
component: AnyComponent(EmojiSuggestionsComponent(
|
||||||
|
context: component.context,
|
||||||
|
userLocation: .other,
|
||||||
|
theme: EmojiSuggestionsComponent.Theme(theme: environment.theme),
|
||||||
|
animationCache: component.context.animationCache,
|
||||||
|
animationRenderer: component.context.animationRenderer,
|
||||||
|
files: value,
|
||||||
|
action: { [weak self] file in
|
||||||
|
guard let self, let textView = (self.introSection.findTaggedView(tag: self.textInputTag) as? ListMultilineTextFieldItemComponent.View)?.textFieldView, let currentEmojiSuggestion = self.textInputState.currentEmojiSuggestion else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioServicesPlaySystemSound(0x450)
|
||||||
|
|
||||||
|
let inputState = textView.getInputState()
|
||||||
|
let inputText = NSMutableAttributedString(attributedString: inputState.inputText)
|
||||||
|
|
||||||
|
var text: String?
|
||||||
|
var emojiAttribute: ChatTextInputTextCustomEmojiAttribute?
|
||||||
|
loop: for attribute in file.attributes {
|
||||||
|
switch attribute {
|
||||||
|
case let .CustomEmoji(_, _, displayText, _):
|
||||||
|
text = displayText
|
||||||
|
emojiAttribute = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: file.fileId.id, file: file)
|
||||||
|
break loop
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let emojiAttribute = emojiAttribute, let text = text {
|
||||||
|
let replacementText = NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: emojiAttribute])
|
||||||
|
|
||||||
|
let range = currentEmojiSuggestion.position.range
|
||||||
|
let previousText = inputText.attributedSubstring(from: range)
|
||||||
|
inputText.replaceCharacters(in: range, with: replacementText)
|
||||||
|
|
||||||
|
var replacedUpperBound = range.lowerBound
|
||||||
|
while true {
|
||||||
|
if inputText.attributedSubstring(from: NSRange(location: 0, length: replacedUpperBound)).string.hasSuffix(previousText.string) {
|
||||||
|
let replaceRange = NSRange(location: replacedUpperBound - previousText.length, length: previousText.length)
|
||||||
|
if replaceRange.location < 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
let adjacentString = inputText.attributedSubstring(from: replaceRange)
|
||||||
|
if adjacentString.string != previousText.string || adjacentString.attribute(ChatTextInputAttributes.customEmoji, at: 0, effectiveRange: nil) != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
inputText.replaceCharacters(in: replaceRange, with: NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: emojiAttribute.interactivelySelectedFromPackId, fileId: emojiAttribute.fileId, file: emojiAttribute.file)]))
|
||||||
|
replacedUpperBound = replaceRange.lowerBound
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let selectionPosition = range.lowerBound + (replacementText.string as NSString).length
|
||||||
|
textView.updateText(inputText, selectionRange: selectionPosition ..< selectionPosition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0)
|
||||||
|
)
|
||||||
|
|
||||||
|
let viewFrame = CGRect(origin: CGPoint(x: min(availableSize.width - sideInset - viewSize.width, max(sideInset, floor(globalPosition.x - viewSize.width / 2.0))), y: globalPosition.y - 4.0 - viewSize.height), size: viewSize)
|
||||||
|
currentEmojiSuggestionView.frame = viewFrame
|
||||||
|
if let componentView = currentEmojiSuggestionView.componentView as? EmojiSuggestionsComponent.View {
|
||||||
|
componentView.adjustBackground(relativePositionX: floor(globalPosition.x + 10.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let introSectionSize = self.introSection.update(
|
let introSectionSize = self.introSection.update(
|
||||||
transition: transition,
|
transition: transition,
|
||||||
component: AnyComponent(ListSectionComponent(
|
component: AnyComponent(ListSectionComponent(
|
||||||
@ -387,64 +664,44 @@ final class GiftSetupScreenComponent: Component {
|
|||||||
}
|
}
|
||||||
contentHeight += introSectionSize.height
|
contentHeight += introSectionSize.height
|
||||||
contentHeight += sectionSpacing
|
contentHeight += sectionSpacing
|
||||||
|
|
||||||
// let titleText: String
|
|
||||||
// if self.titleInputState.text.string.isEmpty {
|
|
||||||
// titleText = environment.strings.Conversation_EmptyPlaceholder
|
|
||||||
// } else {
|
|
||||||
// let rawTitle = self.titleInputState.text.string
|
|
||||||
// titleText = rawTitle.count <= maxTitleLength ? rawTitle : String(rawTitle[rawTitle.startIndex ..< rawTitle.index(rawTitle.startIndex, offsetBy: maxTitleLength)])
|
|
||||||
// }
|
|
||||||
|
|
||||||
// let textText: String
|
|
||||||
// if self.textInputState.text.string.isEmpty {
|
|
||||||
// textText = environment.strings.Conversation_GreetingText
|
|
||||||
// } else {
|
|
||||||
// let rawText = self.textInputState.text.string
|
|
||||||
// textText = rawText.count <= maxTextLength ? rawText : String(rawText[rawText.startIndex ..< rawText.index(rawText.startIndex, offsetBy: maxTextLength)])
|
|
||||||
// }
|
|
||||||
|
|
||||||
let listItemParams = ListViewItemLayoutParams(width: availableSize.width - sideInset * 2.0, leftInset: 0.0, rightInset: 0.0, availableHeight: 10000.0, isStandalone: true)
|
let listItemParams = ListViewItemLayoutParams(width: availableSize.width - sideInset * 2.0, leftInset: 0.0, rightInset: 0.0, availableHeight: 10000.0, isStandalone: true)
|
||||||
let introContentSize = self.introContent.update(
|
if let accountPeer = self.peerMap[component.context.account.peerId] {
|
||||||
transition: transition,
|
let introContentSize = self.introContent.update(
|
||||||
component: AnyComponent(
|
transition: transition,
|
||||||
ListItemComponentAdaptor(
|
component: AnyComponent(
|
||||||
itemGenerator: ChatGiftPreviewItem(
|
ListItemComponentAdaptor(
|
||||||
context: component.context,
|
itemGenerator: ChatGiftPreviewItem(
|
||||||
theme: environment.theme,
|
context: component.context,
|
||||||
componentTheme: environment.theme,
|
theme: environment.theme,
|
||||||
strings: environment.strings,
|
componentTheme: environment.theme,
|
||||||
sectionId: 0,
|
strings: environment.strings,
|
||||||
fontSize: presentationData.chatFontSize,
|
sectionId: 0,
|
||||||
chatBubbleCorners: presentationData.chatBubbleCorners,
|
fontSize: presentationData.chatFontSize,
|
||||||
wallpaper: presentationData.chatWallpaper,
|
chatBubbleCorners: presentationData.chatBubbleCorners,
|
||||||
dateTimeFormat: environment.dateTimeFormat,
|
wallpaper: presentationData.chatWallpaper,
|
||||||
nameDisplayOrder: presentationData.nameDisplayOrder,
|
dateTimeFormat: environment.dateTimeFormat,
|
||||||
accountPeer: self.peerMap[component.context.account.peerId],
|
nameDisplayOrder: presentationData.nameDisplayOrder,
|
||||||
gift: component.gift,
|
accountPeer: accountPeer,
|
||||||
text: self.textInputState.text.string
|
gift: component.gift,
|
||||||
),
|
text: self.textInputState.text.string,
|
||||||
params: listItemParams
|
entities: generateChatInputTextEntities(self.textInputState.text)
|
||||||
)
|
),
|
||||||
),
|
params: listItemParams
|
||||||
environment: {},
|
)
|
||||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
|
),
|
||||||
)
|
environment: {},
|
||||||
if let introContentView = self.introContent.view {
|
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
|
||||||
if introContentView.superview == nil {
|
)
|
||||||
if let placeholderView = self.introSection.findTaggedView(tag: self.introPlaceholderTag) {
|
if let introContentView = self.introContent.view {
|
||||||
placeholderView.addSubview(introContentView)
|
if introContentView.superview == nil {
|
||||||
|
if let placeholderView = self.introSection.findTaggedView(tag: self.introPlaceholderTag) {
|
||||||
|
placeholderView.addSubview(introContentView)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
transition.setFrame(view: introContentView, frame: CGRect(origin: CGPoint(), size: introContentSize))
|
||||||
transition.setFrame(view: introContentView, frame: CGRect(origin: CGPoint(), size: introContentSize))
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.recenterOnTag == nil && self.previousHadInputHeight != (environment.inputHeight > 0.0) {
|
|
||||||
if self.textInputState.isEditing {
|
|
||||||
self.recenterOnTag = self.textInputTag
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.previousHadInputHeight = environment.inputHeight > 0.0
|
|
||||||
|
|
||||||
let peerName = self.peerMap[component.peerId]?.compactDisplayTitle ?? ""
|
let peerName = self.peerMap[component.peerId]?.compactDisplayTitle ?? ""
|
||||||
let hideSectionSize = self.hideSection.update(
|
let hideSectionSize = self.hideSection.update(
|
||||||
@ -498,11 +755,9 @@ final class GiftSetupScreenComponent: Component {
|
|||||||
|
|
||||||
contentHeight += bottomContentInset
|
contentHeight += bottomContentInset
|
||||||
|
|
||||||
let inputHeight: CGFloat = environment.inputHeight
|
|
||||||
let combinedBottomInset = max(inputHeight, environment.safeInsets.bottom)
|
let combinedBottomInset = max(inputHeight, environment.safeInsets.bottom)
|
||||||
contentHeight += combinedBottomInset
|
contentHeight += combinedBottomInset
|
||||||
|
|
||||||
|
|
||||||
if self.starImage == nil || self.starImage?.1 !== environment.theme {
|
if self.starImage == nil || self.starImage?.1 !== environment.theme {
|
||||||
self.starImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: environment.theme.list.itemCheckColors.foregroundColor)!, environment.theme)
|
self.starImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: environment.theme.list.itemCheckColors.foregroundColor)!, environment.theme)
|
||||||
}
|
}
|
||||||
@ -545,6 +800,22 @@ final class GiftSetupScreenComponent: Component {
|
|||||||
|
|
||||||
let previousBounds = self.scrollView.bounds
|
let previousBounds = self.scrollView.bounds
|
||||||
|
|
||||||
|
self.recenterOnTag = nil
|
||||||
|
if let hint = transition.userData(TextFieldComponent.AnimationHint.self), let targetView = hint.view {
|
||||||
|
if let textView = self.introSection.findTaggedView(tag: self.textInputTag) {
|
||||||
|
if targetView.isDescendant(of: textView) {
|
||||||
|
self.recenterOnTag = self.textInputTag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if self.recenterOnTag == nil && self.previousHadInputHeight != (environment.inputHeight > 0.0) {
|
||||||
|
if self.textInputState.isEditing {
|
||||||
|
self.recenterOnTag = self.textInputTag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.previousHadInputHeight = inputHeight > 0.0
|
||||||
|
self.previousInputHeight = inputHeight
|
||||||
|
|
||||||
self.ignoreScrolling = true
|
self.ignoreScrolling = true
|
||||||
let contentSize = CGSize(width: availableSize.width, height: contentHeight)
|
let contentSize = CGSize(width: availableSize.width, height: contentHeight)
|
||||||
if self.scrollView.frame != CGRect(origin: CGPoint(), size: availableSize) {
|
if self.scrollView.frame != CGRect(origin: CGPoint(), size: availableSize) {
|
||||||
@ -592,6 +863,152 @@ final class GiftSetupScreenComponent: Component {
|
|||||||
|
|
||||||
return availableSize
|
return availableSize
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func updateInputMediaNode(
|
||||||
|
component: GiftSetupScreenComponent,
|
||||||
|
availableSize: CGSize,
|
||||||
|
bottomInset: CGFloat,
|
||||||
|
inputHeight: CGFloat,
|
||||||
|
effectiveInputHeight: CGFloat,
|
||||||
|
metrics: LayoutMetrics,
|
||||||
|
deviceMetrics: DeviceMetrics,
|
||||||
|
transition: ComponentTransition
|
||||||
|
) -> CGFloat {
|
||||||
|
let bottomInset: CGFloat = bottomInset + 8.0
|
||||||
|
let bottomContainerInset: CGFloat = 0.0
|
||||||
|
let needsInputActivation: Bool = !"".isEmpty
|
||||||
|
|
||||||
|
var height: CGFloat = 0.0
|
||||||
|
if case .emoji = self.currentInputMode, let inputData = self.inputMediaNodeData {
|
||||||
|
let inputMediaNode: ChatEntityKeyboardInputNode
|
||||||
|
var inputMediaNodeTransition = transition
|
||||||
|
var animateIn = false
|
||||||
|
if let current = self.inputMediaNode {
|
||||||
|
inputMediaNode = current
|
||||||
|
} else {
|
||||||
|
animateIn = true
|
||||||
|
inputMediaNodeTransition = inputMediaNodeTransition.withAnimation(.none)
|
||||||
|
inputMediaNode = ChatEntityKeyboardInputNode(
|
||||||
|
context: component.context,
|
||||||
|
currentInputData: inputData,
|
||||||
|
updatedInputData: self.inputMediaNodeDataPromise.get(),
|
||||||
|
defaultToEmojiTab: true,
|
||||||
|
opaqueTopPanelBackground: false,
|
||||||
|
useOpaqueTheme: true,
|
||||||
|
interaction: self.inputMediaInteraction,
|
||||||
|
chatPeerId: nil,
|
||||||
|
stateContext: self.inputMediaNodeStateContext
|
||||||
|
)
|
||||||
|
inputMediaNode.clipsToBounds = true
|
||||||
|
|
||||||
|
inputMediaNode.externalTopPanelContainerImpl = nil
|
||||||
|
inputMediaNode.useExternalSearchContainer = true
|
||||||
|
if inputMediaNode.view.superview == nil {
|
||||||
|
self.inputMediaNodeBackground.removeAllAnimations()
|
||||||
|
self.layer.addSublayer(self.inputMediaNodeBackground)
|
||||||
|
self.addSubview(inputMediaNode.view)
|
||||||
|
}
|
||||||
|
self.inputMediaNode = inputMediaNode
|
||||||
|
}
|
||||||
|
|
||||||
|
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||||
|
let presentationInterfaceState = ChatPresentationInterfaceState(
|
||||||
|
chatWallpaper: .builtin(WallpaperSettings()),
|
||||||
|
theme: presentationData.theme,
|
||||||
|
strings: presentationData.strings,
|
||||||
|
dateTimeFormat: presentationData.dateTimeFormat,
|
||||||
|
nameDisplayOrder: presentationData.nameDisplayOrder,
|
||||||
|
limitsConfiguration: component.context.currentLimitsConfiguration.with { $0 },
|
||||||
|
fontSize: presentationData.chatFontSize,
|
||||||
|
bubbleCorners: presentationData.chatBubbleCorners,
|
||||||
|
accountPeerId: component.context.account.peerId,
|
||||||
|
mode: .standard(.default),
|
||||||
|
chatLocation: .peer(id: component.context.account.peerId),
|
||||||
|
subject: nil,
|
||||||
|
peerNearbyData: nil,
|
||||||
|
greetingData: nil,
|
||||||
|
pendingUnpinnedAllMessages: false,
|
||||||
|
activeGroupCallInfo: nil,
|
||||||
|
hasActiveGroupCall: false,
|
||||||
|
importState: nil,
|
||||||
|
threadData: nil,
|
||||||
|
isGeneralThreadClosed: nil,
|
||||||
|
replyMessage: nil,
|
||||||
|
accountPeerColor: nil,
|
||||||
|
businessIntro: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
self.inputMediaNodeBackground.backgroundColor = presentationData.theme.rootController.navigationBar.opaqueBackgroundColor.cgColor
|
||||||
|
|
||||||
|
let heightAndOverflow = inputMediaNode.updateLayout(width: availableSize.width, leftInset: 0.0, rightInset: 0.0, bottomInset: bottomInset, standardInputHeight: deviceMetrics.standardInputHeight(inLandscape: false), inputHeight: inputHeight < 100.0 ? inputHeight - bottomContainerInset : inputHeight, maximumHeight: availableSize.height, inputPanelHeight: 0.0, transition: .immediate, interfaceState: presentationInterfaceState, layoutMetrics: metrics, deviceMetrics: deviceMetrics, isVisible: true, isExpanded: false)
|
||||||
|
let inputNodeHeight = heightAndOverflow.0
|
||||||
|
let inputNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - inputNodeHeight), size: CGSize(width: availableSize.width, height: inputNodeHeight))
|
||||||
|
|
||||||
|
let inputNodeBackgroundFrame = CGRect(origin: CGPoint(x: inputNodeFrame.minX, y: inputNodeFrame.minY - 6.0), size: CGSize(width: inputNodeFrame.width, height: inputNodeFrame.height + 6.0))
|
||||||
|
|
||||||
|
if needsInputActivation {
|
||||||
|
let inputNodeFrame = inputNodeFrame.offsetBy(dx: 0.0, dy: inputNodeHeight)
|
||||||
|
ComponentTransition.immediate.setFrame(layer: inputMediaNode.layer, frame: inputNodeFrame)
|
||||||
|
ComponentTransition.immediate.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeBackgroundFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
if animateIn {
|
||||||
|
var targetFrame = inputNodeFrame
|
||||||
|
targetFrame.origin.y = availableSize.height
|
||||||
|
inputMediaNodeTransition.setFrame(layer: inputMediaNode.layer, frame: targetFrame)
|
||||||
|
|
||||||
|
let inputNodeBackgroundTargetFrame = CGRect(origin: CGPoint(x: targetFrame.minX, y: targetFrame.minY - 6.0), size: CGSize(width: targetFrame.width, height: targetFrame.height + 6.0))
|
||||||
|
|
||||||
|
inputMediaNodeTransition.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeBackgroundTargetFrame)
|
||||||
|
|
||||||
|
transition.setFrame(layer: inputMediaNode.layer, frame: inputNodeFrame)
|
||||||
|
transition.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeBackgroundFrame)
|
||||||
|
} else {
|
||||||
|
inputMediaNodeTransition.setFrame(layer: inputMediaNode.layer, frame: inputNodeFrame)
|
||||||
|
inputMediaNodeTransition.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeBackgroundFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
height = heightAndOverflow.0
|
||||||
|
} else {
|
||||||
|
self.inputMediaNodeTargetTag = nil
|
||||||
|
|
||||||
|
if let inputMediaNode = self.inputMediaNode {
|
||||||
|
self.inputMediaNode = nil
|
||||||
|
var targetFrame = inputMediaNode.frame
|
||||||
|
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: { [weak self] _ in
|
||||||
|
Queue.mainQueue().after(0.3) {
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if self.currentInputMode == .keyboard {
|
||||||
|
self.inputMediaNodeBackground.animateAlpha(from: 1.0, to: 0.0, duration: 0.35, removeOnCompletion: false, completion: { [weak self] finished in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if finished {
|
||||||
|
self.inputMediaNodeBackground.removeFromSuperlayer()
|
||||||
|
}
|
||||||
|
self.inputMediaNodeBackground.removeAllAnimations()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return height
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeView() -> View {
|
func makeView() -> View {
|
||||||
|
@ -10,12 +10,15 @@ swift_library(
|
|||||||
"-warnings-as-errors",
|
"-warnings-as-errors",
|
||||||
],
|
],
|
||||||
deps = [
|
deps = [
|
||||||
|
"//submodules/SSignalKit/SwiftSignalKit",
|
||||||
"//submodules/Display",
|
"//submodules/Display",
|
||||||
"//submodules/ComponentFlow",
|
"//submodules/ComponentFlow",
|
||||||
"//submodules/TelegramPresentationData",
|
"//submodules/TelegramPresentationData",
|
||||||
"//submodules/Components/MultilineTextComponent",
|
"//submodules/Components/MultilineTextComponent",
|
||||||
"//submodules/TelegramUI/Components/ListSectionComponent",
|
"//submodules/TelegramUI/Components/ListSectionComponent",
|
||||||
"//submodules/TelegramUI/Components/TextFieldComponent",
|
"//submodules/TelegramUI/Components/TextFieldComponent",
|
||||||
|
"//submodules/TelegramUI/Components/LottieComponent",
|
||||||
|
"//submodules/TelegramUI/Components/PlainButtonComponent",
|
||||||
"//submodules/AccountContext",
|
"//submodules/AccountContext",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
|
@ -2,10 +2,13 @@ import Foundation
|
|||||||
import UIKit
|
import UIKit
|
||||||
import Display
|
import Display
|
||||||
import ComponentFlow
|
import ComponentFlow
|
||||||
|
import SwiftSignalKit
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
import MultilineTextComponent
|
import MultilineTextComponent
|
||||||
import ListSectionComponent
|
import ListSectionComponent
|
||||||
import TextFieldComponent
|
import TextFieldComponent
|
||||||
|
import LottieComponent
|
||||||
|
import PlainButtonComponent
|
||||||
import AccountContext
|
import AccountContext
|
||||||
|
|
||||||
public final class ListMultilineTextFieldItemComponent: Component {
|
public final class ListMultilineTextFieldItemComponent: Component {
|
||||||
@ -14,6 +17,11 @@ public final class ListMultilineTextFieldItemComponent: Component {
|
|||||||
public fileprivate(set) var text: NSAttributedString = NSAttributedString()
|
public fileprivate(set) var text: NSAttributedString = NSAttributedString()
|
||||||
public fileprivate(set) var isEditing: Bool = false
|
public fileprivate(set) var isEditing: Bool = false
|
||||||
|
|
||||||
|
public var hasTrackingView = false
|
||||||
|
|
||||||
|
public var currentEmojiSuggestion: TextFieldComponent.EmojiSuggestion?
|
||||||
|
public var dismissedEmojiSuggestionPosition: TextFieldComponent.EmojiSuggestion.Position?
|
||||||
|
|
||||||
public init() {
|
public init() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -30,6 +38,11 @@ public final class ListMultilineTextFieldItemComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum InputMode {
|
||||||
|
case keyboard
|
||||||
|
case emoji
|
||||||
|
}
|
||||||
|
|
||||||
public enum EmptyLineHandling {
|
public enum EmptyLineHandling {
|
||||||
case allowed
|
case allowed
|
||||||
case oneConsecutive
|
case oneConsecutive
|
||||||
@ -49,10 +62,13 @@ public final class ListMultilineTextFieldItemComponent: Component {
|
|||||||
public let characterLimit: Int?
|
public let characterLimit: Int?
|
||||||
public let displayCharacterLimit: Bool
|
public let displayCharacterLimit: Bool
|
||||||
public let emptyLineHandling: EmptyLineHandling
|
public let emptyLineHandling: EmptyLineHandling
|
||||||
|
public let formatMenuAvailability: TextFieldComponent.FormatMenuAvailability
|
||||||
public let updated: ((String) -> Void)?
|
public let updated: ((String) -> Void)?
|
||||||
public let returnKeyAction: (() -> Void)?
|
public let returnKeyAction: (() -> Void)?
|
||||||
public let backspaceKeyAction: (() -> Void)?
|
public let backspaceKeyAction: (() -> Void)?
|
||||||
public let textUpdateTransition: ComponentTransition
|
public let textUpdateTransition: ComponentTransition
|
||||||
|
public let inputMode: InputMode?
|
||||||
|
public let toggleInputMode: (() -> Void)?
|
||||||
public let tag: AnyObject?
|
public let tag: AnyObject?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
@ -69,10 +85,13 @@ public final class ListMultilineTextFieldItemComponent: Component {
|
|||||||
characterLimit: Int? = nil,
|
characterLimit: Int? = nil,
|
||||||
displayCharacterLimit: Bool = false,
|
displayCharacterLimit: Bool = false,
|
||||||
emptyLineHandling: EmptyLineHandling = .allowed,
|
emptyLineHandling: EmptyLineHandling = .allowed,
|
||||||
|
formatMenuAvailability: TextFieldComponent.FormatMenuAvailability = .none,
|
||||||
updated: ((String) -> Void)? = nil,
|
updated: ((String) -> Void)? = nil,
|
||||||
returnKeyAction: (() -> Void)? = nil,
|
returnKeyAction: (() -> Void)? = nil,
|
||||||
backspaceKeyAction: (() -> Void)? = nil,
|
backspaceKeyAction: (() -> Void)? = nil,
|
||||||
textUpdateTransition: ComponentTransition = .immediate,
|
textUpdateTransition: ComponentTransition = .immediate,
|
||||||
|
inputMode: InputMode? = nil,
|
||||||
|
toggleInputMode: (() -> Void)? = nil,
|
||||||
tag: AnyObject? = nil
|
tag: AnyObject? = nil
|
||||||
) {
|
) {
|
||||||
self.externalState = externalState
|
self.externalState = externalState
|
||||||
@ -88,10 +107,13 @@ public final class ListMultilineTextFieldItemComponent: Component {
|
|||||||
self.characterLimit = characterLimit
|
self.characterLimit = characterLimit
|
||||||
self.displayCharacterLimit = displayCharacterLimit
|
self.displayCharacterLimit = displayCharacterLimit
|
||||||
self.emptyLineHandling = emptyLineHandling
|
self.emptyLineHandling = emptyLineHandling
|
||||||
|
self.formatMenuAvailability = formatMenuAvailability
|
||||||
self.updated = updated
|
self.updated = updated
|
||||||
self.returnKeyAction = returnKeyAction
|
self.returnKeyAction = returnKeyAction
|
||||||
self.backspaceKeyAction = backspaceKeyAction
|
self.backspaceKeyAction = backspaceKeyAction
|
||||||
self.textUpdateTransition = textUpdateTransition
|
self.textUpdateTransition = textUpdateTransition
|
||||||
|
self.inputMode = inputMode
|
||||||
|
self.toggleInputMode = toggleInputMode
|
||||||
self.tag = tag
|
self.tag = tag
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,9 +157,15 @@ public final class ListMultilineTextFieldItemComponent: Component {
|
|||||||
if lhs.emptyLineHandling != rhs.emptyLineHandling {
|
if lhs.emptyLineHandling != rhs.emptyLineHandling {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.formatMenuAvailability != rhs.formatMenuAvailability {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if (lhs.updated == nil) != (rhs.updated == nil) {
|
if (lhs.updated == nil) != (rhs.updated == nil) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.inputMode != rhs.inputMode {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -145,6 +173,8 @@ public final class ListMultilineTextFieldItemComponent: Component {
|
|||||||
private let textField = ComponentView<Empty>()
|
private let textField = ComponentView<Empty>()
|
||||||
private let textFieldExternalState = TextFieldComponent.ExternalState()
|
private let textFieldExternalState = TextFieldComponent.ExternalState()
|
||||||
|
|
||||||
|
private var modeSelector: ComponentView<Empty>?
|
||||||
|
|
||||||
private let placeholder = ComponentView<Empty>()
|
private let placeholder = ComponentView<Empty>()
|
||||||
private var customPlaceholder: ComponentView<Empty>?
|
private var customPlaceholder: ComponentView<Empty>?
|
||||||
|
|
||||||
@ -203,17 +233,40 @@ public final class ListMultilineTextFieldItemComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func insertText(text: NSAttributedString) {
|
||||||
|
if let textFieldView = self.textField.view as? TextFieldComponent.View {
|
||||||
|
textFieldView.insertText(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func backwardsDeleteText() {
|
||||||
|
if let textFieldView = self.textField.view as? TextFieldComponent.View {
|
||||||
|
textFieldView.deleteBackward()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var textFieldView: TextFieldComponent.View? {
|
||||||
|
return self.textField.view as? TextFieldComponent.View
|
||||||
|
}
|
||||||
|
|
||||||
func update(component: ListMultilineTextFieldItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
func update(component: ListMultilineTextFieldItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||||
self.isUpdating = true
|
self.isUpdating = true
|
||||||
defer {
|
defer {
|
||||||
self.isUpdating = false
|
self.isUpdating = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let previousComponent = self.component
|
||||||
self.component = component
|
self.component = component
|
||||||
self.state = state
|
self.state = state
|
||||||
|
|
||||||
let verticalInset: CGFloat = 12.0
|
let verticalInset: CGFloat = 12.0
|
||||||
let sideInset: CGFloat = 16.0
|
let leftInset: CGFloat = 16.0
|
||||||
|
var rightInset: CGFloat = 16.0
|
||||||
|
let modeSelectorSize = CGSize(width: 32.0, height: 32.0)
|
||||||
|
|
||||||
|
if component.inputMode != nil {
|
||||||
|
rightInset += 34.0
|
||||||
|
}
|
||||||
|
|
||||||
let textLimitFont = Font.regular(15.0)
|
let textLimitFont = Font.regular(15.0)
|
||||||
var measureTextLimitInset: CGFloat = 0.0
|
var measureTextLimitInset: CGFloat = 0.0
|
||||||
@ -258,8 +311,8 @@ public final class ListMultilineTextFieldItemComponent: Component {
|
|||||||
fontSize: 17.0,
|
fontSize: 17.0,
|
||||||
textColor: component.theme.list.itemPrimaryTextColor,
|
textColor: component.theme.list.itemPrimaryTextColor,
|
||||||
accentColor: component.theme.list.itemPrimaryTextColor,
|
accentColor: component.theme.list.itemPrimaryTextColor,
|
||||||
insets: UIEdgeInsets(top: verticalInset, left: sideInset - 8.0, bottom: verticalInset, right: sideInset - 8.0 + measureTextLimitInset),
|
insets: UIEdgeInsets(top: verticalInset, left: leftInset - 8.0, bottom: verticalInset, right: rightInset - 8.0 + measureTextLimitInset),
|
||||||
hideKeyboard: false,
|
hideKeyboard: component.inputMode == .emoji,
|
||||||
customInputView: nil,
|
customInputView: nil,
|
||||||
resetText: component.resetText.flatMap { resetText in
|
resetText: component.resetText.flatMap { resetText in
|
||||||
return NSAttributedString(string: resetText.value, font: Font.regular(17.0), textColor: component.theme.list.itemPrimaryTextColor)
|
return NSAttributedString(string: resetText.value, font: Font.regular(17.0), textColor: component.theme.list.itemPrimaryTextColor)
|
||||||
@ -267,7 +320,7 @@ public final class ListMultilineTextFieldItemComponent: Component {
|
|||||||
isOneLineWhenUnfocused: false,
|
isOneLineWhenUnfocused: false,
|
||||||
characterLimit: component.characterLimit,
|
characterLimit: component.characterLimit,
|
||||||
emptyLineHandling: mappedEmptyLineHandling,
|
emptyLineHandling: mappedEmptyLineHandling,
|
||||||
formatMenuAvailability: .none,
|
formatMenuAvailability: component.formatMenuAvailability,
|
||||||
returnKeyType: component.returnKeyType,
|
returnKeyType: component.returnKeyType,
|
||||||
lockedFormatAction: {
|
lockedFormatAction: {
|
||||||
},
|
},
|
||||||
@ -309,9 +362,9 @@ public final class ListMultilineTextFieldItemComponent: Component {
|
|||||||
text: .plain(NSAttributedString(string: component.placeholder.isEmpty ? " " : component.placeholder, font: Font.regular(17.0), textColor: component.theme.list.itemPlaceholderTextColor))
|
text: .plain(NSAttributedString(string: component.placeholder.isEmpty ? " " : component.placeholder, font: Font.regular(17.0), textColor: component.theme.list.itemPlaceholderTextColor))
|
||||||
)),
|
)),
|
||||||
environment: {},
|
environment: {},
|
||||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0)
|
containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0)
|
||||||
)
|
)
|
||||||
let placeholderFrame = CGRect(origin: CGPoint(x: sideInset, y: verticalInset), size: placeholderSize)
|
let placeholderFrame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: placeholderSize)
|
||||||
if let placeholderView = self.placeholder.view {
|
if let placeholderView = self.placeholder.view {
|
||||||
if placeholderView.superview == nil {
|
if placeholderView.superview == nil {
|
||||||
placeholderView.layer.anchorPoint = CGPoint()
|
placeholderView.layer.anchorPoint = CGPoint()
|
||||||
@ -329,6 +382,9 @@ public final class ListMultilineTextFieldItemComponent: Component {
|
|||||||
component.externalState?.hasText = self.textFieldExternalState.hasText
|
component.externalState?.hasText = self.textFieldExternalState.hasText
|
||||||
component.externalState?.text = self.textFieldExternalState.text
|
component.externalState?.text = self.textFieldExternalState.text
|
||||||
component.externalState?.isEditing = self.textFieldExternalState.isEditing
|
component.externalState?.isEditing = self.textFieldExternalState.isEditing
|
||||||
|
component.externalState?.currentEmojiSuggestion = self.textFieldExternalState.currentEmojiSuggestion
|
||||||
|
component.externalState?.dismissedEmojiSuggestionPosition = self.textFieldExternalState.dismissedEmojiSuggestionPosition
|
||||||
|
component.externalState?.hasTrackingView = self.textFieldExternalState.hasTrackingView
|
||||||
|
|
||||||
var displayRemainingLimit: Int?
|
var displayRemainingLimit: Int?
|
||||||
if let characterLimit = component.characterLimit, component.displayCharacterLimit {
|
if let characterLimit = component.characterLimit, component.displayCharacterLimit {
|
||||||
@ -357,7 +413,7 @@ public final class ListMultilineTextFieldItemComponent: Component {
|
|||||||
environment: {},
|
environment: {},
|
||||||
containerSize: CGSize(width: 100.0, height: 100.0)
|
containerSize: CGSize(width: 100.0, height: 100.0)
|
||||||
)
|
)
|
||||||
let textLimitLabelFrame = CGRect(origin: CGPoint(x: availableSize.width - textLimitLabelSize.width - sideInset, y: verticalInset + 2.0), size: textLimitLabelSize)
|
let textLimitLabelFrame = CGRect(origin: CGPoint(x: availableSize.width - textLimitLabelSize.width - rightInset, y: verticalInset + 2.0), size: textLimitLabelSize)
|
||||||
if let textLimitLabelView = textLimitLabel.view {
|
if let textLimitLabelView = textLimitLabel.view {
|
||||||
if textLimitLabelView.superview == nil {
|
if textLimitLabelView.superview == nil {
|
||||||
textLimitLabelView.isUserInteractionEnabled = false
|
textLimitLabelView.isUserInteractionEnabled = false
|
||||||
@ -374,6 +430,91 @@ public final class ListMultilineTextFieldItemComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let inputMode = component.inputMode {
|
||||||
|
var modeSelectorTransition = transition
|
||||||
|
let modeSelector: ComponentView<Empty>
|
||||||
|
if let current = self.modeSelector {
|
||||||
|
modeSelector = current
|
||||||
|
} else {
|
||||||
|
modeSelectorTransition = modeSelectorTransition.withAnimation(.none)
|
||||||
|
modeSelector = ComponentView()
|
||||||
|
self.modeSelector = modeSelector
|
||||||
|
}
|
||||||
|
let animationName: String
|
||||||
|
var playAnimation = false
|
||||||
|
if let previousComponent, let previousInputMode = previousComponent.inputMode {
|
||||||
|
if previousInputMode != inputMode {
|
||||||
|
playAnimation = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch inputMode {
|
||||||
|
case .keyboard:
|
||||||
|
animationName = "input_anim_keyToSmile"
|
||||||
|
case .emoji:
|
||||||
|
animationName = "input_anim_smileToKey"
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = modeSelector.update(
|
||||||
|
transition: modeSelectorTransition,
|
||||||
|
component: AnyComponent(PlainButtonComponent(
|
||||||
|
content: AnyComponent(LottieComponent(
|
||||||
|
content: LottieComponent.AppBundleContent(
|
||||||
|
name: animationName
|
||||||
|
),
|
||||||
|
color: component.theme.chat.inputPanel.inputControlColor.blitOver(component.theme.list.itemBlocksBackgroundColor, alpha: 1.0),
|
||||||
|
size: modeSelectorSize
|
||||||
|
)),
|
||||||
|
effectAlignment: .center,
|
||||||
|
action: { [weak self] in
|
||||||
|
guard let self, let component = self.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
component.toggleInputMode?()
|
||||||
|
},
|
||||||
|
animateScale: false
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: modeSelectorSize
|
||||||
|
)
|
||||||
|
let modeSelectorFrame = CGRect(origin: CGPoint(x: size.width - 4.0 - modeSelectorSize.width, y: floor((size.height - modeSelectorSize.height) * 0.5)), size: modeSelectorSize)
|
||||||
|
if let modeSelectorView = modeSelector.view as? PlainButtonComponent.View {
|
||||||
|
let alphaTransition: ComponentTransition = .easeInOut(duration: 0.2)
|
||||||
|
|
||||||
|
if modeSelectorView.superview == nil {
|
||||||
|
self.addSubview(modeSelectorView)
|
||||||
|
ComponentTransition.immediate.setAlpha(view: modeSelectorView, alpha: 0.0)
|
||||||
|
ComponentTransition.immediate.setScale(view: modeSelectorView, scale: 0.001)
|
||||||
|
}
|
||||||
|
|
||||||
|
if playAnimation, let animationView = modeSelectorView.contentView as? LottieComponent.View {
|
||||||
|
animationView.playOnce()
|
||||||
|
}
|
||||||
|
|
||||||
|
modeSelectorTransition.setPosition(view: modeSelectorView, position: modeSelectorFrame.center)
|
||||||
|
modeSelectorTransition.setBounds(view: modeSelectorView, bounds: CGRect(origin: CGPoint(), size: modeSelectorFrame.size))
|
||||||
|
|
||||||
|
if let externalState = component.externalState {
|
||||||
|
let displaySelector = externalState.isEditing
|
||||||
|
|
||||||
|
alphaTransition.setAlpha(view: modeSelectorView, alpha: displaySelector ? 1.0 : 0.0)
|
||||||
|
alphaTransition.setScale(view: modeSelectorView, scale: displaySelector ? 1.0 : 0.001)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if let modeSelector = self.modeSelector {
|
||||||
|
self.modeSelector = nil
|
||||||
|
if let modeSelectorView = modeSelector.view {
|
||||||
|
if !transition.animation.isImmediate {
|
||||||
|
let alphaTransition: ComponentTransition = .easeInOut(duration: 0.2)
|
||||||
|
alphaTransition.setAlpha(view: modeSelectorView, alpha: 0.0, completion: { [weak modeSelectorView] _ in
|
||||||
|
modeSelectorView?.removeFromSuperview()
|
||||||
|
})
|
||||||
|
alphaTransition.setScale(view: modeSelectorView, scale: 0.001)
|
||||||
|
} else {
|
||||||
|
modeSelectorView.removeFromSuperview()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return size
|
return size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"app": "11.1.2",
|
"app": "11.2",
|
||||||
"xcode": "16.0",
|
"xcode": "16.0",
|
||||||
"bazel": "7.3.1",
|
"bazel": "7.3.1",
|
||||||
"macos": "15.0"
|
"macos": "15.0"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user