mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-10-09 03:20:48 +00:00
Story caption and reply text length limit
This commit is contained in:
parent
14c595b53f
commit
16c66c7bad
@ -17,6 +17,7 @@ public struct UserLimitsConfiguration: Equatable {
|
||||
public let maxReactionsPerMessage: Int32
|
||||
public let maxSharedFolderInviteLinks: Int32
|
||||
public let maxSharedFolderJoin: Int32
|
||||
public let maxStoryCaptionLength: Int32
|
||||
|
||||
public static var defaultValue: UserLimitsConfiguration {
|
||||
return UserLimitsConfiguration(
|
||||
@ -34,7 +35,8 @@ public struct UserLimitsConfiguration: Equatable {
|
||||
maxAnimatedEmojisInText: 10,
|
||||
maxReactionsPerMessage: 1,
|
||||
maxSharedFolderInviteLinks: 3,
|
||||
maxSharedFolderJoin: 2
|
||||
maxSharedFolderJoin: 2,
|
||||
maxStoryCaptionLength: 1024
|
||||
)
|
||||
}
|
||||
|
||||
@ -53,7 +55,8 @@ public struct UserLimitsConfiguration: Equatable {
|
||||
maxAnimatedEmojisInText: Int32,
|
||||
maxReactionsPerMessage: Int32,
|
||||
maxSharedFolderInviteLinks: Int32,
|
||||
maxSharedFolderJoin: Int32
|
||||
maxSharedFolderJoin: Int32,
|
||||
maxStoryCaptionLength: Int32
|
||||
) {
|
||||
self.maxPinnedChatCount = maxPinnedChatCount
|
||||
self.maxArchivedPinnedChatCount = maxArchivedPinnedChatCount
|
||||
@ -70,6 +73,7 @@ public struct UserLimitsConfiguration: Equatable {
|
||||
self.maxReactionsPerMessage = maxReactionsPerMessage
|
||||
self.maxSharedFolderInviteLinks = maxSharedFolderInviteLinks
|
||||
self.maxSharedFolderJoin = maxSharedFolderJoin
|
||||
self.maxStoryCaptionLength = maxStoryCaptionLength
|
||||
}
|
||||
}
|
||||
|
||||
@ -109,5 +113,6 @@ extension UserLimitsConfiguration {
|
||||
self.maxReactionsPerMessage = getValue("reactions_user_max", orElse: 1)
|
||||
self.maxSharedFolderInviteLinks = getValue("chatlist_invites_limit", orElse: isPremium ? 100 : 3)
|
||||
self.maxSharedFolderJoin = getValue("chatlists_joined_limit", orElse: isPremium ? 100 : 2)
|
||||
self.maxStoryCaptionLength = getGeneralValue("story_caption_length_limit", orElse: defaultValue.maxStoryCaptionLength)
|
||||
}
|
||||
}
|
||||
|
@ -51,6 +51,7 @@ public enum EngineConfiguration {
|
||||
public let maxReactionsPerMessage: Int32
|
||||
public let maxSharedFolderInviteLinks: Int32
|
||||
public let maxSharedFolderJoin: Int32
|
||||
public let maxStoryCaptionLength: Int32
|
||||
|
||||
public static var defaultValue: UserLimits {
|
||||
return UserLimits(UserLimitsConfiguration.defaultValue)
|
||||
@ -71,7 +72,8 @@ public enum EngineConfiguration {
|
||||
maxAnimatedEmojisInText: Int32,
|
||||
maxReactionsPerMessage: Int32,
|
||||
maxSharedFolderInviteLinks: Int32,
|
||||
maxSharedFolderJoin: Int32
|
||||
maxSharedFolderJoin: Int32,
|
||||
maxStoryCaptionLength: Int32
|
||||
) {
|
||||
self.maxPinnedChatCount = maxPinnedChatCount
|
||||
self.maxArchivedPinnedChatCount = maxArchivedPinnedChatCount
|
||||
@ -88,6 +90,7 @@ public enum EngineConfiguration {
|
||||
self.maxReactionsPerMessage = maxReactionsPerMessage
|
||||
self.maxSharedFolderInviteLinks = maxSharedFolderInviteLinks
|
||||
self.maxSharedFolderJoin = maxSharedFolderJoin
|
||||
self.maxStoryCaptionLength = maxStoryCaptionLength
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -139,7 +142,8 @@ public extension EngineConfiguration.UserLimits {
|
||||
maxAnimatedEmojisInText: userLimitsConfiguration.maxAnimatedEmojisInText,
|
||||
maxReactionsPerMessage: userLimitsConfiguration.maxReactionsPerMessage,
|
||||
maxSharedFolderInviteLinks: userLimitsConfiguration.maxSharedFolderInviteLinks,
|
||||
maxSharedFolderJoin: userLimitsConfiguration.maxSharedFolderJoin
|
||||
maxSharedFolderJoin: userLimitsConfiguration.maxSharedFolderJoin,
|
||||
maxStoryCaptionLength: userLimitsConfiguration.maxStoryCaptionLength
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -392,11 +392,20 @@ final class MediaEditorScreenComponent: Component {
|
||||
}
|
||||
|
||||
@objc private func deactivateInput() {
|
||||
self.currentInputMode = .text
|
||||
if hasFirstResponder(self) {
|
||||
self.endEditing(true)
|
||||
guard let view = self.inputPanel.view as? MessageInputPanelComponent.View else {
|
||||
return
|
||||
}
|
||||
if view.canDeactivateInput() {
|
||||
self.currentInputMode = .text
|
||||
if hasFirstResponder(self) {
|
||||
if let view = self.inputPanel.view as? MessageInputPanelComponent.View {
|
||||
view.deactivateInput()
|
||||
}
|
||||
} else {
|
||||
self.state?.updated(transition: .spring(duration: 0.4).withUserData(TextFieldComponent.AnimationHint(kind: .textFocusChanged)))
|
||||
}
|
||||
} else {
|
||||
self.state?.updated(transition: .spring(duration: 0.4).withUserData(TextFieldComponent.AnimationHint(kind: .textFocusChanged)))
|
||||
view.animateError()
|
||||
}
|
||||
}
|
||||
|
||||
@ -967,13 +976,14 @@ final class MediaEditorScreenComponent: Component {
|
||||
inputPanelAvailableWidth += 200.0
|
||||
}
|
||||
}
|
||||
if environment.inputHeight > 0.0 || self.currentInputMode == .emoji {
|
||||
|
||||
let keyboardWasHidden = self.inputPanelExternalState.isKeyboardHidden
|
||||
if environment.inputHeight > 0.0 || self.currentInputMode == .emoji || keyboardWasHidden {
|
||||
inputPanelAvailableHeight = 200.0
|
||||
}
|
||||
|
||||
var inputHeight = environment.inputHeight
|
||||
var keyboardHeight = environment.deviceMetrics.standardInputHeight(inLandscape: false)
|
||||
let keyboardWasHidden = self.inputPanelExternalState.isKeyboardHidden
|
||||
|
||||
if case .emoji = self.currentInputMode, let inputData = self.inputMediaNodeData {
|
||||
let inputMediaNode: ChatEntityKeyboardInputNode
|
||||
@ -1078,6 +1088,7 @@ final class MediaEditorScreenComponent: Component {
|
||||
strings: environment.strings,
|
||||
style: .editor,
|
||||
placeholder: "Add a caption...",
|
||||
maxLength: Int(component.context.userLimits.maxStoryCaptionLength),
|
||||
queryTypes: [.mention],
|
||||
alwaysDarkWhenHasText: false,
|
||||
nextInputMode: { _ in return nextInputMode },
|
||||
@ -1424,10 +1435,8 @@ final class MediaEditorScreenComponent: Component {
|
||||
muteButtonView.layer.shadowOpacity = 0.35
|
||||
self.addSubview(muteButtonView)
|
||||
|
||||
if self.animatingButtons {
|
||||
muteButtonView.layer.animateAlpha(from: 0.0, to: muteButtonView.alpha, duration: 0.1)
|
||||
muteButtonView.layer.animateScale(from: 0.4, to: 1.0, duration: 0.1)
|
||||
}
|
||||
muteButtonView.layer.animateAlpha(from: 0.0, to: muteButtonView.alpha, duration: self.animatingButtons ? 0.1 : 0.2)
|
||||
muteButtonView.layer.animateScale(from: 0.4, to: 1.0, duration: self.animatingButtons ? 0.1 : 0.2)
|
||||
}
|
||||
transition.setPosition(view: muteButtonView, position: muteButtonFrame.center)
|
||||
transition.setBounds(view: muteButtonView, bounds: CGRect(origin: .zero, size: muteButtonFrame.size))
|
||||
|
@ -250,6 +250,7 @@ final class StoryPreviewComponent: Component {
|
||||
strings: presentationData.strings,
|
||||
style: .story,
|
||||
placeholder: "Reply Privately...",
|
||||
maxLength: nil,
|
||||
queryTypes: [],
|
||||
alwaysDarkWhenHasText: false,
|
||||
nextInputMode: { _ in return .stickers },
|
||||
|
@ -64,6 +64,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
public let strings: PresentationStrings
|
||||
public let style: Style
|
||||
public let placeholder: String
|
||||
public let maxLength: Int?
|
||||
public let queryTypes: ContextQueryTypes
|
||||
public let alwaysDarkWhenHasText: Bool
|
||||
public let nextInputMode: (Bool) -> InputMode?
|
||||
@ -104,6 +105,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
strings: PresentationStrings,
|
||||
style: Style,
|
||||
placeholder: String,
|
||||
maxLength: Int?,
|
||||
queryTypes: ContextQueryTypes,
|
||||
alwaysDarkWhenHasText: Bool,
|
||||
nextInputMode: @escaping (Bool) -> InputMode?,
|
||||
@ -144,6 +146,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
self.style = style
|
||||
self.nextInputMode = nextInputMode
|
||||
self.placeholder = placeholder
|
||||
self.maxLength = maxLength
|
||||
self.queryTypes = queryTypes
|
||||
self.alwaysDarkWhenHasText = alwaysDarkWhenHasText
|
||||
self.areVoiceMessagesAvailable = areVoiceMessagesAvailable
|
||||
@ -196,6 +199,9 @@ public final class MessageInputPanelComponent: Component {
|
||||
if lhs.placeholder != rhs.placeholder {
|
||||
return false
|
||||
}
|
||||
if lhs.maxLength != rhs.maxLength {
|
||||
return false
|
||||
}
|
||||
if lhs.queryTypes != rhs.queryTypes {
|
||||
return false
|
||||
}
|
||||
@ -266,6 +272,8 @@ public final class MessageInputPanelComponent: Component {
|
||||
private let placeholder = ComponentView<Empty>()
|
||||
private let vibrancyPlaceholder = ComponentView<Empty>()
|
||||
|
||||
private let counter = ComponentView<Empty>()
|
||||
|
||||
private var disabledPlaceholder: ComponentView<Empty>?
|
||||
private let textField = ComponentView<Empty>()
|
||||
private let textFieldExternalState = TextFieldComponent.ExternalState()
|
||||
@ -297,6 +305,8 @@ public final class MessageInputPanelComponent: Component {
|
||||
private var viewForOverlayContent: ViewForOverlayContent?
|
||||
private var currentEmojiSuggestionView: ComponentHostView<Empty>?
|
||||
|
||||
private let hapticFeedback = HapticFeedback()
|
||||
|
||||
private var component: MessageInputPanelComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
@ -376,6 +386,30 @@ public final class MessageInputPanelComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
public func canDeactivateInput() -> Bool {
|
||||
guard let component = self.component else {
|
||||
return true
|
||||
}
|
||||
if let maxLength = component.maxLength, self.textFieldExternalState.textLength > maxLength {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public func deactivateInput() {
|
||||
if self.canDeactivateInput() {
|
||||
if let textFieldView = self.textField.view as? TextFieldComponent.View {
|
||||
textFieldView.deactivateInput()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func animateError() {
|
||||
self.textField.view?.layer.addShakeAnimation()
|
||||
self.hapticFeedback.error()
|
||||
}
|
||||
|
||||
public func updateContextQueries() {
|
||||
guard let component = self.component, let textFieldView = self.textField.view as? TextFieldComponent.View else {
|
||||
return
|
||||
@ -534,7 +568,6 @@ public final class MessageInputPanelComponent: Component {
|
||||
)
|
||||
let isEditing = self.textFieldExternalState.isEditing || component.forceIsEditing
|
||||
|
||||
|
||||
let placeholderSize = self.placeholder.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(Text(
|
||||
@ -568,7 +601,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
} else {
|
||||
fieldBackgroundFrame = fieldFrame
|
||||
}
|
||||
|
||||
|
||||
transition.setFrame(view: self.vibrancyEffectView, frame: CGRect(origin: CGPoint(), size: fieldBackgroundFrame.size))
|
||||
|
||||
transition.setFrame(view: self.fieldBackgroundView, frame: fieldBackgroundFrame)
|
||||
@ -659,6 +692,36 @@ public final class MessageInputPanelComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
if let maxLength = component.maxLength, maxLength - self.textFieldExternalState.textLength < 5 {
|
||||
let remainingLength = max(-999, maxLength - self.textFieldExternalState.textLength)
|
||||
let counterSize = self.counter.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(Text(
|
||||
text: "\(remainingLength)",
|
||||
font: Font.with(size: 14.0, traits: .monospacedNumbers),
|
||||
color: self.textFieldExternalState.textLength > maxLength ? UIColor(rgb: 0xff3b30) : UIColor(rgb: 0xffffff, alpha: 0.25)
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: availableTextFieldSize
|
||||
)
|
||||
let counterFrame = CGRect(origin: CGPoint(x: availableSize.width - insets.right + floorToScreenPixels((insets.right - counterSize.width) * 0.5), y: size.height - insets.bottom - baseFieldHeight - counterSize.height - 5.0), size: counterSize)
|
||||
if let counterView = self.counter.view {
|
||||
if counterView.superview == nil {
|
||||
self.addSubview(counterView)
|
||||
counterView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
|
||||
counterView.center = counterFrame.center
|
||||
} else {
|
||||
transition.setPosition(view: counterView, position: counterFrame.center)
|
||||
}
|
||||
counterView.bounds = CGRect(origin: .zero, size: counterFrame.size)
|
||||
}
|
||||
} else if let counterView = self.counter.view, counterView.superview != nil {
|
||||
counterView.layer.animateAlpha(from: 1.00, to: 0.0, duration: 0.2, completion: { _ in
|
||||
counterView.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
|
||||
if component.attachmentAction != nil {
|
||||
let attachmentButtonMode: MessageInputActionButtonComponent.Mode
|
||||
attachmentButtonMode = .attach
|
||||
@ -830,12 +893,20 @@ public final class MessageInputPanelComponent: Component {
|
||||
component.sendMessageAction()
|
||||
} else if case let .text(string) = self.getSendMessageInput(), string.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
} else {
|
||||
component.sendMessageAction()
|
||||
if let maxLength = component.maxLength, self.textFieldExternalState.textLength > maxLength {
|
||||
self.animateError()
|
||||
} else {
|
||||
component.sendMessageAction()
|
||||
}
|
||||
}
|
||||
}
|
||||
case .apply:
|
||||
if case .up = action {
|
||||
component.sendMessageAction()
|
||||
if let maxLength = component.maxLength, self.textFieldExternalState.textLength > maxLength {
|
||||
self.animateError()
|
||||
} else {
|
||||
component.sendMessageAction()
|
||||
}
|
||||
}
|
||||
case .voiceInput, .videoInput:
|
||||
component.setMediaRecordingActive?(action == .down, mode == .videoInput, sendAction)
|
||||
|
@ -73,6 +73,7 @@ swift_library(
|
||||
"//submodules/ImageBlur",
|
||||
"//submodules/StickerPackPreviewUI",
|
||||
"//submodules/Components/AnimatedStickerComponent",
|
||||
"//submodules/OpenInExternalAppUI",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -44,19 +44,22 @@ final class StoryContentCaptionComponent: Component {
|
||||
let text: String
|
||||
let entities: [MessageTextEntity]
|
||||
let action: (Action) -> Void
|
||||
let longTapAction: (Action) -> Void
|
||||
|
||||
init(
|
||||
externalState: ExternalState,
|
||||
context: AccountContext,
|
||||
text: String,
|
||||
entities: [MessageTextEntity],
|
||||
action: @escaping (Action) -> Void
|
||||
action: @escaping (Action) -> Void,
|
||||
longTapAction: @escaping (Action) -> Void
|
||||
) {
|
||||
self.externalState = externalState
|
||||
self.context = context
|
||||
self.text = text
|
||||
self.entities = entities
|
||||
self.action = action
|
||||
self.longTapAction = longTapAction
|
||||
}
|
||||
|
||||
static func ==(lhs: StoryContentCaptionComponent, rhs: StoryContentCaptionComponent) -> Bool {
|
||||
@ -242,44 +245,47 @@ final class StoryContentCaptionComponent: Component {
|
||||
switch recognizer.state {
|
||||
case .ended:
|
||||
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation, let component = self.component, let textNode = self.textNode {
|
||||
switch gesture {
|
||||
case .tap:
|
||||
let titleFrame = textNode.textNode.view.bounds
|
||||
if titleFrame.contains(location) {
|
||||
if let (index, attributes) = textNode.textNode.attributesAtPoint(CGPoint(x: location.x - titleFrame.minX, y: location.y - titleFrame.minY)) {
|
||||
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Spoiler)], !(self.dustNode?.isRevealed ?? true) {
|
||||
let convertedPoint = recognizer.view?.convert(location, to: self.dustNode?.view) ?? location
|
||||
self.dustNode?.revealAtLocation(convertedPoint)
|
||||
return
|
||||
} else if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
|
||||
var concealed = true
|
||||
if let (attributeText, fullText) = textNode.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) {
|
||||
concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText)
|
||||
}
|
||||
component.action(.url(url: url, concealed: concealed))
|
||||
return
|
||||
} else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention {
|
||||
component.action(.peerMention(peerId: peerMention.peerId, mention: peerMention.mention))
|
||||
return
|
||||
} else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String {
|
||||
component.action(.textMention(peerName))
|
||||
return
|
||||
} else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag {
|
||||
component.action(.hashtag(hashtag.peerName, hashtag.hashtag))
|
||||
return
|
||||
} else if let bankCard = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BankCard)] as? String {
|
||||
component.action(.bankCard(bankCard))
|
||||
return
|
||||
} else if let emoji = attributes[NSAttributedString.Key(rawValue: ChatTextInputAttributes.customEmoji.rawValue)] as? ChatTextInputTextCustomEmojiAttribute, let file = emoji.file {
|
||||
component.action(.customEmoji(file))
|
||||
return
|
||||
let titleFrame = textNode.textNode.view.bounds
|
||||
if titleFrame.contains(location) {
|
||||
if let (index, attributes) = textNode.textNode.attributesAtPoint(CGPoint(x: location.x - titleFrame.minX, y: location.y - titleFrame.minY)) {
|
||||
let action: Action?
|
||||
if case .tap = gesture, let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Spoiler)], !(self.dustNode?.isRevealed ?? true) {
|
||||
let convertedPoint = recognizer.view?.convert(location, to: self.dustNode?.view) ?? location
|
||||
self.dustNode?.revealAtLocation(convertedPoint)
|
||||
return
|
||||
} else if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
|
||||
var concealed = true
|
||||
if let (attributeText, fullText) = textNode.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) {
|
||||
concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText)
|
||||
}
|
||||
action = .url(url: url, concealed: concealed)
|
||||
} else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention {
|
||||
action = .peerMention(peerId: peerMention.peerId, mention: peerMention.mention)
|
||||
} else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String {
|
||||
action = .textMention(peerName)
|
||||
} else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag {
|
||||
action = .hashtag(hashtag.peerName, hashtag.hashtag)
|
||||
} else if let bankCard = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BankCard)] as? String {
|
||||
action = .bankCard(bankCard)
|
||||
} else if let emoji = attributes[NSAttributedString.Key(rawValue: ChatTextInputAttributes.customEmoji.rawValue)] as? ChatTextInputTextCustomEmojiAttribute, let file = emoji.file {
|
||||
action = .customEmoji(file)
|
||||
} else {
|
||||
action = nil
|
||||
}
|
||||
guard let action else {
|
||||
return
|
||||
}
|
||||
switch gesture {
|
||||
case .tap:
|
||||
component.action(action)
|
||||
case .longTap:
|
||||
component.longTapAction(action)
|
||||
default:
|
||||
return
|
||||
}
|
||||
self.expand(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
||||
return
|
||||
}
|
||||
|
||||
self.expand(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
default:
|
||||
|
@ -645,14 +645,25 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
return false
|
||||
}
|
||||
|
||||
private func deactivateInput() {
|
||||
|
||||
}
|
||||
|
||||
@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 case .media = self.sendMessageContext.currentInputMode {
|
||||
self.sendMessageContext.currentInputMode = .text
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
if hasFirstResponder(self) || self.sendMessageContext.currentInputMode == .media {
|
||||
if let view = self.inputPanel.view as? MessageInputPanelComponent.View {
|
||||
if view.canDeactivateInput() {
|
||||
self.sendMessageContext.currentInputMode = .text
|
||||
if hasFirstResponder(self) {
|
||||
view.deactivateInput()
|
||||
} else {
|
||||
self.state?.updated(transition: .spring(duration: 0.4).withUserData(TextFieldComponent.AnimationHint(kind: .textFocusChanged)))
|
||||
}
|
||||
} else {
|
||||
view.animateError()
|
||||
}
|
||||
}
|
||||
} else if self.displayViewList {
|
||||
let point = recognizer.location(in: self)
|
||||
|
||||
@ -1686,6 +1697,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
strings: component.strings,
|
||||
style: .story,
|
||||
placeholder: "Reply Privately...",
|
||||
maxLength: 4096,
|
||||
queryTypes: [.mention, .emoji],
|
||||
alwaysDarkWhenHasText: component.metrics.widthClass == .regular,
|
||||
nextInputMode: { [weak self] hasText in
|
||||
@ -2538,11 +2550,29 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
self.sendMessageContext.openPeerMention(view: self, peerId: peerId)
|
||||
case let .hashtag(username, value):
|
||||
self.sendMessageContext.openHashtag(view: self, hashtag: value, peerName: username)
|
||||
case let .bankCard(value):
|
||||
let _ = value
|
||||
case .bankCard:
|
||||
break
|
||||
case .customEmoji:
|
||||
break
|
||||
}
|
||||
},
|
||||
longTapAction: { [weak self] action in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
self.sendMessageContext.presentTextEntityActions(view: self, action: action, openUrl: { [weak self] url, concealed in
|
||||
openUserGeneratedUrl(context: component.context, peerId: component.slice.peer.id, url: url, concealed: concealed, skipUrlAuth: false, skipConcealedAlert: false, present: { [weak self] c in
|
||||
guard let self, let component = self.component, let controller = component.controller() else {
|
||||
return
|
||||
}
|
||||
controller.present(c, in: .window(.root))
|
||||
}, openResolved: { [weak self] resolved in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.sendMessageContext.openResolved(view: self, result: resolved, forceExternal: false, concealed: concealed)
|
||||
})
|
||||
})
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
|
@ -36,6 +36,8 @@ import OverlayStatusController
|
||||
import PresentationDataUtils
|
||||
import TextFieldComponent
|
||||
import StickerPackPreviewUI
|
||||
import OpenInExternalAppUI
|
||||
import SafariServices
|
||||
|
||||
final class StoryItemSetContainerSendMessage {
|
||||
enum InputMode {
|
||||
@ -526,7 +528,7 @@ final class StoryItemSetContainerSendMessage {
|
||||
if self.videoRecorderValue == nil {
|
||||
if let currentInputPanelFrame = view.inputPanel.view?.frame {
|
||||
self.videoRecorder.set(.single(legacyInstantVideoController(theme: defaultDarkPresentationTheme, forStory: true, panelFrame: view.convert(currentInputPanelFrame, to: nil), context: component.context, peerId: peer.id, slowmodeState: nil, hasSchedule: true, send: { [weak self, weak view] videoController, message in
|
||||
guard let self, let view, let component = view.component else {
|
||||
guard let self, let view else {
|
||||
return
|
||||
}
|
||||
guard let message = message else {
|
||||
@ -541,15 +543,6 @@ final class StoryItemSetContainerSendMessage {
|
||||
self.videoRecorder.set(.single(nil))
|
||||
|
||||
self.sendMessages(view: view, peer: peer, messages: [updatedMessage])
|
||||
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||
view.component?.controller()?.present(UndoOverlayController(
|
||||
presentationData: presentationData,
|
||||
content: .succeed(text: "Message Sent"),
|
||||
elevatedLayout: false,
|
||||
animateInAsReplacement: false,
|
||||
action: { _ in return false }
|
||||
), in: .current)
|
||||
}, displaySlowmodeTooltip: { [weak self] view, rect in
|
||||
//self?.interfaceInteraction?.displaySlowmodeTooltip(view, rect)
|
||||
let _ = self
|
||||
@ -597,15 +590,6 @@ final class StoryItemSetContainerSendMessage {
|
||||
self.sendMessages(view: view, peer: peer, messages: [.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(data.compressedData.count), attributes: [.Audio(isVoice: true, duration: Int(data.duration), title: nil, performer: nil, waveform: waveformBuffer)])), replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])])
|
||||
|
||||
HapticFeedback().tap()
|
||||
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||
view.component?.controller()?.present(UndoOverlayController(
|
||||
presentationData: presentationData,
|
||||
content: .succeed(text: "Message Sent"),
|
||||
elevatedLayout: false,
|
||||
animateInAsReplacement: false,
|
||||
action: { _ in return false }
|
||||
), in: .current)
|
||||
}
|
||||
})
|
||||
} else if let videoRecorderValue = self.videoRecorderValue {
|
||||
@ -2520,4 +2504,97 @@ final class StoryItemSetContainerSendMessage {
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
func presentTextEntityActions(view: StoryItemSetContainerComponent.View, action: StoryContentCaptionComponent.Action, openUrl: @escaping (String, Bool) -> Void) {
|
||||
guard let component = view.component else {
|
||||
return
|
||||
}
|
||||
|
||||
let actionSheet = ActionSheetController(theme: ActionSheetControllerTheme(presentationTheme: component.theme, fontSize: .regular), allowInputInset: false)
|
||||
|
||||
var canOpenIn = false
|
||||
|
||||
let title: String
|
||||
let value: String
|
||||
var openAction: String? = component.strings.Conversation_LinkDialogOpen
|
||||
var copyAction = component.strings.Conversation_ContextMenuCopy
|
||||
switch action {
|
||||
case let .url(url, _):
|
||||
title = url
|
||||
value = url
|
||||
canOpenIn = availableOpenInOptions(context: component.context, item: .url(url: url)).count > 1
|
||||
if canOpenIn {
|
||||
openAction = component.strings.Conversation_FileOpenIn
|
||||
}
|
||||
copyAction = component.strings.Conversation_ContextMenuCopyLink
|
||||
case let .hashtag(_, hashtag):
|
||||
title = hashtag
|
||||
value = hashtag
|
||||
case let .bankCard(bankCard):
|
||||
title = bankCard
|
||||
value = bankCard
|
||||
openAction = nil
|
||||
case let .peerMention(_, mention):
|
||||
title = mention
|
||||
value = mention
|
||||
case let .textMention(mention):
|
||||
title = mention
|
||||
value = mention
|
||||
case .customEmoji:
|
||||
return
|
||||
}
|
||||
|
||||
var items: [ActionSheetItem] = []
|
||||
items.append(ActionSheetTextItem(title: title))
|
||||
|
||||
if let openAction {
|
||||
items.append(ActionSheetButtonItem(title: openAction, color: .accent, action: { [weak self, weak view, weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
if let self, let view {
|
||||
switch action {
|
||||
case let .url(url, concealed):
|
||||
if canOpenIn {
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||
let actionSheet = OpenInActionSheetController(context: component.context, item: .url(url: url), openUrl: { url in
|
||||
if let navigationController = component.controller()?.navigationController as? NavigationController {
|
||||
component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: navigationController, dismissInput: {})
|
||||
}
|
||||
})
|
||||
component.controller()?.present(actionSheet, in: .window(.root))
|
||||
} else {
|
||||
openUrl(url, concealed)
|
||||
}
|
||||
case let .hashtag(peerName, value):
|
||||
self.openHashtag(view: view, hashtag: value, peerName: peerName)
|
||||
case let .peerMention(peerId, _):
|
||||
self.openPeerMention(view: view, peerId: peerId)
|
||||
case let .textMention(mention):
|
||||
self.openPeerMention(view: view, name: mention)
|
||||
case .customEmoji, .bankCard:
|
||||
return
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
items.append(ActionSheetButtonItem(title: copyAction, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
UIPasteboard.general.string = value
|
||||
}))
|
||||
|
||||
if case let .url(url, _) = action, let link = URL(string: url) {
|
||||
items.append(ActionSheetButtonItem(title: component.strings.Conversation_AddToReadingList, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
let _ = try? SSReadingList.default()?.addItem(with: link, title: nil, previewText: nil)
|
||||
}))
|
||||
}
|
||||
|
||||
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: component.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
})
|
||||
])])
|
||||
|
||||
component.controller()?.present(actionSheet, in: .window(.root))
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ public final class TextFieldComponent: Component {
|
||||
public final class ExternalState {
|
||||
public fileprivate(set) var isEditing: Bool = false
|
||||
public fileprivate(set) var hasText: Bool = false
|
||||
public fileprivate(set) var textLength: Int = 0
|
||||
public var initialText: NSAttributedString?
|
||||
|
||||
public var hasTrackingView = false
|
||||
@ -492,6 +493,10 @@ public final class TextFieldComponent: Component {
|
||||
self.textView.becomeFirstResponder()
|
||||
}
|
||||
|
||||
public func deactivateInput() {
|
||||
self.textView.resignFirstResponder()
|
||||
}
|
||||
|
||||
private var spoilersRevealed = false
|
||||
private var spoilerIsDisappearing = false
|
||||
private func updateSpoilersRevealed(animated: Bool = true) {
|
||||
@ -775,6 +780,7 @@ public final class TextFieldComponent: Component {
|
||||
|
||||
component.externalState.hasText = self.textStorage.length != 0
|
||||
component.externalState.isEditing = isEditing
|
||||
component.externalState.textLength = self.textStorage.string.count
|
||||
|
||||
if component.hideKeyboard {
|
||||
if self.textView.inputView == nil {
|
||||
|
Loading…
x
Reference in New Issue
Block a user