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 {
|
||||
return nil
|
||||
}
|
||||
let row = Int(point.y / size.height * 10.0)
|
||||
let col = Int(point.x / size.width * 12.0)
|
||||
let row = max(0, min(10, Int(point.y / size.height * 10.0)))
|
||||
let col = max(0, min(12, Int(point.x / size.width * 12.0)))
|
||||
|
||||
let index = row * 12 + col
|
||||
return DrawingColor(rgb: palleteColors[index])
|
||||
if index < palleteColors.count {
|
||||
return DrawingColor(rgb: palleteColors[index])
|
||||
} else {
|
||||
return DrawingColor(rgb: 0x000000)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func handlePress(_ gestureRecognizer: UILongPressGestureRecognizer) {
|
||||
|
@ -230,7 +230,9 @@ class PremiumCoinComponent: Component {
|
||||
self.sceneView.scene = scene
|
||||
self.sceneView.delegate = self
|
||||
|
||||
let _ = self.sceneView.snapshot()
|
||||
self.didSetReady = true
|
||||
self._ready.set(.single(true))
|
||||
self.onReady()
|
||||
}
|
||||
|
||||
private var didSetReady = false
|
||||
|
@ -20,29 +20,40 @@ import TelegramUIPreferences
|
||||
|
||||
public final class PremiumGradientBackgroundComponent: Component {
|
||||
public let colors: [UIColor]
|
||||
public let cornerRadius: CGFloat
|
||||
public let topOverscroll: Bool
|
||||
|
||||
public init(
|
||||
colors: [UIColor]
|
||||
colors: [UIColor],
|
||||
cornerRadius: CGFloat = 10.0,
|
||||
topOverscroll: Bool = false
|
||||
) {
|
||||
self.colors = colors
|
||||
self.cornerRadius = cornerRadius
|
||||
self.topOverscroll = topOverscroll
|
||||
}
|
||||
|
||||
public static func ==(lhs: PremiumGradientBackgroundComponent, rhs: PremiumGradientBackgroundComponent) -> Bool {
|
||||
if lhs.colors != rhs.colors {
|
||||
return false
|
||||
}
|
||||
if lhs.cornerRadius != rhs.cornerRadius {
|
||||
return false
|
||||
}
|
||||
if lhs.topOverscroll != rhs.topOverscroll {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private let clipLayer: CALayer
|
||||
private let clipLayer: CAReplicatorLayer
|
||||
private let gradientLayer: CAGradientLayer
|
||||
|
||||
private var component: PremiumGradientBackgroundComponent?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.clipLayer = CALayer()
|
||||
self.clipLayer.cornerRadius = 10.0
|
||||
self.clipLayer = CAReplicatorLayer()
|
||||
self.clipLayer.masksToBounds = true
|
||||
|
||||
self.gradientLayer = CAGradientLayer()
|
||||
@ -61,22 +72,36 @@ public final class PremiumGradientBackgroundComponent: Component {
|
||||
func update(component: PremiumGradientBackgroundComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
self.clipLayer.frame = CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: availableSize.height + 10.0))
|
||||
self.gradientLayer.frame = CGRect(origin: .zero, size: availableSize)
|
||||
|
||||
|
||||
var locations: [NSNumber] = []
|
||||
let delta = 1.0 / CGFloat(component.colors.count - 1)
|
||||
for i in 0 ..< component.colors.count {
|
||||
locations.append((delta * CGFloat(i)) as NSNumber)
|
||||
}
|
||||
|
||||
self.gradientLayer.locations = locations
|
||||
self.gradientLayer.colors = component.colors.reversed().map { $0.cgColor }
|
||||
self.gradientLayer.type = .radial
|
||||
self.gradientLayer.startPoint = CGPoint(x: 1.0, y: 0.0)
|
||||
self.gradientLayer.endPoint = CGPoint(x: -2.0, y: 3.0)
|
||||
|
||||
self.clipLayer.cornerRadius = component.cornerRadius
|
||||
|
||||
self.component = component
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -282,7 +282,9 @@ final class PremiumStarComponent: Component {
|
||||
self.sceneView.scene = scene
|
||||
self.sceneView.delegate = self
|
||||
|
||||
let _ = self.sceneView.snapshot()
|
||||
self.didSetReady = true
|
||||
self._ready.set(.single(true))
|
||||
self.onReady()
|
||||
}
|
||||
|
||||
private var didSetReady = false
|
||||
|
@ -308,7 +308,9 @@ private final class BoostHeaderComponent: CombinedComponent {
|
||||
UIColor(rgb: 0x6b93ff),
|
||||
UIColor(rgb: 0x8878ff),
|
||||
UIColor(rgb: 0xe46ace)
|
||||
]
|
||||
],
|
||||
cornerRadius: 0.0,
|
||||
topOverscroll: true
|
||||
),
|
||||
availableSize: size,
|
||||
transition: context.transition
|
||||
|
@ -25,7 +25,9 @@ public class TranslationMessageAttribute: MessageAttribute, Equatable {
|
||||
public let entities: [MessageTextEntity]
|
||||
public let toLang: String
|
||||
|
||||
public let additional: [Additional]
|
||||
public let additional:[Additional]
|
||||
public let pollSolution: Additional?
|
||||
|
||||
|
||||
public var associatedPeerIds: [PeerId] {
|
||||
return []
|
||||
@ -35,12 +37,14 @@ public class TranslationMessageAttribute: MessageAttribute, Equatable {
|
||||
text: String,
|
||||
entities: [MessageTextEntity],
|
||||
additional:[Additional] = [],
|
||||
pollSolution: Additional? = nil,
|
||||
toLang: String
|
||||
) {
|
||||
self.text = text
|
||||
self.entities = entities
|
||||
self.toLang = toLang
|
||||
self.additional = additional
|
||||
self.pollSolution = pollSolution
|
||||
}
|
||||
|
||||
required public init(decoder: PostboxDecoder) {
|
||||
@ -48,6 +52,7 @@ public class TranslationMessageAttribute: MessageAttribute, Equatable {
|
||||
self.entities = decoder.decodeObjectArrayWithDecoderForKey("entities")
|
||||
self.additional = decoder.decodeObjectArrayWithDecoderForKey("additional")
|
||||
self.toLang = decoder.decodeStringForKey("toLang", orElse: "")
|
||||
self.pollSolution = decoder.decodeObjectForKey("pollSolution") as? Additional
|
||||
}
|
||||
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
@ -55,6 +60,12 @@ public class TranslationMessageAttribute: MessageAttribute, Equatable {
|
||||
encoder.encodeObjectArray(self.entities, forKey: "entities")
|
||||
encoder.encodeString(self.toLang, forKey: "toLang")
|
||||
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 {
|
||||
@ -70,6 +81,9 @@ public class TranslationMessageAttribute: MessageAttribute, Equatable {
|
||||
if lhs.additional != rhs.additional {
|
||||
return false
|
||||
}
|
||||
if lhs.pollSolution != rhs.pollSolution {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
@ -512,7 +512,7 @@ public extension TelegramEngine {
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -84,13 +84,18 @@ func _internal_translate_texts(network: Network, texts: [(String, [MessageTextEn
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func _internal_translateMessages(account: Account, messageIds: [EngineMessage.Id], toLang: String) -> Signal<Void, TranslationError> {
|
||||
guard let peerId = messageIds.first?.peerId else {
|
||||
return .never()
|
||||
func _internal_translateMessages(account: Account, messageIds: [EngineMessage.Id], toLang: String) -> Signal<Never, TranslationError> {
|
||||
var signals: [Signal<Void, TranslationError>] = []
|
||||
for (peerId, messageIds) in messagesIdsGroupedByPeerId(messageIds) {
|
||||
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 (transaction.getPeer(peerId).flatMap(apiInputPeer), messageIds.compactMap({ transaction.getMessage($0) }))
|
||||
return (transaction.getPeer(peerId).flatMap(apiInputPeer), messageIds.compactMap({ transaction.getMessage($0) }))
|
||||
}
|
||||
|> castError(TranslationError.self)
|
||||
|> mapToSignal { (inputPeer, messages) -> Signal<Void, TranslationError> in
|
||||
@ -111,6 +116,9 @@ func _internal_translateMessages(account: Account, messageIds: [EngineMessage.Id
|
||||
for option in poll.options {
|
||||
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)
|
||||
}
|
||||
|
||||
@ -167,20 +175,34 @@ func _internal_translateMessages(account: Account, messageIds: [EngineMessage.Id
|
||||
if !pollResults.isEmpty {
|
||||
for (i, poll) in polls.enumerated() {
|
||||
let result = pollResults[i]
|
||||
transaction.updateMessage(poll.1, update: { currentMessage in
|
||||
let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init)
|
||||
var attributes = currentMessage.attributes.filter { !($0 is TranslationMessageAttribute) }
|
||||
var attrOptions: [TranslationMessageAttribute.Additional] = []
|
||||
for (i, _) in poll.0.options.enumerated() {
|
||||
let translated = result[i + 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)
|
||||
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))
|
||||
})
|
||||
|
||||
if !result.isEmpty {
|
||||
transaction.updateMessage(poll.1, update: { currentMessage in
|
||||
let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init)
|
||||
var attributes = currentMessage.attributes.filter { !($0 is TranslationMessageAttribute) }
|
||||
var attrOptions: [TranslationMessageAttribute.Additional] = []
|
||||
for (i, _) in poll.0.options.enumerated() {
|
||||
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))
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
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()
|
||||
})
|
||||
}
|
||||
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 {
|
||||
strongSelf.nameNode?.removeFromSupernode()
|
||||
strongSelf.nameNode = nil
|
||||
@ -3460,19 +3472,23 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
strongSelf.adminBadgeNode = nil
|
||||
strongSelf.credibilityIconView?.removeFromSuperview()
|
||||
strongSelf.credibilityIconView = nil
|
||||
strongSelf.nameButtonNode?.removeFromSupernode()
|
||||
strongSelf.nameButtonNode = nil
|
||||
strongSelf.nameHighlightNode?.removeFromSupernode()
|
||||
strongSelf.nameHighlightNode = nil
|
||||
strongSelf.credibilityButtonNode?.removeFromSupernode()
|
||||
strongSelf.credibilityButtonNode = nil
|
||||
strongSelf.credibilityHighlightNode?.removeFromSupernode()
|
||||
strongSelf.credibilityHighlightNode = nil
|
||||
strongSelf.boostButtonNode?.removeFromSupernode()
|
||||
strongSelf.boostButtonNode = nil
|
||||
strongSelf.boostHighlightNode?.removeFromSupernode()
|
||||
strongSelf.boostHighlightNode = nil
|
||||
strongSelf.boostBadgeNode?.removeFromSupernode()
|
||||
strongSelf.boostBadgeNode = nil
|
||||
strongSelf.boostIconNode?.removeFromSuperview()
|
||||
strongSelf.boostIconNode = nil
|
||||
}
|
||||
strongSelf.nameButtonNode?.removeFromSupernode()
|
||||
strongSelf.nameButtonNode = nil
|
||||
strongSelf.nameHighlightNode?.removeFromSupernode()
|
||||
strongSelf.nameHighlightNode = nil
|
||||
strongSelf.credibilityButtonNode?.removeFromSupernode()
|
||||
strongSelf.credibilityButtonNode = nil
|
||||
strongSelf.credibilityHighlightNode?.removeFromSupernode()
|
||||
strongSelf.credibilityHighlightNode = nil
|
||||
strongSelf.boostButtonNode?.removeFromSupernode()
|
||||
strongSelf.boostButtonNode = nil
|
||||
strongSelf.boostHighlightNode?.removeFromSupernode()
|
||||
strongSelf.boostHighlightNode = nil
|
||||
}
|
||||
|
||||
let timingFunction = kCAMediaTimingFunctionSpring
|
||||
|
@ -755,7 +755,7 @@ public final class ChatMessageAvatarHeaderNodeImpl: ListViewItemHeaderNode, Chat
|
||||
self.controllerInteraction?.displayMessageTooltip(id, self.presentationData.strings.Conversation_ForwardAuthorHiddenTooltip, self, self.avatarNode.frame)
|
||||
} else if let peer = self.peer {
|
||||
if let adMessageId = self.adMessageId {
|
||||
self.controllerInteraction?.activateAdAction(adMessageId)
|
||||
self.controllerInteraction?.activateAdAction(adMessageId, nil)
|
||||
} else {
|
||||
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)
|
||||
|
@ -140,7 +140,7 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent
|
||||
self.contentNode.activateAction = { [weak self] in
|
||||
if let strongSelf = self, let item = strongSelf.item {
|
||||
if let _ = item.message.adAttribute {
|
||||
item.controllerInteraction.activateAdAction(item.message.id)
|
||||
item.controllerInteraction.activateAdAction(item.message.id, strongSelf.contentNode.makeProgress())
|
||||
} else {
|
||||
var webPageContent: TelegramMediaWebpageLoadedContent?
|
||||
for media in item.message.media {
|
||||
|
@ -604,7 +604,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
||||
}, openLargeEmojiInfo: { _, _, _ in
|
||||
}, openJoinLink: { _ in
|
||||
}, openWebView: { _, _, _, _ in
|
||||
}, activateAdAction: { _ in
|
||||
}, activateAdAction: { _, _ in
|
||||
}, openRequestedPeerSelection: { _, _, _, _ in
|
||||
}, saveMediaToFiles: { _ in
|
||||
}, openNoAdsDemo: {
|
||||
|
@ -232,7 +232,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
|
||||
public let openLargeEmojiInfo: (String, String?, TelegramMediaFile) -> Void
|
||||
public let openJoinLink: (String) -> 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 saveMediaToFiles: (EngineMessage.Id) -> Void
|
||||
public let openNoAdsDemo: () -> Void
|
||||
@ -357,7 +357,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
|
||||
openLargeEmojiInfo: @escaping (String, String?, TelegramMediaFile) -> Void,
|
||||
openJoinLink: @escaping (String) -> 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,
|
||||
saveMediaToFiles: @escaping (EngineMessage.Id) -> 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 {
|
||||
mediaEditor.onFirstDisplay = { [weak self] in
|
||||
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 {
|
||||
self.previewContainerView.layer.allowsGroupOpacity = true
|
||||
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 {
|
||||
var destinationTransitionView: UIView?
|
||||
var destinationTransitionRect: CGRect = .zero
|
||||
if !finished {
|
||||
if let transitionIn = controller.transitionIn, case let .gallery(galleryTransitionIn) = transitionIn, let sourceImage = galleryTransitionIn.sourceImage, isNew != true {
|
||||
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)
|
||||
sourceSuperView?.addSubview(destinationTransitionOutView)
|
||||
destinationTransitionView = destinationTransitionOutView
|
||||
destinationTransitionRect = galleryTransitionIn.sourceRect
|
||||
}
|
||||
if let view = self.componentHost.view as? MediaEditorScreenComponent.View {
|
||||
view.animateOut(to: .gallery)
|
||||
@ -3853,7 +3862,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
if let destinationTransitionView {
|
||||
self.previewContainerView.layer.allowsGroupOpacity = true
|
||||
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()
|
||||
})
|
||||
}
|
||||
|
@ -3316,7 +3316,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
}, openLargeEmojiInfo: { _, _, _ in
|
||||
}, openJoinLink: { _ in
|
||||
}, openWebView: { _, _, _, _ in
|
||||
}, activateAdAction: { _ in
|
||||
}, activateAdAction: { _, _ in
|
||||
}, openRequestedPeerSelection: { _, _, _, _ in
|
||||
}, saveMediaToFiles: { _ in
|
||||
}, openNoAdsDemo: {
|
||||
|
@ -43,6 +43,7 @@ swift_library(
|
||||
"//submodules/UndoUI",
|
||||
"//submodules/TelegramUI/Components/PlainButtonComponent",
|
||||
"//submodules/Components/ComponentDisplayAdapters",
|
||||
"//submodules/TelegramUI/Components/MediaEditorScreen",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -33,6 +33,7 @@ import ShareController
|
||||
import UndoUI
|
||||
import PlainButtonComponent
|
||||
import ComponentDisplayAdapters
|
||||
import MediaEditorScreen
|
||||
|
||||
private let mediaBadgeBackgroundColor = UIColor(white: 0.0, alpha: 0.6)
|
||||
private let mediaBadgeTextColor = UIColor.white
|
||||
@ -1266,6 +1267,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
|
||||
|
||||
private let listDisposable = MetaDisposable()
|
||||
private var hiddenMediaDisposable: Disposable?
|
||||
private let updateDisposable = MetaDisposable()
|
||||
|
||||
private var numberOfItemsToRequest: Int = 50
|
||||
private var isRequestingView: Bool = false
|
||||
@ -1765,6 +1767,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
|
||||
self.hiddenMediaDisposable?.dispose()
|
||||
self.animationTimer?.invalidate()
|
||||
self.presentationDataDisposable?.dispose()
|
||||
self.updateDisposable.dispose()
|
||||
}
|
||||
|
||||
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
|
||||
c?.dismiss(completion: {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let _ = self
|
||||
|
||||
|
||||
})
|
||||
})))*/
|
||||
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: {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
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 {
|
||||
@ -1880,7 +1921,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
|
||||
let _ = (self.context.engine.data.get(
|
||||
TelegramEngine.EngineData.Item.Peer.Peer(id: self.peerId)
|
||||
)
|
||||
|> deliverOnMainQueue).startStandalone(next: { [weak self] peer in
|
||||
|> deliverOnMainQueue).startStandalone(next: { [weak self] peer in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
@ -4308,7 +4308,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
self.sendMessageContext.currentSpeechHolder = speechHolder
|
||||
}
|
||||
case .translate:
|
||||
self.sendMessageContext.performTranslateTextAction(view: self, text: text.string)
|
||||
self.sendMessageContext.performTranslateTextAction(view: self, text: text.string, entities: [])
|
||||
case .quote:
|
||||
break
|
||||
}
|
||||
@ -5359,13 +5359,9 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
|
||||
private let updateDisposable = MetaDisposable()
|
||||
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
|
||||
}
|
||||
let context = component.context
|
||||
let peerId = component.slice.peer.id
|
||||
let item = component.slice.item.storyItem
|
||||
let id = item.id
|
||||
|
||||
self.isEditingStory = true
|
||||
self.updateIsProgressPaused()
|
||||
@ -5376,277 +5372,39 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
videoPlaybackPosition = view.videoPlaybackPosition
|
||||
}
|
||||
|
||||
let subject: Signal<MediaEditorScreen.Subject?, NoError>
|
||||
subject = getStorySource(engine: component.context.engine, peerId: component.context.account.peerId, id: Int64(item.id))
|
||||
|> mapToSignal { source in
|
||||
if !repost, let source {
|
||||
return .single(.draft(source, Int64(item.id)))
|
||||
} else {
|
||||
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,
|
||||
guard let controller = MediaEditorScreen.makeEditStoryController(
|
||||
context: component.context,
|
||||
peer: component.slice.peer,
|
||||
storyItem: component.slice.item.storyItem,
|
||||
videoPlaybackPosition: videoPlaybackPosition,
|
||||
repost: repost,
|
||||
transitionIn: .noAnimation,
|
||||
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 nil
|
||||
}
|
||||
},
|
||||
completion: { [weak self] result, commit in
|
||||
transitionOut: nil,
|
||||
completed: { [weak self] in
|
||||
guard let self else {
|
||||
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)
|
||||
|
||||
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: 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 {
|
||||
return
|
||||
}
|
||||
switch result {
|
||||
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({})
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
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 {
|
||||
return
|
||||
}
|
||||
switch result {
|
||||
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({})
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
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({})
|
||||
}
|
||||
self.component?.controller()?.dismiss(animated: false)
|
||||
},
|
||||
willDismiss: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.isEditingStory = false
|
||||
self.rewindCurrentItem()
|
||||
self.updateIsProgressPaused()
|
||||
self.state?.updated(transition: .easeInOut(duration: 0.2))
|
||||
},
|
||||
update: { [weak self] disposable in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.updateDisposable.set(disposable)
|
||||
}
|
||||
)
|
||||
controller.willDismiss = { [weak self] in
|
||||
self?.isEditingStory = false
|
||||
self?.rewindCurrentItem()
|
||||
self?.updateIsProgressPaused()
|
||||
self?.state?.updated(transition: .easeInOut(duration: 0.2))
|
||||
) else {
|
||||
return
|
||||
}
|
||||
controller.navigationPresentation = .flatModal
|
||||
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() {
|
||||
@ -7059,7 +6817,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
guard let self, let component = self.component else {
|
||||
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))
|
||||
}
|
||||
|
||||
func performTranslateTextAction(view: StoryItemSetContainerComponent.View, text: String) {
|
||||
func performTranslateTextAction(view: StoryItemSetContainerComponent.View, text: String, entities: [MessageTextEntity]) {
|
||||
guard let component = view.component else {
|
||||
return
|
||||
}
|
||||
@ -1190,7 +1190,7 @@ final class StoryItemSetContainerSendMessage {
|
||||
|
||||
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
|
||||
guard let view, let component = view.component else {
|
||||
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))
|
||||
}
|
||||
})
|
||||
}, 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 {
|
||||
return
|
||||
}
|
||||
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
|
||||
guard let self else {
|
||||
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) {
|
||||
if !canSendMessagesToChat(self.presentationInterfaceState) {
|
||||
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
|
||||
self?.present(c, in: .window(.root))
|
||||
}, 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)
|
||||
self.navigationActionDisposable.set(disposable)
|
||||
}, performAction: true)
|
||||
|
@ -251,9 +251,46 @@ func openResolvedUrlImpl(
|
||||
navigationController?.pushViewController(InstantPageController(context: context, webPage: webpage, sourceLocation: InstantPageSourceLocation(userLocation: .other, peerType: .channel), anchor: anchor))
|
||||
case let .join(link):
|
||||
dismissInput()
|
||||
present(JoinLinkPreviewController(context: context, link: link, navigateToPeer: { peer, peekData in
|
||||
openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: peekData))
|
||||
}, parentNavigationController: navigationController), nil)
|
||||
|
||||
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
|
||||
openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: peekData))
|
||||
}, parentNavigationController: navigationController), nil)
|
||||
}
|
||||
case let .localization(identifier):
|
||||
dismissInput()
|
||||
present(LanguageLinkPreviewController(context: context, identifier: identifier), nil)
|
||||
|
@ -165,7 +165,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
|
||||
}, openLargeEmojiInfo: { _, _, _ in
|
||||
}, openJoinLink: { _ in
|
||||
}, openWebView: { _, _, _, _ in
|
||||
}, activateAdAction: { _ in
|
||||
}, activateAdAction: { _, _ in
|
||||
}, openRequestedPeerSelection: { _, _, _, _ in
|
||||
}, saveMediaToFiles: { _ in
|
||||
}, openNoAdsDemo: {
|
||||
|
@ -1759,7 +1759,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
}, openLargeEmojiInfo: { _, _, _ in
|
||||
}, openJoinLink: { _ in
|
||||
}, openWebView: { _, _, _, _ in
|
||||
}, activateAdAction: { _ in
|
||||
}, activateAdAction: { _, _ in
|
||||
}, openRequestedPeerSelection: { _, _, _, _ in
|
||||
}, saveMediaToFiles: { _ in
|
||||
}, openNoAdsDemo: {
|
||||
|
@ -27,6 +27,7 @@ swift_library(
|
||||
"//submodules/ComponentFlow:ComponentFlow",
|
||||
"//submodules/Components/ViewControllerComponent:ViewControllerComponent",
|
||||
"//submodules/Components/MultilineTextComponent:MultilineTextComponent",
|
||||
"//submodules/Components/MultilineTextWithEntitiesComponent:MultilineTextWithEntitiesComponent",
|
||||
"//submodules/Components/BundleIconComponent:BundleIconComponent",
|
||||
"//submodules/UndoUI:UndoUI",
|
||||
"//submodules/ActivityIndicator:ActivityIndicator",
|
||||
|
@ -109,8 +109,8 @@ public func updateChatTranslationStateInteractively(engine: TelegramEngine, peer
|
||||
@available(iOS 12.0, *)
|
||||
private let languageRecognizer = NLLanguageRecognizer()
|
||||
|
||||
public func translateMessageIds(context: AccountContext, messageIds: [EngineMessage.Id], toLang: String) -> Signal<Void, NoError> {
|
||||
return context.account.postbox.transaction { transaction -> Signal<Void, NoError> in
|
||||
public func translateMessageIds(context: AccountContext, messageIds: [EngineMessage.Id], toLang: String) -> Signal<Never, NoError> {
|
||||
return context.account.postbox.transaction { transaction -> Signal<Never, NoError> in
|
||||
var messageIdsToTranslate: [EngineMessage.Id] = []
|
||||
var messageIdsSet = Set<EngineMessage.Id>()
|
||||
for messageId in messageIds {
|
||||
@ -152,7 +152,7 @@ public func translateMessageIds(context: AccountContext, messageIds: [EngineMess
|
||||
}
|
||||
}
|
||||
return context.engine.messages.translateMessages(messageIds: messageIdsToTranslate, toLang: toLang)
|
||||
|> `catch` { _ -> Signal<Void, NoError> in
|
||||
|> `catch` { _ -> Signal<Never, NoError> in
|
||||
return .complete()
|
||||
}
|
||||
} |> switchToLatest
|
||||
|
@ -11,6 +11,7 @@ import Speak
|
||||
import ComponentFlow
|
||||
import ViewControllerComponent
|
||||
import MultilineTextComponent
|
||||
import MultilineTextWithEntitiesComponent
|
||||
import BundleIconComponent
|
||||
import UndoUI
|
||||
|
||||
@ -35,15 +36,17 @@ private final class TranslateScreenComponent: CombinedComponent {
|
||||
|
||||
let context: AccountContext
|
||||
let text: String
|
||||
let entities: [MessageTextEntity]
|
||||
let fromLanguage: String?
|
||||
let toLanguage: String
|
||||
let copyTranslation: ((String) -> Void)?
|
||||
let changeLanguage: (String, String, @escaping (String, String) -> Void) -> 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.text = text
|
||||
self.entities = entities
|
||||
self.fromLanguage = fromLanguage
|
||||
self.toLanguage = toLanguage
|
||||
self.copyTranslation = copyTranslation
|
||||
@ -58,6 +61,9 @@ private final class TranslateScreenComponent: CombinedComponent {
|
||||
if lhs.text != rhs.text {
|
||||
return false
|
||||
}
|
||||
if lhs.entities != rhs.entities {
|
||||
return false
|
||||
}
|
||||
if lhs.fromLanguage != rhs.fromLanguage {
|
||||
return false
|
||||
}
|
||||
@ -995,7 +1001,7 @@ public class TranslateScreen: ViewController {
|
||||
|
||||
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 }
|
||||
|
||||
var baseLanguageCode = presentationData.strings.baseLanguageCode
|
||||
@ -1024,7 +1030,7 @@ public class TranslateScreen: ViewController {
|
||||
var copyTranslationImpl: ((String) -> Void)?
|
||||
var changeLanguageImpl: ((String, String, @escaping (String, String) -> Void) -> 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)
|
||||
}, changeLanguage: { fromLang, toLang, completion in
|
||||
changeLanguageImpl?(fromLang, toLang, completion)
|
||||
|
Loading…
x
Reference in New Issue
Block a user