mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Edit message media preview
This commit is contained in:
parent
2456eba1b4
commit
d6ebf1b4ff
@ -47,12 +47,14 @@ public struct ChatEditMessageState: Codable, Equatable {
|
|||||||
public var inputState: ChatTextInputState
|
public var inputState: ChatTextInputState
|
||||||
public var disableUrlPreviews: [String]
|
public var disableUrlPreviews: [String]
|
||||||
public var inputTextMaxLength: Int32?
|
public var inputTextMaxLength: Int32?
|
||||||
|
public var mediaCaptionIsAbove: Bool?
|
||||||
|
|
||||||
public init(messageId: EngineMessage.Id, inputState: ChatTextInputState, disableUrlPreviews: [String], inputTextMaxLength: Int32?) {
|
public init(messageId: EngineMessage.Id, inputState: ChatTextInputState, disableUrlPreviews: [String], inputTextMaxLength: Int32?, mediaCaptionIsAbove: Bool?) {
|
||||||
self.messageId = messageId
|
self.messageId = messageId
|
||||||
self.inputState = inputState
|
self.inputState = inputState
|
||||||
self.disableUrlPreviews = disableUrlPreviews
|
self.disableUrlPreviews = disableUrlPreviews
|
||||||
self.inputTextMaxLength = inputTextMaxLength
|
self.inputTextMaxLength = inputTextMaxLength
|
||||||
|
self.mediaCaptionIsAbove = mediaCaptionIsAbove
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(from decoder: Decoder) throws {
|
public init(from decoder: Decoder) throws {
|
||||||
@ -80,6 +82,8 @@ public struct ChatEditMessageState: Codable, Equatable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.inputTextMaxLength = try? container.decodeIfPresent(Int32.self, forKey: "tl")
|
self.inputTextMaxLength = try? container.decodeIfPresent(Int32.self, forKey: "tl")
|
||||||
|
|
||||||
|
self.mediaCaptionIsAbove = try? container.decodeIfPresent(Bool.self, forKey: "mediaCaptionIsAbove")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -94,18 +98,20 @@ public struct ChatEditMessageState: Codable, Equatable {
|
|||||||
|
|
||||||
try container.encode(self.disableUrlPreviews, forKey: "dupl")
|
try container.encode(self.disableUrlPreviews, forKey: "dupl")
|
||||||
try container.encodeIfPresent(self.inputTextMaxLength, forKey: "tl")
|
try container.encodeIfPresent(self.inputTextMaxLength, forKey: "tl")
|
||||||
|
|
||||||
|
try container.encodeIfPresent(self.mediaCaptionIsAbove, forKey: "mediaCaptionIsAbove")
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func ==(lhs: ChatEditMessageState, rhs: ChatEditMessageState) -> Bool {
|
public static func ==(lhs: ChatEditMessageState, rhs: ChatEditMessageState) -> Bool {
|
||||||
return lhs.messageId == rhs.messageId && lhs.inputState == rhs.inputState && lhs.disableUrlPreviews == rhs.disableUrlPreviews && lhs.inputTextMaxLength == rhs.inputTextMaxLength
|
return lhs.messageId == rhs.messageId && lhs.inputState == rhs.inputState && lhs.disableUrlPreviews == rhs.disableUrlPreviews && lhs.inputTextMaxLength == rhs.inputTextMaxLength && lhs.mediaCaptionIsAbove == rhs.mediaCaptionIsAbove
|
||||||
}
|
}
|
||||||
|
|
||||||
public func withUpdatedInputState(_ inputState: ChatTextInputState) -> ChatEditMessageState {
|
public func withUpdatedInputState(_ inputState: ChatTextInputState) -> ChatEditMessageState {
|
||||||
return ChatEditMessageState(messageId: self.messageId, inputState: inputState, disableUrlPreviews: self.disableUrlPreviews, inputTextMaxLength: self.inputTextMaxLength)
|
return ChatEditMessageState(messageId: self.messageId, inputState: inputState, disableUrlPreviews: self.disableUrlPreviews, inputTextMaxLength: self.inputTextMaxLength, mediaCaptionIsAbove: self.mediaCaptionIsAbove)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func withUpdatedDisableUrlPreviews(_ disableUrlPreviews: [String]) -> ChatEditMessageState {
|
public func withUpdatedDisableUrlPreviews(_ disableUrlPreviews: [String]) -> ChatEditMessageState {
|
||||||
return ChatEditMessageState(messageId: self.messageId, inputState: self.inputState, disableUrlPreviews: disableUrlPreviews, inputTextMaxLength: self.inputTextMaxLength)
|
return ChatEditMessageState(messageId: self.messageId, inputState: self.inputState, disableUrlPreviews: disableUrlPreviews, inputTextMaxLength: self.inputTextMaxLength, mediaCaptionIsAbove: self.mediaCaptionIsAbove)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,10 +40,12 @@ public enum SendMessageActionSheetControllerParams {
|
|||||||
public final class EditMessage {
|
public final class EditMessage {
|
||||||
public let messages: [EngineMessage]
|
public let messages: [EngineMessage]
|
||||||
public let mediaPreview: ChatSendMessageContextScreenMediaPreview?
|
public let mediaPreview: ChatSendMessageContextScreenMediaPreview?
|
||||||
|
public let mediaCaptionIsAbove: (Bool, (Bool) -> Void)?
|
||||||
|
|
||||||
public init(messages: [EngineMessage], mediaPreview: ChatSendMessageContextScreenMediaPreview?) {
|
public init(messages: [EngineMessage], mediaPreview: ChatSendMessageContextScreenMediaPreview?, mediaCaptionIsAbove: (Bool, (Bool) -> Void)?) {
|
||||||
self.messages = messages
|
self.messages = messages
|
||||||
self.mediaPreview = mediaPreview
|
self.mediaPreview = mediaPreview
|
||||||
|
self.mediaCaptionIsAbove = mediaCaptionIsAbove
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -320,9 +320,7 @@ final class ChatSendMessageContextScreenComponent: Component {
|
|||||||
case let .sendMessage(sendMessage):
|
case let .sendMessage(sendMessage):
|
||||||
self.mediaCaptionIsAbove = sendMessage.mediaCaptionIsAbove?.0 ?? false
|
self.mediaCaptionIsAbove = sendMessage.mediaCaptionIsAbove?.0 ?? false
|
||||||
case let .editMessage(editMessage):
|
case let .editMessage(editMessage):
|
||||||
self.mediaCaptionIsAbove = editMessage.messages.contains(where: {
|
self.mediaCaptionIsAbove = editMessage.mediaCaptionIsAbove?.0 ?? false
|
||||||
return $0.attributes.contains(where: { $0 is InvertMediaMessageAttribute })
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
component.gesture.externalUpdated = { [weak self] view, location in
|
component.gesture.externalUpdated = { [weak self] view, location in
|
||||||
@ -483,8 +481,8 @@ final class ChatSendMessageContextScreenComponent: Component {
|
|||||||
switch component.params {
|
switch component.params {
|
||||||
case let .sendMessage(sendMessage):
|
case let .sendMessage(sendMessage):
|
||||||
sendMessage.mediaCaptionIsAbove?.1(self.mediaCaptionIsAbove)
|
sendMessage.mediaCaptionIsAbove?.1(self.mediaCaptionIsAbove)
|
||||||
case .editMessage:
|
case let .editMessage(editMessage):
|
||||||
break
|
editMessage.mediaCaptionIsAbove?.1(self.mediaCaptionIsAbove)
|
||||||
}
|
}
|
||||||
if !self.isUpdating {
|
if !self.isUpdating {
|
||||||
self.state?.updated(transition: .spring(duration: 0.35))
|
self.state?.updated(transition: .spring(duration: 0.35))
|
||||||
@ -704,6 +702,11 @@ final class ChatSendMessageContextScreenComponent: Component {
|
|||||||
messageItemViewContainerSize = CGSize(width: availableSize.width - 16.0 - 40.0, height: availableSize.height)
|
messageItemViewContainerSize = CGSize(width: availableSize.width - 16.0 - 40.0, height: availableSize.height)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isEditMessage = false
|
||||||
|
if case .editMessage = component.params {
|
||||||
|
isEditMessage = true
|
||||||
|
}
|
||||||
|
|
||||||
let messageItemSize = messageItemView.update(
|
let messageItemSize = messageItemView.update(
|
||||||
context: component.context,
|
context: component.context,
|
||||||
presentationData: presentationData,
|
presentationData: presentationData,
|
||||||
@ -719,6 +722,7 @@ final class ChatSendMessageContextScreenComponent: Component {
|
|||||||
maxTextHeight: 20000.0,
|
maxTextHeight: 20000.0,
|
||||||
containerSize: messageItemViewContainerSize,
|
containerSize: messageItemViewContainerSize,
|
||||||
effect: self.presentationAnimationState.key == .animatedIn ? self.selectedMessageEffect : nil,
|
effect: self.presentationAnimationState.key == .animatedIn ? self.selectedMessageEffect : nil,
|
||||||
|
isEditMessage: isEditMessage,
|
||||||
transition: transition
|
transition: transition
|
||||||
)
|
)
|
||||||
let sourceMessageItemFrame = CGRect(origin: CGPoint(x: localSourceTextInputViewFrame.minX - sourceMessageTextInsets.left, y: localSourceTextInputViewFrame.minY - 2.0), size: messageItemSize)
|
let sourceMessageItemFrame = CGRect(origin: CGPoint(x: localSourceTextInputViewFrame.minX - sourceMessageTextInsets.left, y: localSourceTextInputViewFrame.minY - 2.0), size: messageItemSize)
|
||||||
@ -1141,6 +1145,7 @@ final class ChatSendMessageContextScreenComponent: Component {
|
|||||||
|
|
||||||
messageItemView.animateIn(
|
messageItemView.animateIn(
|
||||||
sourceTextInputView: component.textInputView as? ChatInputTextView,
|
sourceTextInputView: component.textInputView as? ChatInputTextView,
|
||||||
|
isEditMessage: isEditMessage,
|
||||||
transition: transition
|
transition: transition
|
||||||
)
|
)
|
||||||
case .animatedOut:
|
case .animatedOut:
|
||||||
@ -1150,6 +1155,7 @@ final class ChatSendMessageContextScreenComponent: Component {
|
|||||||
messageItemView.animateOut(
|
messageItemView.animateOut(
|
||||||
sourceTextInputView: component.textInputView as? ChatInputTextView,
|
sourceTextInputView: component.textInputView as? ChatInputTextView,
|
||||||
toEmpty: self.animateOutToEmpty,
|
toEmpty: self.animateOutToEmpty,
|
||||||
|
isEditMessage: isEditMessage,
|
||||||
transition: transition
|
transition: transition
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -245,23 +245,35 @@ final class MessageItemView: UIView {
|
|||||||
|
|
||||||
func animateIn(
|
func animateIn(
|
||||||
sourceTextInputView: ChatInputTextView?,
|
sourceTextInputView: ChatInputTextView?,
|
||||||
|
isEditMessage: Bool,
|
||||||
transition: Transition
|
transition: Transition
|
||||||
) {
|
) {
|
||||||
if let mediaPreview = self.mediaPreview {
|
if isEditMessage {
|
||||||
mediaPreview.animateIn(transition: transition)
|
transition.animateScale(view: self, from: 0.001, to: 1.0)
|
||||||
|
transition.animateAlpha(view: self, from: 0.0, to: 1.0)
|
||||||
|
} else {
|
||||||
|
if let mediaPreview = self.mediaPreview {
|
||||||
|
mediaPreview.animateIn(transition: transition)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func animateOut(
|
func animateOut(
|
||||||
sourceTextInputView: ChatInputTextView?,
|
sourceTextInputView: ChatInputTextView?,
|
||||||
toEmpty: Bool,
|
toEmpty: Bool,
|
||||||
|
isEditMessage: Bool,
|
||||||
transition: Transition
|
transition: Transition
|
||||||
) {
|
) {
|
||||||
if let mediaPreview = self.mediaPreview {
|
if isEditMessage {
|
||||||
if toEmpty {
|
transition.setScale(view: self, scale: 0.001)
|
||||||
mediaPreview.animateOutOnSend(transition: transition)
|
transition.setAlpha(view: self, alpha: 0.0)
|
||||||
} else {
|
} else {
|
||||||
mediaPreview.animateOut(transition: transition)
|
if let mediaPreview = self.mediaPreview {
|
||||||
|
if toEmpty {
|
||||||
|
mediaPreview.animateOutOnSend(transition: transition)
|
||||||
|
} else {
|
||||||
|
mediaPreview.animateOut(transition: transition)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -281,6 +293,7 @@ final class MessageItemView: UIView {
|
|||||||
maxTextHeight: CGFloat,
|
maxTextHeight: CGFloat,
|
||||||
containerSize: CGSize,
|
containerSize: CGSize,
|
||||||
effect: AvailableMessageEffects.MessageEffect?,
|
effect: AvailableMessageEffects.MessageEffect?,
|
||||||
|
isEditMessage: Bool,
|
||||||
transition: Transition
|
transition: Transition
|
||||||
) -> CGSize {
|
) -> CGSize {
|
||||||
self.emojiViewProvider = emojiViewProvider
|
self.emojiViewProvider = emojiViewProvider
|
||||||
@ -387,6 +400,8 @@ final class MessageItemView: UIView {
|
|||||||
backgroundAlpha = 0.0
|
backgroundAlpha = 0.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let backgroundScale: CGFloat = 1.0
|
||||||
|
|
||||||
var backgroundFrame = mediaPreviewFrame.insetBy(dx: -2.0, dy: -2.0)
|
var backgroundFrame = mediaPreviewFrame.insetBy(dx: -2.0, dy: -2.0)
|
||||||
backgroundFrame.size.width += 6.0
|
backgroundFrame.size.width += 6.0
|
||||||
|
|
||||||
@ -502,10 +517,14 @@ final class MessageItemView: UIView {
|
|||||||
|
|
||||||
transition.setFrame(view: sourceMediaPreview.view, frame: mediaPreviewFrame)
|
transition.setFrame(view: sourceMediaPreview.view, frame: mediaPreviewFrame)
|
||||||
|
|
||||||
transition.setFrame(view: self.backgroundWallpaperNode.view, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size))
|
transition.setPosition(view: self.backgroundWallpaperNode.view, position: CGRect(origin: CGPoint(), size: backgroundFrame.size).center)
|
||||||
|
transition.setBounds(view: self.backgroundWallpaperNode.view, bounds: CGRect(origin: CGPoint(), size: backgroundFrame.size))
|
||||||
alphaTransition.setAlpha(view: self.backgroundWallpaperNode.view, alpha: backgroundAlpha)
|
alphaTransition.setAlpha(view: self.backgroundWallpaperNode.view, alpha: backgroundAlpha)
|
||||||
|
transition.setScale(view: self.backgroundWallpaperNode.view, scale: backgroundScale)
|
||||||
self.backgroundWallpaperNode.updateFrame(backgroundFrame, transition: transition.containedViewLayoutTransition)
|
self.backgroundWallpaperNode.updateFrame(backgroundFrame, transition: transition.containedViewLayoutTransition)
|
||||||
transition.setFrame(view: self.backgroundNode.view, frame: backgroundFrame)
|
transition.setPosition(view: self.backgroundNode.view, position: backgroundFrame.center)
|
||||||
|
transition.setBounds(view: self.backgroundNode.view, bounds: CGRect(origin: CGPoint(), size: backgroundFrame.size))
|
||||||
|
transition.setScale(view: self.backgroundNode.view, scale: backgroundScale)
|
||||||
alphaTransition.setAlpha(view: self.backgroundNode.view, alpha: backgroundAlpha)
|
alphaTransition.setAlpha(view: self.backgroundNode.view, alpha: backgroundAlpha)
|
||||||
self.backgroundNode.updateLayout(size: backgroundFrame.size, transition: transition.containedViewLayoutTransition)
|
self.backgroundNode.updateLayout(size: backgroundFrame.size, transition: transition.containedViewLayoutTransition)
|
||||||
|
|
||||||
|
@ -6,13 +6,15 @@ public final class ChatUpdatingMessageMedia: Equatable {
|
|||||||
public let entities: TextEntitiesMessageAttribute?
|
public let entities: TextEntitiesMessageAttribute?
|
||||||
public let disableUrlPreview: Bool
|
public let disableUrlPreview: Bool
|
||||||
public let media: RequestEditMessageMedia
|
public let media: RequestEditMessageMedia
|
||||||
|
public let invertMediaAttribute: InvertMediaMessageAttribute?
|
||||||
public let progress: Float
|
public let progress: Float
|
||||||
|
|
||||||
init(text: String, entities: TextEntitiesMessageAttribute?, disableUrlPreview: Bool, media: RequestEditMessageMedia, progress: Float) {
|
init(text: String, entities: TextEntitiesMessageAttribute?, disableUrlPreview: Bool, media: RequestEditMessageMedia, invertMediaAttribute: InvertMediaMessageAttribute?, progress: Float) {
|
||||||
self.text = text
|
self.text = text
|
||||||
self.entities = entities
|
self.entities = entities
|
||||||
self.disableUrlPreview = disableUrlPreview
|
self.disableUrlPreview = disableUrlPreview
|
||||||
self.media = media
|
self.media = media
|
||||||
|
self.invertMediaAttribute = invertMediaAttribute
|
||||||
self.progress = progress
|
self.progress = progress
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -29,6 +31,9 @@ public final class ChatUpdatingMessageMedia: Equatable {
|
|||||||
if lhs.media != rhs.media {
|
if lhs.media != rhs.media {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if (lhs.invertMediaAttribute == nil) != (rhs.invertMediaAttribute == nil) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if lhs.progress != rhs.progress {
|
if lhs.progress != rhs.progress {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -36,6 +41,6 @@ public final class ChatUpdatingMessageMedia: Equatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func withProgress(_ progress: Float) -> ChatUpdatingMessageMedia {
|
func withProgress(_ progress: Float) -> ChatUpdatingMessageMedia {
|
||||||
return ChatUpdatingMessageMedia(text: self.text, entities: self.entities, disableUrlPreview: self.disableUrlPreview, media: self.media, progress: progress)
|
return ChatUpdatingMessageMedia(text: self.text, entities: self.entities, disableUrlPreview: self.disableUrlPreview, media: self.media, invertMediaAttribute: self.invertMediaAttribute, progress: progress)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -71,7 +71,7 @@ private final class PendingUpdateMessageManagerImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let disposable = MetaDisposable()
|
let disposable = MetaDisposable()
|
||||||
let context = PendingUpdateMessageContext(value: ChatUpdatingMessageMedia(text: text, entities: entities, disableUrlPreview: disableUrlPreview, media: media, progress: 0.0), disposable: disposable)
|
let context = PendingUpdateMessageContext(value: ChatUpdatingMessageMedia(text: text, entities: entities, disableUrlPreview: disableUrlPreview, media: media, invertMediaAttribute: invertMediaAttribute, progress: 0.0), disposable: disposable)
|
||||||
self.contexts[messageId] = context
|
self.contexts[messageId] = context
|
||||||
|
|
||||||
let queue = self.queue
|
let queue = self.queue
|
||||||
|
@ -250,7 +250,14 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
|
|||||||
messageWithCaptionToAdd = (message, itemAttributes)
|
messageWithCaptionToAdd = (message, itemAttributes)
|
||||||
skipText = true
|
skipText = true
|
||||||
} else {
|
} else {
|
||||||
if let _ = message.attributes.first(where: { $0 is InvertMediaMessageAttribute }) {
|
var isMediaInverted = false
|
||||||
|
if let updatingMedia = itemAttributes.updatingMedia {
|
||||||
|
isMediaInverted = updatingMedia.invertMediaAttribute != nil
|
||||||
|
} else if let _ = message.attributes.first(where: { $0 is InvertMediaMessageAttribute }) {
|
||||||
|
isMediaInverted = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if isMediaInverted {
|
||||||
result.insert((message, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: isFile ? .condensed : .default)), at: 0)
|
result.insert((message, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: isFile ? .condensed : .default)), at: 0)
|
||||||
} else {
|
} else {
|
||||||
result.append((message, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: isFile ? .condensed : .default)))
|
result.append((message, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: isFile ? .condensed : .default)))
|
||||||
@ -303,7 +310,14 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let (messageWithCaptionToAdd, itemAttributes) = messageWithCaptionToAdd {
|
if let (messageWithCaptionToAdd, itemAttributes) = messageWithCaptionToAdd {
|
||||||
if let _ = messageWithCaptionToAdd.attributes.first(where: { $0 is InvertMediaMessageAttribute }) {
|
var isMediaInverted = false
|
||||||
|
if let updatingMedia = itemAttributes.updatingMedia {
|
||||||
|
isMediaInverted = updatingMedia.invertMediaAttribute != nil
|
||||||
|
} else if let _ = messageWithCaptionToAdd.attributes.first(where: { $0 is InvertMediaMessageAttribute }) {
|
||||||
|
isMediaInverted = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if isMediaInverted {
|
||||||
if result.isEmpty {
|
if result.isEmpty {
|
||||||
needReactions = false
|
needReactions = false
|
||||||
}
|
}
|
||||||
|
@ -300,6 +300,8 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
let statusType: ChatMessageDateAndStatusType?
|
let statusType: ChatMessageDateAndStatusType?
|
||||||
if case .customChatContents = item.associatedData.subject {
|
if case .customChatContents = item.associatedData.subject {
|
||||||
statusType = nil
|
statusType = nil
|
||||||
|
} else if item.message.timestamp == 0 {
|
||||||
|
statusType = nil
|
||||||
} else {
|
} else {
|
||||||
switch preparePosition {
|
switch preparePosition {
|
||||||
case .linear(_, .None), .linear(_, .Neighbour(true, _, _)):
|
case .linear(_, .None), .linear(_, .Neighbour(true, _, _)):
|
||||||
|
@ -23,6 +23,12 @@ swift_library(
|
|||||||
"//submodules/WallpaperBackgroundNode",
|
"//submodules/WallpaperBackgroundNode",
|
||||||
"//submodules/AudioWaveform",
|
"//submodules/AudioWaveform",
|
||||||
"//submodules/TelegramUI/Components/Chat/ChatMessageItemView",
|
"//submodules/TelegramUI/Components/Chat/ChatMessageItemView",
|
||||||
|
"//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode",
|
||||||
|
"//submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode",
|
||||||
|
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
|
||||||
|
"//submodules/TelegramUI/Components/Chat/ChatHistoryEntry",
|
||||||
|
"//submodules/TelegramUI/Components/ChatControllerInteraction",
|
||||||
|
"//submodules/TelegramUIPreferences",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
@ -13,6 +13,12 @@ import TelegramCore
|
|||||||
import WallpaperBackgroundNode
|
import WallpaperBackgroundNode
|
||||||
import AudioWaveform
|
import AudioWaveform
|
||||||
import ChatMessageItemView
|
import ChatMessageItemView
|
||||||
|
import ChatMessageItemCommon
|
||||||
|
import ChatMessageBubbleContentNode
|
||||||
|
import ChatMessageMediaBubbleContentNode
|
||||||
|
import ChatControllerInteraction
|
||||||
|
import TelegramUIPreferences
|
||||||
|
import ChatHistoryEntry
|
||||||
|
|
||||||
public final class ChatSendContactMessageContextPreview: UIView, ChatSendMessageContextScreenMediaPreview {
|
public final class ChatSendContactMessageContextPreview: UIView, ChatSendMessageContextScreenMediaPreview {
|
||||||
private let context: AccountContext
|
private let context: AccountContext
|
||||||
@ -318,3 +324,260 @@ public final class ChatSendAudioMessageContextPreview: UIView, ChatSendMessageCo
|
|||||||
return CGSize(width: contentFrame.width - 4.0, height: contentFrame.height + 2.0)
|
return CGSize(width: contentFrame.width - 4.0, height: contentFrame.height + 2.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public final class ChatSendGroupMediaMessageContextPreview: UIView, ChatSendMessageContextScreenMediaPreview {
|
||||||
|
private let context: AccountContext
|
||||||
|
private let presentationData: PresentationData
|
||||||
|
private let wallpaperBackgroundNode: WallpaperBackgroundNode?
|
||||||
|
private let messages: [Message]
|
||||||
|
|
||||||
|
private var chatPresentationData: ChatPresentationData?
|
||||||
|
|
||||||
|
private var messageNode: ChatMessageMediaBubbleContentNode?
|
||||||
|
private let messagesContainer: UIView
|
||||||
|
|
||||||
|
public var isReady: Signal<Bool, NoError> {
|
||||||
|
return .single(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
public var view: UIView {
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
public var globalClippingRect: CGRect? {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
public var layoutType: ChatSendMessageContextScreenMediaPreviewLayoutType {
|
||||||
|
return .media
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(context: AccountContext, presentationData: PresentationData, wallpaperBackgroundNode: WallpaperBackgroundNode?, messages: [EngineMessage]) {
|
||||||
|
self.context = context
|
||||||
|
self.presentationData = presentationData
|
||||||
|
self.wallpaperBackgroundNode = wallpaperBackgroundNode
|
||||||
|
self.messages = messages.map { message in
|
||||||
|
return message._asMessage().withUpdatedTimestamp(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.messagesContainer = UIView()
|
||||||
|
|
||||||
|
super.init(frame: CGRect())
|
||||||
|
|
||||||
|
self.addSubview(self.messagesContainer)
|
||||||
|
}
|
||||||
|
|
||||||
|
required public init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
}
|
||||||
|
|
||||||
|
public func animateIn(transition: Transition) {
|
||||||
|
transition.animateAlpha(view: self.messagesContainer, from: 0.0, to: 1.0)
|
||||||
|
transition.animateScale(view: self.messagesContainer, from: 0.001, to: 1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func animateOut(transition: Transition) {
|
||||||
|
transition.setAlpha(view: self.messagesContainer, alpha: 0.0)
|
||||||
|
transition.setScale(view: self.messagesContainer, scale: 0.001)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func animateOutOnSend(transition: Transition) {
|
||||||
|
transition.setAlpha(view: self.messagesContainer, alpha: 0.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func update(containerSize: CGSize, transition: Transition) -> CGSize {
|
||||||
|
let messageNode: ChatMessageMediaBubbleContentNode
|
||||||
|
if let current = self.messageNode {
|
||||||
|
messageNode = current
|
||||||
|
} else {
|
||||||
|
messageNode = ChatMessageMediaBubbleContentNode()
|
||||||
|
self.messageNode = messageNode
|
||||||
|
self.messagesContainer.addSubview(messageNode.view)
|
||||||
|
}
|
||||||
|
|
||||||
|
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
||||||
|
|
||||||
|
let chatPresentationData: ChatPresentationData
|
||||||
|
if let current = self.chatPresentationData {
|
||||||
|
chatPresentationData = current
|
||||||
|
} else {
|
||||||
|
chatPresentationData = ChatPresentationData(
|
||||||
|
theme: ChatPresentationThemeData(
|
||||||
|
theme: presentationData.theme,
|
||||||
|
wallpaper: presentationData.chatWallpaper
|
||||||
|
),
|
||||||
|
fontSize: presentationData.chatFontSize,
|
||||||
|
strings: presentationData.strings,
|
||||||
|
dateTimeFormat: presentationData.dateTimeFormat,
|
||||||
|
nameDisplayOrder: presentationData.nameDisplayOrder,
|
||||||
|
disableAnimations: false,
|
||||||
|
largeEmoji: false,
|
||||||
|
chatBubbleCorners: presentationData.chatBubbleCorners
|
||||||
|
)
|
||||||
|
self.chatPresentationData = chatPresentationData
|
||||||
|
}
|
||||||
|
|
||||||
|
let controllerInteraction = ChatControllerInteraction(openMessage: { _, _ in
|
||||||
|
return false }, openPeer: { _, _, _, _ in }, openPeerMention: { _, _ in }, openMessageContextMenu: { _, _, _, _, _, _ in }, openMessageReactionContextMenu: { _, _, _, _ in
|
||||||
|
}, updateMessageReaction: { _, _, _, _ in }, activateMessagePinch: { _ in
|
||||||
|
}, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _, _ in }, navigateToMessageStandalone: { _ in
|
||||||
|
}, navigateToThreadMessage: { _, _, _ in
|
||||||
|
}, tapMessage: { _ in
|
||||||
|
}, clickThroughMessage: {
|
||||||
|
}, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _, _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _, _ in
|
||||||
|
return false
|
||||||
|
}, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _, _ in }, openUrl: { _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in
|
||||||
|
}, presentController: { _, _ in
|
||||||
|
}, presentControllerInCurrent: { _, _ in
|
||||||
|
}, navigationController: {
|
||||||
|
return nil
|
||||||
|
}, chatControllerNode: {
|
||||||
|
return nil
|
||||||
|
}, presentGlobalOverlayController: { _, _ in }, callPeer: { _, _ in }, longTap: { _, _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in
|
||||||
|
}, canSetupReply: { _ in
|
||||||
|
return .none
|
||||||
|
}, canSendMessages: {
|
||||||
|
return false
|
||||||
|
}, navigateToFirstDateMessage: { _, _ in
|
||||||
|
}, requestRedeliveryOfFailedMessages: { _ in
|
||||||
|
}, addContact: { _ in
|
||||||
|
}, rateCall: { _, _, _ in
|
||||||
|
}, requestSelectMessagePollOptions: { _, _ in
|
||||||
|
}, requestOpenMessagePollResults: { _, _ in
|
||||||
|
}, openAppStorePage: {
|
||||||
|
}, displayMessageTooltip: { _, _, _, _, _ in
|
||||||
|
}, seekToTimecode: { _, _, _ in
|
||||||
|
}, scheduleCurrentMessage: {
|
||||||
|
}, sendScheduledMessagesNow: { _ in
|
||||||
|
}, editScheduledMessagesTime: { _ in
|
||||||
|
}, performTextSelectionAction: { _, _, _, _ in
|
||||||
|
}, displayImportedMessageTooltip: { _ in
|
||||||
|
}, displaySwipeToReplyHint: {
|
||||||
|
}, dismissReplyMarkupMessage: { _ in
|
||||||
|
}, openMessagePollResults: { _, _ in
|
||||||
|
}, openPollCreation: { _ in
|
||||||
|
}, displayPollSolution: { _, _ in
|
||||||
|
}, displayPsa: { _, _ in
|
||||||
|
}, displayDiceTooltip: { _ in
|
||||||
|
}, animateDiceSuccess: { _, _ in
|
||||||
|
}, displayPremiumStickerTooltip: { _, _ in
|
||||||
|
}, displayEmojiPackTooltip: { _, _ in
|
||||||
|
}, openPeerContextMenu: { _, _, _, _, _ in
|
||||||
|
}, openMessageReplies: { _, _, _ in
|
||||||
|
}, openReplyThreadOriginalMessage: { _ in
|
||||||
|
}, openMessageStats: { _ in
|
||||||
|
}, editMessageMedia: { _, _ in
|
||||||
|
}, copyText: { _ in
|
||||||
|
}, displayUndo: { _ in
|
||||||
|
}, isAnimatingMessage: { _ in
|
||||||
|
return false
|
||||||
|
}, getMessageTransitionNode: {
|
||||||
|
return nil
|
||||||
|
}, updateChoosingSticker: { _ in
|
||||||
|
}, commitEmojiInteraction: { _, _, _, _ in
|
||||||
|
}, openLargeEmojiInfo: { _, _, _ in
|
||||||
|
}, openJoinLink: { _ in
|
||||||
|
}, openWebView: { _, _, _, _ in
|
||||||
|
}, activateAdAction: { _, _ in
|
||||||
|
}, openRequestedPeerSelection: { _, _, _, _ in
|
||||||
|
}, saveMediaToFiles: { _ in
|
||||||
|
}, openNoAdsDemo: {
|
||||||
|
}, openAdsInfo: {
|
||||||
|
}, displayGiveawayParticipationStatus: { _ in
|
||||||
|
}, openPremiumStatusInfo: { _, _, _, _ in
|
||||||
|
}, openRecommendedChannelContextMenu: { _, _, _ in
|
||||||
|
}, openGroupBoostInfo: { _, _ in
|
||||||
|
}, openStickerEditor: {
|
||||||
|
}, openPhoneContextMenu: { _ in
|
||||||
|
}, openAgeRestrictedMessageMedia: { _, _ in
|
||||||
|
}, playMessageEffect: { _ in
|
||||||
|
}, editMessageFactCheck: { _ in
|
||||||
|
}, requestMessageUpdate: { _, _ in
|
||||||
|
}, cancelInteractiveKeyboardGestures: {
|
||||||
|
}, dismissTextInput: {
|
||||||
|
}, scrollToMessageId: { _ in
|
||||||
|
}, navigateToStory: { _, _ in
|
||||||
|
}, attemptedNavigationToPrivateQuote: { _ in
|
||||||
|
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
|
||||||
|
pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: self.context, backgroundNode: self.wallpaperBackgroundNode))
|
||||||
|
|
||||||
|
let associatedData = ChatMessageItemAssociatedData(
|
||||||
|
automaticDownloadPeerType: .channel,
|
||||||
|
automaticDownloadPeerId: nil,
|
||||||
|
automaticDownloadNetworkType: .cellular,
|
||||||
|
isRecentActions: false,
|
||||||
|
availableReactions: nil,
|
||||||
|
availableMessageEffects: nil,
|
||||||
|
savedMessageTags: nil,
|
||||||
|
defaultReaction: nil,
|
||||||
|
isPremium: false,
|
||||||
|
accountPeer: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
let entryAttributes = ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil)
|
||||||
|
|
||||||
|
let item = ChatMessageBubbleContentItem(
|
||||||
|
context: self.context,
|
||||||
|
controllerInteraction: controllerInteraction,
|
||||||
|
message: self.messages[0],
|
||||||
|
topMessage: self.messages[0],
|
||||||
|
read: true,
|
||||||
|
chatLocation: .peer(id: self.context.account.peerId),
|
||||||
|
presentationData: chatPresentationData,
|
||||||
|
associatedData: associatedData,
|
||||||
|
attributes: entryAttributes,
|
||||||
|
isItemPinned: false,
|
||||||
|
isItemEdited: false
|
||||||
|
)
|
||||||
|
|
||||||
|
let makeMessageLayout = messageNode.asyncLayoutContent()
|
||||||
|
let layoutConstants = chatMessageItemLayoutConstants(
|
||||||
|
(ChatMessageItemLayoutConstants.compact, ChatMessageItemLayoutConstants.regular),
|
||||||
|
params: ListViewItemLayoutParams(
|
||||||
|
width: containerSize.width,
|
||||||
|
leftInset: 0.0,
|
||||||
|
rightInset: 0.0,
|
||||||
|
availableHeight: 10000.0
|
||||||
|
),
|
||||||
|
presentationData: chatPresentationData
|
||||||
|
)
|
||||||
|
|
||||||
|
let (_, _, _, continueMessageLayout) = makeMessageLayout(
|
||||||
|
item,
|
||||||
|
layoutConstants,
|
||||||
|
ChatMessageBubblePreparePosition.linear(
|
||||||
|
top: ChatMessageBubbleRelativePosition.None(.None(.None)),
|
||||||
|
bottom: ChatMessageBubbleRelativePosition.None(.None(.None))
|
||||||
|
),
|
||||||
|
nil,
|
||||||
|
CGSize(width: containerSize.width, height: 10000.0),
|
||||||
|
0.0
|
||||||
|
)
|
||||||
|
|
||||||
|
let (finalizedWidth, finalizeMessageLayout) = continueMessageLayout(
|
||||||
|
CGSize(width: containerSize.width, height: 10000.0),
|
||||||
|
ChatMessageBubbleContentPosition.linear(
|
||||||
|
top: ChatMessageBubbleRelativePosition.None(.None(.None)),
|
||||||
|
bottom: ChatMessageBubbleRelativePosition.None(.None(.None))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
let _ = finalizedWidth
|
||||||
|
|
||||||
|
let (finalizedSize, apply) = finalizeMessageLayout(finalizedWidth)
|
||||||
|
apply(.None, true, nil)
|
||||||
|
|
||||||
|
let contentFrameInset = UIEdgeInsets(top: -2.0, left: -2.0, bottom: -2.0, right: -2.0)
|
||||||
|
|
||||||
|
let contentFrame = CGRect(origin: CGPoint(x: contentFrameInset.left, y: contentFrameInset.top), size: finalizedSize)
|
||||||
|
messageNode.frame = contentFrame
|
||||||
|
|
||||||
|
let messagesContainerFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: contentFrame.width + contentFrameInset.left + contentFrameInset.right, height: contentFrame.height + contentFrameInset.top + contentFrameInset.bottom))
|
||||||
|
|
||||||
|
self.messagesContainer.frame = messagesContainerFrame
|
||||||
|
|
||||||
|
return messagesContainerFrame.size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1824,7 +1824,7 @@ extension ChatControllerImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var updated = state.updatedInterfaceState { interfaceState in
|
var updated = state.updatedInterfaceState { interfaceState in
|
||||||
return interfaceState.withUpdatedEditMessage(ChatEditMessageState(messageId: messageId, inputState: ChatTextInputState(inputText: inputText), disableUrlPreviews: disableUrlPreviews, inputTextMaxLength: inputTextMaxLength))
|
return interfaceState.withUpdatedEditMessage(ChatEditMessageState(messageId: messageId, inputState: ChatTextInputState(inputText: inputText), disableUrlPreviews: disableUrlPreviews, inputTextMaxLength: inputTextMaxLength, mediaCaptionIsAbove: nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
let (updatedState, updatedPreviewQueryState) = updatedChatEditInterfaceMessageState(context: strongSelf.context, state: updated, message: message)
|
let (updatedState, updatedPreviewQueryState) = updatedChatEditInterfaceMessageState(context: strongSelf.context, state: updated, message: message)
|
||||||
@ -2169,6 +2169,14 @@ extension ChatControllerImpl {
|
|||||||
invertedMediaAttribute = attribute as? InvertMediaMessageAttribute
|
invertedMediaAttribute = attribute as? InvertMediaMessageAttribute
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let mediaCaptionIsAbove = editMessage.mediaCaptionIsAbove {
|
||||||
|
if mediaCaptionIsAbove {
|
||||||
|
invertedMediaAttribute = InvertMediaMessageAttribute()
|
||||||
|
} else {
|
||||||
|
invertedMediaAttribute = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let text = trimChatInputText(convertMarkdownToAttributes(expandedInputStateAttributedString(editMessage.inputState.inputText)))
|
let text = trimChatInputText(convertMarkdownToAttributes(expandedInputStateAttributedString(editMessage.inputState.inputText)))
|
||||||
|
|
||||||
let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text))
|
let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text))
|
||||||
|
@ -58,15 +58,8 @@ func chatMessageDisplaySendMessageOptions(selfController: ChatControllerImpl, no
|
|||||||
let editMessages: Signal<[EngineMessage], NoError>
|
let editMessages: Signal<[EngineMessage], NoError>
|
||||||
if let editMessage = selfController.presentationInterfaceState.interfaceState.editMessage {
|
if let editMessage = selfController.presentationInterfaceState.interfaceState.editMessage {
|
||||||
editMessages = selfController.context.engine.data.get(
|
editMessages = selfController.context.engine.data.get(
|
||||||
TelegramEngine.EngineData.Item.Messages.Message(id: editMessage.messageId)
|
TelegramEngine.EngineData.Item.Messages.MessageGroup(id: editMessage.messageId)
|
||||||
)
|
)
|
||||||
|> map { message -> [EngineMessage] in
|
|
||||||
if let message {
|
|
||||||
return [message]
|
|
||||||
} else {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
editMessages = .single([])
|
editMessages = .single([])
|
||||||
}
|
}
|
||||||
@ -83,17 +76,62 @@ func chatMessageDisplaySendMessageOptions(selfController: ChatControllerImpl, no
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if let _ = selfController.presentationInterfaceState.interfaceState.editMessage {
|
if let editMessage = selfController.presentationInterfaceState.interfaceState.editMessage {
|
||||||
if editMessages.isEmpty {
|
if editMessages.isEmpty {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var mediaPreview: ChatSendMessageContextScreenMediaPreview?
|
||||||
|
if editMessages.contains(where: { message in
|
||||||
|
return message.media.contains(where: { media in
|
||||||
|
if media is TelegramMediaImage {
|
||||||
|
return true
|
||||||
|
} else if let file = media as? TelegramMediaFile, file.isVideo {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
}) {
|
||||||
|
mediaPreview = ChatSendGroupMediaMessageContextPreview(
|
||||||
|
context: selfController.context,
|
||||||
|
presentationData: selfController.presentationData,
|
||||||
|
wallpaperBackgroundNode: selfController.chatDisplayNode.backgroundNode,
|
||||||
|
messages: editMessages
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let mediaCaptionIsAbove: Bool
|
||||||
|
if let value = editMessage.mediaCaptionIsAbove {
|
||||||
|
mediaCaptionIsAbove = value
|
||||||
|
} else {
|
||||||
|
mediaCaptionIsAbove = editMessages.contains(where: {
|
||||||
|
$0.attributes.contains(where: {
|
||||||
|
$0 is InvertMediaMessageAttribute
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
let controller = makeChatSendMessageActionSheetController(
|
let controller = makeChatSendMessageActionSheetController(
|
||||||
context: selfController.context,
|
context: selfController.context,
|
||||||
updatedPresentationData: selfController.updatedPresentationData,
|
updatedPresentationData: selfController.updatedPresentationData,
|
||||||
peerId: selfController.presentationInterfaceState.chatLocation.peerId,
|
peerId: selfController.presentationInterfaceState.chatLocation.peerId,
|
||||||
params: .editMessage(SendMessageActionSheetControllerParams.EditMessage(
|
params: .editMessage(SendMessageActionSheetControllerParams.EditMessage(
|
||||||
messages: editMessages,
|
messages: editMessages,
|
||||||
mediaPreview: nil
|
mediaPreview: mediaPreview,
|
||||||
|
mediaCaptionIsAbove: (mediaCaptionIsAbove, { [weak selfController] updatedMediaCaptionIsAbove in
|
||||||
|
guard let selfController else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
selfController.updateChatPresentationInterfaceState(animated: false, interactive: false, { state in
|
||||||
|
return state.updatedInterfaceState { interfaceState in
|
||||||
|
guard var editMessage = interfaceState.editMessage else {
|
||||||
|
return interfaceState
|
||||||
|
}
|
||||||
|
editMessage.mediaCaptionIsAbove = updatedMediaCaptionIsAbove
|
||||||
|
return interfaceState.withUpdatedEditMessage(editMessage)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
)),
|
)),
|
||||||
hasEntityKeyboard: hasEntityKeyboard,
|
hasEntityKeyboard: hasEntityKeyboard,
|
||||||
gesture: gesture,
|
gesture: gesture,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user