mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Merge commit '02958f42ea74027c15158431297f7ba0e5d12b56'
This commit is contained in:
commit
e8821fbd3f
@ -591,11 +591,15 @@ final class ColorGridComponent: Component {
|
|||||||
else {
|
else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
let row = Int(point.y / size.height * 10.0)
|
let row = max(0, min(10, Int(point.y / size.height * 10.0)))
|
||||||
let col = Int(point.x / size.width * 12.0)
|
let col = max(0, min(12, Int(point.x / size.width * 12.0)))
|
||||||
|
|
||||||
let index = row * 12 + col
|
let index = row * 12 + col
|
||||||
|
if index < palleteColors.count {
|
||||||
return DrawingColor(rgb: palleteColors[index])
|
return DrawingColor(rgb: palleteColors[index])
|
||||||
|
} else {
|
||||||
|
return DrawingColor(rgb: 0x000000)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func handlePress(_ gestureRecognizer: UILongPressGestureRecognizer) {
|
@objc func handlePress(_ gestureRecognizer: UILongPressGestureRecognizer) {
|
||||||
|
@ -230,7 +230,9 @@ class PremiumCoinComponent: Component {
|
|||||||
self.sceneView.scene = scene
|
self.sceneView.scene = scene
|
||||||
self.sceneView.delegate = self
|
self.sceneView.delegate = self
|
||||||
|
|
||||||
let _ = self.sceneView.snapshot()
|
self.didSetReady = true
|
||||||
|
self._ready.set(.single(true))
|
||||||
|
self.onReady()
|
||||||
}
|
}
|
||||||
|
|
||||||
private var didSetReady = false
|
private var didSetReady = false
|
||||||
|
@ -20,29 +20,40 @@ import TelegramUIPreferences
|
|||||||
|
|
||||||
public final class PremiumGradientBackgroundComponent: Component {
|
public final class PremiumGradientBackgroundComponent: Component {
|
||||||
public let colors: [UIColor]
|
public let colors: [UIColor]
|
||||||
|
public let cornerRadius: CGFloat
|
||||||
|
public let topOverscroll: Bool
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
colors: [UIColor]
|
colors: [UIColor],
|
||||||
|
cornerRadius: CGFloat = 10.0,
|
||||||
|
topOverscroll: Bool = false
|
||||||
) {
|
) {
|
||||||
self.colors = colors
|
self.colors = colors
|
||||||
|
self.cornerRadius = cornerRadius
|
||||||
|
self.topOverscroll = topOverscroll
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func ==(lhs: PremiumGradientBackgroundComponent, rhs: PremiumGradientBackgroundComponent) -> Bool {
|
public static func ==(lhs: PremiumGradientBackgroundComponent, rhs: PremiumGradientBackgroundComponent) -> Bool {
|
||||||
if lhs.colors != rhs.colors {
|
if lhs.colors != rhs.colors {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.cornerRadius != rhs.cornerRadius {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.topOverscroll != rhs.topOverscroll {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class View: UIView {
|
public final class View: UIView {
|
||||||
private let clipLayer: CALayer
|
private let clipLayer: CAReplicatorLayer
|
||||||
private let gradientLayer: CAGradientLayer
|
private let gradientLayer: CAGradientLayer
|
||||||
|
|
||||||
private var component: PremiumGradientBackgroundComponent?
|
private var component: PremiumGradientBackgroundComponent?
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
self.clipLayer = CALayer()
|
self.clipLayer = CAReplicatorLayer()
|
||||||
self.clipLayer.cornerRadius = 10.0
|
|
||||||
self.clipLayer.masksToBounds = true
|
self.clipLayer.masksToBounds = true
|
||||||
|
|
||||||
self.gradientLayer = CAGradientLayer()
|
self.gradientLayer = CAGradientLayer()
|
||||||
@ -67,16 +78,30 @@ public final class PremiumGradientBackgroundComponent: Component {
|
|||||||
for i in 0 ..< component.colors.count {
|
for i in 0 ..< component.colors.count {
|
||||||
locations.append((delta * CGFloat(i)) as NSNumber)
|
locations.append((delta * CGFloat(i)) as NSNumber)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.gradientLayer.locations = locations
|
self.gradientLayer.locations = locations
|
||||||
self.gradientLayer.colors = component.colors.reversed().map { $0.cgColor }
|
self.gradientLayer.colors = component.colors.reversed().map { $0.cgColor }
|
||||||
self.gradientLayer.type = .radial
|
self.gradientLayer.type = .radial
|
||||||
self.gradientLayer.startPoint = CGPoint(x: 1.0, y: 0.0)
|
self.gradientLayer.startPoint = CGPoint(x: 1.0, y: 0.0)
|
||||||
self.gradientLayer.endPoint = CGPoint(x: -2.0, y: 3.0)
|
self.gradientLayer.endPoint = CGPoint(x: -2.0, y: 3.0)
|
||||||
|
|
||||||
|
self.clipLayer.cornerRadius = component.cornerRadius
|
||||||
|
|
||||||
self.component = component
|
self.component = component
|
||||||
|
|
||||||
self.setupGradientAnimations()
|
self.setupGradientAnimations()
|
||||||
|
|
||||||
|
if component.topOverscroll {
|
||||||
|
self.clipLayer.instanceCount = 2
|
||||||
|
var instanceTransform = CATransform3DIdentity
|
||||||
|
instanceTransform = CATransform3DTranslate(instanceTransform, 0.0, -availableSize.height * 1.5, 0.0)
|
||||||
|
instanceTransform = CATransform3DScale(instanceTransform, 1.0, -2.0, 1.0)
|
||||||
|
self.clipLayer.instanceTransform = instanceTransform
|
||||||
|
self.clipLayer.masksToBounds = false
|
||||||
|
} else {
|
||||||
|
self.clipLayer.masksToBounds = true
|
||||||
|
}
|
||||||
|
|
||||||
return availableSize
|
return availableSize
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -282,7 +282,9 @@ final class PremiumStarComponent: Component {
|
|||||||
self.sceneView.scene = scene
|
self.sceneView.scene = scene
|
||||||
self.sceneView.delegate = self
|
self.sceneView.delegate = self
|
||||||
|
|
||||||
let _ = self.sceneView.snapshot()
|
self.didSetReady = true
|
||||||
|
self._ready.set(.single(true))
|
||||||
|
self.onReady()
|
||||||
}
|
}
|
||||||
|
|
||||||
private var didSetReady = false
|
private var didSetReady = false
|
||||||
|
@ -308,7 +308,9 @@ private final class BoostHeaderComponent: CombinedComponent {
|
|||||||
UIColor(rgb: 0x6b93ff),
|
UIColor(rgb: 0x6b93ff),
|
||||||
UIColor(rgb: 0x8878ff),
|
UIColor(rgb: 0x8878ff),
|
||||||
UIColor(rgb: 0xe46ace)
|
UIColor(rgb: 0xe46ace)
|
||||||
]
|
],
|
||||||
|
cornerRadius: 0.0,
|
||||||
|
topOverscroll: true
|
||||||
),
|
),
|
||||||
availableSize: size,
|
availableSize: size,
|
||||||
transition: context.transition
|
transition: context.transition
|
||||||
|
@ -25,7 +25,9 @@ public class TranslationMessageAttribute: MessageAttribute, Equatable {
|
|||||||
public let entities: [MessageTextEntity]
|
public let entities: [MessageTextEntity]
|
||||||
public let toLang: String
|
public let toLang: String
|
||||||
|
|
||||||
public let additional: [Additional]
|
public let additional:[Additional]
|
||||||
|
public let pollSolution: Additional?
|
||||||
|
|
||||||
|
|
||||||
public var associatedPeerIds: [PeerId] {
|
public var associatedPeerIds: [PeerId] {
|
||||||
return []
|
return []
|
||||||
@ -35,12 +37,14 @@ public class TranslationMessageAttribute: MessageAttribute, Equatable {
|
|||||||
text: String,
|
text: String,
|
||||||
entities: [MessageTextEntity],
|
entities: [MessageTextEntity],
|
||||||
additional:[Additional] = [],
|
additional:[Additional] = [],
|
||||||
|
pollSolution: Additional? = nil,
|
||||||
toLang: String
|
toLang: String
|
||||||
) {
|
) {
|
||||||
self.text = text
|
self.text = text
|
||||||
self.entities = entities
|
self.entities = entities
|
||||||
self.toLang = toLang
|
self.toLang = toLang
|
||||||
self.additional = additional
|
self.additional = additional
|
||||||
|
self.pollSolution = pollSolution
|
||||||
}
|
}
|
||||||
|
|
||||||
required public init(decoder: PostboxDecoder) {
|
required public init(decoder: PostboxDecoder) {
|
||||||
@ -48,6 +52,7 @@ public class TranslationMessageAttribute: MessageAttribute, Equatable {
|
|||||||
self.entities = decoder.decodeObjectArrayWithDecoderForKey("entities")
|
self.entities = decoder.decodeObjectArrayWithDecoderForKey("entities")
|
||||||
self.additional = decoder.decodeObjectArrayWithDecoderForKey("additional")
|
self.additional = decoder.decodeObjectArrayWithDecoderForKey("additional")
|
||||||
self.toLang = decoder.decodeStringForKey("toLang", orElse: "")
|
self.toLang = decoder.decodeStringForKey("toLang", orElse: "")
|
||||||
|
self.pollSolution = decoder.decodeObjectForKey("pollSolution") as? Additional
|
||||||
}
|
}
|
||||||
|
|
||||||
public func encode(_ encoder: PostboxEncoder) {
|
public func encode(_ encoder: PostboxEncoder) {
|
||||||
@ -55,6 +60,12 @@ public class TranslationMessageAttribute: MessageAttribute, Equatable {
|
|||||||
encoder.encodeObjectArray(self.entities, forKey: "entities")
|
encoder.encodeObjectArray(self.entities, forKey: "entities")
|
||||||
encoder.encodeString(self.toLang, forKey: "toLang")
|
encoder.encodeString(self.toLang, forKey: "toLang")
|
||||||
encoder.encodeObjectArray(self.additional, forKey: "additional")
|
encoder.encodeObjectArray(self.additional, forKey: "additional")
|
||||||
|
|
||||||
|
if let pollSolution {
|
||||||
|
encoder.encodeObject(pollSolution, forKey: "pollSolution")
|
||||||
|
} else {
|
||||||
|
encoder.encodeNil(forKey: "pollSolution")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func ==(lhs: TranslationMessageAttribute, rhs: TranslationMessageAttribute) -> Bool {
|
public static func ==(lhs: TranslationMessageAttribute, rhs: TranslationMessageAttribute) -> Bool {
|
||||||
@ -70,6 +81,9 @@ public class TranslationMessageAttribute: MessageAttribute, Equatable {
|
|||||||
if lhs.additional != rhs.additional {
|
if lhs.additional != rhs.additional {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.pollSolution != rhs.pollSolution {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -512,7 +512,7 @@ public extension TelegramEngine {
|
|||||||
return _internal_translate_texts(network: self.account.network, texts: texts, toLang: toLang)
|
return _internal_translate_texts(network: self.account.network, texts: texts, toLang: toLang)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func translateMessages(messageIds: [EngineMessage.Id], toLang: String) -> Signal<Void, TranslationError> {
|
public func translateMessages(messageIds: [EngineMessage.Id], toLang: String) -> Signal<Never, TranslationError> {
|
||||||
return _internal_translateMessages(account: self.account, messageIds: messageIds, toLang: toLang)
|
return _internal_translateMessages(account: self.account, messageIds: messageIds, toLang: toLang)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,11 +84,16 @@ func _internal_translate_texts(network: Network, texts: [(String, [MessageTextEn
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func _internal_translateMessages(account: Account, messageIds: [EngineMessage.Id], toLang: String) -> Signal<Never, TranslationError> {
|
||||||
func _internal_translateMessages(account: Account, messageIds: [EngineMessage.Id], toLang: String) -> Signal<Void, TranslationError> {
|
var signals: [Signal<Void, TranslationError>] = []
|
||||||
guard let peerId = messageIds.first?.peerId else {
|
for (peerId, messageIds) in messagesIdsGroupedByPeerId(messageIds) {
|
||||||
return .never()
|
signals.append(_internal_translateMessagesByPeerId(account: account, peerId: peerId, messageIds: messageIds, toLang: toLang))
|
||||||
}
|
}
|
||||||
|
return combineLatest(signals)
|
||||||
|
|> ignoreValues
|
||||||
|
}
|
||||||
|
|
||||||
|
private func _internal_translateMessagesByPeerId(account: Account, peerId: EnginePeer.Id, messageIds: [EngineMessage.Id], toLang: String) -> Signal<Void, TranslationError> {
|
||||||
return account.postbox.transaction { transaction -> (Api.InputPeer?, [Message]) in
|
return account.postbox.transaction { transaction -> (Api.InputPeer?, [Message]) in
|
||||||
return (transaction.getPeer(peerId).flatMap(apiInputPeer), messageIds.compactMap({ transaction.getMessage($0) }))
|
return (transaction.getPeer(peerId).flatMap(apiInputPeer), messageIds.compactMap({ transaction.getMessage($0) }))
|
||||||
}
|
}
|
||||||
@ -111,6 +116,9 @@ func _internal_translateMessages(account: Account, messageIds: [EngineMessage.Id
|
|||||||
for option in poll.options {
|
for option in poll.options {
|
||||||
texts.append((option.text, option.entities))
|
texts.append((option.text, option.entities))
|
||||||
}
|
}
|
||||||
|
if let solution = poll.results.solution {
|
||||||
|
texts.append((solution.text, solution.entities))
|
||||||
|
}
|
||||||
return _internal_translate_texts(network: account.network, texts: texts, toLang: toLang)
|
return _internal_translate_texts(network: account.network, texts: texts, toLang: toLang)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -167,20 +175,34 @@ func _internal_translateMessages(account: Account, messageIds: [EngineMessage.Id
|
|||||||
if !pollResults.isEmpty {
|
if !pollResults.isEmpty {
|
||||||
for (i, poll) in polls.enumerated() {
|
for (i, poll) in polls.enumerated() {
|
||||||
let result = pollResults[i]
|
let result = pollResults[i]
|
||||||
|
if !result.isEmpty {
|
||||||
transaction.updateMessage(poll.1, update: { currentMessage in
|
transaction.updateMessage(poll.1, update: { currentMessage in
|
||||||
let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init)
|
let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init)
|
||||||
var attributes = currentMessage.attributes.filter { !($0 is TranslationMessageAttribute) }
|
var attributes = currentMessage.attributes.filter { !($0 is TranslationMessageAttribute) }
|
||||||
var attrOptions: [TranslationMessageAttribute.Additional] = []
|
var attrOptions: [TranslationMessageAttribute.Additional] = []
|
||||||
for (i, _) in poll.0.options.enumerated() {
|
for (i, _) in poll.0.options.enumerated() {
|
||||||
let translated = result[i + 1]
|
var translated = result.count > i + 1 ? result[i + 1] : (poll.0.options[i].text, poll.0.options[i].entities)
|
||||||
|
if translated.0.isEmpty {
|
||||||
|
translated = (poll.0.options[i].text, poll.0.options[i].entities)
|
||||||
|
}
|
||||||
attrOptions.append(.init(text: translated.0, entities: translated.1))
|
attrOptions.append(.init(text: translated.0, entities: translated.1))
|
||||||
}
|
}
|
||||||
let updatedAttribute: TranslationMessageAttribute = TranslationMessageAttribute(text: result[0].0, entities: result[0].1, additional: attrOptions, toLang: toLang)
|
|
||||||
|
let solution: TranslationMessageAttribute.Additional?
|
||||||
|
if result.count > 1 + poll.0.options.count, !result[result.count - 1].0.isEmpty {
|
||||||
|
solution = .init(text: result[result.count - 1].0, entities: result[result.count - 1].1)
|
||||||
|
} else {
|
||||||
|
solution = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let title = result[0].0.isEmpty ? (poll.0.text, poll.0.textEntities) : result[0]
|
||||||
|
|
||||||
|
let updatedAttribute: TranslationMessageAttribute = TranslationMessageAttribute(text: title.0, entities: title.1, additional: attrOptions, pollSolution: solution, toLang: toLang)
|
||||||
attributes.append(updatedAttribute)
|
attributes.append(updatedAttribute)
|
||||||
|
|
||||||
return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
|
return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
|
||||||
})
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3453,6 +3453,18 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||||||
credibilityIconView?.removeFromSuperview()
|
credibilityIconView?.removeFromSuperview()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if let boostBadgeNode = strongSelf.boostBadgeNode {
|
||||||
|
strongSelf.boostBadgeNode = nil
|
||||||
|
boostBadgeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, removeOnCompletion: false, completion: { [weak boostBadgeNode] _ in
|
||||||
|
boostBadgeNode?.removeFromSupernode()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if let boostIconNode = strongSelf.boostIconNode {
|
||||||
|
strongSelf.boostIconNode = nil
|
||||||
|
boostIconNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, removeOnCompletion: false, completion: { [weak boostIconNode] _ in
|
||||||
|
boostIconNode?.removeFromSuperview()
|
||||||
|
})
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
strongSelf.nameNode?.removeFromSupernode()
|
strongSelf.nameNode?.removeFromSupernode()
|
||||||
strongSelf.nameNode = nil
|
strongSelf.nameNode = nil
|
||||||
@ -3460,6 +3472,11 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||||||
strongSelf.adminBadgeNode = nil
|
strongSelf.adminBadgeNode = nil
|
||||||
strongSelf.credibilityIconView?.removeFromSuperview()
|
strongSelf.credibilityIconView?.removeFromSuperview()
|
||||||
strongSelf.credibilityIconView = nil
|
strongSelf.credibilityIconView = nil
|
||||||
|
strongSelf.boostBadgeNode?.removeFromSupernode()
|
||||||
|
strongSelf.boostBadgeNode = nil
|
||||||
|
strongSelf.boostIconNode?.removeFromSuperview()
|
||||||
|
strongSelf.boostIconNode = nil
|
||||||
|
}
|
||||||
strongSelf.nameButtonNode?.removeFromSupernode()
|
strongSelf.nameButtonNode?.removeFromSupernode()
|
||||||
strongSelf.nameButtonNode = nil
|
strongSelf.nameButtonNode = nil
|
||||||
strongSelf.nameHighlightNode?.removeFromSupernode()
|
strongSelf.nameHighlightNode?.removeFromSupernode()
|
||||||
@ -3473,7 +3490,6 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||||||
strongSelf.boostHighlightNode?.removeFromSupernode()
|
strongSelf.boostHighlightNode?.removeFromSupernode()
|
||||||
strongSelf.boostHighlightNode = nil
|
strongSelf.boostHighlightNode = nil
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let timingFunction = kCAMediaTimingFunctionSpring
|
let timingFunction = kCAMediaTimingFunctionSpring
|
||||||
if let forwardInfoNode = forwardInfoSizeApply.1(bubbleContentWidth) {
|
if let forwardInfoNode = forwardInfoSizeApply.1(bubbleContentWidth) {
|
||||||
|
@ -755,7 +755,7 @@ public final class ChatMessageAvatarHeaderNodeImpl: ListViewItemHeaderNode, Chat
|
|||||||
self.controllerInteraction?.displayMessageTooltip(id, self.presentationData.strings.Conversation_ForwardAuthorHiddenTooltip, self, self.avatarNode.frame)
|
self.controllerInteraction?.displayMessageTooltip(id, self.presentationData.strings.Conversation_ForwardAuthorHiddenTooltip, self, self.avatarNode.frame)
|
||||||
} else if let peer = self.peer {
|
} else if let peer = self.peer {
|
||||||
if let adMessageId = self.adMessageId {
|
if let adMessageId = self.adMessageId {
|
||||||
self.controllerInteraction?.activateAdAction(adMessageId)
|
self.controllerInteraction?.activateAdAction(adMessageId, nil)
|
||||||
} else {
|
} else {
|
||||||
if let channel = peer as? TelegramChannel, case .broadcast = channel.info {
|
if let channel = peer as? TelegramChannel, case .broadcast = channel.info {
|
||||||
self.controllerInteraction?.openPeer(EnginePeer(peer), .chat(textInputState: nil, subject: nil, peekData: nil), self.messageReference, .default)
|
self.controllerInteraction?.openPeer(EnginePeer(peer), .chat(textInputState: nil, subject: nil, peekData: nil), self.messageReference, .default)
|
||||||
|
@ -140,7 +140,7 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent
|
|||||||
self.contentNode.activateAction = { [weak self] in
|
self.contentNode.activateAction = { [weak self] in
|
||||||
if let strongSelf = self, let item = strongSelf.item {
|
if let strongSelf = self, let item = strongSelf.item {
|
||||||
if let _ = item.message.adAttribute {
|
if let _ = item.message.adAttribute {
|
||||||
item.controllerInteraction.activateAdAction(item.message.id)
|
item.controllerInteraction.activateAdAction(item.message.id, strongSelf.contentNode.makeProgress())
|
||||||
} else {
|
} else {
|
||||||
var webPageContent: TelegramMediaWebpageLoadedContent?
|
var webPageContent: TelegramMediaWebpageLoadedContent?
|
||||||
for media in item.message.media {
|
for media in item.message.media {
|
||||||
|
@ -604,7 +604,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
|||||||
}, openLargeEmojiInfo: { _, _, _ in
|
}, openLargeEmojiInfo: { _, _, _ in
|
||||||
}, openJoinLink: { _ in
|
}, openJoinLink: { _ in
|
||||||
}, openWebView: { _, _, _, _ in
|
}, openWebView: { _, _, _, _ in
|
||||||
}, activateAdAction: { _ in
|
}, activateAdAction: { _, _ in
|
||||||
}, openRequestedPeerSelection: { _, _, _, _ in
|
}, openRequestedPeerSelection: { _, _, _, _ in
|
||||||
}, saveMediaToFiles: { _ in
|
}, saveMediaToFiles: { _ in
|
||||||
}, openNoAdsDemo: {
|
}, openNoAdsDemo: {
|
||||||
|
@ -232,7 +232,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
|
|||||||
public let openLargeEmojiInfo: (String, String?, TelegramMediaFile) -> Void
|
public let openLargeEmojiInfo: (String, String?, TelegramMediaFile) -> Void
|
||||||
public let openJoinLink: (String) -> Void
|
public let openJoinLink: (String) -> Void
|
||||||
public let openWebView: (String, String, Bool, ChatOpenWebViewSource) -> Void
|
public let openWebView: (String, String, Bool, ChatOpenWebViewSource) -> Void
|
||||||
public let activateAdAction: (EngineMessage.Id) -> Void
|
public let activateAdAction: (EngineMessage.Id, Promise<Bool>?) -> Void
|
||||||
public let openRequestedPeerSelection: (EngineMessage.Id, ReplyMarkupButtonRequestPeerType, Int32, Int32) -> Void
|
public let openRequestedPeerSelection: (EngineMessage.Id, ReplyMarkupButtonRequestPeerType, Int32, Int32) -> Void
|
||||||
public let saveMediaToFiles: (EngineMessage.Id) -> Void
|
public let saveMediaToFiles: (EngineMessage.Id) -> Void
|
||||||
public let openNoAdsDemo: () -> Void
|
public let openNoAdsDemo: () -> Void
|
||||||
@ -357,7 +357,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
|
|||||||
openLargeEmojiInfo: @escaping (String, String?, TelegramMediaFile) -> Void,
|
openLargeEmojiInfo: @escaping (String, String?, TelegramMediaFile) -> Void,
|
||||||
openJoinLink: @escaping (String) -> Void,
|
openJoinLink: @escaping (String) -> Void,
|
||||||
openWebView: @escaping (String, String, Bool, ChatOpenWebViewSource) -> Void,
|
openWebView: @escaping (String, String, Bool, ChatOpenWebViewSource) -> Void,
|
||||||
activateAdAction: @escaping (EngineMessage.Id) -> Void,
|
activateAdAction: @escaping (EngineMessage.Id, Promise<Bool>?) -> Void,
|
||||||
openRequestedPeerSelection: @escaping (EngineMessage.Id, ReplyMarkupButtonRequestPeerType, Int32, Int32) -> Void,
|
openRequestedPeerSelection: @escaping (EngineMessage.Id, ReplyMarkupButtonRequestPeerType, Int32, Int32) -> Void,
|
||||||
saveMediaToFiles: @escaping (EngineMessage.Id) -> Void,
|
saveMediaToFiles: @escaping (EngineMessage.Id) -> Void,
|
||||||
openNoAdsDemo: @escaping () -> Void,
|
openNoAdsDemo: @escaping () -> Void,
|
||||||
|
@ -0,0 +1,274 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Display
|
||||||
|
import SwiftSignalKit
|
||||||
|
import Postbox
|
||||||
|
import TelegramCore
|
||||||
|
import AccountContext
|
||||||
|
import TextFormat
|
||||||
|
import SaveToCameraRoll
|
||||||
|
import ImageCompression
|
||||||
|
import LocalMediaResources
|
||||||
|
|
||||||
|
public extension MediaEditorScreen {
|
||||||
|
static func makeEditStoryController(
|
||||||
|
context: AccountContext,
|
||||||
|
peer: EnginePeer,
|
||||||
|
storyItem: EngineStoryItem,
|
||||||
|
videoPlaybackPosition: Double?,
|
||||||
|
repost: Bool,
|
||||||
|
transitionIn: MediaEditorScreen.TransitionIn,
|
||||||
|
transitionOut: MediaEditorScreen.TransitionOut?,
|
||||||
|
completed: @escaping () -> Void = {},
|
||||||
|
willDismiss: @escaping () -> Void = {},
|
||||||
|
update: @escaping (Disposable?) -> Void
|
||||||
|
) -> MediaEditorScreen? {
|
||||||
|
guard let peerReference = PeerReference(peer._asPeer()) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let subject: Signal<MediaEditorScreen.Subject?, NoError>
|
||||||
|
subject = getStorySource(engine: context.engine, peerId: peer.id, id: Int64(storyItem.id))
|
||||||
|
|> mapToSignal { source in
|
||||||
|
if !repost, let source {
|
||||||
|
return .single(.draft(source, Int64(storyItem.id)))
|
||||||
|
} else {
|
||||||
|
let media = storyItem.media._asMedia()
|
||||||
|
return fetchMediaData(context: context, postbox: context.account.postbox, userLocation: .peer(peerReference.id), customUserContentType: .story, mediaReference: .story(peer: peerReference, id: storyItem.id, media: media))
|
||||||
|
|> mapToSignal { (value, isImage) -> Signal<MediaEditorScreen.Subject?, NoError> in
|
||||||
|
guard case let .data(data) = value, data.complete else {
|
||||||
|
return .complete()
|
||||||
|
}
|
||||||
|
if let image = UIImage(contentsOfFile: data.path) {
|
||||||
|
return .single(nil)
|
||||||
|
|> then(
|
||||||
|
.single(.image(image, PixelDimensions(image.size), nil, .bottomRight))
|
||||||
|
|> delay(0.1, queue: Queue.mainQueue())
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
var duration: Double?
|
||||||
|
if let file = media as? TelegramMediaFile {
|
||||||
|
duration = file.duration
|
||||||
|
}
|
||||||
|
let symlinkPath = data.path + ".mp4"
|
||||||
|
if fileSize(symlinkPath) == nil {
|
||||||
|
let _ = try? FileManager.default.linkItem(atPath: data.path, toPath: symlinkPath)
|
||||||
|
}
|
||||||
|
return .single(nil)
|
||||||
|
|> then(
|
||||||
|
.single(.video(symlinkPath, nil, false, nil, nil, PixelDimensions(width: 720, height: 1280), duration ?? 0.0, [], .bottomRight))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let initialCaption: NSAttributedString?
|
||||||
|
let initialPrivacy: EngineStoryPrivacy?
|
||||||
|
let initialMediaAreas: [MediaArea]
|
||||||
|
if repost {
|
||||||
|
initialCaption = nil
|
||||||
|
initialPrivacy = nil
|
||||||
|
initialMediaAreas = []
|
||||||
|
} else {
|
||||||
|
initialCaption = chatInputStateStringWithAppliedEntities(storyItem.text, entities: storyItem.entities)
|
||||||
|
initialPrivacy = storyItem.privacy
|
||||||
|
initialMediaAreas = storyItem.mediaAreas
|
||||||
|
}
|
||||||
|
|
||||||
|
let externalState = MediaEditorTransitionOutExternalState(
|
||||||
|
storyTarget: nil,
|
||||||
|
isForcedTarget: false,
|
||||||
|
isPeerArchived: false,
|
||||||
|
transitionOut: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
var updateProgressImpl: ((Float) -> Void)?
|
||||||
|
let controller = MediaEditorScreen(
|
||||||
|
context: context,
|
||||||
|
mode: .storyEditor,
|
||||||
|
subject: subject,
|
||||||
|
isEditing: !repost,
|
||||||
|
forwardSource: repost ? (peer, storyItem) : nil,
|
||||||
|
initialCaption: initialCaption,
|
||||||
|
initialPrivacy: initialPrivacy,
|
||||||
|
initialMediaAreas: initialMediaAreas,
|
||||||
|
initialVideoPosition: videoPlaybackPosition,
|
||||||
|
transitionIn: transitionIn,
|
||||||
|
transitionOut: { finished, isNew in
|
||||||
|
if repost && finished {
|
||||||
|
if let transitionOut = externalState.transitionOut?(externalState.storyTarget, externalState.isPeerArchived), let destinationView = transitionOut.destinationView {
|
||||||
|
return MediaEditorScreen.TransitionOut(
|
||||||
|
destinationView: destinationView,
|
||||||
|
destinationRect: transitionOut.destinationRect,
|
||||||
|
destinationCornerRadius: transitionOut.destinationCornerRadius
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return transitionOut
|
||||||
|
}
|
||||||
|
},
|
||||||
|
completion: { result, commit in
|
||||||
|
let entities = generateChatInputTextEntities(result.caption)
|
||||||
|
|
||||||
|
if repost {
|
||||||
|
let target: Stories.PendingTarget
|
||||||
|
let targetPeerId: EnginePeer.Id
|
||||||
|
if let sendAsPeerId = result.options.sendAsPeerId {
|
||||||
|
target = .peer(sendAsPeerId)
|
||||||
|
targetPeerId = sendAsPeerId
|
||||||
|
} else {
|
||||||
|
target = .myStories
|
||||||
|
targetPeerId = context.account.peerId
|
||||||
|
}
|
||||||
|
externalState.storyTarget = target
|
||||||
|
|
||||||
|
completed()
|
||||||
|
|
||||||
|
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: targetPeerId))
|
||||||
|
|> deliverOnMainQueue).startStandalone(next: { peer in
|
||||||
|
guard let peer else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if case let .user(user) = peer {
|
||||||
|
externalState.isPeerArchived = user.storiesHidden ?? false
|
||||||
|
|
||||||
|
} else if case let .channel(channel) = peer {
|
||||||
|
externalState.isPeerArchived = channel.storiesHidden ?? false
|
||||||
|
}
|
||||||
|
|
||||||
|
let forwardInfo = Stories.PendingForwardInfo(peerId: peerReference.id, storyId: storyItem.id, isModified: result.media != nil)
|
||||||
|
|
||||||
|
if let rootController = context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface {
|
||||||
|
var existingMedia: EngineMedia?
|
||||||
|
if let _ = result.media {
|
||||||
|
} else {
|
||||||
|
existingMedia = storyItem.media
|
||||||
|
}
|
||||||
|
rootController.proceedWithStoryUpload(target: target, result: result as! MediaEditorScreenResult, existingMedia: existingMedia, forwardInfo: forwardInfo, externalState: externalState, commit: commit)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
var updatedText: String?
|
||||||
|
var updatedEntities: [MessageTextEntity]?
|
||||||
|
if result.caption.string != storyItem.text || entities != storyItem.entities {
|
||||||
|
updatedText = result.caption.string
|
||||||
|
updatedEntities = entities
|
||||||
|
}
|
||||||
|
|
||||||
|
if let mediaResult = result.media {
|
||||||
|
switch mediaResult {
|
||||||
|
case let .image(image, dimensions):
|
||||||
|
updateProgressImpl?(0.0)
|
||||||
|
|
||||||
|
let tempFile = TempBox.shared.tempFile(fileName: "file")
|
||||||
|
defer {
|
||||||
|
TempBox.shared.dispose(tempFile)
|
||||||
|
}
|
||||||
|
if let imageData = compressImageToJPEG(image, quality: 0.7, tempFilePath: tempFile.path) {
|
||||||
|
update((context.engine.messages.editStory(peerId: peer.id, id: storyItem.id, media: .image(dimensions: dimensions, data: imageData, stickers: result.stickers), mediaAreas: result.mediaAreas, text: updatedText, entities: updatedEntities, privacy: nil)
|
||||||
|
|> deliverOnMainQueue).startStrict(next: { result in
|
||||||
|
switch result {
|
||||||
|
case let .progress(progress):
|
||||||
|
updateProgressImpl?(progress)
|
||||||
|
case .completed:
|
||||||
|
Queue.mainQueue().after(0.1) {
|
||||||
|
willDismiss()
|
||||||
|
|
||||||
|
HapticFeedback().success()
|
||||||
|
|
||||||
|
commit({})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
case let .video(content, firstFrameImage, values, duration, dimensions):
|
||||||
|
updateProgressImpl?(0.0)
|
||||||
|
|
||||||
|
if let valuesData = try? JSONEncoder().encode(values) {
|
||||||
|
let data = MemoryBuffer(data: valuesData)
|
||||||
|
let digest = MemoryBuffer(data: data.md5Digest())
|
||||||
|
let adjustments = VideoMediaResourceAdjustments(data: data, digest: digest, isStory: true)
|
||||||
|
|
||||||
|
let resource: TelegramMediaResource
|
||||||
|
switch content {
|
||||||
|
case let .imageFile(path):
|
||||||
|
resource = LocalFileVideoMediaResource(randomId: Int64.random(in: .min ... .max), path: path, adjustments: adjustments)
|
||||||
|
case let .videoFile(path):
|
||||||
|
resource = LocalFileVideoMediaResource(randomId: Int64.random(in: .min ... .max), path: path, adjustments: adjustments)
|
||||||
|
case let .asset(localIdentifier):
|
||||||
|
resource = VideoLibraryMediaResource(localIdentifier: localIdentifier, conversion: .compress(adjustments))
|
||||||
|
}
|
||||||
|
|
||||||
|
let tempFile = TempBox.shared.tempFile(fileName: "file")
|
||||||
|
defer {
|
||||||
|
TempBox.shared.dispose(tempFile)
|
||||||
|
}
|
||||||
|
let firstFrameImageData = firstFrameImage.flatMap { compressImageToJPEG($0, quality: 0.6, tempFilePath: tempFile.path) }
|
||||||
|
let firstFrameFile = firstFrameImageData.flatMap { data -> TempBoxFile? in
|
||||||
|
let file = TempBox.shared.tempFile(fileName: "image.jpg")
|
||||||
|
if let _ = try? data.write(to: URL(fileURLWithPath: file.path)) {
|
||||||
|
return file
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update((context.engine.messages.editStory(peerId: peer.id, id: storyItem.id, media: .video(dimensions: dimensions, duration: duration, resource: resource, firstFrameFile: firstFrameFile, stickers: result.stickers), mediaAreas: result.mediaAreas, text: updatedText, entities: updatedEntities, privacy: nil)
|
||||||
|
|> deliverOnMainQueue).startStrict(next: { result in
|
||||||
|
switch result {
|
||||||
|
case let .progress(progress):
|
||||||
|
updateProgressImpl?(progress)
|
||||||
|
case .completed:
|
||||||
|
Queue.mainQueue().after(0.1) {
|
||||||
|
willDismiss()
|
||||||
|
|
||||||
|
HapticFeedback().success()
|
||||||
|
|
||||||
|
commit({})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else if updatedText != nil {
|
||||||
|
let _ = (context.engine.messages.editStory(peerId: peer.id, id: storyItem.id, media: nil, mediaAreas: nil, text: updatedText, entities: updatedEntities, privacy: nil)
|
||||||
|
|> deliverOnMainQueue).startStandalone(next: { result in
|
||||||
|
switch result {
|
||||||
|
case .completed:
|
||||||
|
Queue.mainQueue().after(0.1) {
|
||||||
|
willDismiss()
|
||||||
|
|
||||||
|
HapticFeedback().success()
|
||||||
|
commit({})
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
willDismiss()
|
||||||
|
|
||||||
|
HapticFeedback().success()
|
||||||
|
|
||||||
|
commit({})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
controller.willDismiss = willDismiss
|
||||||
|
controller.navigationPresentation = .flatModal
|
||||||
|
|
||||||
|
updateProgressImpl = { [weak controller] progress in
|
||||||
|
controller?.updateEditProgress(progress, cancel: {
|
||||||
|
update(nil)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return controller
|
||||||
|
}
|
||||||
|
}
|
@ -3109,6 +3109,13 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||||||
if controller.isEmbeddedEditor == true {
|
if controller.isEmbeddedEditor == true {
|
||||||
mediaEditor.onFirstDisplay = { [weak self] in
|
mediaEditor.onFirstDisplay = { [weak self] in
|
||||||
if let self {
|
if let self {
|
||||||
|
if let transitionInView = self.transitionInView {
|
||||||
|
self.transitionInView = nil
|
||||||
|
transitionInView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak transitionInView] _ in
|
||||||
|
transitionInView?.removeFromSuperview()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if effectiveSubject.isPhoto {
|
if effectiveSubject.isPhoto {
|
||||||
self.previewContainerView.layer.allowsGroupOpacity = true
|
self.previewContainerView.layer.allowsGroupOpacity = true
|
||||||
self.previewContainerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, completion: { _ in
|
self.previewContainerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, completion: { _ in
|
||||||
@ -3765,6 +3772,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||||||
|
|
||||||
if let transitionOut = controller.transitionOut(finished, isNew), let destinationView = transitionOut.destinationView {
|
if let transitionOut = controller.transitionOut(finished, isNew), let destinationView = transitionOut.destinationView {
|
||||||
var destinationTransitionView: UIView?
|
var destinationTransitionView: UIView?
|
||||||
|
var destinationTransitionRect: CGRect = .zero
|
||||||
if !finished {
|
if !finished {
|
||||||
if let transitionIn = controller.transitionIn, case let .gallery(galleryTransitionIn) = transitionIn, let sourceImage = galleryTransitionIn.sourceImage, isNew != true {
|
if let transitionIn = controller.transitionIn, case let .gallery(galleryTransitionIn) = transitionIn, let sourceImage = galleryTransitionIn.sourceImage, isNew != true {
|
||||||
let sourceSuperView = galleryTransitionIn.sourceView?.superview?.superview
|
let sourceSuperView = galleryTransitionIn.sourceView?.superview?.superview
|
||||||
@ -3774,6 +3782,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||||||
destinationTransitionOutView.frame = self.previewContainerView.convert(self.previewContainerView.bounds, to: sourceSuperView)
|
destinationTransitionOutView.frame = self.previewContainerView.convert(self.previewContainerView.bounds, to: sourceSuperView)
|
||||||
sourceSuperView?.addSubview(destinationTransitionOutView)
|
sourceSuperView?.addSubview(destinationTransitionOutView)
|
||||||
destinationTransitionView = destinationTransitionOutView
|
destinationTransitionView = destinationTransitionOutView
|
||||||
|
destinationTransitionRect = galleryTransitionIn.sourceRect
|
||||||
}
|
}
|
||||||
if let view = self.componentHost.view as? MediaEditorScreenComponent.View {
|
if let view = self.componentHost.view as? MediaEditorScreenComponent.View {
|
||||||
view.animateOut(to: .gallery)
|
view.animateOut(to: .gallery)
|
||||||
@ -3853,7 +3862,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||||||
if let destinationTransitionView {
|
if let destinationTransitionView {
|
||||||
self.previewContainerView.layer.allowsGroupOpacity = true
|
self.previewContainerView.layer.allowsGroupOpacity = true
|
||||||
self.previewContainerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false)
|
self.previewContainerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false)
|
||||||
destinationTransitionView.layer.animateFrame(from: destinationTransitionView.frame, to: destinationView.convert(destinationView.bounds, to: destinationTransitionView.superview), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { [weak destinationTransitionView] _ in
|
destinationTransitionView.layer.animateFrame(from: destinationTransitionView.frame, to: destinationView.convert(destinationTransitionRect, to: destinationTransitionView.superview), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { [weak destinationTransitionView] _ in
|
||||||
destinationTransitionView?.removeFromSuperview()
|
destinationTransitionView?.removeFromSuperview()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -3316,7 +3316,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
|||||||
}, openLargeEmojiInfo: { _, _, _ in
|
}, openLargeEmojiInfo: { _, _, _ in
|
||||||
}, openJoinLink: { _ in
|
}, openJoinLink: { _ in
|
||||||
}, openWebView: { _, _, _, _ in
|
}, openWebView: { _, _, _, _ in
|
||||||
}, activateAdAction: { _ in
|
}, activateAdAction: { _, _ in
|
||||||
}, openRequestedPeerSelection: { _, _, _, _ in
|
}, openRequestedPeerSelection: { _, _, _, _ in
|
||||||
}, saveMediaToFiles: { _ in
|
}, saveMediaToFiles: { _ in
|
||||||
}, openNoAdsDemo: {
|
}, openNoAdsDemo: {
|
||||||
|
@ -43,6 +43,7 @@ swift_library(
|
|||||||
"//submodules/UndoUI",
|
"//submodules/UndoUI",
|
||||||
"//submodules/TelegramUI/Components/PlainButtonComponent",
|
"//submodules/TelegramUI/Components/PlainButtonComponent",
|
||||||
"//submodules/Components/ComponentDisplayAdapters",
|
"//submodules/Components/ComponentDisplayAdapters",
|
||||||
|
"//submodules/TelegramUI/Components/MediaEditorScreen",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
@ -33,6 +33,7 @@ import ShareController
|
|||||||
import UndoUI
|
import UndoUI
|
||||||
import PlainButtonComponent
|
import PlainButtonComponent
|
||||||
import ComponentDisplayAdapters
|
import ComponentDisplayAdapters
|
||||||
|
import MediaEditorScreen
|
||||||
|
|
||||||
private let mediaBadgeBackgroundColor = UIColor(white: 0.0, alpha: 0.6)
|
private let mediaBadgeBackgroundColor = UIColor(white: 0.0, alpha: 0.6)
|
||||||
private let mediaBadgeTextColor = UIColor.white
|
private let mediaBadgeTextColor = UIColor.white
|
||||||
@ -1266,6 +1267,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
|
|||||||
|
|
||||||
private let listDisposable = MetaDisposable()
|
private let listDisposable = MetaDisposable()
|
||||||
private var hiddenMediaDisposable: Disposable?
|
private var hiddenMediaDisposable: Disposable?
|
||||||
|
private let updateDisposable = MetaDisposable()
|
||||||
|
|
||||||
private var numberOfItemsToRequest: Int = 50
|
private var numberOfItemsToRequest: Int = 50
|
||||||
private var isRequestingView: Bool = false
|
private var isRequestingView: Bool = false
|
||||||
@ -1765,6 +1767,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
|
|||||||
self.hiddenMediaDisposable?.dispose()
|
self.hiddenMediaDisposable?.dispose()
|
||||||
self.animationTimer?.invalidate()
|
self.animationTimer?.invalidate()
|
||||||
self.presentationDataDisposable?.dispose()
|
self.presentationDataDisposable?.dispose()
|
||||||
|
self.updateDisposable.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func loadHole(anchor: SparseItemGrid.HoleAnchor, at location: SparseItemGrid.HoleLocation) -> Signal<Never, NoError> {
|
public func loadHole(anchor: SparseItemGrid.HoleAnchor, at location: SparseItemGrid.HoleLocation) -> Signal<Never, NoError> {
|
||||||
@ -1858,16 +1861,54 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
|
|||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
/*items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.StoryList_ItemAction_Edit, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in
|
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.StoryList_ItemAction_Edit, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in
|
||||||
c?.dismiss(completion: {
|
c?.dismiss(completion: {
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let _ = self
|
let _ = (self.context.engine.data.get(
|
||||||
|
TelegramEngine.EngineData.Item.Peer.Peer(id: self.peerId)
|
||||||
|
)
|
||||||
|
|> deliverOnMainQueue).startStandalone(next: { [weak self] peer in
|
||||||
|
guard let self, let peer else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var foundItemLayer: SparseItemGridLayer?
|
||||||
|
var sourceImage: UIImage?
|
||||||
|
self.itemGrid.forEachVisibleItem { gridItem in
|
||||||
|
guard let itemLayer = gridItem.layer as? ItemLayer else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if let listItem = itemLayer.item, listItem.story.id == item.id {
|
||||||
|
foundItemLayer = itemLayer
|
||||||
|
if let contents = itemLayer.contents, CFGetTypeID(contents as CFTypeRef) == CGImage.typeID {
|
||||||
|
sourceImage = UIImage(cgImage: contents as! CGImage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let controller = MediaEditorScreen.makeEditStoryController(
|
||||||
|
context: self.context,
|
||||||
|
peer: peer,
|
||||||
|
storyItem: item,
|
||||||
|
videoPlaybackPosition: nil,
|
||||||
|
repost: false,
|
||||||
|
transitionIn: .gallery(MediaEditorScreen.TransitionIn.GalleryTransitionIn(sourceView: self.itemGrid.view, sourceRect: foundItemLayer?.frame ?? .zero, sourceImage: sourceImage)),
|
||||||
|
transitionOut: MediaEditorScreen.TransitionOut(destinationView: self.itemGrid.view, destinationRect: foundItemLayer?.frame ?? .zero, destinationCornerRadius: 0.0),
|
||||||
|
update: { [weak self] disposable in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.updateDisposable.set(disposable)
|
||||||
|
}
|
||||||
|
) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.parentController?.push(controller)
|
||||||
})
|
})
|
||||||
})))*/
|
})
|
||||||
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
if !item.isForwardingDisabled, case .everyone = item.privacy?.base {
|
if !item.isForwardingDisabled, case .everyone = item.privacy?.base {
|
||||||
|
@ -4308,7 +4308,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
self.sendMessageContext.currentSpeechHolder = speechHolder
|
self.sendMessageContext.currentSpeechHolder = speechHolder
|
||||||
}
|
}
|
||||||
case .translate:
|
case .translate:
|
||||||
self.sendMessageContext.performTranslateTextAction(view: self, text: text.string)
|
self.sendMessageContext.performTranslateTextAction(view: self, text: text.string, entities: [])
|
||||||
case .quote:
|
case .quote:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -5359,13 +5359,9 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
|
|
||||||
private let updateDisposable = MetaDisposable()
|
private let updateDisposable = MetaDisposable()
|
||||||
func openStoryEditing(repost: Bool = false) {
|
func openStoryEditing(repost: Bool = false) {
|
||||||
guard let component = self.component, let peerReference = PeerReference(component.slice.peer._asPeer()) else {
|
guard let component = self.component else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let context = component.context
|
|
||||||
let peerId = component.slice.peer.id
|
|
||||||
let item = component.slice.item.storyItem
|
|
||||||
let id = item.id
|
|
||||||
|
|
||||||
self.isEditingStory = true
|
self.isEditingStory = true
|
||||||
self.updateIsProgressPaused()
|
self.updateIsProgressPaused()
|
||||||
@ -5376,277 +5372,39 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
videoPlaybackPosition = view.videoPlaybackPosition
|
videoPlaybackPosition = view.videoPlaybackPosition
|
||||||
}
|
}
|
||||||
|
|
||||||
let subject: Signal<MediaEditorScreen.Subject?, NoError>
|
guard let controller = MediaEditorScreen.makeEditStoryController(
|
||||||
subject = getStorySource(engine: component.context.engine, peerId: component.context.account.peerId, id: Int64(item.id))
|
context: component.context,
|
||||||
|> mapToSignal { source in
|
peer: component.slice.peer,
|
||||||
if !repost, let source {
|
storyItem: component.slice.item.storyItem,
|
||||||
return .single(.draft(source, Int64(item.id)))
|
videoPlaybackPosition: videoPlaybackPosition,
|
||||||
} else {
|
repost: repost,
|
||||||
let media = item.media._asMedia()
|
|
||||||
return fetchMediaData(context: context, postbox: context.account.postbox, userLocation: .peer(peerReference.id), customUserContentType: .story, mediaReference: .story(peer: peerReference, id: item.id, media: media))
|
|
||||||
|> mapToSignal { (value, isImage) -> Signal<MediaEditorScreen.Subject?, NoError> in
|
|
||||||
guard case let .data(data) = value, data.complete else {
|
|
||||||
return .complete()
|
|
||||||
}
|
|
||||||
if let image = UIImage(contentsOfFile: data.path) {
|
|
||||||
return .single(nil)
|
|
||||||
|> then(
|
|
||||||
.single(.image(image, PixelDimensions(image.size), nil, .bottomRight))
|
|
||||||
|> delay(0.1, queue: Queue.mainQueue())
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
var duration: Double?
|
|
||||||
if let file = media as? TelegramMediaFile {
|
|
||||||
duration = file.duration
|
|
||||||
}
|
|
||||||
let symlinkPath = data.path + ".mp4"
|
|
||||||
if fileSize(symlinkPath) == nil {
|
|
||||||
let _ = try? FileManager.default.linkItem(atPath: data.path, toPath: symlinkPath)
|
|
||||||
}
|
|
||||||
return .single(nil)
|
|
||||||
|> then(
|
|
||||||
.single(.video(symlinkPath, nil, false, nil, nil, PixelDimensions(width: 720, height: 1280), duration ?? 0.0, [], .bottomRight))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let initialCaption: NSAttributedString?
|
|
||||||
let initialPrivacy: EngineStoryPrivacy?
|
|
||||||
let initialMediaAreas: [MediaArea]
|
|
||||||
if repost {
|
|
||||||
initialCaption = nil
|
|
||||||
initialPrivacy = nil
|
|
||||||
initialMediaAreas = []
|
|
||||||
} else {
|
|
||||||
initialCaption = chatInputStateStringWithAppliedEntities(item.text, entities: item.entities)
|
|
||||||
initialPrivacy = item.privacy
|
|
||||||
initialMediaAreas = item.mediaAreas
|
|
||||||
}
|
|
||||||
|
|
||||||
let externalState = MediaEditorTransitionOutExternalState(
|
|
||||||
storyTarget: nil,
|
|
||||||
isForcedTarget: false,
|
|
||||||
isPeerArchived: false,
|
|
||||||
transitionOut: nil
|
|
||||||
)
|
|
||||||
|
|
||||||
var updateProgressImpl: ((Float) -> Void)?
|
|
||||||
let controller = MediaEditorScreen(
|
|
||||||
context: context,
|
|
||||||
mode: .storyEditor,
|
|
||||||
subject: subject,
|
|
||||||
isEditing: !repost,
|
|
||||||
forwardSource: repost ? (component.slice.peer, item) : nil,
|
|
||||||
initialCaption: initialCaption,
|
|
||||||
initialPrivacy: initialPrivacy,
|
|
||||||
initialMediaAreas: initialMediaAreas,
|
|
||||||
initialVideoPosition: videoPlaybackPosition,
|
|
||||||
transitionIn: .noAnimation,
|
transitionIn: .noAnimation,
|
||||||
transitionOut: { finished, isNew in
|
transitionOut: nil,
|
||||||
if repost && finished {
|
completed: { [weak self] in
|
||||||
if let transitionOut = externalState.transitionOut?(externalState.storyTarget, externalState.isPeerArchived), let destinationView = transitionOut.destinationView {
|
|
||||||
return MediaEditorScreen.TransitionOut(
|
|
||||||
destinationView: destinationView,
|
|
||||||
destinationRect: transitionOut.destinationRect,
|
|
||||||
destinationCornerRadius: transitionOut.destinationCornerRadius
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
},
|
|
||||||
completion: { [weak self] result, commit in
|
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let entities = generateChatInputTextEntities(result.caption)
|
|
||||||
|
|
||||||
if repost {
|
|
||||||
let target: Stories.PendingTarget
|
|
||||||
let targetPeerId: EnginePeer.Id
|
|
||||||
if let sendAsPeerId = result.options.sendAsPeerId {
|
|
||||||
target = .peer(sendAsPeerId)
|
|
||||||
targetPeerId = sendAsPeerId
|
|
||||||
} else {
|
|
||||||
target = .myStories
|
|
||||||
targetPeerId = context.account.peerId
|
|
||||||
}
|
|
||||||
externalState.storyTarget = target
|
|
||||||
|
|
||||||
self.component?.controller()?.dismiss(animated: false)
|
self.component?.controller()?.dismiss(animated: false)
|
||||||
|
},
|
||||||
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: targetPeerId))
|
willDismiss: { [weak self] in
|
||||||
|> deliverOnMainQueue).startStandalone(next: { peer in
|
|
||||||
guard let peer else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if case let .user(user) = peer {
|
|
||||||
externalState.isPeerArchived = user.storiesHidden ?? false
|
|
||||||
|
|
||||||
} else if case let .channel(channel) = peer {
|
|
||||||
externalState.isPeerArchived = channel.storiesHidden ?? false
|
|
||||||
}
|
|
||||||
|
|
||||||
let forwardInfo = Stories.PendingForwardInfo(peerId: component.slice.peer.id, storyId: item.id, isModified: result.media != nil)
|
|
||||||
|
|
||||||
if let rootController = context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface {
|
|
||||||
var existingMedia: EngineMedia?
|
|
||||||
if let _ = result.media {
|
|
||||||
} else {
|
|
||||||
existingMedia = item.media
|
|
||||||
}
|
|
||||||
rootController.proceedWithStoryUpload(target: target, result: result as! MediaEditorScreenResult, existingMedia: existingMedia, forwardInfo: forwardInfo, externalState: externalState, commit: commit)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
var updatedText: String?
|
|
||||||
var updatedEntities: [MessageTextEntity]?
|
|
||||||
if result.caption.string != item.text || entities != item.entities {
|
|
||||||
updatedText = result.caption.string
|
|
||||||
updatedEntities = entities
|
|
||||||
}
|
|
||||||
|
|
||||||
if let mediaResult = result.media {
|
|
||||||
switch mediaResult {
|
|
||||||
case let .image(image, dimensions):
|
|
||||||
updateProgressImpl?(0.0)
|
|
||||||
|
|
||||||
let tempFile = TempBox.shared.tempFile(fileName: "file")
|
|
||||||
defer {
|
|
||||||
TempBox.shared.dispose(tempFile)
|
|
||||||
}
|
|
||||||
if let imageData = compressImageToJPEG(image, quality: 0.7, tempFilePath: tempFile.path) {
|
|
||||||
self.updateDisposable.set((context.engine.messages.editStory(peerId: peerId, id: id, media: .image(dimensions: dimensions, data: imageData, stickers: result.stickers), mediaAreas: result.mediaAreas, text: updatedText, entities: updatedEntities, privacy: nil)
|
|
||||||
|> deliverOnMainQueue).startStrict(next: { [weak self] result in
|
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
switch result {
|
|
||||||
case let .progress(progress):
|
|
||||||
updateProgressImpl?(progress)
|
|
||||||
case .completed:
|
|
||||||
Queue.mainQueue().after(0.1) {
|
|
||||||
self.isEditingStory = false
|
self.isEditingStory = false
|
||||||
self.rewindCurrentItem()
|
self.rewindCurrentItem()
|
||||||
self.updateIsProgressPaused()
|
self.updateIsProgressPaused()
|
||||||
self.state?.updated(transition: .easeInOut(duration: 0.2))
|
self.state?.updated(transition: .easeInOut(duration: 0.2))
|
||||||
|
},
|
||||||
HapticFeedback().success()
|
update: { [weak self] disposable in
|
||||||
|
|
||||||
commit({})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
case let .video(content, firstFrameImage, values, duration, dimensions):
|
|
||||||
updateProgressImpl?(0.0)
|
|
||||||
|
|
||||||
if let valuesData = try? JSONEncoder().encode(values) {
|
|
||||||
let data = MemoryBuffer(data: valuesData)
|
|
||||||
let digest = MemoryBuffer(data: data.md5Digest())
|
|
||||||
let adjustments = VideoMediaResourceAdjustments(data: data, digest: digest, isStory: true)
|
|
||||||
|
|
||||||
let resource: TelegramMediaResource
|
|
||||||
switch content {
|
|
||||||
case let .imageFile(path):
|
|
||||||
resource = LocalFileVideoMediaResource(randomId: Int64.random(in: .min ... .max), path: path, adjustments: adjustments)
|
|
||||||
case let .videoFile(path):
|
|
||||||
resource = LocalFileVideoMediaResource(randomId: Int64.random(in: .min ... .max), path: path, adjustments: adjustments)
|
|
||||||
case let .asset(localIdentifier):
|
|
||||||
resource = VideoLibraryMediaResource(localIdentifier: localIdentifier, conversion: .compress(adjustments))
|
|
||||||
}
|
|
||||||
|
|
||||||
let tempFile = TempBox.shared.tempFile(fileName: "file")
|
|
||||||
defer {
|
|
||||||
TempBox.shared.dispose(tempFile)
|
|
||||||
}
|
|
||||||
let firstFrameImageData = firstFrameImage.flatMap { compressImageToJPEG($0, quality: 0.6, tempFilePath: tempFile.path) }
|
|
||||||
let firstFrameFile = firstFrameImageData.flatMap { data -> TempBoxFile? in
|
|
||||||
let file = TempBox.shared.tempFile(fileName: "image.jpg")
|
|
||||||
if let _ = try? data.write(to: URL(fileURLWithPath: file.path)) {
|
|
||||||
return file
|
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.updateDisposable.set((context.engine.messages.editStory(peerId: peerId, id: id, media: .video(dimensions: dimensions, duration: duration, resource: resource, firstFrameFile: firstFrameFile, stickers: result.stickers), mediaAreas: result.mediaAreas, text: updatedText, entities: updatedEntities, privacy: nil)
|
|
||||||
|> deliverOnMainQueue).startStrict(next: { [weak self] result in
|
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
switch result {
|
self.updateDisposable.set(disposable)
|
||||||
case let .progress(progress):
|
|
||||||
updateProgressImpl?(progress)
|
|
||||||
case .completed:
|
|
||||||
Queue.mainQueue().after(0.1) {
|
|
||||||
self.isEditingStory = false
|
|
||||||
self.rewindCurrentItem()
|
|
||||||
self.updateIsProgressPaused()
|
|
||||||
self.state?.updated(transition: .easeInOut(duration: 0.2))
|
|
||||||
|
|
||||||
HapticFeedback().success()
|
|
||||||
|
|
||||||
commit({})
|
|
||||||
}
|
}
|
||||||
|
) else {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}))
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
} else if updatedText != nil {
|
|
||||||
let _ = (context.engine.messages.editStory(peerId: peerId, id: id, media: nil, mediaAreas: nil, text: updatedText, entities: updatedEntities, privacy: nil)
|
|
||||||
|> deliverOnMainQueue).startStandalone(next: { [weak self] result in
|
|
||||||
switch result {
|
|
||||||
case .completed:
|
|
||||||
Queue.mainQueue().after(0.1) {
|
|
||||||
if let self {
|
|
||||||
self.isEditingStory = false
|
|
||||||
self.rewindCurrentItem()
|
|
||||||
self.updateIsProgressPaused()
|
|
||||||
self.state?.updated(transition: .easeInOut(duration: 0.2))
|
|
||||||
|
|
||||||
HapticFeedback().success()
|
|
||||||
}
|
|
||||||
commit({})
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
self.isEditingStory = false
|
|
||||||
self.rewindCurrentItem()
|
|
||||||
self.updateIsProgressPaused()
|
|
||||||
self.state?.updated(transition: .easeInOut(duration: 0.2))
|
|
||||||
|
|
||||||
HapticFeedback().success()
|
|
||||||
|
|
||||||
commit({})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
controller.willDismiss = { [weak self] in
|
|
||||||
self?.isEditingStory = false
|
|
||||||
self?.rewindCurrentItem()
|
|
||||||
self?.updateIsProgressPaused()
|
|
||||||
self?.state?.updated(transition: .easeInOut(duration: 0.2))
|
|
||||||
}
|
|
||||||
controller.navigationPresentation = .flatModal
|
|
||||||
self.component?.controller()?.push(controller)
|
self.component?.controller()?.push(controller)
|
||||||
updateProgressImpl = { [weak controller, weak self] progress in
|
|
||||||
controller?.updateEditProgress(progress, cancel: { [weak self] in
|
|
||||||
self?.updateDisposable.set(nil)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func presentSaveUpgradeScreen() {
|
private func presentSaveUpgradeScreen() {
|
||||||
@ -7059,7 +6817,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
guard let self, let component = self.component else {
|
guard let self, let component = self.component else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.sendMessageContext.performTranslateTextAction(view: self, text: component.slice.item.storyItem.text)
|
self.sendMessageContext.performTranslateTextAction(view: self, text: component.slice.item.storyItem.text, entities: component.slice.item.storyItem.entities)
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1159,7 +1159,7 @@ final class StoryItemSetContainerSendMessage {
|
|||||||
controller.present(shareController, in: .window(.root))
|
controller.present(shareController, in: .window(.root))
|
||||||
}
|
}
|
||||||
|
|
||||||
func performTranslateTextAction(view: StoryItemSetContainerComponent.View, text: String) {
|
func performTranslateTextAction(view: StoryItemSetContainerComponent.View, text: String, entities: [MessageTextEntity]) {
|
||||||
guard let component = view.component else {
|
guard let component = view.component else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -1190,7 +1190,7 @@ final class StoryItemSetContainerSendMessage {
|
|||||||
|
|
||||||
let _ = ApplicationSpecificNotice.incrementTranslationSuggestion(accountManager: component.context.sharedContext.accountManager, timestamp: Int32(Date().timeIntervalSince1970)).start()
|
let _ = ApplicationSpecificNotice.incrementTranslationSuggestion(accountManager: component.context.sharedContext.accountManager, timestamp: Int32(Date().timeIntervalSince1970)).start()
|
||||||
|
|
||||||
let translateController = TranslateScreen(context: component.context, forceTheme: defaultDarkPresentationTheme, text: text, canCopy: true, fromLanguage: language, ignoredLanguages: translationSettings.ignoredLanguages)
|
let translateController = TranslateScreen(context: component.context, forceTheme: defaultDarkPresentationTheme, text: text, entities: entities, canCopy: true, fromLanguage: language, ignoredLanguages: translationSettings.ignoredLanguages)
|
||||||
translateController.pushController = { [weak view] c in
|
translateController.pushController = { [weak view] c in
|
||||||
guard let view, let component = view.component else {
|
guard let view, let component = view.component else {
|
||||||
return
|
return
|
||||||
|
231
submodules/TelegramUI/Sources/Chat/ChatControllerPaste.swift
Normal file
231
submodules/TelegramUI/Sources/Chat/ChatControllerPaste.swift
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Display
|
||||||
|
import SwiftSignalKit
|
||||||
|
import Postbox
|
||||||
|
import TelegramCore
|
||||||
|
import TelegramUIPreferences
|
||||||
|
import AccountContext
|
||||||
|
import MediaPickerUI
|
||||||
|
import MediaPasteboardUI
|
||||||
|
import LegacyMediaPickerUI
|
||||||
|
import MediaEditor
|
||||||
|
|
||||||
|
extension ChatControllerImpl {
|
||||||
|
func displayPasteMenu(_ subjects: [MediaPickerScreen.Subject.Media]) {
|
||||||
|
let _ = (self.context.sharedContext.accountManager.transaction { transaction -> GeneratedMediaStoreSettings in
|
||||||
|
let entry = transaction.getSharedData(ApplicationSpecificSharedDataKeys.generatedMediaStoreSettings)?.get(GeneratedMediaStoreSettings.self)
|
||||||
|
return entry ?? GeneratedMediaStoreSettings.defaultSettings
|
||||||
|
}
|
||||||
|
|> deliverOnMainQueue).startStandalone(next: { [weak self] settings in
|
||||||
|
if let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer {
|
||||||
|
strongSelf.chatDisplayNode.dismissInput()
|
||||||
|
let controller = mediaPasteboardScreen(
|
||||||
|
context: strongSelf.context,
|
||||||
|
updatedPresentationData: strongSelf.updatedPresentationData,
|
||||||
|
peer: EnginePeer(peer),
|
||||||
|
subjects: subjects,
|
||||||
|
presentMediaPicker: { [weak self] subject, saveEditedPhotos, bannedSendPhotos, bannedSendVideos, present in
|
||||||
|
if let strongSelf = self {
|
||||||
|
strongSelf.presentMediaPicker(subject: subject, saveEditedPhotos: saveEditedPhotos, bannedSendPhotos: bannedSendPhotos, bannedSendVideos: bannedSendVideos, present: present, updateMediaPickerContext: { _ in }, completion: { [weak self] signals, silentPosting, scheduleTime, getAnimatedTransitionSource, completion in
|
||||||
|
self?.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime, getAnimatedTransitionSource: getAnimatedTransitionSource, completion: completion)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getSourceRect: nil
|
||||||
|
)
|
||||||
|
controller.navigationPresentation = .flatModal
|
||||||
|
strongSelf.push(controller)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func enqueueGifData(_ data: Data) {
|
||||||
|
self.enqueueMediaMessageDisposable.set((legacyEnqueueGifMessage(account: self.context.account, data: data) |> deliverOnMainQueue).startStrict(next: { [weak self] message in
|
||||||
|
if let strongSelf = self {
|
||||||
|
let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject
|
||||||
|
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
|
||||||
|
if let strongSelf = self {
|
||||||
|
strongSelf.chatDisplayNode.collapseInput()
|
||||||
|
|
||||||
|
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
|
||||||
|
$0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, nil)
|
||||||
|
strongSelf.sendMessages([message].map { $0.withUpdatedReplyToMessageId(replyMessageSubject?.subjectModel) })
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func enqueueVideoData(_ data: Data) {
|
||||||
|
self.enqueueMediaMessageDisposable.set((legacyEnqueueGifMessage(account: self.context.account, data: data) |> deliverOnMainQueue).startStrict(next: { [weak self] message in
|
||||||
|
if let strongSelf = self {
|
||||||
|
let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject
|
||||||
|
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
|
||||||
|
if let strongSelf = self {
|
||||||
|
strongSelf.chatDisplayNode.collapseInput()
|
||||||
|
|
||||||
|
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
|
||||||
|
$0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, nil)
|
||||||
|
strongSelf.sendMessages([message].map { $0.withUpdatedReplyToMessageId(replyMessageSubject?.subjectModel) })
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func enqueueStickerImage(_ image: UIImage, isMemoji: Bool) {
|
||||||
|
let size = image.size.aspectFitted(CGSize(width: 512.0, height: 512.0))
|
||||||
|
self.enqueueMediaMessageDisposable.set((convertToWebP(image: image, targetSize: size, targetBoundingSize: size, quality: 0.9) |> deliverOnMainQueue).startStrict(next: { [weak self] data in
|
||||||
|
if let strongSelf = self, !data.isEmpty {
|
||||||
|
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
|
||||||
|
strongSelf.context.account.postbox.mediaBox.storeResourceData(resource.id, data: data)
|
||||||
|
|
||||||
|
var fileAttributes: [TelegramMediaFileAttribute] = []
|
||||||
|
fileAttributes.append(.FileName(fileName: "sticker.webp"))
|
||||||
|
fileAttributes.append(.Sticker(displayText: "", packReference: nil, maskData: nil))
|
||||||
|
fileAttributes.append(.ImageSize(size: PixelDimensions(size)))
|
||||||
|
|
||||||
|
let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "image/webp", size: Int64(data.count), attributes: fileAttributes)
|
||||||
|
let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: strongSelf.chatLocation.threadId, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])
|
||||||
|
|
||||||
|
let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject
|
||||||
|
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
|
||||||
|
if let strongSelf = self {
|
||||||
|
strongSelf.chatDisplayNode.collapseInput()
|
||||||
|
|
||||||
|
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
|
||||||
|
$0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, nil)
|
||||||
|
strongSelf.sendMessages([message].map { $0.withUpdatedReplyToMessageId(replyMessageSubject?.subjectModel) })
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func enqueueStickerFile(_ file: TelegramMediaFile) {
|
||||||
|
let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: self.chatLocation.threadId, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])
|
||||||
|
|
||||||
|
let replyMessageSubject = self.presentationInterfaceState.interfaceState.replyMessageSubject
|
||||||
|
self.chatDisplayNode.setupSendActionOnViewUpdate({ [weak self] in
|
||||||
|
if let strongSelf = self {
|
||||||
|
strongSelf.chatDisplayNode.collapseInput()
|
||||||
|
|
||||||
|
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
|
||||||
|
$0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, nil)
|
||||||
|
self.sendMessages([message].map { $0.withUpdatedReplyToMessageId(replyMessageSubject?.subjectModel) })
|
||||||
|
|
||||||
|
Queue.mainQueue().after(3.0) {
|
||||||
|
if let message = self.chatDisplayNode.historyNode.lastVisbleMesssage(), let file = message.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile, file.isSticker {
|
||||||
|
self.context.engine.stickers.addRecentlyUsedSticker(fileReference: .message(message: MessageReference(message), media: file))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func enqueueAnimatedStickerData(_ data: Data) {
|
||||||
|
guard let animatedImage = UIImage.animatedImageFromData(data: data), let thumbnailImage = animatedImage.images.first else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let dimensions = PixelDimensions(width: 1080, height: 1920)
|
||||||
|
let image = generateImage(dimensions.cgSize, opaque: false, scale: 1.0, rotatedContext: { size, context in
|
||||||
|
context.clear(CGRect(origin: .zero, size: size))
|
||||||
|
})!
|
||||||
|
|
||||||
|
let blackImage = generateImage(dimensions.cgSize, opaque: true, scale: 1.0, rotatedContext: { size, context in
|
||||||
|
context.setFillColor(UIColor.black.cgColor)
|
||||||
|
context.fill(CGRect(origin: .zero, size: size))
|
||||||
|
})!
|
||||||
|
|
||||||
|
let stickerEntity = DrawingStickerEntity(content: .animatedImage(data, thumbnailImage))
|
||||||
|
stickerEntity.referenceDrawingSize = dimensions.cgSize
|
||||||
|
stickerEntity.position = CGPoint(x: dimensions.cgSize.width / 2.0, y: dimensions.cgSize.height / 2.0)
|
||||||
|
stickerEntity.scale = 3.5
|
||||||
|
|
||||||
|
let entities: [CodableDrawingEntity] = [
|
||||||
|
.sticker(stickerEntity)
|
||||||
|
]
|
||||||
|
|
||||||
|
let values = MediaEditorValues(
|
||||||
|
peerId: self.context.account.peerId,
|
||||||
|
originalDimensions: dimensions,
|
||||||
|
cropOffset: .zero,
|
||||||
|
cropRect: nil,
|
||||||
|
cropScale: 1.0,
|
||||||
|
cropRotation: 1.0,
|
||||||
|
cropMirroring: false,
|
||||||
|
cropOrientation: .up,
|
||||||
|
gradientColors: [.clear, .clear],
|
||||||
|
videoTrimRange: nil,
|
||||||
|
videoIsMuted: false,
|
||||||
|
videoIsFullHd: false,
|
||||||
|
videoIsMirrored: false,
|
||||||
|
videoVolume: nil,
|
||||||
|
additionalVideoPath: nil,
|
||||||
|
additionalVideoIsDual: false,
|
||||||
|
additionalVideoPosition: nil,
|
||||||
|
additionalVideoScale: nil,
|
||||||
|
additionalVideoRotation: nil,
|
||||||
|
additionalVideoPositionChanges: [],
|
||||||
|
additionalVideoTrimRange: nil,
|
||||||
|
additionalVideoOffset: nil,
|
||||||
|
additionalVideoVolume: nil,
|
||||||
|
nightTheme: false,
|
||||||
|
drawing: nil,
|
||||||
|
maskDrawing: blackImage,
|
||||||
|
entities: entities,
|
||||||
|
toolValues: [:],
|
||||||
|
audioTrack: nil,
|
||||||
|
audioTrackTrimRange: nil,
|
||||||
|
audioTrackOffset: nil,
|
||||||
|
audioTrackVolume: nil,
|
||||||
|
audioTrackSamples: nil,
|
||||||
|
qualityPreset: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
let configuration = recommendedVideoExportConfiguration(values: values, duration: animatedImage.duration, frameRate: 30.0, isSticker: true)
|
||||||
|
|
||||||
|
let path = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).webm"
|
||||||
|
let videoExport = MediaEditorVideoExport(
|
||||||
|
postbox: self.context.account.postbox,
|
||||||
|
subject: .image(image: image),
|
||||||
|
configuration: configuration,
|
||||||
|
outputPath: path
|
||||||
|
)
|
||||||
|
videoExport.start()
|
||||||
|
|
||||||
|
let _ = (videoExport.status
|
||||||
|
|> deliverOnMainQueue).startStandalone(next: { [weak self] status in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch status {
|
||||||
|
case .completed:
|
||||||
|
var fileAttributes: [TelegramMediaFileAttribute] = []
|
||||||
|
fileAttributes.append(.FileName(fileName: "sticker.webm"))
|
||||||
|
fileAttributes.append(.Sticker(displayText: "", packReference: nil, maskData: nil))
|
||||||
|
fileAttributes.append(.Video(duration: animatedImage.duration, size: PixelDimensions(width: 512, height: 512), flags: [], preloadSize: nil))
|
||||||
|
|
||||||
|
let previewRepresentations: [TelegramMediaImageRepresentation] = []
|
||||||
|
// if let thumbnailResource {
|
||||||
|
// previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: dimensions, resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil))
|
||||||
|
// }
|
||||||
|
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
|
||||||
|
self.context.account.postbox.mediaBox.copyResourceData(resource.id, fromTempPath: path)
|
||||||
|
|
||||||
|
let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/webm", size: 0, attributes: fileAttributes)
|
||||||
|
self.enqueueStickerFile(file)
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// self.stickerVideoExport = videoExport
|
||||||
|
}
|
||||||
|
}
|
@ -4095,12 +4095,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
strongSelf.present(controller, in: .window(.root))
|
strongSelf.present(controller, in: .window(.root))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, activateAdAction: { [weak self] messageId in
|
}, activateAdAction: { [weak self] messageId, progress in
|
||||||
guard let self, let message = self.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId), let adAttribute = message.adAttribute else {
|
guard let self, let message = self.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId), let adAttribute = message.adAttribute else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.chatDisplayNode.historyNode.adMessagesContext?.markAction(opaqueId: adAttribute.opaqueId)
|
self.chatDisplayNode.historyNode.adMessagesContext?.markAction(opaqueId: adAttribute.opaqueId)
|
||||||
self.controllerInteraction?.openUrl(ChatControllerInteraction.OpenUrl(url: adAttribute.url, concealed: false, external: true))
|
self.controllerInteraction?.openUrl(ChatControllerInteraction.OpenUrl(url: adAttribute.url, concealed: false, external: true, progress: progress))
|
||||||
}, openRequestedPeerSelection: { [weak self] messageId, peerType, buttonId, maxQuantity in
|
}, openRequestedPeerSelection: { [weak self] messageId, peerType, buttonId, maxQuantity in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
@ -9182,122 +9182,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
func displayPasteMenu(_ subjects: [MediaPickerScreen.Subject.Media]) {
|
|
||||||
let _ = (self.context.sharedContext.accountManager.transaction { transaction -> GeneratedMediaStoreSettings in
|
|
||||||
let entry = transaction.getSharedData(ApplicationSpecificSharedDataKeys.generatedMediaStoreSettings)?.get(GeneratedMediaStoreSettings.self)
|
|
||||||
return entry ?? GeneratedMediaStoreSettings.defaultSettings
|
|
||||||
}
|
|
||||||
|> deliverOnMainQueue).startStandalone(next: { [weak self] settings in
|
|
||||||
if let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer {
|
|
||||||
strongSelf.chatDisplayNode.dismissInput()
|
|
||||||
let controller = mediaPasteboardScreen(
|
|
||||||
context: strongSelf.context,
|
|
||||||
updatedPresentationData: strongSelf.updatedPresentationData,
|
|
||||||
peer: EnginePeer(peer),
|
|
||||||
subjects: subjects,
|
|
||||||
presentMediaPicker: { [weak self] subject, saveEditedPhotos, bannedSendPhotos, bannedSendVideos, present in
|
|
||||||
if let strongSelf = self {
|
|
||||||
strongSelf.presentMediaPicker(subject: subject, saveEditedPhotos: saveEditedPhotos, bannedSendPhotos: bannedSendPhotos, bannedSendVideos: bannedSendVideos, present: present, updateMediaPickerContext: { _ in }, completion: { [weak self] signals, silentPosting, scheduleTime, getAnimatedTransitionSource, completion in
|
|
||||||
self?.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime, getAnimatedTransitionSource: getAnimatedTransitionSource, completion: completion)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getSourceRect: nil
|
|
||||||
)
|
|
||||||
controller.navigationPresentation = .flatModal
|
|
||||||
strongSelf.push(controller)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func enqueueGifData(_ data: Data) {
|
|
||||||
self.enqueueMediaMessageDisposable.set((legacyEnqueueGifMessage(account: self.context.account, data: data) |> deliverOnMainQueue).startStrict(next: { [weak self] message in
|
|
||||||
if let strongSelf = self {
|
|
||||||
let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject
|
|
||||||
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
|
|
||||||
if let strongSelf = self {
|
|
||||||
strongSelf.chatDisplayNode.collapseInput()
|
|
||||||
|
|
||||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
|
|
||||||
$0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, nil)
|
|
||||||
strongSelf.sendMessages([message].map { $0.withUpdatedReplyToMessageId(replyMessageSubject?.subjectModel) })
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
func enqueueVideoData(_ data: Data) {
|
|
||||||
self.enqueueMediaMessageDisposable.set((legacyEnqueueGifMessage(account: self.context.account, data: data) |> deliverOnMainQueue).startStrict(next: { [weak self] message in
|
|
||||||
if let strongSelf = self {
|
|
||||||
let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject
|
|
||||||
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
|
|
||||||
if let strongSelf = self {
|
|
||||||
strongSelf.chatDisplayNode.collapseInput()
|
|
||||||
|
|
||||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
|
|
||||||
$0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, nil)
|
|
||||||
strongSelf.sendMessages([message].map { $0.withUpdatedReplyToMessageId(replyMessageSubject?.subjectModel) })
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
func enqueueStickerImage(_ image: UIImage, isMemoji: Bool) {
|
|
||||||
let size = image.size.aspectFitted(CGSize(width: 512.0, height: 512.0))
|
|
||||||
self.enqueueMediaMessageDisposable.set((convertToWebP(image: image, targetSize: size, targetBoundingSize: size, quality: 0.9) |> deliverOnMainQueue).startStrict(next: { [weak self] data in
|
|
||||||
if let strongSelf = self, !data.isEmpty {
|
|
||||||
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
|
|
||||||
strongSelf.context.account.postbox.mediaBox.storeResourceData(resource.id, data: data)
|
|
||||||
|
|
||||||
var fileAttributes: [TelegramMediaFileAttribute] = []
|
|
||||||
fileAttributes.append(.FileName(fileName: "sticker.webp"))
|
|
||||||
fileAttributes.append(.Sticker(displayText: "", packReference: nil, maskData: nil))
|
|
||||||
fileAttributes.append(.ImageSize(size: PixelDimensions(size)))
|
|
||||||
|
|
||||||
let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "image/webp", size: Int64(data.count), attributes: fileAttributes)
|
|
||||||
let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: strongSelf.chatLocation.threadId, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])
|
|
||||||
|
|
||||||
let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject
|
|
||||||
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
|
|
||||||
if let strongSelf = self {
|
|
||||||
strongSelf.chatDisplayNode.collapseInput()
|
|
||||||
|
|
||||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
|
|
||||||
$0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, nil)
|
|
||||||
strongSelf.sendMessages([message].map { $0.withUpdatedReplyToMessageId(replyMessageSubject?.subjectModel) })
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
func enqueueStickerFile(_ file: TelegramMediaFile) {
|
|
||||||
let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: self.chatLocation.threadId, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])
|
|
||||||
|
|
||||||
let replyMessageSubject = self.presentationInterfaceState.interfaceState.replyMessageSubject
|
|
||||||
self.chatDisplayNode.setupSendActionOnViewUpdate({ [weak self] in
|
|
||||||
if let strongSelf = self {
|
|
||||||
strongSelf.chatDisplayNode.collapseInput()
|
|
||||||
|
|
||||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
|
|
||||||
$0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, nil)
|
|
||||||
self.sendMessages([message].map { $0.withUpdatedReplyToMessageId(replyMessageSubject?.subjectModel) })
|
|
||||||
|
|
||||||
Queue.mainQueue().after(3.0) {
|
|
||||||
if let message = self.chatDisplayNode.historyNode.lastVisbleMesssage(), let file = message.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile, file.isSticker {
|
|
||||||
self.context.engine.stickers.addRecentlyUsedSticker(fileReference: .message(message: MessageReference(message), media: file))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func enqueueChatContextResult(_ results: ChatContextResultCollection, _ result: ChatContextResult, hideVia: Bool = false, closeMediaInput: Bool = false, silentPosting: Bool = false, resetTextInputState: Bool = true) {
|
func enqueueChatContextResult(_ results: ChatContextResultCollection, _ result: ChatContextResult, hideVia: Bool = false, closeMediaInput: Bool = false, silentPosting: Bool = false, resetTextInputState: Bool = true) {
|
||||||
if !canSendMessagesToChat(self.presentationInterfaceState) {
|
if !canSendMessagesToChat(self.presentationInterfaceState) {
|
||||||
return
|
return
|
||||||
@ -9815,7 +9699,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
let disposable = openUserGeneratedUrl(context: self.context, peerId: self.peerView?.peerId, url: url, concealed: concealed, skipUrlAuth: skipUrlAuth, skipConcealedAlert: skipConcealedAlert, present: { [weak self] c in
|
let disposable = openUserGeneratedUrl(context: self.context, peerId: self.peerView?.peerId, url: url, concealed: concealed, skipUrlAuth: skipUrlAuth, skipConcealedAlert: skipConcealedAlert, present: { [weak self] c in
|
||||||
self?.present(c, in: .window(.root))
|
self?.present(c, in: .window(.root))
|
||||||
}, openResolved: { [weak self] resolved in
|
}, openResolved: { [weak self] resolved in
|
||||||
self?.openResolved(result: resolved, sourceMessageId: message?.id, forceExternal: forceExternal, concealed: concealed, commit: commit)
|
self?.openResolved(result: resolved, sourceMessageId: message?.id, progress: progress, forceExternal: forceExternal, concealed: concealed, commit: commit)
|
||||||
}, progress: progress)
|
}, progress: progress)
|
||||||
self.navigationActionDisposable.set(disposable)
|
self.navigationActionDisposable.set(disposable)
|
||||||
}, performAction: true)
|
}, performAction: true)
|
||||||
|
@ -251,9 +251,46 @@ func openResolvedUrlImpl(
|
|||||||
navigationController?.pushViewController(InstantPageController(context: context, webPage: webpage, sourceLocation: InstantPageSourceLocation(userLocation: .other, peerType: .channel), anchor: anchor))
|
navigationController?.pushViewController(InstantPageController(context: context, webPage: webpage, sourceLocation: InstantPageSourceLocation(userLocation: .other, peerType: .channel), anchor: anchor))
|
||||||
case let .join(link):
|
case let .join(link):
|
||||||
dismissInput()
|
dismissInput()
|
||||||
|
|
||||||
|
if let progress {
|
||||||
|
let progressSignal = Signal<Never, NoError> { subscriber in
|
||||||
|
progress.set(.single(true))
|
||||||
|
return ActionDisposable {
|
||||||
|
Queue.mainQueue().async() {
|
||||||
|
progress.set(.single(false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|> runOn(Queue.mainQueue())
|
||||||
|
|> delay(0.1, queue: Queue.mainQueue())
|
||||||
|
let progressDisposable = progressSignal.startStrict()
|
||||||
|
|
||||||
|
var signal = context.engine.peers.joinLinkInformation(link)
|
||||||
|
signal = signal
|
||||||
|
|> afterDisposed {
|
||||||
|
Queue.mainQueue().async {
|
||||||
|
progressDisposable.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = (signal
|
||||||
|
|> deliverOnMainQueue).startStandalone(next: { [weak navigationController] resolvedState in
|
||||||
|
switch resolvedState {
|
||||||
|
case let .alreadyJoined(peer):
|
||||||
|
openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: nil))
|
||||||
|
case let .peek(peer, deadline):
|
||||||
|
openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: ChatPeekTimeout(deadline: deadline, linkData: link)))
|
||||||
|
default:
|
||||||
|
present(JoinLinkPreviewController(context: context, link: link, navigateToPeer: { peer, peekData in
|
||||||
|
openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: peekData))
|
||||||
|
}, parentNavigationController: navigationController, resolvedState: resolvedState), nil)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
present(JoinLinkPreviewController(context: context, link: link, navigateToPeer: { peer, peekData in
|
present(JoinLinkPreviewController(context: context, link: link, navigateToPeer: { peer, peekData in
|
||||||
openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: peekData))
|
openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: peekData))
|
||||||
}, parentNavigationController: navigationController), nil)
|
}, parentNavigationController: navigationController), nil)
|
||||||
|
}
|
||||||
case let .localization(identifier):
|
case let .localization(identifier):
|
||||||
dismissInput()
|
dismissInput()
|
||||||
present(LanguageLinkPreviewController(context: context, identifier: identifier), nil)
|
present(LanguageLinkPreviewController(context: context, identifier: identifier), nil)
|
||||||
|
@ -165,7 +165,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
|
|||||||
}, openLargeEmojiInfo: { _, _, _ in
|
}, openLargeEmojiInfo: { _, _, _ in
|
||||||
}, openJoinLink: { _ in
|
}, openJoinLink: { _ in
|
||||||
}, openWebView: { _, _, _, _ in
|
}, openWebView: { _, _, _, _ in
|
||||||
}, activateAdAction: { _ in
|
}, activateAdAction: { _, _ in
|
||||||
}, openRequestedPeerSelection: { _, _, _, _ in
|
}, openRequestedPeerSelection: { _, _, _, _ in
|
||||||
}, saveMediaToFiles: { _ in
|
}, saveMediaToFiles: { _ in
|
||||||
}, openNoAdsDemo: {
|
}, openNoAdsDemo: {
|
||||||
|
@ -1759,7 +1759,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
|||||||
}, openLargeEmojiInfo: { _, _, _ in
|
}, openLargeEmojiInfo: { _, _, _ in
|
||||||
}, openJoinLink: { _ in
|
}, openJoinLink: { _ in
|
||||||
}, openWebView: { _, _, _, _ in
|
}, openWebView: { _, _, _, _ in
|
||||||
}, activateAdAction: { _ in
|
}, activateAdAction: { _, _ in
|
||||||
}, openRequestedPeerSelection: { _, _, _, _ in
|
}, openRequestedPeerSelection: { _, _, _, _ in
|
||||||
}, saveMediaToFiles: { _ in
|
}, saveMediaToFiles: { _ in
|
||||||
}, openNoAdsDemo: {
|
}, openNoAdsDemo: {
|
||||||
|
@ -27,6 +27,7 @@ swift_library(
|
|||||||
"//submodules/ComponentFlow:ComponentFlow",
|
"//submodules/ComponentFlow:ComponentFlow",
|
||||||
"//submodules/Components/ViewControllerComponent:ViewControllerComponent",
|
"//submodules/Components/ViewControllerComponent:ViewControllerComponent",
|
||||||
"//submodules/Components/MultilineTextComponent:MultilineTextComponent",
|
"//submodules/Components/MultilineTextComponent:MultilineTextComponent",
|
||||||
|
"//submodules/Components/MultilineTextWithEntitiesComponent:MultilineTextWithEntitiesComponent",
|
||||||
"//submodules/Components/BundleIconComponent:BundleIconComponent",
|
"//submodules/Components/BundleIconComponent:BundleIconComponent",
|
||||||
"//submodules/UndoUI:UndoUI",
|
"//submodules/UndoUI:UndoUI",
|
||||||
"//submodules/ActivityIndicator:ActivityIndicator",
|
"//submodules/ActivityIndicator:ActivityIndicator",
|
||||||
|
@ -109,8 +109,8 @@ public func updateChatTranslationStateInteractively(engine: TelegramEngine, peer
|
|||||||
@available(iOS 12.0, *)
|
@available(iOS 12.0, *)
|
||||||
private let languageRecognizer = NLLanguageRecognizer()
|
private let languageRecognizer = NLLanguageRecognizer()
|
||||||
|
|
||||||
public func translateMessageIds(context: AccountContext, messageIds: [EngineMessage.Id], toLang: String) -> Signal<Void, NoError> {
|
public func translateMessageIds(context: AccountContext, messageIds: [EngineMessage.Id], toLang: String) -> Signal<Never, NoError> {
|
||||||
return context.account.postbox.transaction { transaction -> Signal<Void, NoError> in
|
return context.account.postbox.transaction { transaction -> Signal<Never, NoError> in
|
||||||
var messageIdsToTranslate: [EngineMessage.Id] = []
|
var messageIdsToTranslate: [EngineMessage.Id] = []
|
||||||
var messageIdsSet = Set<EngineMessage.Id>()
|
var messageIdsSet = Set<EngineMessage.Id>()
|
||||||
for messageId in messageIds {
|
for messageId in messageIds {
|
||||||
@ -152,7 +152,7 @@ public func translateMessageIds(context: AccountContext, messageIds: [EngineMess
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return context.engine.messages.translateMessages(messageIds: messageIdsToTranslate, toLang: toLang)
|
return context.engine.messages.translateMessages(messageIds: messageIdsToTranslate, toLang: toLang)
|
||||||
|> `catch` { _ -> Signal<Void, NoError> in
|
|> `catch` { _ -> Signal<Never, NoError> in
|
||||||
return .complete()
|
return .complete()
|
||||||
}
|
}
|
||||||
} |> switchToLatest
|
} |> switchToLatest
|
||||||
|
@ -11,6 +11,7 @@ import Speak
|
|||||||
import ComponentFlow
|
import ComponentFlow
|
||||||
import ViewControllerComponent
|
import ViewControllerComponent
|
||||||
import MultilineTextComponent
|
import MultilineTextComponent
|
||||||
|
import MultilineTextWithEntitiesComponent
|
||||||
import BundleIconComponent
|
import BundleIconComponent
|
||||||
import UndoUI
|
import UndoUI
|
||||||
|
|
||||||
@ -35,15 +36,17 @@ private final class TranslateScreenComponent: CombinedComponent {
|
|||||||
|
|
||||||
let context: AccountContext
|
let context: AccountContext
|
||||||
let text: String
|
let text: String
|
||||||
|
let entities: [MessageTextEntity]
|
||||||
let fromLanguage: String?
|
let fromLanguage: String?
|
||||||
let toLanguage: String
|
let toLanguage: String
|
||||||
let copyTranslation: ((String) -> Void)?
|
let copyTranslation: ((String) -> Void)?
|
||||||
let changeLanguage: (String, String, @escaping (String, String) -> Void) -> Void
|
let changeLanguage: (String, String, @escaping (String, String) -> Void) -> Void
|
||||||
let expand: () -> Void
|
let expand: () -> Void
|
||||||
|
|
||||||
init(context: AccountContext, text: String, fromLanguage: String?, toLanguage: String, copyTranslation: ((String) -> Void)?, changeLanguage: @escaping (String, String, @escaping (String, String) -> Void) -> Void, expand: @escaping () -> Void) {
|
init(context: AccountContext, text: String, entities: [MessageTextEntity], fromLanguage: String?, toLanguage: String, copyTranslation: ((String) -> Void)?, changeLanguage: @escaping (String, String, @escaping (String, String) -> Void) -> Void, expand: @escaping () -> Void) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.text = text
|
self.text = text
|
||||||
|
self.entities = entities
|
||||||
self.fromLanguage = fromLanguage
|
self.fromLanguage = fromLanguage
|
||||||
self.toLanguage = toLanguage
|
self.toLanguage = toLanguage
|
||||||
self.copyTranslation = copyTranslation
|
self.copyTranslation = copyTranslation
|
||||||
@ -58,6 +61,9 @@ private final class TranslateScreenComponent: CombinedComponent {
|
|||||||
if lhs.text != rhs.text {
|
if lhs.text != rhs.text {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.entities != rhs.entities {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if lhs.fromLanguage != rhs.fromLanguage {
|
if lhs.fromLanguage != rhs.fromLanguage {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -995,7 +1001,7 @@ public class TranslateScreen: ViewController {
|
|||||||
|
|
||||||
public var wasDismissed: (() -> Void)?
|
public var wasDismissed: (() -> Void)?
|
||||||
|
|
||||||
public convenience init(context: AccountContext, forceTheme: PresentationTheme? = nil, text: String, canCopy: Bool, fromLanguage: String?, toLanguage: String? = nil, isExpanded: Bool = false, ignoredLanguages: [String]? = nil) {
|
public convenience init(context: AccountContext, forceTheme: PresentationTheme? = nil, text: String, entities: [MessageTextEntity] = [], canCopy: Bool, fromLanguage: String?, toLanguage: String? = nil, isExpanded: Bool = false, ignoredLanguages: [String]? = nil) {
|
||||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||||
|
|
||||||
var baseLanguageCode = presentationData.strings.baseLanguageCode
|
var baseLanguageCode = presentationData.strings.baseLanguageCode
|
||||||
@ -1024,7 +1030,7 @@ public class TranslateScreen: ViewController {
|
|||||||
var copyTranslationImpl: ((String) -> Void)?
|
var copyTranslationImpl: ((String) -> Void)?
|
||||||
var changeLanguageImpl: ((String, String, @escaping (String, String) -> Void) -> Void)?
|
var changeLanguageImpl: ((String, String, @escaping (String, String) -> Void) -> Void)?
|
||||||
var expandImpl: (() -> Void)?
|
var expandImpl: (() -> Void)?
|
||||||
self.init(context: context, component: TranslateScreenComponent(context: context, text: text, fromLanguage: fromLanguage, toLanguage: toLanguage, copyTranslation: !canCopy ? nil : { text in
|
self.init(context: context, component: TranslateScreenComponent(context: context, text: text, entities: entities, fromLanguage: fromLanguage, toLanguage: toLanguage, copyTranslation: !canCopy ? nil : { text in
|
||||||
copyTranslationImpl?(text)
|
copyTranslationImpl?(text)
|
||||||
}, changeLanguage: { fromLang, toLang, completion in
|
}, changeLanguage: { fromLang, toLang, completion in
|
||||||
changeLanguageImpl?(fromLang, toLang, completion)
|
changeLanguageImpl?(fromLang, toLang, completion)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user