mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-18 20:30:51 +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 maxReactionsPerMessage: Int32
|
||||||
public let maxSharedFolderInviteLinks: Int32
|
public let maxSharedFolderInviteLinks: Int32
|
||||||
public let maxSharedFolderJoin: Int32
|
public let maxSharedFolderJoin: Int32
|
||||||
|
public let maxStoryCaptionLength: Int32
|
||||||
|
|
||||||
public static var defaultValue: UserLimitsConfiguration {
|
public static var defaultValue: UserLimitsConfiguration {
|
||||||
return UserLimitsConfiguration(
|
return UserLimitsConfiguration(
|
||||||
@ -34,7 +35,8 @@ public struct UserLimitsConfiguration: Equatable {
|
|||||||
maxAnimatedEmojisInText: 10,
|
maxAnimatedEmojisInText: 10,
|
||||||
maxReactionsPerMessage: 1,
|
maxReactionsPerMessage: 1,
|
||||||
maxSharedFolderInviteLinks: 3,
|
maxSharedFolderInviteLinks: 3,
|
||||||
maxSharedFolderJoin: 2
|
maxSharedFolderJoin: 2,
|
||||||
|
maxStoryCaptionLength: 1024
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,7 +55,8 @@ public struct UserLimitsConfiguration: Equatable {
|
|||||||
maxAnimatedEmojisInText: Int32,
|
maxAnimatedEmojisInText: Int32,
|
||||||
maxReactionsPerMessage: Int32,
|
maxReactionsPerMessage: Int32,
|
||||||
maxSharedFolderInviteLinks: Int32,
|
maxSharedFolderInviteLinks: Int32,
|
||||||
maxSharedFolderJoin: Int32
|
maxSharedFolderJoin: Int32,
|
||||||
|
maxStoryCaptionLength: Int32
|
||||||
) {
|
) {
|
||||||
self.maxPinnedChatCount = maxPinnedChatCount
|
self.maxPinnedChatCount = maxPinnedChatCount
|
||||||
self.maxArchivedPinnedChatCount = maxArchivedPinnedChatCount
|
self.maxArchivedPinnedChatCount = maxArchivedPinnedChatCount
|
||||||
@ -70,6 +73,7 @@ public struct UserLimitsConfiguration: Equatable {
|
|||||||
self.maxReactionsPerMessage = maxReactionsPerMessage
|
self.maxReactionsPerMessage = maxReactionsPerMessage
|
||||||
self.maxSharedFolderInviteLinks = maxSharedFolderInviteLinks
|
self.maxSharedFolderInviteLinks = maxSharedFolderInviteLinks
|
||||||
self.maxSharedFolderJoin = maxSharedFolderJoin
|
self.maxSharedFolderJoin = maxSharedFolderJoin
|
||||||
|
self.maxStoryCaptionLength = maxStoryCaptionLength
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,5 +113,6 @@ extension UserLimitsConfiguration {
|
|||||||
self.maxReactionsPerMessage = getValue("reactions_user_max", orElse: 1)
|
self.maxReactionsPerMessage = getValue("reactions_user_max", orElse: 1)
|
||||||
self.maxSharedFolderInviteLinks = getValue("chatlist_invites_limit", orElse: isPremium ? 100 : 3)
|
self.maxSharedFolderInviteLinks = getValue("chatlist_invites_limit", orElse: isPremium ? 100 : 3)
|
||||||
self.maxSharedFolderJoin = getValue("chatlists_joined_limit", orElse: isPremium ? 100 : 2)
|
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 maxReactionsPerMessage: Int32
|
||||||
public let maxSharedFolderInviteLinks: Int32
|
public let maxSharedFolderInviteLinks: Int32
|
||||||
public let maxSharedFolderJoin: Int32
|
public let maxSharedFolderJoin: Int32
|
||||||
|
public let maxStoryCaptionLength: Int32
|
||||||
|
|
||||||
public static var defaultValue: UserLimits {
|
public static var defaultValue: UserLimits {
|
||||||
return UserLimits(UserLimitsConfiguration.defaultValue)
|
return UserLimits(UserLimitsConfiguration.defaultValue)
|
||||||
@ -71,7 +72,8 @@ public enum EngineConfiguration {
|
|||||||
maxAnimatedEmojisInText: Int32,
|
maxAnimatedEmojisInText: Int32,
|
||||||
maxReactionsPerMessage: Int32,
|
maxReactionsPerMessage: Int32,
|
||||||
maxSharedFolderInviteLinks: Int32,
|
maxSharedFolderInviteLinks: Int32,
|
||||||
maxSharedFolderJoin: Int32
|
maxSharedFolderJoin: Int32,
|
||||||
|
maxStoryCaptionLength: Int32
|
||||||
) {
|
) {
|
||||||
self.maxPinnedChatCount = maxPinnedChatCount
|
self.maxPinnedChatCount = maxPinnedChatCount
|
||||||
self.maxArchivedPinnedChatCount = maxArchivedPinnedChatCount
|
self.maxArchivedPinnedChatCount = maxArchivedPinnedChatCount
|
||||||
@ -88,6 +90,7 @@ public enum EngineConfiguration {
|
|||||||
self.maxReactionsPerMessage = maxReactionsPerMessage
|
self.maxReactionsPerMessage = maxReactionsPerMessage
|
||||||
self.maxSharedFolderInviteLinks = maxSharedFolderInviteLinks
|
self.maxSharedFolderInviteLinks = maxSharedFolderInviteLinks
|
||||||
self.maxSharedFolderJoin = maxSharedFolderJoin
|
self.maxSharedFolderJoin = maxSharedFolderJoin
|
||||||
|
self.maxStoryCaptionLength = maxStoryCaptionLength
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -139,7 +142,8 @@ public extension EngineConfiguration.UserLimits {
|
|||||||
maxAnimatedEmojisInText: userLimitsConfiguration.maxAnimatedEmojisInText,
|
maxAnimatedEmojisInText: userLimitsConfiguration.maxAnimatedEmojisInText,
|
||||||
maxReactionsPerMessage: userLimitsConfiguration.maxReactionsPerMessage,
|
maxReactionsPerMessage: userLimitsConfiguration.maxReactionsPerMessage,
|
||||||
maxSharedFolderInviteLinks: userLimitsConfiguration.maxSharedFolderInviteLinks,
|
maxSharedFolderInviteLinks: userLimitsConfiguration.maxSharedFolderInviteLinks,
|
||||||
maxSharedFolderJoin: userLimitsConfiguration.maxSharedFolderJoin
|
maxSharedFolderJoin: userLimitsConfiguration.maxSharedFolderJoin,
|
||||||
|
maxStoryCaptionLength: userLimitsConfiguration.maxStoryCaptionLength
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -392,12 +392,21 @@ final class MediaEditorScreenComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc private func deactivateInput() {
|
@objc private func deactivateInput() {
|
||||||
|
guard let view = self.inputPanel.view as? MessageInputPanelComponent.View else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if view.canDeactivateInput() {
|
||||||
self.currentInputMode = .text
|
self.currentInputMode = .text
|
||||||
if hasFirstResponder(self) {
|
if hasFirstResponder(self) {
|
||||||
self.endEditing(true)
|
if let view = self.inputPanel.view as? MessageInputPanelComponent.View {
|
||||||
|
view.deactivateInput()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
self.state?.updated(transition: .spring(duration: 0.4).withUserData(TextFieldComponent.AnimationHint(kind: .textFocusChanged)))
|
self.state?.updated(transition: .spring(duration: 0.4).withUserData(TextFieldComponent.AnimationHint(kind: .textFocusChanged)))
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
view.animateError()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var animatingButtons = false
|
private var animatingButtons = false
|
||||||
@ -967,13 +976,14 @@ final class MediaEditorScreenComponent: Component {
|
|||||||
inputPanelAvailableWidth += 200.0
|
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
|
inputPanelAvailableHeight = 200.0
|
||||||
}
|
}
|
||||||
|
|
||||||
var inputHeight = environment.inputHeight
|
var inputHeight = environment.inputHeight
|
||||||
var keyboardHeight = environment.deviceMetrics.standardInputHeight(inLandscape: false)
|
var keyboardHeight = environment.deviceMetrics.standardInputHeight(inLandscape: false)
|
||||||
let keyboardWasHidden = self.inputPanelExternalState.isKeyboardHidden
|
|
||||||
|
|
||||||
if case .emoji = self.currentInputMode, let inputData = self.inputMediaNodeData {
|
if case .emoji = self.currentInputMode, let inputData = self.inputMediaNodeData {
|
||||||
let inputMediaNode: ChatEntityKeyboardInputNode
|
let inputMediaNode: ChatEntityKeyboardInputNode
|
||||||
@ -1078,6 +1088,7 @@ final class MediaEditorScreenComponent: Component {
|
|||||||
strings: environment.strings,
|
strings: environment.strings,
|
||||||
style: .editor,
|
style: .editor,
|
||||||
placeholder: "Add a caption...",
|
placeholder: "Add a caption...",
|
||||||
|
maxLength: Int(component.context.userLimits.maxStoryCaptionLength),
|
||||||
queryTypes: [.mention],
|
queryTypes: [.mention],
|
||||||
alwaysDarkWhenHasText: false,
|
alwaysDarkWhenHasText: false,
|
||||||
nextInputMode: { _ in return nextInputMode },
|
nextInputMode: { _ in return nextInputMode },
|
||||||
@ -1424,10 +1435,8 @@ final class MediaEditorScreenComponent: Component {
|
|||||||
muteButtonView.layer.shadowOpacity = 0.35
|
muteButtonView.layer.shadowOpacity = 0.35
|
||||||
self.addSubview(muteButtonView)
|
self.addSubview(muteButtonView)
|
||||||
|
|
||||||
if self.animatingButtons {
|
muteButtonView.layer.animateAlpha(from: 0.0, to: muteButtonView.alpha, duration: self.animatingButtons ? 0.1 : 0.2)
|
||||||
muteButtonView.layer.animateAlpha(from: 0.0, to: muteButtonView.alpha, duration: 0.1)
|
muteButtonView.layer.animateScale(from: 0.4, to: 1.0, duration: self.animatingButtons ? 0.1 : 0.2)
|
||||||
muteButtonView.layer.animateScale(from: 0.4, to: 1.0, duration: 0.1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
transition.setPosition(view: muteButtonView, position: muteButtonFrame.center)
|
transition.setPosition(view: muteButtonView, position: muteButtonFrame.center)
|
||||||
transition.setBounds(view: muteButtonView, bounds: CGRect(origin: .zero, size: muteButtonFrame.size))
|
transition.setBounds(view: muteButtonView, bounds: CGRect(origin: .zero, size: muteButtonFrame.size))
|
||||||
|
|||||||
@ -250,6 +250,7 @@ final class StoryPreviewComponent: Component {
|
|||||||
strings: presentationData.strings,
|
strings: presentationData.strings,
|
||||||
style: .story,
|
style: .story,
|
||||||
placeholder: "Reply Privately...",
|
placeholder: "Reply Privately...",
|
||||||
|
maxLength: nil,
|
||||||
queryTypes: [],
|
queryTypes: [],
|
||||||
alwaysDarkWhenHasText: false,
|
alwaysDarkWhenHasText: false,
|
||||||
nextInputMode: { _ in return .stickers },
|
nextInputMode: { _ in return .stickers },
|
||||||
|
|||||||
@ -64,6 +64,7 @@ public final class MessageInputPanelComponent: Component {
|
|||||||
public let strings: PresentationStrings
|
public let strings: PresentationStrings
|
||||||
public let style: Style
|
public let style: Style
|
||||||
public let placeholder: String
|
public let placeholder: String
|
||||||
|
public let maxLength: Int?
|
||||||
public let queryTypes: ContextQueryTypes
|
public let queryTypes: ContextQueryTypes
|
||||||
public let alwaysDarkWhenHasText: Bool
|
public let alwaysDarkWhenHasText: Bool
|
||||||
public let nextInputMode: (Bool) -> InputMode?
|
public let nextInputMode: (Bool) -> InputMode?
|
||||||
@ -104,6 +105,7 @@ public final class MessageInputPanelComponent: Component {
|
|||||||
strings: PresentationStrings,
|
strings: PresentationStrings,
|
||||||
style: Style,
|
style: Style,
|
||||||
placeholder: String,
|
placeholder: String,
|
||||||
|
maxLength: Int?,
|
||||||
queryTypes: ContextQueryTypes,
|
queryTypes: ContextQueryTypes,
|
||||||
alwaysDarkWhenHasText: Bool,
|
alwaysDarkWhenHasText: Bool,
|
||||||
nextInputMode: @escaping (Bool) -> InputMode?,
|
nextInputMode: @escaping (Bool) -> InputMode?,
|
||||||
@ -144,6 +146,7 @@ public final class MessageInputPanelComponent: Component {
|
|||||||
self.style = style
|
self.style = style
|
||||||
self.nextInputMode = nextInputMode
|
self.nextInputMode = nextInputMode
|
||||||
self.placeholder = placeholder
|
self.placeholder = placeholder
|
||||||
|
self.maxLength = maxLength
|
||||||
self.queryTypes = queryTypes
|
self.queryTypes = queryTypes
|
||||||
self.alwaysDarkWhenHasText = alwaysDarkWhenHasText
|
self.alwaysDarkWhenHasText = alwaysDarkWhenHasText
|
||||||
self.areVoiceMessagesAvailable = areVoiceMessagesAvailable
|
self.areVoiceMessagesAvailable = areVoiceMessagesAvailable
|
||||||
@ -196,6 +199,9 @@ public final class MessageInputPanelComponent: Component {
|
|||||||
if lhs.placeholder != rhs.placeholder {
|
if lhs.placeholder != rhs.placeholder {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.maxLength != rhs.maxLength {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if lhs.queryTypes != rhs.queryTypes {
|
if lhs.queryTypes != rhs.queryTypes {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -266,6 +272,8 @@ public final class MessageInputPanelComponent: Component {
|
|||||||
private let placeholder = ComponentView<Empty>()
|
private let placeholder = ComponentView<Empty>()
|
||||||
private let vibrancyPlaceholder = ComponentView<Empty>()
|
private let vibrancyPlaceholder = ComponentView<Empty>()
|
||||||
|
|
||||||
|
private let counter = ComponentView<Empty>()
|
||||||
|
|
||||||
private var disabledPlaceholder: ComponentView<Empty>?
|
private var disabledPlaceholder: ComponentView<Empty>?
|
||||||
private let textField = ComponentView<Empty>()
|
private let textField = ComponentView<Empty>()
|
||||||
private let textFieldExternalState = TextFieldComponent.ExternalState()
|
private let textFieldExternalState = TextFieldComponent.ExternalState()
|
||||||
@ -297,6 +305,8 @@ public final class MessageInputPanelComponent: Component {
|
|||||||
private var viewForOverlayContent: ViewForOverlayContent?
|
private var viewForOverlayContent: ViewForOverlayContent?
|
||||||
private var currentEmojiSuggestionView: ComponentHostView<Empty>?
|
private var currentEmojiSuggestionView: ComponentHostView<Empty>?
|
||||||
|
|
||||||
|
private let hapticFeedback = HapticFeedback()
|
||||||
|
|
||||||
private var component: MessageInputPanelComponent?
|
private var component: MessageInputPanelComponent?
|
||||||
private weak var state: EmptyComponentState?
|
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() {
|
public func updateContextQueries() {
|
||||||
guard let component = self.component, let textFieldView = self.textField.view as? TextFieldComponent.View else {
|
guard let component = self.component, let textFieldView = self.textField.view as? TextFieldComponent.View else {
|
||||||
return
|
return
|
||||||
@ -534,7 +568,6 @@ public final class MessageInputPanelComponent: Component {
|
|||||||
)
|
)
|
||||||
let isEditing = self.textFieldExternalState.isEditing || component.forceIsEditing
|
let isEditing = self.textFieldExternalState.isEditing || component.forceIsEditing
|
||||||
|
|
||||||
|
|
||||||
let placeholderSize = self.placeholder.update(
|
let placeholderSize = self.placeholder.update(
|
||||||
transition: .immediate,
|
transition: .immediate,
|
||||||
component: AnyComponent(Text(
|
component: AnyComponent(Text(
|
||||||
@ -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 {
|
if component.attachmentAction != nil {
|
||||||
let attachmentButtonMode: MessageInputActionButtonComponent.Mode
|
let attachmentButtonMode: MessageInputActionButtonComponent.Mode
|
||||||
attachmentButtonMode = .attach
|
attachmentButtonMode = .attach
|
||||||
@ -829,14 +892,22 @@ public final class MessageInputPanelComponent: Component {
|
|||||||
} else if component.hasRecordedVideoPreview {
|
} else if component.hasRecordedVideoPreview {
|
||||||
component.sendMessageAction()
|
component.sendMessageAction()
|
||||||
} else if case let .text(string) = self.getSendMessageInput(), string.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
} else if case let .text(string) = self.getSendMessageInput(), string.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||||
|
} else {
|
||||||
|
if let maxLength = component.maxLength, self.textFieldExternalState.textLength > maxLength {
|
||||||
|
self.animateError()
|
||||||
} else {
|
} else {
|
||||||
component.sendMessageAction()
|
component.sendMessageAction()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
case .apply:
|
case .apply:
|
||||||
if case .up = action {
|
if case .up = action {
|
||||||
|
if let maxLength = component.maxLength, self.textFieldExternalState.textLength > maxLength {
|
||||||
|
self.animateError()
|
||||||
|
} else {
|
||||||
component.sendMessageAction()
|
component.sendMessageAction()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
case .voiceInput, .videoInput:
|
case .voiceInput, .videoInput:
|
||||||
component.setMediaRecordingActive?(action == .down, mode == .videoInput, sendAction)
|
component.setMediaRecordingActive?(action == .down, mode == .videoInput, sendAction)
|
||||||
case .forward:
|
case .forward:
|
||||||
|
|||||||
@ -73,6 +73,7 @@ swift_library(
|
|||||||
"//submodules/ImageBlur",
|
"//submodules/ImageBlur",
|
||||||
"//submodules/StickerPackPreviewUI",
|
"//submodules/StickerPackPreviewUI",
|
||||||
"//submodules/Components/AnimatedStickerComponent",
|
"//submodules/Components/AnimatedStickerComponent",
|
||||||
|
"//submodules/OpenInExternalAppUI",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
|||||||
@ -44,19 +44,22 @@ final class StoryContentCaptionComponent: Component {
|
|||||||
let text: String
|
let text: String
|
||||||
let entities: [MessageTextEntity]
|
let entities: [MessageTextEntity]
|
||||||
let action: (Action) -> Void
|
let action: (Action) -> Void
|
||||||
|
let longTapAction: (Action) -> Void
|
||||||
|
|
||||||
init(
|
init(
|
||||||
externalState: ExternalState,
|
externalState: ExternalState,
|
||||||
context: AccountContext,
|
context: AccountContext,
|
||||||
text: String,
|
text: String,
|
||||||
entities: [MessageTextEntity],
|
entities: [MessageTextEntity],
|
||||||
action: @escaping (Action) -> Void
|
action: @escaping (Action) -> Void,
|
||||||
|
longTapAction: @escaping (Action) -> Void
|
||||||
) {
|
) {
|
||||||
self.externalState = externalState
|
self.externalState = externalState
|
||||||
self.context = context
|
self.context = context
|
||||||
self.text = text
|
self.text = text
|
||||||
self.entities = entities
|
self.entities = entities
|
||||||
self.action = action
|
self.action = action
|
||||||
|
self.longTapAction = longTapAction
|
||||||
}
|
}
|
||||||
|
|
||||||
static func ==(lhs: StoryContentCaptionComponent, rhs: StoryContentCaptionComponent) -> Bool {
|
static func ==(lhs: StoryContentCaptionComponent, rhs: StoryContentCaptionComponent) -> Bool {
|
||||||
@ -242,12 +245,11 @@ final class StoryContentCaptionComponent: Component {
|
|||||||
switch recognizer.state {
|
switch recognizer.state {
|
||||||
case .ended:
|
case .ended:
|
||||||
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation, let component = self.component, let textNode = self.textNode {
|
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation, let component = self.component, let textNode = self.textNode {
|
||||||
switch gesture {
|
|
||||||
case .tap:
|
|
||||||
let titleFrame = textNode.textNode.view.bounds
|
let titleFrame = textNode.textNode.view.bounds
|
||||||
if titleFrame.contains(location) {
|
if titleFrame.contains(location) {
|
||||||
if let (index, attributes) = textNode.textNode.attributesAtPoint(CGPoint(x: location.x - titleFrame.minX, y: location.y - titleFrame.minY)) {
|
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 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
|
let convertedPoint = recognizer.view?.convert(location, to: self.dustNode?.view) ?? location
|
||||||
self.dustNode?.revealAtLocation(convertedPoint)
|
self.dustNode?.revealAtLocation(convertedPoint)
|
||||||
return
|
return
|
||||||
@ -256,30 +258,34 @@ final class StoryContentCaptionComponent: Component {
|
|||||||
if let (attributeText, fullText) = textNode.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) {
|
if let (attributeText, fullText) = textNode.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) {
|
||||||
concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText)
|
concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText)
|
||||||
}
|
}
|
||||||
component.action(.url(url: url, concealed: concealed))
|
action = .url(url: url, concealed: concealed)
|
||||||
return
|
|
||||||
} else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention {
|
} else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention {
|
||||||
component.action(.peerMention(peerId: peerMention.peerId, mention: peerMention.mention))
|
action = .peerMention(peerId: peerMention.peerId, mention: peerMention.mention)
|
||||||
return
|
|
||||||
} else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String {
|
} else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String {
|
||||||
component.action(.textMention(peerName))
|
action = .textMention(peerName)
|
||||||
return
|
|
||||||
} else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag {
|
} else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag {
|
||||||
component.action(.hashtag(hashtag.peerName, hashtag.hashtag))
|
action = .hashtag(hashtag.peerName, hashtag.hashtag)
|
||||||
return
|
|
||||||
} else if let bankCard = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BankCard)] as? String {
|
} else if let bankCard = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BankCard)] as? String {
|
||||||
component.action(.bankCard(bankCard))
|
action = .bankCard(bankCard)
|
||||||
return
|
|
||||||
} else if let emoji = attributes[NSAttributedString.Key(rawValue: ChatTextInputAttributes.customEmoji.rawValue)] as? ChatTextInputTextCustomEmojiAttribute, let file = emoji.file {
|
} else if let emoji = attributes[NSAttributedString.Key(rawValue: ChatTextInputAttributes.customEmoji.rawValue)] as? ChatTextInputTextCustomEmojiAttribute, let file = emoji.file {
|
||||||
component.action(.customEmoji(file))
|
action = .customEmoji(file)
|
||||||
|
} else {
|
||||||
|
action = nil
|
||||||
|
}
|
||||||
|
guard let action else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
switch gesture {
|
||||||
}
|
case .tap:
|
||||||
|
component.action(action)
|
||||||
self.expand(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
case .longTap:
|
||||||
|
component.longTapAction(action)
|
||||||
default:
|
default:
|
||||||
break
|
return
|
||||||
|
}
|
||||||
|
self.expand(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
|||||||
@ -645,14 +645,25 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func deactivateInput() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||||
if case .ended = recognizer.state, let component = self.component, let itemLayout = self.itemLayout {
|
if case .ended = recognizer.state, let component = self.component, let itemLayout = self.itemLayout {
|
||||||
|
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) {
|
if hasFirstResponder(self) {
|
||||||
self.sendMessageContext.currentInputMode = .text
|
view.deactivateInput()
|
||||||
self.endEditing(true)
|
} else {
|
||||||
} else if case .media = self.sendMessageContext.currentInputMode {
|
self.state?.updated(transition: .spring(duration: 0.4).withUserData(TextFieldComponent.AnimationHint(kind: .textFocusChanged)))
|
||||||
self.sendMessageContext.currentInputMode = .text
|
}
|
||||||
self.state?.updated(transition: .spring(duration: 0.4))
|
} else {
|
||||||
|
view.animateError()
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if self.displayViewList {
|
} else if self.displayViewList {
|
||||||
let point = recognizer.location(in: self)
|
let point = recognizer.location(in: self)
|
||||||
|
|
||||||
@ -1686,6 +1697,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
strings: component.strings,
|
strings: component.strings,
|
||||||
style: .story,
|
style: .story,
|
||||||
placeholder: "Reply Privately...",
|
placeholder: "Reply Privately...",
|
||||||
|
maxLength: 4096,
|
||||||
queryTypes: [.mention, .emoji],
|
queryTypes: [.mention, .emoji],
|
||||||
alwaysDarkWhenHasText: component.metrics.widthClass == .regular,
|
alwaysDarkWhenHasText: component.metrics.widthClass == .regular,
|
||||||
nextInputMode: { [weak self] hasText in
|
nextInputMode: { [weak self] hasText in
|
||||||
@ -2538,11 +2550,29 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
self.sendMessageContext.openPeerMention(view: self, peerId: peerId)
|
self.sendMessageContext.openPeerMention(view: self, peerId: peerId)
|
||||||
case let .hashtag(username, value):
|
case let .hashtag(username, value):
|
||||||
self.sendMessageContext.openHashtag(view: self, hashtag: value, peerName: username)
|
self.sendMessageContext.openHashtag(view: self, hashtag: value, peerName: username)
|
||||||
case let .bankCard(value):
|
case .bankCard:
|
||||||
let _ = value
|
break
|
||||||
case .customEmoji:
|
case .customEmoji:
|
||||||
break
|
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: {},
|
environment: {},
|
||||||
|
|||||||
@ -36,6 +36,8 @@ import OverlayStatusController
|
|||||||
import PresentationDataUtils
|
import PresentationDataUtils
|
||||||
import TextFieldComponent
|
import TextFieldComponent
|
||||||
import StickerPackPreviewUI
|
import StickerPackPreviewUI
|
||||||
|
import OpenInExternalAppUI
|
||||||
|
import SafariServices
|
||||||
|
|
||||||
final class StoryItemSetContainerSendMessage {
|
final class StoryItemSetContainerSendMessage {
|
||||||
enum InputMode {
|
enum InputMode {
|
||||||
@ -526,7 +528,7 @@ final class StoryItemSetContainerSendMessage {
|
|||||||
if self.videoRecorderValue == nil {
|
if self.videoRecorderValue == nil {
|
||||||
if let currentInputPanelFrame = view.inputPanel.view?.frame {
|
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
|
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
|
return
|
||||||
}
|
}
|
||||||
guard let message = message else {
|
guard let message = message else {
|
||||||
@ -541,15 +543,6 @@ final class StoryItemSetContainerSendMessage {
|
|||||||
self.videoRecorder.set(.single(nil))
|
self.videoRecorder.set(.single(nil))
|
||||||
|
|
||||||
self.sendMessages(view: view, peer: peer, messages: [updatedMessage])
|
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
|
}, displaySlowmodeTooltip: { [weak self] view, rect in
|
||||||
//self?.interfaceInteraction?.displaySlowmodeTooltip(view, rect)
|
//self?.interfaceInteraction?.displaySlowmodeTooltip(view, rect)
|
||||||
let _ = self
|
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: [])])
|
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()
|
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 {
|
} 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 final class ExternalState {
|
||||||
public fileprivate(set) var isEditing: Bool = false
|
public fileprivate(set) var isEditing: Bool = false
|
||||||
public fileprivate(set) var hasText: Bool = false
|
public fileprivate(set) var hasText: Bool = false
|
||||||
|
public fileprivate(set) var textLength: Int = 0
|
||||||
public var initialText: NSAttributedString?
|
public var initialText: NSAttributedString?
|
||||||
|
|
||||||
public var hasTrackingView = false
|
public var hasTrackingView = false
|
||||||
@ -492,6 +493,10 @@ public final class TextFieldComponent: Component {
|
|||||||
self.textView.becomeFirstResponder()
|
self.textView.becomeFirstResponder()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func deactivateInput() {
|
||||||
|
self.textView.resignFirstResponder()
|
||||||
|
}
|
||||||
|
|
||||||
private var spoilersRevealed = false
|
private var spoilersRevealed = false
|
||||||
private var spoilerIsDisappearing = false
|
private var spoilerIsDisappearing = false
|
||||||
private func updateSpoilersRevealed(animated: Bool = true) {
|
private func updateSpoilersRevealed(animated: Bool = true) {
|
||||||
@ -775,6 +780,7 @@ public final class TextFieldComponent: Component {
|
|||||||
|
|
||||||
component.externalState.hasText = self.textStorage.length != 0
|
component.externalState.hasText = self.textStorage.length != 0
|
||||||
component.externalState.isEditing = isEditing
|
component.externalState.isEditing = isEditing
|
||||||
|
component.externalState.textLength = self.textStorage.string.count
|
||||||
|
|
||||||
if component.hideKeyboard {
|
if component.hideKeyboard {
|
||||||
if self.textView.inputView == nil {
|
if self.textView.inputView == nil {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user