mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 21:45:19 +00:00
Merge commit '73987dff5d4708433fe4e3527c4738a6d784915b'
# Conflicts: # submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift
This commit is contained in:
commit
780168d30b
@ -9355,3 +9355,5 @@ Sorry for the inconvenience.";
|
||||
"Privacy.Bio.CustomHelp" = "You can restrict who can see your profile bio with granular precision.";
|
||||
"Privacy.Bio.AlwaysShareWith.Title" = "Always Share With";
|
||||
"Privacy.Bio.NeverShareWith.Title" = "Never Share With";
|
||||
|
||||
"Conversation.OpenLink" = "OPEN LINK";
|
||||
|
@ -277,7 +277,7 @@ public func makeVideoStickerDirectFrameSource(queue: Queue, path: String, width:
|
||||
return VideoStickerDirectFrameSource(queue: queue, path: path, width: width, height: height, cachePathPrefix: cachePathPrefix, unpremultiplyAlpha: unpremultiplyAlpha)
|
||||
}
|
||||
|
||||
final class VideoStickerDirectFrameSource: AnimatedStickerFrameSource {
|
||||
public final class VideoStickerDirectFrameSource: AnimatedStickerFrameSource {
|
||||
private let queue: Queue
|
||||
private let path: String
|
||||
private let width: Int
|
||||
@ -285,13 +285,13 @@ final class VideoStickerDirectFrameSource: AnimatedStickerFrameSource {
|
||||
private let cache: VideoStickerFrameSourceCache?
|
||||
private let image: UIImage?
|
||||
private let bytesPerRow: Int
|
||||
var frameCount: Int
|
||||
let frameRate: Int
|
||||
public var frameCount: Int
|
||||
public let frameRate: Int
|
||||
fileprivate var currentFrame: Int
|
||||
|
||||
private let source: SoftwareVideoSource?
|
||||
|
||||
var frameIndex: Int {
|
||||
public var frameIndex: Int {
|
||||
if self.frameCount == 0 {
|
||||
return 0
|
||||
} else {
|
||||
@ -299,7 +299,7 @@ final class VideoStickerDirectFrameSource: AnimatedStickerFrameSource {
|
||||
}
|
||||
}
|
||||
|
||||
init?(queue: Queue, path: String, width: Int, height: Int, cachePathPrefix: String?, unpremultiplyAlpha: Bool = true) {
|
||||
public init?(queue: Queue, path: String, width: Int, height: Int, cachePathPrefix: String?, unpremultiplyAlpha: Bool = true) {
|
||||
self.queue = queue
|
||||
self.path = path
|
||||
self.width = width
|
||||
@ -334,7 +334,7 @@ final class VideoStickerDirectFrameSource: AnimatedStickerFrameSource {
|
||||
assert(self.queue.isCurrent())
|
||||
}
|
||||
|
||||
func takeFrame(draw: Bool) -> AnimatedStickerFrame? {
|
||||
public func takeFrame(draw: Bool) -> AnimatedStickerFrame? {
|
||||
let frameIndex: Int
|
||||
if self.frameCount > 0 {
|
||||
frameIndex = self.currentFrame % self.frameCount
|
||||
@ -415,11 +415,11 @@ final class VideoStickerDirectFrameSource: AnimatedStickerFrameSource {
|
||||
}
|
||||
}
|
||||
|
||||
func skipToEnd() {
|
||||
public func skipToEnd() {
|
||||
self.currentFrame = self.frameCount - 1
|
||||
}
|
||||
|
||||
func skipToFrameIndex(_ index: Int) {
|
||||
public func skipToFrameIndex(_ index: Int) {
|
||||
self.currentFrame = index
|
||||
}
|
||||
}
|
||||
|
@ -80,6 +80,7 @@ final class AttachmentContainer: ASDisplayNode, UIGestureRecognizerDelegate {
|
||||
})
|
||||
self.container.clipsToBounds = true
|
||||
self.container.overflowInset = overflowInset
|
||||
self.container.shouldAnimateDisappearance = true
|
||||
|
||||
super.init()
|
||||
|
||||
|
@ -439,6 +439,8 @@ public final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelega
|
||||
}
|
||||
}
|
||||
|
||||
public var shouldAnimateDisappearance: Bool = false
|
||||
|
||||
private func topTransition(from fromValue: Child?, to toValue: Child?, transitionType: PendingChild.TransitionType, layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
if case .animated = transition, let fromValue = fromValue, let toValue = toValue {
|
||||
if let currentTransition = self.state.transition {
|
||||
@ -501,9 +503,16 @@ public final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelega
|
||||
strongSelf.keyboardViewManager?.dismissEditingWithoutAnimation(view: topTransition.previous.value.view)
|
||||
strongSelf.state.transition = nil
|
||||
|
||||
topTransition.previous.value.setIgnoreAppearanceMethodInvocations(true)
|
||||
topTransition.previous.value.displayNode.removeFromSupernode()
|
||||
topTransition.previous.value.setIgnoreAppearanceMethodInvocations(false)
|
||||
if strongSelf.shouldAnimateDisappearance {
|
||||
let displayNode = topTransition.previous.value.displayNode
|
||||
displayNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak displayNode] _ in
|
||||
displayNode?.removeFromSupernode()
|
||||
})
|
||||
} else {
|
||||
topTransition.previous.value.setIgnoreAppearanceMethodInvocations(true)
|
||||
topTransition.previous.value.displayNode.removeFromSupernode()
|
||||
topTransition.previous.value.setIgnoreAppearanceMethodInvocations(false)
|
||||
}
|
||||
topTransition.previous.value.viewDidDisappear(true)
|
||||
if let toValue = strongSelf.state.top, let layout = strongSelf.state.layout {
|
||||
toValue.value.displayNode.frame = CGRect(origin: CGPoint(), size: layout.size)
|
||||
|
@ -1144,7 +1144,8 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
}
|
||||
|
||||
let previewSize = CGSize(width: context.availableSize.width, height: floorToScreenPixels(context.availableSize.width * 1.77778))
|
||||
let previewTopInset: CGFloat = floorToScreenPixels(context.availableSize.height - previewSize.height) / 2.0
|
||||
let previewTopInset: CGFloat = environment.statusBarHeight + 12.0
|
||||
let previewBottomInset = context.availableSize.height - previewSize.height - previewTopInset
|
||||
|
||||
var topInset = environment.safeInsets.top + 31.0
|
||||
if component.sourceHint == .storyEditor {
|
||||
@ -1966,7 +1967,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
var doneButtonPosition = CGPoint(x: context.availableSize.width - environment.safeInsets.right - doneButton.size.width / 2.0 - 3.0, y: context.availableSize.height - environment.safeInsets.bottom - doneButton.size.height / 2.0 - 2.0 - UIScreenPixel)
|
||||
if component.sourceHint == .storyEditor {
|
||||
doneButtonPosition.x = doneButtonPosition.x - 2.0
|
||||
doneButtonPosition.y = floorToScreenPixels(context.availableSize.height - previewTopInset + 3.0 + doneButton.size.height / 2.0)
|
||||
doneButtonPosition.y = floorToScreenPixels(context.availableSize.height - previewBottomInset + 3.0 + doneButton.size.height / 2.0)
|
||||
}
|
||||
context.add(doneButton
|
||||
.position(doneButtonPosition)
|
||||
@ -2044,7 +2045,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
)
|
||||
var modeAndSizePosition = CGPoint(x: context.availableSize.width / 2.0 - (modeRightInset - 57.0) / 2.0, y: context.availableSize.height - environment.safeInsets.bottom - modeAndSize.size.height / 2.0 - 9.0)
|
||||
if component.sourceHint == .storyEditor {
|
||||
modeAndSizePosition.y = floorToScreenPixels(context.availableSize.height - previewTopInset + 8.0 + modeAndSize.size.height / 2.0)
|
||||
modeAndSizePosition.y = floorToScreenPixels(context.availableSize.height - previewBottomInset + 8.0 + modeAndSize.size.height / 2.0)
|
||||
}
|
||||
context.add(modeAndSize
|
||||
.position(modeAndSizePosition)
|
||||
@ -2083,7 +2084,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
var backButtonPosition = CGPoint(x: environment.safeInsets.left + backButton.size.width / 2.0 + 3.0, y: context.availableSize.height - environment.safeInsets.bottom - backButton.size.height / 2.0 - 2.0 - UIScreenPixel)
|
||||
if component.sourceHint == .storyEditor {
|
||||
backButtonPosition.x = backButtonPosition.x + 2.0
|
||||
backButtonPosition.y = floorToScreenPixels(context.availableSize.height - previewTopInset + 3.0 + backButton.size.height / 2.0)
|
||||
backButtonPosition.y = floorToScreenPixels(context.availableSize.height - previewBottomInset + 3.0 + backButton.size.height / 2.0)
|
||||
}
|
||||
context.add(backButton
|
||||
.position(backButtonPosition)
|
||||
|
@ -10,6 +10,7 @@ public final class AdMessageAttribute: MessageAttribute {
|
||||
public enum MessageTarget {
|
||||
case peer(id: EnginePeer.Id, message: EngineMessage.Id?, startParam: String?)
|
||||
case join(title: String, joinHash: String)
|
||||
case webPage(title: String, url: String)
|
||||
}
|
||||
|
||||
public let opaqueId: Data
|
||||
|
@ -32,6 +32,7 @@ private class AdMessagesHistoryContextImpl {
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case peer
|
||||
case invite
|
||||
case webPage
|
||||
}
|
||||
|
||||
struct Invite: Equatable, Codable {
|
||||
@ -39,8 +40,14 @@ private class AdMessagesHistoryContextImpl {
|
||||
var joinHash: String
|
||||
}
|
||||
|
||||
struct WebPage: Equatable, Codable {
|
||||
var title: String
|
||||
var url: String
|
||||
}
|
||||
|
||||
case peer(PeerId)
|
||||
case invite(Invite)
|
||||
case webPage(WebPage)
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
@ -49,6 +56,8 @@ private class AdMessagesHistoryContextImpl {
|
||||
self = .peer(PeerId(peer))
|
||||
} else if let invite = try container.decodeIfPresent(Invite.self, forKey: .invite) {
|
||||
self = .invite(invite)
|
||||
} else if let webPage = try container.decodeIfPresent(WebPage.self, forKey: .webPage) {
|
||||
self = .webPage(webPage)
|
||||
} else {
|
||||
throw DecodingError.generic
|
||||
}
|
||||
@ -62,6 +71,8 @@ private class AdMessagesHistoryContextImpl {
|
||||
try container.encode(peerId.toInt64(), forKey: .peer)
|
||||
case let .invite(invite):
|
||||
try container.encode(invite, forKey: .invite)
|
||||
case let .webPage(webPage):
|
||||
try container.encode(webPage, forKey: .webPage)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -205,6 +216,8 @@ private class AdMessagesHistoryContextImpl {
|
||||
target = .peer(id: peerId, message: self.messageId, startParam: self.startParam)
|
||||
case let .invite(invite):
|
||||
target = .join(title: invite.title, joinHash: invite.joinHash)
|
||||
case let .webPage(webPage):
|
||||
target = .webPage(title: webPage.title, url: webPage.url)
|
||||
}
|
||||
let mappedMessageType: AdMessageAttribute.MessageType
|
||||
switch self.messageType {
|
||||
@ -251,6 +264,23 @@ private class AdMessagesHistoryContextImpl {
|
||||
defaultBannedRights: nil,
|
||||
usernames: []
|
||||
)
|
||||
case let .webPage(webPage):
|
||||
author = TelegramChannel(
|
||||
id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(1)),
|
||||
accessHash: nil,
|
||||
title: webPage.title,
|
||||
username: nil,
|
||||
photo: [],
|
||||
creationDate: 0,
|
||||
version: 0,
|
||||
participationStatus: .left,
|
||||
info: .broadcast(TelegramChannelBroadcastInfo(flags: [])),
|
||||
flags: [],
|
||||
restrictionInfo: nil,
|
||||
adminRights: nil,
|
||||
bannedRights: nil,
|
||||
defaultBannedRights: nil,
|
||||
usernames: [])
|
||||
}
|
||||
|
||||
messagePeers[author.id] = author
|
||||
@ -471,8 +501,7 @@ private class AdMessagesHistoryContextImpl {
|
||||
|
||||
for message in messages {
|
||||
switch message {
|
||||
case let .sponsoredMessage(flags, randomId, fromId, chatInvite, chatInviteHash, channelPost, startParam, webpage, message, entities, sponsorInfo, additionalInfo):
|
||||
let _ = webpage
|
||||
case let .sponsoredMessage(flags, randomId, fromId, chatInvite, chatInviteHash, channelPost, startParam, webPage, message, entities, sponsorInfo, additionalInfo):
|
||||
var parsedEntities: [MessageTextEntity] = []
|
||||
if let entities = entities {
|
||||
parsedEntities = messageTextEntitiesFromApiEntities(entities)
|
||||
@ -484,6 +513,11 @@ private class AdMessagesHistoryContextImpl {
|
||||
var target: CachedMessage.Target?
|
||||
if let fromId = fromId {
|
||||
target = .peer(fromId.peerId)
|
||||
} else if let webPage = webPage {
|
||||
if case let .sponsoredWebPage(_, url, siteName, photo) = webPage {
|
||||
let _ = photo
|
||||
target = .webPage(CachedMessage.Target.WebPage(title: siteName, url: url))
|
||||
}
|
||||
} else if let chatInvite = chatInvite, let chatInviteHash = chatInviteHash {
|
||||
switch chatInvite {
|
||||
case let .chatInvite(flags, title, _, photo, participantsCount, participants):
|
||||
|
@ -507,10 +507,10 @@ public final class EngineStorySubscriptions: Equatable {
|
||||
|
||||
public enum StoryUploadResult {
|
||||
case progress(Float)
|
||||
case completed
|
||||
case completed(Int32?)
|
||||
}
|
||||
|
||||
func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text: String, entities: [MessageTextEntity], pin: Bool, privacy: EngineStoryPrivacy, period: Int) -> Signal<StoryUploadResult, NoError> {
|
||||
private func uploadedStoryContent(account: Account, media: EngineStoryInputMedia) -> (signal: Signal<PendingMessageUploadedContentResult?, NoError>, media: Media) {
|
||||
let originalMedia: Media
|
||||
let contentToUpload: MessageContentToUpload
|
||||
|
||||
@ -589,48 +589,60 @@ func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text:
|
||||
contentSignal = signal
|
||||
}
|
||||
|
||||
return contentSignal
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<PendingMessageUploadedContentResult?, NoError> in
|
||||
return .single(nil)
|
||||
return (
|
||||
contentSignal
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<PendingMessageUploadedContentResult?, NoError> in
|
||||
return .single(nil)
|
||||
},
|
||||
originalMedia
|
||||
)
|
||||
}
|
||||
|
||||
private func apiInputPrivacyRules(privacy: EngineStoryPrivacy, transaction: Transaction) -> [Api.InputPrivacyRule] {
|
||||
var privacyRules: [Api.InputPrivacyRule]
|
||||
switch privacy.base {
|
||||
case .everyone:
|
||||
privacyRules = [.inputPrivacyValueAllowAll]
|
||||
case .contacts:
|
||||
privacyRules = [.inputPrivacyValueAllowContacts]
|
||||
case .closeFriends:
|
||||
privacyRules = [.inputPrivacyValueAllowCloseFriends]
|
||||
case .nobody:
|
||||
privacyRules = [.inputPrivacyValueDisallowAll]
|
||||
}
|
||||
var privacyUsers: [Api.InputUser] = []
|
||||
var privacyChats: [Int64] = []
|
||||
for peerId in privacy.additionallyIncludePeers {
|
||||
if let peer = transaction.getPeer(peerId) {
|
||||
if let _ = peer as? TelegramUser {
|
||||
if let inputUser = apiInputUser(peer) {
|
||||
privacyUsers.append(inputUser)
|
||||
}
|
||||
} else if peer is TelegramGroup || peer is TelegramChannel {
|
||||
privacyChats.append(peer.id.id._internalGetInt64Value())
|
||||
}
|
||||
}
|
||||
}
|
||||
if !privacyUsers.isEmpty {
|
||||
privacyRules.append(.inputPrivacyValueAllowUsers(users: privacyUsers))
|
||||
}
|
||||
if !privacyChats.isEmpty {
|
||||
privacyRules.append(.inputPrivacyValueAllowChatParticipants(chats: privacyChats))
|
||||
}
|
||||
return privacyRules
|
||||
}
|
||||
|
||||
func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text: String, entities: [MessageTextEntity], pin: Bool, privacy: EngineStoryPrivacy, period: Int, randomId: Int64) -> Signal<StoryUploadResult, NoError> {
|
||||
let (contentSignal, originalMedia) = uploadedStoryContent(account: account, media: media)
|
||||
return contentSignal
|
||||
|> mapToSignal { result -> Signal<StoryUploadResult, NoError> in
|
||||
return account.postbox.transaction { transaction -> Signal<StoryUploadResult, NoError> in
|
||||
switch result {
|
||||
case let .progress(progress):
|
||||
return .single(.progress(progress))
|
||||
case let .content(content):
|
||||
var privacyRules: [Api.InputPrivacyRule]
|
||||
switch privacy.base {
|
||||
case .everyone:
|
||||
privacyRules = [.inputPrivacyValueAllowAll]
|
||||
case .contacts:
|
||||
privacyRules = [.inputPrivacyValueAllowContacts]
|
||||
case .closeFriends:
|
||||
privacyRules = [.inputPrivacyValueAllowCloseFriends]
|
||||
case .nobody:
|
||||
privacyRules = [.inputPrivacyValueDisallowAll]
|
||||
}
|
||||
var privacyUsers: [Api.InputUser] = []
|
||||
var privacyChats: [Int64] = []
|
||||
for peerId in privacy.additionallyIncludePeers {
|
||||
if let peer = transaction.getPeer(peerId) {
|
||||
if let _ = peer as? TelegramUser {
|
||||
if let inputUser = apiInputUser(peer) {
|
||||
privacyUsers.append(inputUser)
|
||||
}
|
||||
} else if peer is TelegramGroup || peer is TelegramChannel {
|
||||
privacyChats.append(peer.id.id._internalGetInt64Value())
|
||||
}
|
||||
}
|
||||
}
|
||||
if !privacyUsers.isEmpty {
|
||||
privacyRules.append(.inputPrivacyValueAllowUsers(users: privacyUsers))
|
||||
}
|
||||
if !privacyChats.isEmpty {
|
||||
privacyRules.append(.inputPrivacyValueAllowChatParticipants(chats: privacyChats))
|
||||
}
|
||||
|
||||
switch result {
|
||||
case let .progress(progress):
|
||||
return .single(.progress(progress))
|
||||
case let .content(content):
|
||||
return account.postbox.transaction { transaction -> Signal<StoryUploadResult, NoError> in
|
||||
let privacyRules = apiInputPrivacyRules(privacy: privacy, transaction: transaction)
|
||||
switch content.content {
|
||||
case let .media(inputMedia, _):
|
||||
var flags: Int32 = 0
|
||||
@ -640,7 +652,6 @@ func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text:
|
||||
if pin {
|
||||
flags |= 1 << 2
|
||||
}
|
||||
|
||||
if !text.isEmpty {
|
||||
flags |= 1 << 0
|
||||
apiCaption = text
|
||||
@ -666,7 +677,7 @@ func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text:
|
||||
caption: apiCaption,
|
||||
entities: apiEntities,
|
||||
privacyRules: privacyRules,
|
||||
randomId: Int64.random(in: Int64.min ... Int64.max),
|
||||
randomId: randomId,
|
||||
period: Int32(period)
|
||||
))
|
||||
|> map(Optional.init)
|
||||
@ -674,11 +685,13 @@ func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text:
|
||||
return .single(nil)
|
||||
}
|
||||
|> mapToSignal { updates -> Signal<StoryUploadResult, NoError> in
|
||||
var id: Int32?
|
||||
if let updates = updates {
|
||||
for update in updates.allUpdates {
|
||||
if case let .updateStory(_, story) = update {
|
||||
switch story {
|
||||
case let .storyItem(_, _, _, _, _, _, media, _, _):
|
||||
case let .storyItem(_, idValue, _, _, _, _, media, _, _):
|
||||
id = idValue
|
||||
let (parsedMedia, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, account.peerId)
|
||||
if let parsedMedia = parsedMedia {
|
||||
applyMediaResourceChanges(from: originalMedia, to: parsedMedia, postbox: account.postbox, force: false)
|
||||
@ -692,13 +705,105 @@ func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text:
|
||||
account.stateManager.addUpdates(updates)
|
||||
}
|
||||
|
||||
return .single(.completed)
|
||||
return .single(.completed(id))
|
||||
}
|
||||
default:
|
||||
return .complete()
|
||||
}
|
||||
default:
|
||||
return .complete()
|
||||
}
|
||||
|> switchToLatest
|
||||
default:
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func _internal_editStory(account: Account, media: EngineStoryInputMedia?, id: Int32, text: String, entities: [MessageTextEntity], privacy: EngineStoryPrivacy?) -> Signal<StoryUploadResult, NoError> {
|
||||
let contentSignal: Signal<PendingMessageUploadedContentResult?, NoError>
|
||||
let originalMedia: Media?
|
||||
if let media = media {
|
||||
(contentSignal, originalMedia) = uploadedStoryContent(account: account, media: media)
|
||||
} else {
|
||||
contentSignal = .single(nil)
|
||||
originalMedia = nil
|
||||
}
|
||||
|
||||
return contentSignal
|
||||
|> mapToSignal { result -> Signal<StoryUploadResult, NoError> in
|
||||
if let result = result, case let .progress(progress) = result {
|
||||
return .single(.progress(progress))
|
||||
}
|
||||
|
||||
let inputMedia: Api.InputMedia?
|
||||
if let result = result, case let .content(uploadedContent) = result, case let .media(media, _) = uploadedContent.content {
|
||||
inputMedia = media
|
||||
} else {
|
||||
inputMedia = nil
|
||||
}
|
||||
|
||||
return account.postbox.transaction { transaction -> Signal<StoryUploadResult, NoError> in
|
||||
var flags: Int32 = 0
|
||||
var apiCaption: String?
|
||||
var apiEntities: [Api.MessageEntity]?
|
||||
var privacyRules: [Api.InputPrivacyRule]?
|
||||
|
||||
if let _ = inputMedia {
|
||||
flags |= 1 << 0
|
||||
}
|
||||
if !text.isEmpty {
|
||||
flags |= 1 << 1
|
||||
apiCaption = text
|
||||
|
||||
if !entities.isEmpty {
|
||||
flags |= 1 << 1
|
||||
|
||||
var associatedPeers: [PeerId: Peer] = [:]
|
||||
for entity in entities {
|
||||
for entityPeerId in entity.associatedPeerIds {
|
||||
if let peer = transaction.getPeer(entityPeerId) {
|
||||
associatedPeers[peer.id] = peer
|
||||
}
|
||||
}
|
||||
}
|
||||
apiEntities = apiEntitiesFromMessageTextEntities(entities, associatedPeers: SimpleDictionary(associatedPeers))
|
||||
}
|
||||
}
|
||||
if let privacy = privacy {
|
||||
privacyRules = apiInputPrivacyRules(privacy: privacy, transaction: transaction)
|
||||
flags |= 1 << 2
|
||||
}
|
||||
|
||||
return account.network.request(Api.functions.stories.editStory(
|
||||
flags: flags,
|
||||
id: id,
|
||||
media: inputMedia,
|
||||
caption: apiCaption,
|
||||
entities: apiEntities,
|
||||
privacyRules: privacyRules
|
||||
))
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
|> mapToSignal { updates -> Signal<StoryUploadResult, NoError> in
|
||||
if let updates = updates {
|
||||
for update in updates.allUpdates {
|
||||
if case let .updateStory(_, story) = update {
|
||||
switch story {
|
||||
case let .storyItem(_, _, _, _, _, _, media, _, _):
|
||||
let (parsedMedia, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, account.peerId)
|
||||
if let parsedMedia = parsedMedia, let originalMedia = originalMedia {
|
||||
applyMediaResourceChanges(from: originalMedia, to: parsedMedia, postbox: account.postbox, force: false)
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
account.stateManager.addUpdates(updates)
|
||||
}
|
||||
|
||||
return .single(.completed(id))
|
||||
}
|
||||
}
|
||||
|> switchToLatest
|
||||
|
@ -918,8 +918,12 @@ public extension TelegramEngine {
|
||||
}
|
||||
}
|
||||
|
||||
public func uploadStory(media: EngineStoryInputMedia, text: String, entities: [MessageTextEntity], pin: Bool, privacy: EngineStoryPrivacy, period: Int) -> Signal<StoryUploadResult, NoError> {
|
||||
return _internal_uploadStory(account: self.account, media: media, text: text, entities: entities, pin: pin, privacy: privacy, period: period)
|
||||
public func uploadStory(media: EngineStoryInputMedia, text: String, entities: [MessageTextEntity], pin: Bool, privacy: EngineStoryPrivacy, period: Int, randomId: Int64) -> Signal<StoryUploadResult, NoError> {
|
||||
return _internal_uploadStory(account: self.account, media: media, text: text, entities: entities, pin: pin, privacy: privacy, period: period, randomId: randomId)
|
||||
}
|
||||
|
||||
public func editStory(media: EngineStoryInputMedia?, id: Int32, text: String, entities: [MessageTextEntity], privacy: EngineStoryPrivacy?) -> Signal<StoryUploadResult, NoError> {
|
||||
return _internal_editStory(account: self.account, media: media, id: id, text: text, entities: entities, privacy: privacy)
|
||||
}
|
||||
|
||||
public func deleteStory(id: Int32) -> Signal<Never, NoError> {
|
||||
|
@ -236,6 +236,13 @@ public enum PresentationResourceKey: Int32 {
|
||||
case chatMessageAttachedContentHighlightedButtonIconInstantOutgoingWithWallpaper
|
||||
case chatMessageAttachedContentHighlightedButtonIconInstantOutgoingWithoutWallpaper
|
||||
|
||||
case chatMessageAttachedContentButtonIconLinkIncoming
|
||||
case chatMessageAttachedContentHighlightedButtonIconLinkIncomingWithWallpaper
|
||||
case chatMessageAttachedContentHighlightedButtonIconLinkIncomingWithoutWallpaper
|
||||
case chatMessageAttachedContentButtonIconLinkOutgoing
|
||||
case chatMessageAttachedContentHighlightedButtonIconLinkOutgoingWithWallpaper
|
||||
case chatMessageAttachedContentHighlightedButtonIconLinkOutgoingWithoutWallpaper
|
||||
|
||||
case chatCommandPanelArrowImage
|
||||
|
||||
case sharedMediaFileDownloadStartIcon
|
||||
|
@ -854,25 +854,25 @@ public struct PresentationResourcesChat {
|
||||
|
||||
public static func chatMessageAttachedContentButtonIncoming(_ theme: PresentationTheme) -> UIImage? {
|
||||
return theme.image(PresentationResourceKey.chatMessageAttachedContentButtonIncoming.rawValue, { theme in
|
||||
return generateStretchableFilledCircleImage(diameter: 9.0, color: nil, strokeColor: theme.chat.message.incoming.accentControlColor, strokeWidth: 1.0, backgroundColor: nil)
|
||||
return generateStretchableFilledCircleImage(diameter: 16.0, color: nil, strokeColor: theme.chat.message.incoming.accentControlColor, strokeWidth: 1.0, backgroundColor: nil)
|
||||
})
|
||||
}
|
||||
|
||||
public static func chatMessageAttachedContentHighlightedButtonIncoming(_ theme: PresentationTheme) -> UIImage? {
|
||||
return theme.image(PresentationResourceKey.chatMessageAttachedContentHighlightedButtonIncoming.rawValue, { theme in
|
||||
return generateStretchableFilledCircleImage(diameter: 9.0, color: theme.chat.message.incoming.accentControlColor)
|
||||
return generateStretchableFilledCircleImage(diameter: 16.0, color: theme.chat.message.incoming.accentControlColor)
|
||||
})
|
||||
}
|
||||
|
||||
public static func chatMessageAttachedContentButtonOutgoing(_ theme: PresentationTheme) -> UIImage? {
|
||||
return theme.image(PresentationResourceKey.chatMessageAttachedContentButtonOutgoing.rawValue, { theme in
|
||||
return generateStretchableFilledCircleImage(diameter: 9.0, color: nil, strokeColor: theme.chat.message.outgoing.accentControlColor, strokeWidth: 1.0, backgroundColor: nil)
|
||||
return generateStretchableFilledCircleImage(diameter: 16.0, color: nil, strokeColor: theme.chat.message.outgoing.accentControlColor, strokeWidth: 1.0, backgroundColor: nil)
|
||||
})
|
||||
}
|
||||
|
||||
public static func chatMessageAttachedContentHighlightedButtonOutgoing(_ theme: PresentationTheme) -> UIImage? {
|
||||
return theme.image(PresentationResourceKey.chatMessageAttachedContentHighlightedButtonOutgoing.rawValue, { theme in
|
||||
return generateStretchableFilledCircleImage(diameter: 9.0, color: theme.chat.message.outgoing.accentControlColor)
|
||||
return generateStretchableFilledCircleImage(diameter: 16.0, color: theme.chat.message.outgoing.accentControlColor)
|
||||
})
|
||||
}
|
||||
|
||||
@ -902,6 +902,32 @@ public struct PresentationResourcesChat {
|
||||
})
|
||||
}
|
||||
|
||||
public static func chatMessageAttachedContentButtonIconLinkIncoming(_ theme: PresentationTheme) -> UIImage? {
|
||||
return theme.image(PresentationResourceKey.chatMessageAttachedContentButtonIconLinkIncoming.rawValue, { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotLink"), color: theme.chat.message.incoming.accentControlColor)
|
||||
})
|
||||
}
|
||||
|
||||
public static func chatMessageAttachedContentHighlightedButtonIconLinkIncoming(_ theme: PresentationTheme, wallpaper: Bool) -> UIImage? {
|
||||
let key: PresentationResourceKey = !wallpaper ? PresentationResourceKey.chatMessageAttachedContentHighlightedButtonIconLinkIncomingWithoutWallpaper : PresentationResourceKey.chatMessageAttachedContentHighlightedButtonIconLinkIncomingWithWallpaper
|
||||
return theme.image(key.rawValue, { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotLink"), color: bubbleColorComponents(theme: theme, incoming: true, wallpaper: wallpaper).fill[0])
|
||||
})
|
||||
}
|
||||
|
||||
public static func chatMessageAttachedContentButtonIconLinkOutgoing(_ theme: PresentationTheme) -> UIImage? {
|
||||
return theme.image(PresentationResourceKey.chatMessageAttachedContentButtonIconLinkOutgoing.rawValue, { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotLink"), color: theme.chat.message.outgoing.accentControlColor)
|
||||
})
|
||||
}
|
||||
|
||||
public static func chatMessageAttachedContentHighlightedButtonIconLinkOutgoing(_ theme: PresentationTheme, wallpaper: Bool) -> UIImage? {
|
||||
let key: PresentationResourceKey = !wallpaper ? PresentationResourceKey.chatMessageAttachedContentHighlightedButtonIconLinkOutgoingWithoutWallpaper : PresentationResourceKey.chatMessageAttachedContentHighlightedButtonIconLinkOutgoingWithWallpaper
|
||||
return theme.image(key.rawValue, { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotLink"), color: bubbleColorComponents(theme: theme, incoming: false, wallpaper: wallpaper).fill[0])
|
||||
})
|
||||
}
|
||||
|
||||
public static func chatBubbleReplyThumbnailPlayImage(_ theme: PresentationTheme) -> UIImage? {
|
||||
return theme.image(PresentationResourceKey.chatBubbleReplyThumbnailPlayImage.rawValue, { theme in
|
||||
return generateImage(CGSize(width: 16.0, height: 16.0), rotatedContext: { size, context in
|
||||
|
@ -1077,7 +1077,11 @@ public class CameraScreen: ViewController {
|
||||
|
||||
let progress = 1.0 - value
|
||||
let maxScale = (layout.size.width - 16.0 * 2.0) / layout.size.width
|
||||
let maxOffset = -56.0
|
||||
|
||||
let topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 12.0
|
||||
|
||||
let maxOffset = (topInset - (layout.size.height - layout.size.height * maxScale) / 2.0)
|
||||
//let maxOffset = -56.0
|
||||
|
||||
let scale = 1.0 * progress + (1.0 - progress) * maxScale
|
||||
let offset = (1.0 - progress) * maxOffset
|
||||
@ -1129,7 +1133,8 @@ public class CameraScreen: ViewController {
|
||||
self.validLayout = layout
|
||||
|
||||
let previewSize = CGSize(width: layout.size.width, height: floorToScreenPixels(layout.size.width * 1.77778))
|
||||
let topInset: CGFloat = floorToScreenPixels(layout.size.height - previewSize.height) / 2.0
|
||||
let topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 12.0
|
||||
let bottomInset = layout.size.height - previewSize.height - topInset
|
||||
|
||||
let environment = ViewControllerComponentContainer.Environment(
|
||||
statusBarHeight: layout.statusBarHeight ?? 0.0,
|
||||
@ -1137,7 +1142,7 @@ public class CameraScreen: ViewController {
|
||||
safeInsets: UIEdgeInsets(
|
||||
top: topInset,
|
||||
left: layout.safeInsets.left,
|
||||
bottom: topInset,
|
||||
bottom: bottomInset,
|
||||
right: layout.safeInsets.right
|
||||
),
|
||||
inputHeight: layout.inputHeight ?? 0.0,
|
||||
|
@ -54,7 +54,7 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
|
||||
}
|
||||
|
||||
public var baseSize: CGSize {
|
||||
let size = max(10.0, min(self.referenceDrawingSize.width, self.referenceDrawingSize.height) * 0.2)
|
||||
let size = max(10.0, min(self.referenceDrawingSize.width, self.referenceDrawingSize.height) * 0.25)
|
||||
return CGSize(width: size, height: size)
|
||||
}
|
||||
|
||||
|
@ -82,6 +82,11 @@ public final class MediaEditor {
|
||||
}
|
||||
set {
|
||||
self.histogramCalculationPass.isEnabled = newValue
|
||||
if newValue {
|
||||
Queue.mainQueue().justDispatch {
|
||||
self.updateRenderChain()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,10 +7,6 @@ import MetalKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import AnimatedStickerNode
|
||||
import TelegramAnimatedStickerNode
|
||||
import YuvConversion
|
||||
import StickerResources
|
||||
|
||||
public func mediaEditorGenerateGradientImage(size: CGSize, colors: [UIColor]) -> UIImage? {
|
||||
UIGraphicsBeginImageContextWithOptions(size, false, 1.0)
|
||||
@ -280,298 +276,3 @@ private func makeEditorImageFrameComposition(inputImage: CIImage, gradientImage:
|
||||
}
|
||||
maybeFinalize()
|
||||
}
|
||||
|
||||
private func composerEntityForDrawingEntity(account: Account, entity: DrawingEntity, colorSpace: CGColorSpace) -> MediaEditorComposerEntity? {
|
||||
if let entity = entity as? DrawingStickerEntity {
|
||||
let content: MediaEditorComposerStickerEntity.Content
|
||||
switch entity.content {
|
||||
case let .file(file):
|
||||
content = .file(file)
|
||||
case let .image(image):
|
||||
content = .image(image)
|
||||
}
|
||||
return MediaEditorComposerStickerEntity(account: account, content: content, position: entity.position, scale: entity.scale, rotation: entity.rotation, baseSize: entity.baseSize, mirrored: entity.mirrored, colorSpace: colorSpace)
|
||||
} else if let renderImage = entity.renderImage, let image = CIImage(image: renderImage, options: [.colorSpace: colorSpace]) {
|
||||
if let entity = entity as? DrawingBubbleEntity {
|
||||
return MediaEditorComposerStaticEntity(image: image, position: entity.position, scale: 1.0, rotation: entity.rotation, baseSize: entity.size, mirrored: false)
|
||||
} else if let entity = entity as? DrawingSimpleShapeEntity {
|
||||
return MediaEditorComposerStaticEntity(image: image, position: entity.position, scale: 1.0, rotation: entity.rotation, baseSize: entity.size, mirrored: false)
|
||||
} else if let entity = entity as? DrawingVectorEntity {
|
||||
return MediaEditorComposerStaticEntity(image: image, position: CGPoint(x: entity.drawingSize.width * 0.5, y: entity.drawingSize.height * 0.5), scale: 1.0, rotation: 0.0, baseSize: entity.drawingSize, mirrored: false)
|
||||
} else if let entity = entity as? DrawingTextEntity {
|
||||
return MediaEditorComposerStaticEntity(image: image, position: entity.position, scale: entity.scale, rotation: entity.rotation, baseSize: nil, mirrored: false)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private class MediaEditorComposerStaticEntity: MediaEditorComposerEntity {
|
||||
let image: CIImage
|
||||
let position: CGPoint
|
||||
let scale: CGFloat
|
||||
let rotation: CGFloat
|
||||
let baseSize: CGSize?
|
||||
let mirrored: Bool
|
||||
|
||||
init(image: CIImage, position: CGPoint, scale: CGFloat, rotation: CGFloat, baseSize: CGSize?, mirrored: Bool) {
|
||||
self.image = image
|
||||
self.position = position
|
||||
self.scale = scale
|
||||
self.rotation = rotation
|
||||
self.baseSize = baseSize
|
||||
self.mirrored = mirrored
|
||||
}
|
||||
|
||||
func image(for time: CMTime, frameRate: Float, completion: @escaping (CIImage?) -> Void) {
|
||||
completion(self.image)
|
||||
}
|
||||
}
|
||||
|
||||
private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity {
|
||||
public enum Content {
|
||||
case file(TelegramMediaFile)
|
||||
case image(UIImage)
|
||||
|
||||
var file: TelegramMediaFile? {
|
||||
if case let .file(file) = self {
|
||||
return file
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
let content: Content
|
||||
let position: CGPoint
|
||||
let scale: CGFloat
|
||||
let rotation: CGFloat
|
||||
let baseSize: CGSize?
|
||||
let mirrored: Bool
|
||||
let colorSpace: CGColorSpace
|
||||
|
||||
var isAnimated: Bool
|
||||
var source: AnimatedStickerNodeSource?
|
||||
var frameSource = Promise<QueueLocalObject<AnimatedStickerDirectFrameSource>?>()
|
||||
|
||||
var frameCount: Int?
|
||||
var frameRate: Int?
|
||||
var currentFrameIndex: Int?
|
||||
var totalDuration: Double?
|
||||
let durationPromise = Promise<Double>()
|
||||
|
||||
let queue = Queue()
|
||||
let disposables = DisposableSet()
|
||||
|
||||
var image: CIImage?
|
||||
var imagePixelBuffer: CVPixelBuffer?
|
||||
let imagePromise = Promise<UIImage>()
|
||||
|
||||
init(account: Account, content: Content, position: CGPoint, scale: CGFloat, rotation: CGFloat, baseSize: CGSize, mirrored: Bool, colorSpace: CGColorSpace) {
|
||||
self.content = content
|
||||
self.position = position
|
||||
self.scale = scale
|
||||
self.rotation = rotation
|
||||
self.baseSize = baseSize
|
||||
self.mirrored = mirrored
|
||||
self.colorSpace = colorSpace
|
||||
|
||||
switch content {
|
||||
case let .file(file):
|
||||
if file.isAnimatedSticker || file.isVideoSticker || file.mimeType == "video/webm" {
|
||||
self.isAnimated = true
|
||||
self.source = AnimatedStickerResourceSource(account: account, resource: file.resource, isVideo: file.isVideoSticker || file.mimeType == "video/webm")
|
||||
let pathPrefix = account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id)
|
||||
if let source = self.source {
|
||||
let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512)
|
||||
let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 384, height: 384))
|
||||
self.disposables.add((source.directDataPath(attemptSynchronously: true)
|
||||
|> deliverOn(self.queue)).start(next: { [weak self] path in
|
||||
if let strongSelf = self, let path {
|
||||
if let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedRead]) {
|
||||
let queue = strongSelf.queue
|
||||
let frameSource = QueueLocalObject<AnimatedStickerDirectFrameSource>(queue: queue, generate: {
|
||||
return AnimatedStickerDirectFrameSource(queue: queue, data: data, width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), cachePathPrefix: pathPrefix, useMetalCache: false, fitzModifier: nil)!
|
||||
})
|
||||
frameSource.syncWith { frameSource in
|
||||
strongSelf.frameCount = frameSource.frameCount
|
||||
strongSelf.frameRate = frameSource.frameRate
|
||||
|
||||
let duration = Double(frameSource.frameCount) / Double(frameSource.frameRate)
|
||||
strongSelf.totalDuration = duration
|
||||
strongSelf.durationPromise.set(.single(duration))
|
||||
}
|
||||
|
||||
strongSelf.frameSource.set(.single(frameSource))
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
} else {
|
||||
self.isAnimated = false
|
||||
self.disposables.add((chatMessageSticker(account: account, userLocation: .other, file: file, small: false, fetched: true, onlyFullSize: true, thumbnail: false, synchronousLoad: false, colorSpace: self.colorSpace)
|
||||
|> deliverOn(self.queue)).start(next: { [weak self] generator in
|
||||
if let self {
|
||||
let context = generator(TransformImageArguments(corners: ImageCorners(), imageSize: baseSize, boundingSize: baseSize, intrinsicInsets: UIEdgeInsets()))
|
||||
let image = context?.generateImage(colorSpace: self.colorSpace)
|
||||
if let image {
|
||||
self.imagePromise.set(.single(image))
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
case let .image(image):
|
||||
self.isAnimated = false
|
||||
self.imagePromise.set(.single(image))
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.disposables.dispose()
|
||||
}
|
||||
|
||||
var tested = false
|
||||
func image(for time: CMTime, frameRate: Float, completion: @escaping (CIImage?) -> Void) {
|
||||
if self.isAnimated {
|
||||
let currentTime = CMTimeGetSeconds(time)
|
||||
|
||||
var tintColor: UIColor?
|
||||
if let file = self.content.file, file.isCustomTemplateEmoji {
|
||||
tintColor = .white
|
||||
}
|
||||
|
||||
self.disposables.add((self.frameSource.get()
|
||||
|> take(1)
|
||||
|> deliverOn(self.queue)).start(next: { [weak self] frameSource in
|
||||
guard let strongSelf = self else {
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
|
||||
guard let frameSource, let duration = strongSelf.totalDuration, let frameCount = strongSelf.frameCount else {
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
|
||||
let relativeTime = currentTime - floor(currentTime / duration) * duration
|
||||
var t = relativeTime / duration
|
||||
t = max(0.0, t)
|
||||
t = min(1.0, t)
|
||||
|
||||
let startFrame: Double = 0
|
||||
let endFrame = Double(frameCount)
|
||||
|
||||
let frameOffset = Int(Double(startFrame) * (1.0 - t) + Double(endFrame - 1) * t)
|
||||
let lowerBound: Int = 0
|
||||
let upperBound = frameCount - 1
|
||||
let frameIndex = max(lowerBound, min(upperBound, frameOffset))
|
||||
|
||||
let currentFrameIndex = strongSelf.currentFrameIndex
|
||||
if currentFrameIndex != frameIndex {
|
||||
let previousFrameIndex = currentFrameIndex
|
||||
strongSelf.currentFrameIndex = frameIndex
|
||||
|
||||
var delta = 1
|
||||
if let previousFrameIndex = previousFrameIndex {
|
||||
delta = max(1, frameIndex - previousFrameIndex)
|
||||
}
|
||||
|
||||
var frame: AnimatedStickerFrame?
|
||||
frameSource.syncWith { frameSource in
|
||||
for i in 0 ..< delta {
|
||||
frame = frameSource.takeFrame(draw: i == delta - 1)
|
||||
}
|
||||
}
|
||||
if let frame {
|
||||
var imagePixelBuffer: CVPixelBuffer?
|
||||
if let pixelBuffer = strongSelf.imagePixelBuffer {
|
||||
imagePixelBuffer = pixelBuffer
|
||||
} else {
|
||||
let ioSurfaceProperties = NSMutableDictionary()
|
||||
let options = NSMutableDictionary()
|
||||
options.setObject(ioSurfaceProperties, forKey: kCVPixelBufferIOSurfacePropertiesKey as NSString)
|
||||
|
||||
var pixelBuffer: CVPixelBuffer?
|
||||
CVPixelBufferCreate(
|
||||
kCFAllocatorDefault,
|
||||
frame.width,
|
||||
frame.height,
|
||||
kCVPixelFormatType_32BGRA,
|
||||
options,
|
||||
&pixelBuffer
|
||||
)
|
||||
|
||||
imagePixelBuffer = pixelBuffer
|
||||
strongSelf.imagePixelBuffer = pixelBuffer
|
||||
}
|
||||
|
||||
if let imagePixelBuffer {
|
||||
let image = render(width: frame.width, height: frame.height, bytesPerRow: frame.bytesPerRow, data: frame.data, type: frame.type, pixelBuffer: imagePixelBuffer, colorSpace: strongSelf.colorSpace, tintColor: tintColor)
|
||||
strongSelf.image = image
|
||||
}
|
||||
completion(strongSelf.image)
|
||||
} else {
|
||||
completion(nil)
|
||||
}
|
||||
} else {
|
||||
completion(strongSelf.image)
|
||||
}
|
||||
}))
|
||||
} else {
|
||||
var image: CIImage?
|
||||
if let cachedImage = self.image {
|
||||
image = cachedImage
|
||||
completion(image)
|
||||
} else {
|
||||
let _ = (self.imagePromise.get()
|
||||
|> take(1)
|
||||
|> deliverOn(self.queue)).start(next: { [weak self] image in
|
||||
if let self {
|
||||
self.image = CIImage(image: image, options: [.colorSpace: self.colorSpace])
|
||||
completion(self.image)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protocol MediaEditorComposerEntity {
|
||||
var position: CGPoint { get }
|
||||
var scale: CGFloat { get }
|
||||
var rotation: CGFloat { get }
|
||||
var baseSize: CGSize? { get }
|
||||
var mirrored: Bool { get }
|
||||
|
||||
func image(for time: CMTime, frameRate: Float, completion: @escaping (CIImage?) -> Void)
|
||||
}
|
||||
|
||||
private func render(width: Int, height: Int, bytesPerRow: Int, data: Data, type: AnimationRendererFrameType, pixelBuffer: CVPixelBuffer, colorSpace: CGColorSpace, tintColor: UIColor?) -> CIImage? {
|
||||
//let calculatedBytesPerRow = (4 * Int(width) + 31) & (~31)
|
||||
//assert(bytesPerRow == calculatedBytesPerRow)
|
||||
|
||||
|
||||
CVPixelBufferLockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0))
|
||||
let dest = CVPixelBufferGetBaseAddress(pixelBuffer)
|
||||
|
||||
switch type {
|
||||
case .yuva:
|
||||
data.withUnsafeBytes { buffer -> Void in
|
||||
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||
return
|
||||
}
|
||||
decodeYUVAToRGBA(bytes, dest, Int32(width), Int32(height), Int32(width * 4))
|
||||
}
|
||||
case .argb:
|
||||
data.withUnsafeBytes { buffer -> Void in
|
||||
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||
return
|
||||
}
|
||||
memcpy(dest, bytes, data.count)
|
||||
}
|
||||
case .dct:
|
||||
break
|
||||
}
|
||||
|
||||
CVPixelBufferUnlockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0))
|
||||
|
||||
return CIImage(cvPixelBuffer: pixelBuffer, options: [.colorSpace: colorSpace])
|
||||
}
|
||||
|
@ -0,0 +1,329 @@
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
import UIKit
|
||||
import CoreImage
|
||||
import Metal
|
||||
import MetalKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import AnimatedStickerNode
|
||||
import TelegramAnimatedStickerNode
|
||||
import YuvConversion
|
||||
import StickerResources
|
||||
|
||||
func composerEntityForDrawingEntity(account: Account, entity: DrawingEntity, colorSpace: CGColorSpace) -> MediaEditorComposerEntity? {
|
||||
if let entity = entity as? DrawingStickerEntity {
|
||||
let content: MediaEditorComposerStickerEntity.Content
|
||||
switch entity.content {
|
||||
case let .file(file):
|
||||
content = .file(file)
|
||||
case let .image(image):
|
||||
content = .image(image)
|
||||
}
|
||||
return MediaEditorComposerStickerEntity(account: account, content: content, position: entity.position, scale: entity.scale, rotation: entity.rotation, baseSize: entity.baseSize, mirrored: entity.mirrored, colorSpace: colorSpace)
|
||||
} else if let renderImage = entity.renderImage, let image = CIImage(image: renderImage, options: [.colorSpace: colorSpace]) {
|
||||
if let entity = entity as? DrawingBubbleEntity {
|
||||
return MediaEditorComposerStaticEntity(image: image, position: entity.position, scale: 1.0, rotation: entity.rotation, baseSize: entity.size, mirrored: false)
|
||||
} else if let entity = entity as? DrawingSimpleShapeEntity {
|
||||
return MediaEditorComposerStaticEntity(image: image, position: entity.position, scale: 1.0, rotation: entity.rotation, baseSize: entity.size, mirrored: false)
|
||||
} else if let entity = entity as? DrawingVectorEntity {
|
||||
return MediaEditorComposerStaticEntity(image: image, position: CGPoint(x: entity.drawingSize.width * 0.5, y: entity.drawingSize.height * 0.5), scale: 1.0, rotation: 0.0, baseSize: entity.drawingSize, mirrored: false)
|
||||
} else if let entity = entity as? DrawingTextEntity {
|
||||
return MediaEditorComposerStaticEntity(image: image, position: entity.position, scale: entity.scale, rotation: entity.rotation, baseSize: nil, mirrored: false)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private class MediaEditorComposerStaticEntity: MediaEditorComposerEntity {
|
||||
let image: CIImage
|
||||
let position: CGPoint
|
||||
let scale: CGFloat
|
||||
let rotation: CGFloat
|
||||
let baseSize: CGSize?
|
||||
let mirrored: Bool
|
||||
|
||||
init(image: CIImage, position: CGPoint, scale: CGFloat, rotation: CGFloat, baseSize: CGSize?, mirrored: Bool) {
|
||||
self.image = image
|
||||
self.position = position
|
||||
self.scale = scale
|
||||
self.rotation = rotation
|
||||
self.baseSize = baseSize
|
||||
self.mirrored = mirrored
|
||||
}
|
||||
|
||||
func image(for time: CMTime, frameRate: Float, completion: @escaping (CIImage?) -> Void) {
|
||||
completion(self.image)
|
||||
}
|
||||
}
|
||||
|
||||
private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity {
|
||||
public enum Content {
|
||||
case file(TelegramMediaFile)
|
||||
case image(UIImage)
|
||||
|
||||
var file: TelegramMediaFile? {
|
||||
if case let .file(file) = self {
|
||||
return file
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
let content: Content
|
||||
let position: CGPoint
|
||||
let scale: CGFloat
|
||||
let rotation: CGFloat
|
||||
let baseSize: CGSize?
|
||||
let mirrored: Bool
|
||||
let colorSpace: CGColorSpace
|
||||
|
||||
var isAnimated: Bool
|
||||
var source: AnimatedStickerNodeSource?
|
||||
var frameSource = Promise<QueueLocalObject<AnimatedStickerDirectFrameSource>?>()
|
||||
var videoFrameSource = Promise<QueueLocalObject<VideoStickerDirectFrameSource>?>()
|
||||
var isVideo = false
|
||||
|
||||
var frameCount: Int?
|
||||
var frameRate: Int?
|
||||
var currentFrameIndex: Int?
|
||||
var totalDuration: Double?
|
||||
let durationPromise = Promise<Double>()
|
||||
|
||||
let queue = Queue()
|
||||
let disposables = DisposableSet()
|
||||
|
||||
var image: CIImage?
|
||||
var imagePixelBuffer: CVPixelBuffer?
|
||||
let imagePromise = Promise<UIImage>()
|
||||
|
||||
init(account: Account, content: Content, position: CGPoint, scale: CGFloat, rotation: CGFloat, baseSize: CGSize, mirrored: Bool, colorSpace: CGColorSpace) {
|
||||
self.content = content
|
||||
self.position = position
|
||||
self.scale = scale
|
||||
self.rotation = rotation
|
||||
self.baseSize = baseSize
|
||||
self.mirrored = mirrored
|
||||
self.colorSpace = colorSpace
|
||||
|
||||
switch content {
|
||||
case let .file(file):
|
||||
if file.isAnimatedSticker || file.isVideoSticker || file.mimeType == "video/webm" {
|
||||
self.isAnimated = true
|
||||
self.isVideo = file.isVideoSticker || file.mimeType == "video/webm"
|
||||
|
||||
self.source = AnimatedStickerResourceSource(account: account, resource: file.resource, isVideo: isVideo)
|
||||
let pathPrefix = account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id)
|
||||
if let source = self.source {
|
||||
let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512)
|
||||
let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 384, height: 384))
|
||||
self.disposables.add((source.directDataPath(attemptSynchronously: true)
|
||||
|> deliverOn(self.queue)).start(next: { [weak self] path in
|
||||
if let strongSelf = self, let path {
|
||||
if let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedRead]) {
|
||||
let queue = strongSelf.queue
|
||||
|
||||
if strongSelf.isVideo {
|
||||
let frameSource = QueueLocalObject<VideoStickerDirectFrameSource>(queue: queue, generate: {
|
||||
return VideoStickerDirectFrameSource(queue: queue, path: path, width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), cachePathPrefix: pathPrefix, unpremultiplyAlpha: false)!
|
||||
})
|
||||
frameSource.syncWith { frameSource in
|
||||
strongSelf.frameCount = frameSource.frameCount
|
||||
strongSelf.frameRate = frameSource.frameRate
|
||||
|
||||
let duration = Double(frameSource.frameCount) / Double(frameSource.frameRate)
|
||||
strongSelf.totalDuration = duration
|
||||
strongSelf.durationPromise.set(.single(duration))
|
||||
}
|
||||
|
||||
strongSelf.videoFrameSource.set(.single(frameSource))
|
||||
} else {
|
||||
let frameSource = QueueLocalObject<AnimatedStickerDirectFrameSource>(queue: queue, generate: {
|
||||
return AnimatedStickerDirectFrameSource(queue: queue, data: data, width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), cachePathPrefix: pathPrefix, useMetalCache: false, fitzModifier: nil)!
|
||||
})
|
||||
frameSource.syncWith { frameSource in
|
||||
strongSelf.frameCount = frameSource.frameCount
|
||||
strongSelf.frameRate = frameSource.frameRate
|
||||
|
||||
let duration = Double(frameSource.frameCount) / Double(frameSource.frameRate)
|
||||
strongSelf.totalDuration = duration
|
||||
strongSelf.durationPromise.set(.single(duration))
|
||||
}
|
||||
|
||||
strongSelf.frameSource.set(.single(frameSource))
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
} else {
|
||||
self.isAnimated = false
|
||||
self.disposables.add((chatMessageSticker(account: account, userLocation: .other, file: file, small: false, fetched: true, onlyFullSize: true, thumbnail: false, synchronousLoad: false, colorSpace: self.colorSpace)
|
||||
|> deliverOn(self.queue)).start(next: { [weak self] generator in
|
||||
if let self {
|
||||
let context = generator(TransformImageArguments(corners: ImageCorners(), imageSize: baseSize, boundingSize: baseSize, intrinsicInsets: UIEdgeInsets()))
|
||||
let image = context?.generateImage(colorSpace: self.colorSpace)
|
||||
if let image {
|
||||
self.imagePromise.set(.single(image))
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
case let .image(image):
|
||||
self.isAnimated = false
|
||||
self.imagePromise.set(.single(image))
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.disposables.dispose()
|
||||
}
|
||||
|
||||
var tested = false
|
||||
func image(for time: CMTime, frameRate: Float, completion: @escaping (CIImage?) -> Void) {
|
||||
if self.isAnimated {
|
||||
let currentTime = CMTimeGetSeconds(time)
|
||||
|
||||
var tintColor: UIColor?
|
||||
if let file = self.content.file, file.isCustomTemplateEmoji {
|
||||
tintColor = .white
|
||||
}
|
||||
|
||||
self.disposables.add((self.frameSource.get()
|
||||
|> take(1)
|
||||
|> deliverOn(self.queue)).start(next: { [weak self] frameSource in
|
||||
guard let strongSelf = self else {
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
|
||||
guard let frameSource, let duration = strongSelf.totalDuration, let frameCount = strongSelf.frameCount else {
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
|
||||
let relativeTime = currentTime - floor(currentTime / duration) * duration
|
||||
var t = relativeTime / duration
|
||||
t = max(0.0, t)
|
||||
t = min(1.0, t)
|
||||
|
||||
let startFrame: Double = 0
|
||||
let endFrame = Double(frameCount)
|
||||
|
||||
let frameOffset = Int(Double(startFrame) * (1.0 - t) + Double(endFrame - 1) * t)
|
||||
let lowerBound: Int = 0
|
||||
let upperBound = frameCount - 1
|
||||
let frameIndex = max(lowerBound, min(upperBound, frameOffset))
|
||||
|
||||
let currentFrameIndex = strongSelf.currentFrameIndex
|
||||
if currentFrameIndex != frameIndex {
|
||||
let previousFrameIndex = currentFrameIndex
|
||||
strongSelf.currentFrameIndex = frameIndex
|
||||
|
||||
var delta = 1
|
||||
if let previousFrameIndex = previousFrameIndex {
|
||||
delta = max(1, frameIndex - previousFrameIndex)
|
||||
}
|
||||
|
||||
var frame: AnimatedStickerFrame?
|
||||
frameSource.syncWith { frameSource in
|
||||
for i in 0 ..< delta {
|
||||
frame = frameSource.takeFrame(draw: i == delta - 1)
|
||||
}
|
||||
}
|
||||
if let frame {
|
||||
var imagePixelBuffer: CVPixelBuffer?
|
||||
if let pixelBuffer = strongSelf.imagePixelBuffer {
|
||||
imagePixelBuffer = pixelBuffer
|
||||
} else {
|
||||
let ioSurfaceProperties = NSMutableDictionary()
|
||||
let options = NSMutableDictionary()
|
||||
options.setObject(ioSurfaceProperties, forKey: kCVPixelBufferIOSurfacePropertiesKey as NSString)
|
||||
|
||||
var pixelBuffer: CVPixelBuffer?
|
||||
CVPixelBufferCreate(
|
||||
kCFAllocatorDefault,
|
||||
frame.width,
|
||||
frame.height,
|
||||
kCVPixelFormatType_32BGRA,
|
||||
options,
|
||||
&pixelBuffer
|
||||
)
|
||||
|
||||
imagePixelBuffer = pixelBuffer
|
||||
strongSelf.imagePixelBuffer = pixelBuffer
|
||||
}
|
||||
|
||||
if let imagePixelBuffer {
|
||||
let image = render(width: frame.width, height: frame.height, bytesPerRow: frame.bytesPerRow, data: frame.data, type: frame.type, pixelBuffer: imagePixelBuffer, tintColor: tintColor)
|
||||
strongSelf.image = image
|
||||
}
|
||||
completion(strongSelf.image)
|
||||
} else {
|
||||
completion(nil)
|
||||
}
|
||||
} else {
|
||||
completion(strongSelf.image)
|
||||
}
|
||||
}))
|
||||
} else {
|
||||
var image: CIImage?
|
||||
if let cachedImage = self.image {
|
||||
image = cachedImage
|
||||
completion(image)
|
||||
} else {
|
||||
let _ = (self.imagePromise.get()
|
||||
|> take(1)
|
||||
|> deliverOn(self.queue)).start(next: { [weak self] image in
|
||||
if let self {
|
||||
self.image = CIImage(image: image, options: [.colorSpace: self.colorSpace])
|
||||
completion(self.image)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protocol MediaEditorComposerEntity {
|
||||
var position: CGPoint { get }
|
||||
var scale: CGFloat { get }
|
||||
var rotation: CGFloat { get }
|
||||
var baseSize: CGSize? { get }
|
||||
var mirrored: Bool { get }
|
||||
|
||||
func image(for time: CMTime, frameRate: Float, completion: @escaping (CIImage?) -> Void)
|
||||
}
|
||||
|
||||
private func render(width: Int, height: Int, bytesPerRow: Int, data: Data, type: AnimationRendererFrameType, pixelBuffer: CVPixelBuffer, tintColor: UIColor?) -> CIImage? {
|
||||
//let calculatedBytesPerRow = (4 * Int(width) + 31) & (~31)
|
||||
//assert(bytesPerRow == calculatedBytesPerRow)
|
||||
|
||||
|
||||
CVPixelBufferLockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0))
|
||||
let dest = CVPixelBufferGetBaseAddress(pixelBuffer)
|
||||
|
||||
switch type {
|
||||
case .yuva:
|
||||
data.withUnsafeBytes { buffer -> Void in
|
||||
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||
return
|
||||
}
|
||||
decodeYUVAToRGBA(bytes, dest, Int32(width), Int32(height), Int32(width * 4))
|
||||
}
|
||||
case .argb:
|
||||
data.withUnsafeBytes { buffer -> Void in
|
||||
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||
return
|
||||
}
|
||||
memcpy(dest, bytes, data.count)
|
||||
}
|
||||
case .dct:
|
||||
break
|
||||
}
|
||||
|
||||
CVPixelBufferUnlockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0))
|
||||
|
||||
return CIImage(cvPixelBuffer: pixelBuffer, options: [.colorSpace: deviceColorSpace])
|
||||
}
|
@ -681,7 +681,7 @@ public extension MediaEditorValues {
|
||||
}
|
||||
|
||||
var hasBlur: Bool {
|
||||
if let blurValue = self.toolValues[.blur] as? BlurValue, blurValue.mode != .off || blurValue.intensity > toolEpsilon {
|
||||
if let blurValue = self.toolValues[.blur] as? BlurValue, blurValue.mode != .off && blurValue.intensity > toolEpsilon {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
|
@ -23,6 +23,7 @@ swift_library(
|
||||
"//submodules/AccountContext:AccountContext",
|
||||
"//submodules/AppBundle:AppBundle",
|
||||
"//submodules/TelegramStringFormatting:TelegramStringFormatting",
|
||||
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
|
||||
"//submodules/PresentationDataUtils:PresentationDataUtils",
|
||||
"//submodules/ContextUI",
|
||||
"//submodules/LegacyComponents:LegacyComponents",
|
||||
|
@ -650,14 +650,28 @@ final class MediaEditorScreenComponent: Component {
|
||||
|
||||
}
|
||||
|
||||
let timeoutValue: Int32
|
||||
var timeoutValue: String
|
||||
let timeoutSelected: Bool
|
||||
switch component.privacy {
|
||||
case let .story(_, archive):
|
||||
timeoutValue = 24
|
||||
timeoutSelected = !archive
|
||||
case let .story(_, timeout, archive):
|
||||
switch timeout {
|
||||
case 21600:
|
||||
timeoutValue = "6"
|
||||
case 43200:
|
||||
timeoutValue = "12"
|
||||
case 86400:
|
||||
timeoutValue = "24"
|
||||
case 172800:
|
||||
timeoutValue = "2d"
|
||||
default:
|
||||
timeoutValue = "24"
|
||||
}
|
||||
if archive {
|
||||
timeoutValue = "∞"
|
||||
}
|
||||
timeoutSelected = false
|
||||
case let .message(_, timeout):
|
||||
timeoutValue = timeout ?? 1
|
||||
timeoutValue = "\(timeout ?? 1)"
|
||||
timeoutSelected = timeout != nil
|
||||
}
|
||||
|
||||
@ -671,6 +685,7 @@ final class MediaEditorScreenComponent: Component {
|
||||
strings: environment.strings,
|
||||
style: .editor,
|
||||
placeholder: "Add a caption...",
|
||||
alwaysDarkWhenHasText: false,
|
||||
presentController: { [weak self] c in
|
||||
guard let self, let _ = self.component else {
|
||||
return
|
||||
@ -693,13 +708,7 @@ final class MediaEditorScreenComponent: Component {
|
||||
guard let self, let controller = self.environment?.controller() as? MediaEditorScreen else {
|
||||
return
|
||||
}
|
||||
switch controller.state.privacy {
|
||||
case let .story(privacy, archive):
|
||||
controller.state.privacy = .story(privacy: privacy, archive: !archive)
|
||||
controller.node.presentStoryArchiveTooltip(sourceView: view)
|
||||
case .message:
|
||||
controller.presentTimeoutSetup(sourceView: view)
|
||||
}
|
||||
controller.presentTimeoutSetup(sourceView: view)
|
||||
},
|
||||
audioRecorder: nil,
|
||||
videoRecordingStatus: nil,
|
||||
@ -741,7 +750,7 @@ final class MediaEditorScreenComponent: Component {
|
||||
|
||||
let privacyText: String
|
||||
switch component.privacy {
|
||||
case let .story(privacy, _):
|
||||
case let .story(privacy, _, _):
|
||||
switch privacy.base {
|
||||
case .everyone:
|
||||
privacyText = "Everyone"
|
||||
@ -773,7 +782,7 @@ final class MediaEditorScreenComponent: Component {
|
||||
),
|
||||
action: {
|
||||
if let controller = environment.controller() as? MediaEditorScreen {
|
||||
controller.presentPrivacySettings()
|
||||
controller.openPrivacySettings()
|
||||
}
|
||||
}
|
||||
).tagged(privacyButtonTag)),
|
||||
@ -1026,8 +1035,8 @@ private let storyDimensions = CGSize(width: 1080.0, height: 1920.0)
|
||||
private let storyMaxVideoDuration: Double = 60.0
|
||||
|
||||
public enum MediaEditorResultPrivacy: Equatable {
|
||||
case story(privacy: EngineStoryPrivacy, archive: Bool)
|
||||
case message(peers: [EnginePeer.Id], timeout: Int32?)
|
||||
case story(privacy: EngineStoryPrivacy, timeout: Int, archive: Bool)
|
||||
case message(peers: [EnginePeer.Id], timeout: Int?)
|
||||
}
|
||||
|
||||
public final class MediaEditorScreen: ViewController {
|
||||
@ -1069,7 +1078,7 @@ public final class MediaEditorScreen: ViewController {
|
||||
}
|
||||
|
||||
struct State {
|
||||
var privacy: MediaEditorResultPrivacy = .story(privacy: EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []), archive: false)
|
||||
var privacy: MediaEditorResultPrivacy = .story(privacy: EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []), timeout: 86400, archive: false)
|
||||
}
|
||||
|
||||
var state = State() {
|
||||
@ -1295,7 +1304,7 @@ public final class MediaEditorScreen: ViewController {
|
||||
}
|
||||
|
||||
let initialValues: MediaEditorValues?
|
||||
if case let .draft(draft) = subject {
|
||||
if case let .draft(draft, _) = subject {
|
||||
initialValues = draft.values
|
||||
|
||||
for entity in draft.values.entities {
|
||||
@ -1567,6 +1576,10 @@ public final class MediaEditorScreen: ViewController {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if let view = self.componentHost.view as? MediaEditorScreenComponent.View {
|
||||
view.animateIn(from: .camera)
|
||||
}
|
||||
}
|
||||
|
||||
// Queue.mainQueue().after(0.5) {
|
||||
@ -1757,6 +1770,34 @@ public final class MediaEditorScreen: ViewController {
|
||||
}
|
||||
}
|
||||
|
||||
func updateEditProgress(_ progress: Float) {
|
||||
guard let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
|
||||
if let saveTooltip = self.saveTooltip {
|
||||
if case .completion = saveTooltip.content {
|
||||
saveTooltip.dismiss()
|
||||
self.saveTooltip = nil
|
||||
}
|
||||
}
|
||||
|
||||
let text = "Uploading..."
|
||||
|
||||
if let tooltipController = self.saveTooltip {
|
||||
tooltipController.content = .progress(text, progress)
|
||||
} else {
|
||||
let tooltipController = SaveProgressScreen(context: self.context, content: .progress(text, 0.0))
|
||||
tooltipController.cancelled = { [weak self] in
|
||||
if let self, let controller = self.controller {
|
||||
controller.cancelVideoExport()
|
||||
}
|
||||
}
|
||||
controller.present(tooltipController, in: .current)
|
||||
self.saveTooltip = tooltipController
|
||||
}
|
||||
}
|
||||
|
||||
func updateVideoExportProgress(_ progress: Float) {
|
||||
guard let controller = self.controller else {
|
||||
return
|
||||
@ -1787,7 +1828,7 @@ public final class MediaEditorScreen: ViewController {
|
||||
|
||||
private weak var storyArchiveTooltip: ViewController?
|
||||
func presentStoryArchiveTooltip(sourceView: UIView) {
|
||||
guard let controller = self.controller, case let .story(_, archive) = controller.state.privacy else {
|
||||
guard let controller = self.controller, case let .story(_, _, archive) = controller.state.privacy else {
|
||||
return
|
||||
}
|
||||
|
||||
@ -1838,7 +1879,8 @@ public final class MediaEditorScreen: ViewController {
|
||||
self.validLayout = layout
|
||||
|
||||
let previewSize = CGSize(width: layout.size.width, height: floorToScreenPixels(layout.size.width * 1.77778))
|
||||
let topInset: CGFloat = floorToScreenPixels(layout.size.height - previewSize.height) / 2.0
|
||||
let topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 12.0 //floorToScreenPixels(layout.size.height - previewSize.height) / 2.0
|
||||
let bottomInset = layout.size.height - previewSize.height - topInset
|
||||
|
||||
let environment = ViewControllerComponentContainer.Environment(
|
||||
statusBarHeight: layout.statusBarHeight ?? 0.0,
|
||||
@ -1846,7 +1888,7 @@ public final class MediaEditorScreen: ViewController {
|
||||
safeInsets: UIEdgeInsets(
|
||||
top: topInset,
|
||||
left: layout.safeInsets.left,
|
||||
bottom: topInset,
|
||||
bottom: bottomInset,
|
||||
right: layout.safeInsets.right
|
||||
),
|
||||
inputHeight: layout.inputHeight ?? 0.0,
|
||||
@ -2022,7 +2064,7 @@ public final class MediaEditorScreen: ViewController {
|
||||
if self.entitiesView.selectedEntityView != nil || self.isDisplayingTool {
|
||||
bottomInputOffset = inputHeight / 2.0
|
||||
} else {
|
||||
bottomInputOffset = inputHeight - topInset - 17.0
|
||||
bottomInputOffset = inputHeight - bottomInset - 17.0
|
||||
}
|
||||
}
|
||||
|
||||
@ -2052,7 +2094,7 @@ public final class MediaEditorScreen: ViewController {
|
||||
case image(UIImage, PixelDimensions)
|
||||
case video(String, UIImage?, PixelDimensions)
|
||||
case asset(PHAsset)
|
||||
case draft(MediaEditorDraft)
|
||||
case draft(MediaEditorDraft, Int64?)
|
||||
|
||||
var dimensions: PixelDimensions {
|
||||
switch self {
|
||||
@ -2060,7 +2102,7 @@ public final class MediaEditorScreen: ViewController {
|
||||
return dimensions
|
||||
case let .asset(asset):
|
||||
return PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight))
|
||||
case let .draft(draft):
|
||||
case let .draft(draft, _):
|
||||
return draft.dimensions
|
||||
}
|
||||
}
|
||||
@ -2073,7 +2115,7 @@ public final class MediaEditorScreen: ViewController {
|
||||
return .video(videoPath, transitionImage, dimensions)
|
||||
case let .asset(asset):
|
||||
return .asset(asset)
|
||||
case let .draft(draft):
|
||||
case let .draft(draft, _):
|
||||
return .draft(draft)
|
||||
}
|
||||
}
|
||||
@ -2086,7 +2128,7 @@ public final class MediaEditorScreen: ViewController {
|
||||
return .video(videoPath, dimensions)
|
||||
case let .asset(asset):
|
||||
return .asset(asset)
|
||||
case let .draft(draft):
|
||||
case let .draft(draft, _):
|
||||
return .image(draft.thumbnail, draft.dimensions)
|
||||
}
|
||||
}
|
||||
@ -2108,7 +2150,7 @@ public final class MediaEditorScreen: ViewController {
|
||||
fileprivate let transitionOut: (Bool) -> TransitionOut?
|
||||
|
||||
public var cancelled: (Bool) -> Void = { _ in }
|
||||
public var completion: (MediaEditorScreen.Result, MediaEditorResultPrivacy, @escaping (@escaping () -> Void) -> Void) -> Void = { _, _, _ in }
|
||||
public var completion: (Int64, MediaEditorScreen.Result, MediaEditorResultPrivacy, @escaping (@escaping () -> Void) -> Void) -> Void = { _, _, _, _ in }
|
||||
public var dismissed: () -> Void = { }
|
||||
|
||||
public init(
|
||||
@ -2116,7 +2158,7 @@ public final class MediaEditorScreen: ViewController {
|
||||
subject: Signal<Subject?, NoError>,
|
||||
transitionIn: TransitionIn?,
|
||||
transitionOut: @escaping (Bool) -> TransitionOut?,
|
||||
completion: @escaping (MediaEditorScreen.Result, MediaEditorResultPrivacy, @escaping (@escaping () -> Void) -> Void) -> Void
|
||||
completion: @escaping (Int64, MediaEditorScreen.Result, MediaEditorResultPrivacy, @escaping (@escaping () -> Void) -> Void) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.subject = subject
|
||||
@ -2147,9 +2189,9 @@ public final class MediaEditorScreen: ViewController {
|
||||
super.displayNodeDidLoad()
|
||||
}
|
||||
|
||||
func presentPrivacySettings() {
|
||||
func openPrivacySettings() {
|
||||
if case .message(_, _) = self.state.privacy {
|
||||
self.presentSendAsMessage()
|
||||
self.openSendAsMessage()
|
||||
} else {
|
||||
let stateContext = ShareWithPeersScreen.StateContext(context: self.context, subject: .stories)
|
||||
let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in
|
||||
@ -2157,9 +2199,13 @@ public final class MediaEditorScreen: ViewController {
|
||||
return
|
||||
}
|
||||
|
||||
var archive = true
|
||||
var timeout: Int = 86400
|
||||
let initialPrivacy: EngineStoryPrivacy
|
||||
if case let .story(privacy, _) = self.state.privacy {
|
||||
if case let .story(privacy, timeoutValue, archiveValue) = self.state.privacy {
|
||||
initialPrivacy = privacy
|
||||
timeout = timeoutValue
|
||||
archive = archiveValue
|
||||
} else {
|
||||
initialPrivacy = EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: [])
|
||||
}
|
||||
@ -2173,25 +2219,25 @@ public final class MediaEditorScreen: ViewController {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.state.privacy = .story(privacy: privacy, archive: true)
|
||||
self.state.privacy = .story(privacy: privacy, timeout: timeout, archive: archive)
|
||||
},
|
||||
editCategory: { [weak self] privacy in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.presentEditCategory(privacy: privacy, completion: { [weak self] privacy in
|
||||
self.openEditCategory(privacy: privacy, completion: { [weak self] privacy in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.state.privacy = .story(privacy: privacy, archive: true)
|
||||
self.presentPrivacySettings()
|
||||
self.state.privacy = .story(privacy: privacy, timeout: timeout, archive: archive)
|
||||
self.openPrivacySettings()
|
||||
})
|
||||
},
|
||||
secondaryAction: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.presentSendAsMessage()
|
||||
self.openSendAsMessage()
|
||||
}
|
||||
)
|
||||
)
|
||||
@ -2199,7 +2245,7 @@ public final class MediaEditorScreen: ViewController {
|
||||
}
|
||||
}
|
||||
|
||||
private func presentEditCategory(privacy: EngineStoryPrivacy, completion: @escaping (EngineStoryPrivacy) -> Void) {
|
||||
private func openEditCategory(privacy: EngineStoryPrivacy, completion: @escaping (EngineStoryPrivacy) -> Void) {
|
||||
let stateContext = ShareWithPeersScreen.StateContext(context: self.context, subject: .contacts(privacy.base))
|
||||
let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in
|
||||
guard let self else {
|
||||
@ -2225,14 +2271,14 @@ public final class MediaEditorScreen: ViewController {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.presentSendAsMessage()
|
||||
self.openSendAsMessage()
|
||||
}
|
||||
)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
private func presentSendAsMessage() {
|
||||
private func openSendAsMessage() {
|
||||
var initialPeerIds = Set<EnginePeer.Id>()
|
||||
if case let .message(peers, _) = self.state.privacy {
|
||||
initialPeerIds = Set(peers)
|
||||
@ -2264,54 +2310,109 @@ public final class MediaEditorScreen: ViewController {
|
||||
func presentTimeoutSetup(sourceView: UIView) {
|
||||
var items: [ContextMenuItem] = []
|
||||
|
||||
let updateTimeout: (Int32?) -> Void = { [weak self] timeout in
|
||||
let updateTimeout: (Int?, Bool) -> Void = { [weak self] timeout, archive in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if case let .message(peers, _) = self.state.privacy {
|
||||
switch self.state.privacy {
|
||||
case let .story(privacy, _, _):
|
||||
self.state.privacy = .story(privacy: privacy, timeout: timeout ?? 86400, archive: archive)
|
||||
case let .message(peers, _):
|
||||
self.state.privacy = .message(peers: peers, timeout: timeout)
|
||||
}
|
||||
}
|
||||
|
||||
var currentValue: Int?
|
||||
var currentArchived = false
|
||||
let emptyAction: ((ContextMenuActionItem.Action) -> Void)? = nil
|
||||
items.append(.action(ContextMenuActionItem(text: "Choose how long the media will be kept after opening.", textLayout: .multiline, textFont: .small, icon: { _ in return nil }, action: emptyAction)))
|
||||
let title: String
|
||||
switch self.state.privacy {
|
||||
case let .story(_, timeoutValue, archivedValue):
|
||||
title = "Choose how long the story will be kept."
|
||||
currentValue = timeoutValue
|
||||
currentArchived = archivedValue
|
||||
case let .message(_, timeoutValue):
|
||||
title = "Choose how long the media will be kept after opening."
|
||||
currentValue = timeoutValue
|
||||
}
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: title, textLayout: .multiline, textFont: .small, icon: { _ in return nil }, action: emptyAction)))
|
||||
|
||||
switch self.state.privacy {
|
||||
case .story:
|
||||
items.append(.action(ContextMenuActionItem(text: "6 Hours", icon: { theme in
|
||||
return currentValue == 3600 * 6 ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil
|
||||
}, action: { _, a in
|
||||
a(.default)
|
||||
|
||||
updateTimeout(3600 * 6, false)
|
||||
})))
|
||||
items.append(.action(ContextMenuActionItem(text: "12 Hours", icon: { theme in
|
||||
return currentValue == 3600 * 12 ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil
|
||||
}, action: { _, a in
|
||||
a(.default)
|
||||
|
||||
updateTimeout(3600 * 12, false)
|
||||
})))
|
||||
items.append(.action(ContextMenuActionItem(text: "24 Hours", icon: { theme in
|
||||
return currentValue == 86400 && !currentArchived ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil
|
||||
}, action: { _, a in
|
||||
a(.default)
|
||||
|
||||
updateTimeout(86400, false)
|
||||
})))
|
||||
items.append(.action(ContextMenuActionItem(text: "2 Days", icon: { theme in
|
||||
return currentValue == 86400 * 2 ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil
|
||||
}, action: { _, a in
|
||||
a(.default)
|
||||
|
||||
updateTimeout(86400 * 2, false)
|
||||
})))
|
||||
items.append(.action(ContextMenuActionItem(text: "Forever", icon: { theme in
|
||||
return currentArchived ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil
|
||||
}, action: { _, a in
|
||||
a(.default)
|
||||
|
||||
updateTimeout(86400, true)
|
||||
})))
|
||||
case .message:
|
||||
items.append(.action(ContextMenuActionItem(text: "Until First View", icon: { _ in
|
||||
return nil
|
||||
}, action: { _, a in
|
||||
a(.default)
|
||||
|
||||
updateTimeout(1, false)
|
||||
})))
|
||||
items.append(.action(ContextMenuActionItem(text: "3 Seconds", icon: { _ in
|
||||
return nil
|
||||
}, action: { _, a in
|
||||
a(.default)
|
||||
|
||||
updateTimeout(3, false)
|
||||
})))
|
||||
items.append(.action(ContextMenuActionItem(text: "10 Seconds", icon: { _ in
|
||||
return nil
|
||||
}, action: { _, a in
|
||||
a(.default)
|
||||
|
||||
updateTimeout(10, false)
|
||||
})))
|
||||
items.append(.action(ContextMenuActionItem(text: "1 Minute", icon: { _ in
|
||||
return nil
|
||||
}, action: { _, a in
|
||||
a(.default)
|
||||
|
||||
updateTimeout(60, false)
|
||||
})))
|
||||
items.append(.action(ContextMenuActionItem(text: "Keep Always", icon: { _ in
|
||||
return nil
|
||||
}, action: { _, a in
|
||||
a(.default)
|
||||
|
||||
updateTimeout(nil, false)
|
||||
})))
|
||||
}
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: "Until First View", icon: { _ in
|
||||
return nil
|
||||
}, action: { _, a in
|
||||
a(.default)
|
||||
|
||||
updateTimeout(1)
|
||||
})))
|
||||
items.append(.action(ContextMenuActionItem(text: "3 Seconds", icon: { _ in
|
||||
return nil
|
||||
}, action: { _, a in
|
||||
a(.default)
|
||||
|
||||
updateTimeout(3)
|
||||
})))
|
||||
items.append(.action(ContextMenuActionItem(text: "10 Seconds", icon: { _ in
|
||||
return nil
|
||||
}, action: { _, a in
|
||||
a(.default)
|
||||
|
||||
updateTimeout(10)
|
||||
})))
|
||||
items.append(.action(ContextMenuActionItem(text: "1 Minute", icon: { _ in
|
||||
return nil
|
||||
}, action: { _, a in
|
||||
a(.default)
|
||||
|
||||
updateTimeout(60)
|
||||
})))
|
||||
items.append(.action(ContextMenuActionItem(text: "Keep Always", icon: { _ in
|
||||
return nil
|
||||
}, action: { _, a in
|
||||
a(.default)
|
||||
|
||||
updateTimeout(nil)
|
||||
})))
|
||||
|
||||
let presentationData = self.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme)
|
||||
let contextController = ContextController(account: self.context.account, presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: self, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: nil)
|
||||
self.present(contextController, in: .window(.root))
|
||||
@ -2365,11 +2466,11 @@ public final class MediaEditorScreen: ViewController {
|
||||
self.dismissAllTooltips()
|
||||
|
||||
if saveDraft {
|
||||
self.saveDraft()
|
||||
self.saveDraft(id: nil)
|
||||
} else {
|
||||
if case let .draft(draft) = self.node.subject {
|
||||
removeStoryDraft(engine: self.context.engine, path: draft.path, delete: true)
|
||||
}
|
||||
// if case let .draft(draft) = self.node.subject {
|
||||
// removeStoryDraft(engine: self.context.engine, path: draft.path, delete: true)
|
||||
// }
|
||||
}
|
||||
|
||||
if let mediaEditor = self.node.mediaEditor {
|
||||
@ -2384,7 +2485,7 @@ public final class MediaEditorScreen: ViewController {
|
||||
})
|
||||
}
|
||||
|
||||
private func saveDraft() {
|
||||
private func saveDraft(id: Int64?) {
|
||||
guard let subject = self.node.subject, let values = self.node.mediaEditor?.values else {
|
||||
return
|
||||
}
|
||||
@ -2396,22 +2497,63 @@ public final class MediaEditorScreen: ViewController {
|
||||
return
|
||||
}
|
||||
let fittedSize = resultImage.size.aspectFitted(CGSize(width: 128.0, height: 128.0))
|
||||
if case let .image(image, dimensions) = subject {
|
||||
|
||||
let saveImageDraft: (UIImage, PixelDimensions) -> Void = { image, dimensions in
|
||||
if let thumbnailImage = generateScaledImage(image: resultImage, size: fittedSize) {
|
||||
let path = draftPath() + "\(Int64.random(in: .min ... .max)).jpg"
|
||||
let path = draftPath() + "/\(Int64.random(in: .min ... .max)).jpg"
|
||||
if let data = image.jpegData(compressionQuality: 0.87) {
|
||||
try? data.write(to: URL(fileURLWithPath: path))
|
||||
let draft = MediaEditorDraft(path: path, isVideo: false, thumbnail: thumbnailImage, dimensions: dimensions, values: values)
|
||||
addStoryDraft(engine: self.context.engine, item: draft)
|
||||
if let id {
|
||||
saveStorySource(engine: self.context.engine, item: draft, id: id)
|
||||
} else {
|
||||
addStoryDraft(engine: self.context.engine, item: draft)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if case let .draft(draft) = subject {
|
||||
}
|
||||
|
||||
let saveVideoDraft: (String, PixelDimensions) -> Void = { videoPath, dimensions in
|
||||
if let thumbnailImage = generateScaledImage(image: resultImage, size: fittedSize) {
|
||||
removeStoryDraft(engine: self.context.engine, path: draft.path, delete: false)
|
||||
let draft = MediaEditorDraft(path: draft.path, isVideo: draft.isVideo, thumbnail: thumbnailImage, dimensions: draft.dimensions, values: values)
|
||||
addStoryDraft(engine: self.context.engine, item: draft)
|
||||
let path = draftPath() + "/\(Int64.random(in: .min ... .max)).mp4"
|
||||
_ = thumbnailImage
|
||||
_ = path
|
||||
_ = videoPath
|
||||
_ = dimensions
|
||||
}
|
||||
}
|
||||
|
||||
switch subject {
|
||||
case let .image(image, dimensions):
|
||||
saveImageDraft(image, dimensions)
|
||||
case let .video(path, _, dimensions):
|
||||
saveVideoDraft(path, dimensions)
|
||||
case let .asset(asset):
|
||||
if asset.mediaType == .video {
|
||||
PHImageManager.default().requestAVAsset(forVideo: asset, options: nil) { avAsset, _, _ in
|
||||
let _ = avAsset
|
||||
}
|
||||
} else {
|
||||
let options = PHImageRequestOptions()
|
||||
options.deliveryMode = .highQualityFormat
|
||||
PHImageManager.default().requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .default, options: options) { image, _ in
|
||||
if let image {
|
||||
saveImageDraft(image, PixelDimensions(image.size))
|
||||
}
|
||||
}
|
||||
}
|
||||
case let .draft(draft, _):
|
||||
if draft.isVideo {
|
||||
|
||||
} else if let image = UIImage(contentsOfFile: draft.path) {
|
||||
saveImageDraft(image, PixelDimensions(image.size))
|
||||
}
|
||||
// if let thumbnailImage = generateScaledImage(image: resultImage, size: fittedSize) {
|
||||
// removeStoryDraft(engine: self.context.engine, path: draft.path, delete: false)
|
||||
// let draft = MediaEditorDraft(path: draft.path, isVideo: draft.isVideo, thumbnail: thumbnailImage, dimensions: draft.dimensions, values: values)
|
||||
// addStoryDraft(engine: self.context.engine, item: draft)
|
||||
// }
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -2435,6 +2577,13 @@ public final class MediaEditorScreen: ViewController {
|
||||
let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.node.entitiesView)
|
||||
mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities)
|
||||
|
||||
let randomId: Int64
|
||||
if case let .draft(_, id) = subject, let id {
|
||||
randomId = id
|
||||
} else {
|
||||
randomId = Int64.random(in: .min ... .max)
|
||||
}
|
||||
|
||||
if mediaEditor.resultIsVideo {
|
||||
let videoResult: Result.VideoResult
|
||||
let duration: Double
|
||||
@ -2464,7 +2613,7 @@ public final class MediaEditorScreen: ViewController {
|
||||
} else {
|
||||
duration = 5.0
|
||||
}
|
||||
case let .draft(draft):
|
||||
case let .draft(draft, _):
|
||||
if draft.isVideo {
|
||||
videoResult = .videoFile(path: draft.path)
|
||||
if let videoTrimRange = mediaEditor.values.videoTrimRange {
|
||||
@ -2477,7 +2626,7 @@ public final class MediaEditorScreen: ViewController {
|
||||
duration = 5.0
|
||||
}
|
||||
}
|
||||
self.completion(.video(video: videoResult, coverImage: nil, values: mediaEditor.values, duration: duration, dimensions: mediaEditor.values.resultDimensions, caption: caption), self.state.privacy, { [weak self] finished in
|
||||
self.completion(randomId, .video(video: videoResult, coverImage: nil, values: mediaEditor.values, duration: duration, dimensions: mediaEditor.values.resultDimensions, caption: caption), self.state.privacy, { [weak self] finished in
|
||||
self?.node.animateOut(finished: true, completion: { [weak self] in
|
||||
self?.dismiss()
|
||||
Queue.mainQueue().justDispatch {
|
||||
@ -2486,14 +2635,16 @@ public final class MediaEditorScreen: ViewController {
|
||||
})
|
||||
})
|
||||
|
||||
if case let .draft(draft) = subject {
|
||||
if case let .draft(draft, id) = subject, id == nil {
|
||||
removeStoryDraft(engine: self.context.engine, path: draft.path, delete: true)
|
||||
}
|
||||
} else {
|
||||
if let image = mediaEditor.resultImage {
|
||||
makeEditorImageComposition(account: self.context.account, inputImage: image, dimensions: storyDimensions, values: mediaEditor.values, time: .zero, completion: { resultImage in
|
||||
if let resultImage {
|
||||
self.completion(.image(image: resultImage, dimensions: PixelDimensions(resultImage.size), caption: caption), self.state.privacy, { [weak self] finished in
|
||||
self.saveDraft(id: randomId)
|
||||
|
||||
makeEditorImageComposition(account: self.context.account, inputImage: image, dimensions: storyDimensions, values: mediaEditor.values, time: .zero, completion: { [weak self] resultImage in
|
||||
if let self, let resultImage {
|
||||
self.completion(randomId, .image(image: resultImage, dimensions: PixelDimensions(resultImage.size), caption: caption), self.state.privacy, { [weak self] finished in
|
||||
self?.node.animateOut(finished: true, completion: { [weak self] in
|
||||
self?.dismiss()
|
||||
Queue.mainQueue().justDispatch {
|
||||
@ -2501,7 +2652,7 @@ public final class MediaEditorScreen: ViewController {
|
||||
}
|
||||
})
|
||||
})
|
||||
if case let .draft(draft) = subject {
|
||||
if case let .draft(draft, id) = subject, id == nil {
|
||||
removeStoryDraft(engine: self.context.engine, path: draft.path, delete: true)
|
||||
}
|
||||
}
|
||||
@ -2581,7 +2732,7 @@ public final class MediaEditorScreen: ViewController {
|
||||
}
|
||||
return EmptyDisposable
|
||||
}
|
||||
case let .draft(draft):
|
||||
case let .draft(draft, _):
|
||||
if draft.isVideo {
|
||||
let asset = AVURLAsset(url: NSURL(fileURLWithPath: draft.path) as URL)
|
||||
exportSubject = .single(.video(asset))
|
||||
@ -2665,6 +2816,10 @@ public final class MediaEditorScreen: ViewController {
|
||||
}
|
||||
}
|
||||
|
||||
public func updateEditProgress(_ progress: Float) {
|
||||
self.node.updateEditProgress(progress)
|
||||
}
|
||||
|
||||
private func dismissAllTooltips() {
|
||||
self.window?.forEachController({ controller in
|
||||
if let controller = controller as? TooltipScreen {
|
||||
|
@ -662,6 +662,17 @@ private final class MediaToolsScreenComponent: Component {
|
||||
controller.mediaEditor.setToolValue(.highlightsTint, value: value)
|
||||
state?.updated()
|
||||
}
|
||||
},
|
||||
isTrackingUpdated: { [weak self] isTracking in
|
||||
if let self {
|
||||
let transition: Transition
|
||||
if isTracking {
|
||||
transition = .immediate
|
||||
} else {
|
||||
transition = .easeInOut(duration: 0.25)
|
||||
}
|
||||
transition.setAlpha(view: self.optionsBackgroundView, alpha: isTracking ? 0.0 : 1.0)
|
||||
}
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
@ -867,6 +878,9 @@ public final class MediaToolsScreen: ViewController {
|
||||
}
|
||||
|
||||
func animateOutToEditor(completion: @escaping () -> Void) {
|
||||
if let mediaEditor = self.controller?.mediaEditor {
|
||||
mediaEditor.play()
|
||||
}
|
||||
if let view = self.componentHost.view as? MediaToolsScreenComponent.View {
|
||||
view.animateOutToEditor(completion: completion)
|
||||
}
|
||||
@ -880,7 +894,8 @@ public final class MediaToolsScreen: ViewController {
|
||||
self.validLayout = layout
|
||||
|
||||
let previewSize = CGSize(width: layout.size.width, height: floorToScreenPixels(layout.size.width * 1.77778))
|
||||
let topInset: CGFloat = floorToScreenPixels(layout.size.height - previewSize.height) / 2.0
|
||||
let topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 12.0
|
||||
let bottomInset = layout.size.height - previewSize.height - topInset
|
||||
|
||||
let environment = ViewControllerComponentContainer.Environment(
|
||||
statusBarHeight: layout.statusBarHeight ?? 0.0,
|
||||
@ -888,7 +903,7 @@ public final class MediaToolsScreen: ViewController {
|
||||
safeInsets: UIEdgeInsets(
|
||||
top: topInset,
|
||||
left: layout.safeInsets.left,
|
||||
bottom: topInset,
|
||||
bottom: bottomInset,
|
||||
right: layout.safeInsets.right
|
||||
),
|
||||
inputHeight: layout.inputHeight ?? 0.0,
|
||||
@ -921,6 +936,13 @@ public final class MediaToolsScreen: ViewController {
|
||||
sectionUpdated: { [weak self] section in
|
||||
if let self {
|
||||
self.currentSection = section
|
||||
if let mediaEditor = self.controller?.mediaEditor {
|
||||
if section == .curves {
|
||||
mediaEditor.stop()
|
||||
} else {
|
||||
mediaEditor.play()
|
||||
}
|
||||
}
|
||||
if let layout = self.validLayout {
|
||||
self.containerLayoutUpdated(layout: layout, transition: Transition(animation: .curve(duration: 0.3, curve: .spring)))
|
||||
}
|
||||
|
@ -250,6 +250,7 @@ final class StoryPreviewComponent: Component {
|
||||
strings: presentationData.strings,
|
||||
style: .story,
|
||||
placeholder: "Reply Privately...",
|
||||
alwaysDarkWhenHasText: false,
|
||||
presentController: { _ in
|
||||
},
|
||||
sendMessageAction: {
|
||||
|
@ -0,0 +1,48 @@
|
||||
import Foundation
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import TelegramUIPreferences
|
||||
import MediaEditor
|
||||
|
||||
|
||||
public func saveStorySource(engine: TelegramEngine, item: MediaEditorDraft, id: Int64) {
|
||||
let key = EngineDataBuffer(length: 8)
|
||||
key.setInt64(0, value: id)
|
||||
let _ = engine.itemCache.put(collectionId: ApplicationSpecificItemCacheCollectionId.storySource, id: key, item: item).start()
|
||||
}
|
||||
|
||||
public func removeStorySource(engine: TelegramEngine, id: Int64) {
|
||||
let key = EngineDataBuffer(length: 8)
|
||||
key.setInt64(0, value: id)
|
||||
let _ = engine.itemCache.remove(collectionId: ApplicationSpecificItemCacheCollectionId.storySource, id: key).start()
|
||||
}
|
||||
|
||||
public func getStorySource(engine: TelegramEngine, id: Int64) -> Signal<MediaEditorDraft?, NoError> {
|
||||
let key = EngineDataBuffer(length: 8)
|
||||
key.setInt64(0, value: id)
|
||||
return engine.data.get(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: ApplicationSpecificItemCacheCollectionId.storySource, id: key))
|
||||
|> map { result -> MediaEditorDraft? in
|
||||
return result?.get(MediaEditorDraft.self)
|
||||
}
|
||||
}
|
||||
|
||||
public func moveStorySource(engine: TelegramEngine, from fromId: Int64, to toId: Int64) {
|
||||
let fromKey = EngineDataBuffer(length: 8)
|
||||
fromKey.setInt64(0, value: fromId)
|
||||
|
||||
let toKey = EngineDataBuffer(length: 8)
|
||||
toKey.setInt64(0, value: toId)
|
||||
|
||||
let _ = (engine.data.get(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: ApplicationSpecificItemCacheCollectionId.storySource, id: fromKey))
|
||||
|> mapToSignal { item -> Signal<Never, NoError> in
|
||||
if let item = item?.get(MediaEditorDraft.self) {
|
||||
return engine.itemCache.put(collectionId: ApplicationSpecificItemCacheCollectionId.storySource, id: toKey, item: item)
|
||||
|> then(
|
||||
engine.itemCache.remove(collectionId: ApplicationSpecificItemCacheCollectionId.storySource, id: fromKey)
|
||||
)
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
}).start()
|
||||
}
|
@ -110,17 +110,20 @@ final class TintComponent: Component {
|
||||
let highlightsValue: TintValue
|
||||
let shadowsValueUpdated: (TintValue) -> Void
|
||||
let highlightsValueUpdated: (TintValue) -> Void
|
||||
let isTrackingUpdated: (Bool) -> Void
|
||||
|
||||
init(
|
||||
shadowsValue: TintValue,
|
||||
highlightsValue: TintValue,
|
||||
shadowsValueUpdated: @escaping (TintValue) -> Void,
|
||||
highlightsValueUpdated: @escaping (TintValue) -> Void
|
||||
highlightsValueUpdated: @escaping (TintValue) -> Void,
|
||||
isTrackingUpdated: @escaping (Bool) -> Void
|
||||
) {
|
||||
self.shadowsValue = shadowsValue
|
||||
self.highlightsValue = highlightsValue
|
||||
self.shadowsValueUpdated = shadowsValueUpdated
|
||||
self.highlightsValueUpdated = highlightsValueUpdated
|
||||
self.isTrackingUpdated = isTrackingUpdated
|
||||
}
|
||||
|
||||
static func ==(lhs: TintComponent, rhs: TintComponent) -> Bool {
|
||||
@ -300,6 +303,32 @@ final class TintComponent: Component {
|
||||
sizes.append(size)
|
||||
}
|
||||
|
||||
let isTrackingUpdated: (Bool) -> Void = { [weak self] isTracking in
|
||||
component.isTrackingUpdated(isTracking)
|
||||
|
||||
if let self {
|
||||
let transition: Transition
|
||||
if isTracking {
|
||||
transition = .immediate
|
||||
} else {
|
||||
transition = .easeInOut(duration: 0.25)
|
||||
}
|
||||
|
||||
let alpha: CGFloat = isTracking ? 0.0 : 1.0
|
||||
if let view = self.shadowsButton.view {
|
||||
transition.setAlpha(view: view, alpha: alpha)
|
||||
}
|
||||
if let view = self.highlightsButton.view {
|
||||
transition.setAlpha(view: view, alpha: alpha)
|
||||
}
|
||||
for color in self.colorViews {
|
||||
if let view = color.view {
|
||||
transition.setAlpha(view: view, alpha: alpha)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let sliderSize = self.slider.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
@ -321,6 +350,9 @@ final class TintComponent: Component {
|
||||
highlightsValueUpdated(state.highlightsValue.withUpdatedIntensity(value))
|
||||
}
|
||||
}
|
||||
},
|
||||
isTrackingUpdated: { isTracking in
|
||||
isTrackingUpdated(isTracking)
|
||||
}
|
||||
)
|
||||
),
|
||||
|
@ -29,6 +29,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
public let strings: PresentationStrings
|
||||
public let style: Style
|
||||
public let placeholder: String
|
||||
public let alwaysDarkWhenHasText: Bool
|
||||
public let presentController: (ViewController) -> Void
|
||||
public let sendMessageAction: () -> Void
|
||||
public let setMediaRecordingActive: ((Bool, Bool, Bool) -> Void)?
|
||||
@ -43,7 +44,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
public let isRecordingLocked: Bool
|
||||
public let recordedAudioPreview: ChatRecordedMediaPreview?
|
||||
public let wasRecordingDismissed: Bool
|
||||
public let timeoutValue: Int32?
|
||||
public let timeoutValue: String?
|
||||
public let timeoutSelected: Bool
|
||||
public let displayGradient: Bool
|
||||
public let bottomInset: CGFloat
|
||||
@ -55,6 +56,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
strings: PresentationStrings,
|
||||
style: Style,
|
||||
placeholder: String,
|
||||
alwaysDarkWhenHasText: Bool,
|
||||
presentController: @escaping (ViewController) -> Void,
|
||||
sendMessageAction: @escaping () -> Void,
|
||||
setMediaRecordingActive: ((Bool, Bool, Bool) -> Void)?,
|
||||
@ -69,7 +71,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
isRecordingLocked: Bool,
|
||||
recordedAudioPreview: ChatRecordedMediaPreview?,
|
||||
wasRecordingDismissed: Bool,
|
||||
timeoutValue: Int32?,
|
||||
timeoutValue: String?,
|
||||
timeoutSelected: Bool,
|
||||
displayGradient: Bool,
|
||||
bottomInset: CGFloat
|
||||
@ -80,6 +82,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
self.strings = strings
|
||||
self.style = style
|
||||
self.placeholder = placeholder
|
||||
self.alwaysDarkWhenHasText = alwaysDarkWhenHasText
|
||||
self.presentController = presentController
|
||||
self.sendMessageAction = sendMessageAction
|
||||
self.setMediaRecordingActive = setMediaRecordingActive
|
||||
@ -119,6 +122,9 @@ public final class MessageInputPanelComponent: Component {
|
||||
if lhs.placeholder != rhs.placeholder {
|
||||
return false
|
||||
}
|
||||
if lhs.alwaysDarkWhenHasText != rhs.alwaysDarkWhenHasText {
|
||||
return false
|
||||
}
|
||||
if lhs.audioRecorder !== rhs.audioRecorder {
|
||||
return false
|
||||
}
|
||||
@ -667,10 +673,9 @@ public final class MessageInputPanelComponent: Component {
|
||||
}
|
||||
|
||||
if let timeoutAction = component.timeoutAction, let timeoutValue = component.timeoutValue {
|
||||
func generateIcon(value: Int32) -> UIImage? {
|
||||
func generateIcon(value: String) -> UIImage? {
|
||||
let image = UIImage(bundleImageName: "Media Editor/Timeout")!
|
||||
let string = "\(value)"
|
||||
let valueString = NSAttributedString(string: "\(value)", font: Font.with(size: string.count == 1 ? 12.0 : 10.0, design: .round, weight: .semibold), textColor: .white, paragraphAlignment: .center)
|
||||
let valueString = NSAttributedString(string: value, font: Font.with(size: value.count == 1 ? 12.0 : 10.0, design: .round, weight: .semibold), textColor: .white, paragraphAlignment: .center)
|
||||
|
||||
return generateImage(image.size, contextGenerator: { size, context in
|
||||
let bounds = CGRect(origin: CGPoint(), size: size)
|
||||
@ -680,8 +685,14 @@ public final class MessageInputPanelComponent: Component {
|
||||
context.draw(cgImage, in: CGRect(origin: .zero, size: size))
|
||||
}
|
||||
|
||||
var offset: CGPoint = CGPoint(x: 0.0, y: -3.0 - UIScreenPixel)
|
||||
if value == "∞" {
|
||||
offset.x += UIScreenPixel
|
||||
offset.y += 1.0 - UIScreenPixel
|
||||
}
|
||||
|
||||
let valuePath = CGMutablePath()
|
||||
valuePath.addRect(bounds.offsetBy(dx: 0.0, dy: -3.0 - UIScreenPixel))
|
||||
valuePath.addRect(bounds.offsetBy(dx: offset.x, dy: offset.y))
|
||||
let valueFramesetter = CTFramesetterCreateWithAttributedString(valueString as CFAttributedString)
|
||||
let valyeFrame = CTFramesetterCreateFrame(valueFramesetter, CFRangeMake(0, valueString.length), valuePath, nil)
|
||||
CTFrameDraw(valyeFrame, context)
|
||||
@ -692,7 +703,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
let timeoutButtonSize = self.timeoutButton.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(Button(
|
||||
content: AnyComponent(Image(image: icon, tintColor: component.timeoutSelected ? UIColor(rgb: 0xf8d74a) : UIColor(white: 1.0, alpha: 0.5), size: CGSize(width: 20.0, height: 20.0))),
|
||||
content: AnyComponent(Image(image: icon, tintColor: component.timeoutSelected ? UIColor(rgb: 0xf8d74a) : UIColor(white: 1.0, alpha: 1.0), size: CGSize(width: 20.0, height: 20.0))),
|
||||
action: { [weak self] in
|
||||
guard let self, let timeoutButtonView = self.timeoutButton.view else {
|
||||
return
|
||||
@ -718,7 +729,13 @@ public final class MessageInputPanelComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
self.fieldBackgroundView.updateColor(color: self.textFieldExternalState.isEditing || component.style == .editor ? UIColor(white: 0.0, alpha: 0.5) : UIColor(white: 1.0, alpha: 0.09), transition: transition.containedViewLayoutTransition)
|
||||
var fieldBackgroundIsDark = false
|
||||
if self.textFieldExternalState.hasText && component.alwaysDarkWhenHasText {
|
||||
fieldBackgroundIsDark = true
|
||||
} else if self.textFieldExternalState.isEditing || component.style == .editor {
|
||||
fieldBackgroundIsDark = true
|
||||
}
|
||||
self.fieldBackgroundView.updateColor(color: fieldBackgroundIsDark ? UIColor(white: 0.0, alpha: 0.5) : UIColor(white: 1.0, alpha: 0.09), transition: transition.containedViewLayoutTransition)
|
||||
if let placeholder = self.placeholder.view, let vibrancyPlaceholderView = self.vibrancyPlaceholder.view {
|
||||
placeholder.isHidden = self.textFieldExternalState.hasText
|
||||
vibrancyPlaceholderView.isHidden = placeholder.isHidden
|
||||
|
@ -46,6 +46,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/EntityKeyboard",
|
||||
"//submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent",
|
||||
"//submodules/TelegramUI/Components/ShareWithPeersScreen",
|
||||
"//submodules/TelegramUI/Components/MediaEditorScreen",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/ReactionSelectionNode",
|
||||
"//submodules/ContextUI",
|
||||
@ -53,6 +54,7 @@ swift_library(
|
||||
"//submodules/ChatPresentationInterfaceState",
|
||||
"//submodules/TelegramStringFormatting",
|
||||
"//submodules/ShimmerEffect",
|
||||
"//submodules/ImageCompression",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -166,6 +166,9 @@ private final class StoryContainerScreenComponent: Component {
|
||||
let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.longPressGesture(_:)))
|
||||
longPressRecognizer.delegate = self
|
||||
self.addGestureRecognizer(longPressRecognizer)
|
||||
|
||||
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
|
||||
self.backgroundEffectView.addGestureRecognizer(tapGestureRecognizer)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
@ -330,6 +333,31 @@ private final class StoryContainerScreenComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
guard let component = self.component, let environment = self.environment, let stateValue = component.content.stateValue, case .recognized = recognizer.state else {
|
||||
return
|
||||
}
|
||||
|
||||
let location = recognizer.location(in: recognizer.view)
|
||||
if let currentItemView = self.visibleItemSetViews.first?.value {
|
||||
if location.x < currentItemView.frame.minX {
|
||||
if stateValue.previousSlice == nil {
|
||||
|
||||
} else {
|
||||
self.beginHorizontalPan()
|
||||
self.commitHorizontalPan(velocity: CGPoint(x: 100.0, y: 0.0))
|
||||
}
|
||||
} else if location.x > currentItemView.frame.maxX {
|
||||
if stateValue.nextSlice == nil {
|
||||
environment.controller()?.dismiss()
|
||||
} else {
|
||||
self.beginHorizontalPan()
|
||||
self.commitHorizontalPan(velocity: CGPoint(x: -100.0, y: 0.0))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
for subview in self.subviews.reversed() {
|
||||
if !subview.isUserInteractionEnabled || subview.isHidden || subview.alpha == 0.0 {
|
||||
@ -561,14 +589,15 @@ private final class StoryContainerScreenComponent: Component {
|
||||
}
|
||||
|
||||
var itemSetContainerSize = availableSize
|
||||
var itemSetContainerTopInset = environment.statusBarHeight + 12.0
|
||||
var itemSetContainerInsets = UIEdgeInsets(top: environment.statusBarHeight + 12.0, left: 0.0, bottom: 0.0, right: 0.0)
|
||||
var itemSetContainerSafeInsets = environment.safeInsets
|
||||
if case .regular = environment.metrics.widthClass {
|
||||
let availableHeight = min(1080.0, availableSize.height - max(45.0, environment.safeInsets.bottom) * 2.0)
|
||||
let mediaHeight = availableHeight - 40.0
|
||||
let mediaWidth = floor(mediaHeight * 0.5625)
|
||||
itemSetContainerSize = CGSize(width: mediaWidth, height: availableHeight)
|
||||
itemSetContainerTopInset = 0.0
|
||||
itemSetContainerInsets.top = 0.0
|
||||
itemSetContainerInsets.bottom = floorToScreenPixels((availableSize.height - itemSetContainerSize.height) / 2.0)
|
||||
itemSetContainerSafeInsets.bottom = 0.0
|
||||
}
|
||||
|
||||
@ -580,13 +609,14 @@ private final class StoryContainerScreenComponent: Component {
|
||||
slice: slice,
|
||||
theme: environment.theme,
|
||||
strings: environment.strings,
|
||||
containerInsets: UIEdgeInsets(top: itemSetContainerTopInset, left: 0.0, bottom: environment.inputHeight, right: 0.0),
|
||||
containerInsets: itemSetContainerInsets,
|
||||
safeInsets: itemSetContainerSafeInsets,
|
||||
inputHeight: environment.inputHeight,
|
||||
metrics: environment.metrics,
|
||||
isProgressPaused: isProgressPaused || i != focusedIndex,
|
||||
hideUI: i == focusedIndex && self.itemSetPanState?.didBegin == false,
|
||||
visibilityFraction: 1.0 - abs(panFraction + cubeAdditionalRotationFraction),
|
||||
isPanning: self.itemSetPanState?.didBegin == true,
|
||||
presentController: { [weak self] c in
|
||||
guard let self, let environment = self.environment else {
|
||||
return
|
||||
@ -675,6 +705,7 @@ private final class StoryContainerScreenComponent: Component {
|
||||
self.addSubview(itemSetView)
|
||||
}
|
||||
if itemSetComponentView.superview == nil {
|
||||
itemSetView.tintLayer.isDoubleSided = false
|
||||
itemSetComponentView.layer.isDoubleSided = false
|
||||
itemSetView.addSubview(itemSetComponentView)
|
||||
itemSetView.layer.addSublayer(itemSetView.tintLayer)
|
||||
|
@ -17,6 +17,8 @@ import ContextUI
|
||||
import TelegramCore
|
||||
import Postbox
|
||||
import AvatarNode
|
||||
import MediaEditorScreen
|
||||
import ImageCompression
|
||||
|
||||
public final class StoryItemSetContainerComponent: Component {
|
||||
public final class ExternalState {
|
||||
@ -44,6 +46,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
public let isProgressPaused: Bool
|
||||
public let hideUI: Bool
|
||||
public let visibilityFraction: CGFloat
|
||||
public let isPanning: Bool
|
||||
public let presentController: (ViewController) -> Void
|
||||
public let close: () -> Void
|
||||
public let navigate: (NavigationDirection) -> Void
|
||||
@ -63,6 +66,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
isProgressPaused: Bool,
|
||||
hideUI: Bool,
|
||||
visibilityFraction: CGFloat,
|
||||
isPanning: Bool,
|
||||
presentController: @escaping (ViewController) -> Void,
|
||||
close: @escaping () -> Void,
|
||||
navigate: @escaping (NavigationDirection) -> Void,
|
||||
@ -81,6 +85,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
self.isProgressPaused = isProgressPaused
|
||||
self.hideUI = hideUI
|
||||
self.visibilityFraction = visibilityFraction
|
||||
self.isPanning = isPanning
|
||||
self.presentController = presentController
|
||||
self.close = close
|
||||
self.navigate = navigate
|
||||
@ -122,6 +127,9 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
if lhs.visibilityFraction != rhs.visibilityFraction {
|
||||
return false
|
||||
}
|
||||
if lhs.isPanning != rhs.isPanning {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -208,6 +216,8 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
var displayViewList: Bool = false
|
||||
var viewList: ViewList?
|
||||
|
||||
var isEditingStory: Bool = false
|
||||
|
||||
var itemLayout: ItemLayout?
|
||||
var ignoreScrolling: Bool = false
|
||||
|
||||
@ -237,6 +247,9 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
|
||||
self.contentContainerView = UIView()
|
||||
self.contentContainerView.clipsToBounds = true
|
||||
if #available(iOS 13.0, *) {
|
||||
self.contentContainerView.layer.cornerCurve = .continuous
|
||||
}
|
||||
|
||||
self.topContentGradientLayer = SimpleGradientLayer()
|
||||
self.bottomContentGradientLayer = SimpleGradientLayer()
|
||||
@ -537,7 +550,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
for (_, visibleItem) in self.visibleItems {
|
||||
if let view = visibleItem.view.view {
|
||||
if let view = view as? StoryContentItem.View {
|
||||
view.setIsProgressPaused(self.inputPanelExternalState.isEditing || component.isProgressPaused || self.displayReactions || self.actionSheet != nil || self.contextController != nil || self.sendMessageContext.audioRecorderValue != nil || self.sendMessageContext.videoRecorderValue != nil || self.displayViewList)
|
||||
view.setIsProgressPaused(self.inputPanelExternalState.isEditing || component.isProgressPaused || self.displayReactions || self.actionSheet != nil || self.contextController != nil || self.sendMessageContext.audioRecorderValue != nil || self.sendMessageContext.videoRecorderValue != nil || self.displayViewList || self.isEditingStory)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -788,6 +801,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
|
||||
//self.updatePreloads()
|
||||
|
||||
let wasPanning = self.component?.isPanning ?? false
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
@ -797,10 +811,23 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
} else {
|
||||
bottomContentInset = 0.0
|
||||
}
|
||||
|
||||
var inputPanelAvailableWidth = availableSize.width
|
||||
var inputPanelTransition = transition
|
||||
if case .regular = component.metrics.widthClass {
|
||||
if (self.inputPanelExternalState.isEditing || self.inputPanelExternalState.hasText) {
|
||||
if wasPanning != component.isPanning {
|
||||
inputPanelTransition = .easeInOut(duration: 0.25)
|
||||
}
|
||||
if !component.isPanning {
|
||||
inputPanelAvailableWidth += 200.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.inputPanel.parentState = state
|
||||
let inputPanelSize = self.inputPanel.update(
|
||||
transition: transition,
|
||||
transition: inputPanelTransition,
|
||||
component: AnyComponent(MessageInputPanelComponent(
|
||||
externalState: self.inputPanelExternalState,
|
||||
context: component.context,
|
||||
@ -808,6 +835,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
strings: component.strings,
|
||||
style: .story,
|
||||
placeholder: "Reply Privately...",
|
||||
alwaysDarkWhenHasText: component.metrics.widthClass == .regular,
|
||||
presentController: { [weak self] c in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
@ -881,11 +909,11 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
wasRecordingDismissed: self.sendMessageContext.wasRecordingDismissed,
|
||||
timeoutValue: nil,
|
||||
timeoutSelected: false,
|
||||
displayGradient: component.inputHeight != 0.0,
|
||||
displayGradient: component.inputHeight != 0.0 && component.metrics.widthClass != .regular,
|
||||
bottomInset: component.inputHeight != 0.0 ? 0.0 : bottomContentInset
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: 200.0)
|
||||
containerSize: CGSize(width: inputPanelAvailableWidth, height: 200.0)
|
||||
)
|
||||
|
||||
var currentItem: StoryContentItem?
|
||||
@ -1012,6 +1040,17 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
self.openItemPrivacySettings()
|
||||
})))
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: "Edit Story", icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] _, a in
|
||||
a(.default)
|
||||
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.openStoryEditing()
|
||||
})))
|
||||
|
||||
items.append(.separator)
|
||||
|
||||
component.controller()?.forEachController { c in
|
||||
@ -1119,11 +1158,15 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
let inputPanelIsOverlay: Bool
|
||||
if component.inputHeight == 0.0 {
|
||||
inputPanelBottomInset = bottomContentInset
|
||||
bottomContentInset += inputPanelSize.height
|
||||
if case .regular = component.metrics.widthClass {
|
||||
bottomContentInset += 60.0
|
||||
} else {
|
||||
bottomContentInset += inputPanelSize.height
|
||||
}
|
||||
inputPanelIsOverlay = false
|
||||
} else {
|
||||
bottomContentInset += 44.0
|
||||
inputPanelBottomInset = component.inputHeight
|
||||
inputPanelBottomInset = component.inputHeight - component.containerInsets.bottom
|
||||
inputPanelIsOverlay = true
|
||||
}
|
||||
|
||||
@ -1201,7 +1244,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
transition.setPosition(view: self.contentContainerView, position: contentFrame.center)
|
||||
transition.setBounds(view: self.contentContainerView, bounds: CGRect(origin: CGPoint(), size: contentFrame.size))
|
||||
transition.setScale(view: self.contentContainerView, scale: contentVisualScale)
|
||||
transition.setCornerRadius(layer: self.contentContainerView.layer, cornerRadius: 10.0 * (1.0 / contentVisualScale))
|
||||
transition.setCornerRadius(layer: self.contentContainerView.layer, cornerRadius: 12.0 * (1.0 / contentVisualScale))
|
||||
|
||||
if self.closeButtonIconView.image == nil {
|
||||
self.closeButtonIconView.image = UIImage(bundleImageName: "Media Gallery/Close")?.withRenderingMode(.alwaysTemplate)
|
||||
@ -1318,7 +1361,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
let itemLayout = ItemLayout(size: CGSize(width: contentFrame.width, height: availableSize.height - component.containerInsets.top - 44.0 - bottomContentInsetWithoutInput))
|
||||
self.itemLayout = itemLayout
|
||||
|
||||
let inputPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - inputPanelBottomInset - inputPanelSize.height), size: inputPanelSize)
|
||||
let inputPanelFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - inputPanelSize.width) / 2.0), y: availableSize.height - inputPanelBottomInset - inputPanelSize.height), size: inputPanelSize)
|
||||
var inputPanelAlpha: CGFloat = focusedItem?.isMy == true ? 0.0 : 1.0
|
||||
if case .regular = component.metrics.widthClass {
|
||||
inputPanelAlpha *= component.visibilityFraction
|
||||
@ -1327,7 +1370,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
if inputPanelView.superview == nil {
|
||||
self.addSubview(inputPanelView)
|
||||
}
|
||||
transition.setFrame(view: inputPanelView, frame: inputPanelFrame)
|
||||
inputPanelTransition.setFrame(view: inputPanelView, frame: inputPanelFrame)
|
||||
transition.setAlpha(view: inputPanelView, alpha: inputPanelAlpha)
|
||||
}
|
||||
|
||||
@ -1705,6 +1748,101 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
|
||||
private func openItemPrivacySettings() {
|
||||
}
|
||||
|
||||
private func openStoryEditing() {
|
||||
guard let context = self.component?.context, let id = self.component?.slice.item.storyItem.id else {
|
||||
return
|
||||
}
|
||||
let _ = (getStorySource(engine: context.engine, id: Int64(id))
|
||||
|> deliverOnMainQueue).start(next: { [weak self] source in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.isEditingStory = true
|
||||
self.updateIsProgressPaused()
|
||||
|
||||
if let source {
|
||||
var updateProgressImpl: ((Float) -> Void)?
|
||||
let controller = MediaEditorScreen(
|
||||
context: context,
|
||||
subject: .single(.draft(source, Int64(id))),
|
||||
transitionIn: nil,
|
||||
transitionOut: { _ in return nil },
|
||||
completion: { [weak self] _, mediaResult, privacy, commit in
|
||||
switch mediaResult {
|
||||
case let .image(image, dimensions, caption):
|
||||
if let imageData = compressImageToJPEG(image, quality: 0.6), case let .story(storyPrivacy, _, _) = privacy {
|
||||
let _ = (context.engine.messages.editStory(media: .image(dimensions: dimensions, data: imageData), id: id, text: caption?.string ?? "", entities: [], privacy: storyPrivacy)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] result in
|
||||
switch result {
|
||||
case let .progress(progress):
|
||||
updateProgressImpl?(progress)
|
||||
case .completed:
|
||||
Queue.mainQueue().after(0.1) {
|
||||
if let self {
|
||||
self.isEditingStory = false
|
||||
self.rewindCurrentItem()
|
||||
self.updateIsProgressPaused()
|
||||
}
|
||||
commit({})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
default:
|
||||
break
|
||||
// case let .video(content, _, values, duration, dimensions, caption):
|
||||
// let adjustments: VideoMediaResourceAdjustments
|
||||
// if let valuesData = try? JSONEncoder().encode(values) {
|
||||
// let data = MemoryBuffer(data: valuesData)
|
||||
// let digest = MemoryBuffer(data: data.md5Digest())
|
||||
// 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))
|
||||
// }
|
||||
// if case let .story(storyPrivacy, period, pin) = privacy {
|
||||
// let _ = (context.engine.messages.uploadStory(media: .video(dimensions: dimensions, duration: duration, resource: resource), text: caption?.string ?? "", entities: [], pin: pin, privacy: storyPrivacy, period: period, randomId: randomId)
|
||||
// |> deliverOnMainQueue).start(next: { [weak chatListController] result in
|
||||
// if let chatListController {
|
||||
// switch result {
|
||||
// case let .progress(progress):
|
||||
// let _ = progress
|
||||
// break
|
||||
// case .completed:
|
||||
// Queue.mainQueue().after(0.1) {
|
||||
// commit()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
// Queue.mainQueue().justDispatch {
|
||||
// commit({ [weak chatListController] in
|
||||
// chatListController?.animateStoryUploadRipple()
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
)
|
||||
controller.dismissed = { [weak self] in
|
||||
self?.isEditingStory = false
|
||||
self?.updateIsProgressPaused()
|
||||
}
|
||||
self.component?.controller()?.push(controller)
|
||||
updateProgressImpl = { [weak controller] progress in
|
||||
controller?.updateEditProgress(progress)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
|
@ -2576,6 +2576,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
if let author = message?.author, author.isVerified {
|
||||
skipConcealedAlert = true
|
||||
}
|
||||
|
||||
if let message, let adAttribute = message.attributes.first(where: { $0 is AdMessageAttribute }) as? AdMessageAttribute {
|
||||
strongSelf.chatDisplayNode.historyNode.adMessagesContext?.markAction(opaqueId: adAttribute.opaqueId)
|
||||
}
|
||||
|
||||
strongSelf.openUrl(url, concealed: concealed, skipConcealedAlert: skipConcealedAlert, message: message)
|
||||
}
|
||||
}, shareCurrentLocation: { [weak self] in
|
||||
@ -4351,7 +4356,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
navigationData = .chat(textInputState: nil, subject: subject, peekData: nil)
|
||||
}
|
||||
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: id))
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peer in
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peer in
|
||||
if let self, let peer = peer {
|
||||
self.openPeer(peer: peer, navigation: navigationData, fromMessage: nil)
|
||||
}
|
||||
@ -4359,6 +4364,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
case let .join(_, joinHash):
|
||||
self.controllerInteraction?.openJoinLink(joinHash)
|
||||
case let .webPage(_, url):
|
||||
self.controllerInteraction?.openUrl(url, false, false, nil)
|
||||
}
|
||||
}, openRequestedPeerSelection: { [weak self] messageId, peerType, buttonId in
|
||||
guard let self else {
|
||||
|
@ -24,6 +24,7 @@ private let buttonFont = Font.semibold(13.0)
|
||||
|
||||
enum ChatMessageAttachedContentActionIcon {
|
||||
case instant
|
||||
case link
|
||||
}
|
||||
|
||||
struct ChatMessageAttachedContentNodeMediaFlags: OptionSet {
|
||||
@ -135,7 +136,7 @@ final class ChatMessageAttachedContentButtonNode: HighlightTrackingButtonNode {
|
||||
})
|
||||
}
|
||||
|
||||
static func asyncLayout(_ current: ChatMessageAttachedContentButtonNode?) -> (_ width: CGFloat, _ regularImage: UIImage, _ highlightedImage: UIImage, _ iconImage: UIImage?, _ highlightedIconImage: UIImage?, _ title: String, _ titleColor: UIColor, _ highlightedTitleColor: UIColor, _ inProgress: Bool) -> (CGFloat, (CGFloat) -> (CGSize, () -> ChatMessageAttachedContentButtonNode)) {
|
||||
static func asyncLayout(_ current: ChatMessageAttachedContentButtonNode?) -> (_ width: CGFloat, _ regularImage: UIImage, _ highlightedImage: UIImage, _ iconImage: UIImage?, _ highlightedIconImage: UIImage?, _ cornerIcon: Bool, _ title: String, _ titleColor: UIColor, _ highlightedTitleColor: UIColor, _ inProgress: Bool) -> (CGFloat, (CGFloat) -> (CGSize, () -> ChatMessageAttachedContentButtonNode)) {
|
||||
let previousRegularImage = current?.regularImage
|
||||
let previousHighlightedImage = current?.highlightedImage
|
||||
let previousRegularIconImage = current?.regularIconImage
|
||||
@ -144,7 +145,7 @@ final class ChatMessageAttachedContentButtonNode: HighlightTrackingButtonNode {
|
||||
let maybeMakeTextLayout = (current?.textNode).flatMap(TextNode.asyncLayout)
|
||||
let maybeMakeHighlightedTextLayout = (current?.highlightedTextNode).flatMap(TextNode.asyncLayout)
|
||||
|
||||
return { width, regularImage, highlightedImage, iconImage, highlightedIconImage, title, titleColor, highlightedTitleColor, inProgress in
|
||||
return { width, regularImage, highlightedImage, iconImage, highlightedIconImage, cornerIcon, title, titleColor, highlightedTitleColor, inProgress in
|
||||
let targetNode: ChatMessageAttachedContentButtonNode
|
||||
if let current = current {
|
||||
targetNode = current
|
||||
@ -235,8 +236,12 @@ final class ChatMessageAttachedContentButtonNode: HighlightTrackingButtonNode {
|
||||
var textFrame = CGRect(origin: CGPoint(x: floor((refinedWidth - textSize.size.width) / 2.0), y: floor((34.0 - textSize.size.height) / 2.0)), size: textSize.size)
|
||||
targetNode.backgroundNode.frame = backgroundFrame
|
||||
if let image = targetNode.iconNode.image {
|
||||
textFrame.origin.x += floor(image.size.width / 2.0)
|
||||
targetNode.iconNode.frame = CGRect(origin: CGPoint(x: textFrame.minX - image.size.width - 5.0, y: textFrame.minY + 2.0), size: image.size)
|
||||
if cornerIcon {
|
||||
targetNode.iconNode.frame = CGRect(origin: CGPoint(x: backgroundFrame.maxX - image.size.width - 5.0, y: 5.0), size: image.size)
|
||||
} else {
|
||||
textFrame.origin.x += floor(image.size.width / 2.0)
|
||||
targetNode.iconNode.frame = CGRect(origin: CGPoint(x: textFrame.minX - image.size.width - 5.0, y: textFrame.minY + 2.0), size: image.size)
|
||||
}
|
||||
if targetNode.iconNode.supernode == nil {
|
||||
targetNode.addSubnode(targetNode.iconNode)
|
||||
}
|
||||
@ -791,14 +796,22 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
|
||||
let buttonHighlightedImage: UIImage
|
||||
var buttonIconImage: UIImage?
|
||||
var buttonHighlightedIconImage: UIImage?
|
||||
var cornerIcon = false
|
||||
let titleColor: UIColor
|
||||
let titleHighlightedColor: UIColor
|
||||
if incoming {
|
||||
buttonImage = PresentationResourcesChat.chatMessageAttachedContentButtonIncoming(presentationData.theme.theme)!
|
||||
buttonHighlightedImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIncoming(presentationData.theme.theme)!
|
||||
if let actionIcon = actionIcon, case .instant = actionIcon {
|
||||
buttonIconImage = PresentationResourcesChat.chatMessageAttachedContentButtonIconInstantIncoming(presentationData.theme.theme)!
|
||||
buttonHighlightedIconImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIconInstantIncoming(presentationData.theme.theme, wallpaper: !presentationData.theme.wallpaper.isEmpty)!
|
||||
if let actionIcon {
|
||||
switch actionIcon {
|
||||
case .instant:
|
||||
buttonIconImage = PresentationResourcesChat.chatMessageAttachedContentButtonIconInstantIncoming(presentationData.theme.theme)!
|
||||
buttonHighlightedIconImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIconInstantIncoming(presentationData.theme.theme, wallpaper: !presentationData.theme.wallpaper.isEmpty)!
|
||||
case .link:
|
||||
buttonIconImage = PresentationResourcesChat.chatMessageAttachedContentButtonIconLinkIncoming(presentationData.theme.theme)!
|
||||
buttonHighlightedIconImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIconLinkIncoming(presentationData.theme.theme, wallpaper: !presentationData.theme.wallpaper.isEmpty)!
|
||||
cornerIcon = true
|
||||
}
|
||||
}
|
||||
titleColor = presentationData.theme.theme.chat.message.incoming.accentTextColor
|
||||
let bubbleColor = bubbleColorComponents(theme: presentationData.theme.theme, incoming: true, wallpaper: !presentationData.theme.wallpaper.isEmpty)
|
||||
@ -806,15 +819,22 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
|
||||
} else {
|
||||
buttonImage = PresentationResourcesChat.chatMessageAttachedContentButtonOutgoing(presentationData.theme.theme)!
|
||||
buttonHighlightedImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonOutgoing(presentationData.theme.theme)!
|
||||
if let actionIcon = actionIcon, case .instant = actionIcon {
|
||||
buttonIconImage = PresentationResourcesChat.chatMessageAttachedContentButtonIconInstantOutgoing(presentationData.theme.theme)!
|
||||
buttonHighlightedIconImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIconInstantOutgoing(presentationData.theme.theme, wallpaper: !presentationData.theme.wallpaper.isEmpty)!
|
||||
if let actionIcon {
|
||||
switch actionIcon {
|
||||
case .instant:
|
||||
buttonIconImage = PresentationResourcesChat.chatMessageAttachedContentButtonIconInstantOutgoing(presentationData.theme.theme)!
|
||||
buttonHighlightedIconImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIconInstantOutgoing(presentationData.theme.theme, wallpaper: !presentationData.theme.wallpaper.isEmpty)!
|
||||
case .link:
|
||||
buttonIconImage = PresentationResourcesChat.chatMessageAttachedContentButtonIconLinkOutgoing(presentationData.theme.theme)!
|
||||
buttonHighlightedIconImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIconLinkOutgoing(presentationData.theme.theme, wallpaper: !presentationData.theme.wallpaper.isEmpty)!
|
||||
cornerIcon = true
|
||||
}
|
||||
}
|
||||
titleColor = presentationData.theme.theme.chat.message.outgoing.accentTextColor
|
||||
let bubbleColor = bubbleColorComponents(theme: presentationData.theme.theme, incoming: false, wallpaper: !presentationData.theme.wallpaper.isEmpty)
|
||||
titleHighlightedColor = bubbleColor.fill[0]
|
||||
}
|
||||
let (buttonWidth, continueLayout) = makeButtonLayout(constrainedSize.width, buttonImage, buttonHighlightedImage, buttonIconImage, buttonHighlightedIconImage, actionTitle, titleColor, titleHighlightedColor, false)
|
||||
let (buttonWidth, continueLayout) = makeButtonLayout(constrainedSize.width, buttonImage, buttonHighlightedImage, buttonIconImage, buttonHighlightedIconImage, cornerIcon, actionTitle, titleColor, titleHighlightedColor, false)
|
||||
boundingSize.width = max(buttonWidth, boundingSize.width)
|
||||
continueActionButtonLayout = continueLayout
|
||||
}
|
||||
|
@ -265,7 +265,7 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
avatarPlaceholderColor = item.presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor
|
||||
}
|
||||
|
||||
let (buttonWidth, continueLayout) = makeButtonLayout(constrainedSize.width, buttonImage, buttonHighlightedImage, nil, nil, item.presentationData.strings.Conversation_ViewContactDetails, titleColor, titleHighlightedColor, false)
|
||||
let (buttonWidth, continueLayout) = makeButtonLayout(constrainedSize.width, buttonImage, buttonHighlightedImage, nil, nil, false, item.presentationData.strings.Conversation_ViewContactDetails, titleColor, titleHighlightedColor, false)
|
||||
|
||||
var maxContentWidth: CGFloat = avatarSize.width + 7.0
|
||||
if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue {
|
||||
|
@ -56,7 +56,7 @@ final class ChatMessageUnsupportedBubbleContentNode: ChatMessageBubbleContentNod
|
||||
let bubbleColor = bubbleColorComponents(theme: presentationData.theme.theme, incoming: false, wallpaper: !presentationData.theme.wallpaper.isEmpty)
|
||||
titleHighlightedColor = bubbleColor.fill[0]
|
||||
}
|
||||
let (buttonWidth, continueActionButtonLayout) = makeButtonLayout(constrainedSize.width, buttonImage, buttonHighlightedImage, nil, nil, presentationData.strings.Conversation_UpdateTelegram, titleColor, titleHighlightedColor, false)
|
||||
let (buttonWidth, continueActionButtonLayout) = makeButtonLayout(constrainedSize.width, buttonImage, buttonHighlightedImage, nil, nil, false, presentationData.strings.Conversation_UpdateTelegram, titleColor, titleHighlightedColor, false)
|
||||
|
||||
let initialWidth = buttonWidth + insets.left + insets.right
|
||||
|
||||
|
@ -354,7 +354,10 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
actionTitle = item.presentationData.strings.Conversation_ViewGroup
|
||||
}
|
||||
} else {
|
||||
if case let .peer(_, messageId, _) = adAttribute.target, messageId != nil {
|
||||
if case .webPage = adAttribute.target {
|
||||
actionTitle = item.presentationData.strings.Conversation_OpenLink
|
||||
actionIcon = .link
|
||||
} else if case let .peer(_, messageId, _) = adAttribute.target, messageId != nil {
|
||||
actionTitle = item.presentationData.strings.Conversation_ViewMessage
|
||||
} else {
|
||||
actionTitle = item.presentationData.strings.Conversation_ViewChannel
|
||||
|
@ -273,18 +273,6 @@ public func fetchVideoLibraryMediaResource(account: Account, resource: VideoLibr
|
||||
TempBox.shared.dispose(tempFile)
|
||||
subscriber.putNext(.moveTempFile(file: remuxedTempFile))
|
||||
} else {
|
||||
let tempVideoPath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).mp4"
|
||||
if let _ = try? FileManager.default.copyItem(atPath: tempFile.path, toPath: tempVideoPath) {
|
||||
PHPhotoLibrary.shared().performChanges({
|
||||
PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: URL(fileURLWithPath: tempVideoPath))
|
||||
}, completionHandler: { _, error in
|
||||
if let error = error {
|
||||
print("\(error)")
|
||||
}
|
||||
let _ = try? FileManager.default.removeItem(atPath: tempVideoPath)
|
||||
})
|
||||
}
|
||||
|
||||
TempBox.shared.dispose(remuxedTempFile)
|
||||
if let data = try? Data(contentsOf: URL(fileURLWithPath: tempFile.path), options: [.mappedRead]) {
|
||||
var range: Range<Int64>?
|
||||
|
@ -297,7 +297,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
case let .asset(asset):
|
||||
return .asset(asset)
|
||||
case let .draft(draft):
|
||||
return .draft(draft)
|
||||
return .draft(draft, nil)
|
||||
}
|
||||
}
|
||||
|
||||
@ -334,7 +334,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}, completion: { [weak self] mediaResult, privacy, commit in
|
||||
}, completion: { [weak self] randomId, mediaResult, privacy, commit in
|
||||
guard let self else {
|
||||
dismissCameraImpl?()
|
||||
commit({})
|
||||
@ -347,15 +347,18 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
case let .image(image, dimensions, caption):
|
||||
if let imageData = compressImageToJPEG(image, quality: 0.6) {
|
||||
switch privacy {
|
||||
case let .story(storyPrivacy, pin):
|
||||
case let .story(storyPrivacy, period, pin):
|
||||
chatListController.updateStoryUploadProgress(0.0)
|
||||
let _ = (self.context.engine.messages.uploadStory(media: .image(dimensions: dimensions, data: imageData), text: caption?.string ?? "", entities: [], pin: pin, privacy: storyPrivacy, period: 86400)
|
||||
let _ = (self.context.engine.messages.uploadStory(media: .image(dimensions: dimensions, data: imageData), text: caption?.string ?? "", entities: [], pin: pin, privacy: storyPrivacy, period: period, randomId: randomId)
|
||||
|> deliverOnMainQueue).start(next: { [weak chatListController] result in
|
||||
if let chatListController {
|
||||
switch result {
|
||||
case let .progress(progress):
|
||||
chatListController.updateStoryUploadProgress(progress)
|
||||
case .completed:
|
||||
case let .completed(id):
|
||||
if let id {
|
||||
moveStorySource(engine: context.engine, from: randomId, to: Int64(id))
|
||||
}
|
||||
Queue.mainQueue().after(0.2) {
|
||||
chatListController.updateStoryUploadProgress(nil)
|
||||
}
|
||||
@ -363,9 +366,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
}
|
||||
})
|
||||
Queue.mainQueue().justDispatch {
|
||||
commit({ [weak chatListController] in
|
||||
chatListController?.animateStoryUploadRipple()
|
||||
})
|
||||
commit({})
|
||||
}
|
||||
case let .message(peerIds, timeout):
|
||||
var randomId: Int64 = 0
|
||||
@ -382,7 +383,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
|
||||
let media = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: randomId), representations: representations, immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: imageFlags)
|
||||
if let timeout, timeout > 0 && timeout <= 60 {
|
||||
attributes.append(AutoremoveTimeoutMessageAttribute(timeout: timeout, countdownBeginTime: nil))
|
||||
attributes.append(AutoremoveTimeoutMessageAttribute(timeout: Int32(timeout), countdownBeginTime: nil))
|
||||
}
|
||||
|
||||
let text = trimChatInputText(convertMarkdownToAttributes(caption ?? NSAttributedString()))
|
||||
@ -435,15 +436,18 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
case let .asset(localIdentifier):
|
||||
resource = VideoLibraryMediaResource(localIdentifier: localIdentifier, conversion: .compress(adjustments))
|
||||
}
|
||||
if case let .story(storyPrivacy, pin) = privacy {
|
||||
if case let .story(storyPrivacy, period, pin) = privacy {
|
||||
chatListController.updateStoryUploadProgress(0.0)
|
||||
let _ = (self.context.engine.messages.uploadStory(media: .video(dimensions: dimensions, duration: duration, resource: resource), text: caption?.string ?? "", entities: [], pin: pin, privacy: storyPrivacy, period: 86400)
|
||||
let _ = (self.context.engine.messages.uploadStory(media: .video(dimensions: dimensions, duration: duration, resource: resource), text: caption?.string ?? "", entities: [], pin: pin, privacy: storyPrivacy, period: period, randomId: randomId)
|
||||
|> deliverOnMainQueue).start(next: { [weak chatListController] result in
|
||||
if let chatListController {
|
||||
switch result {
|
||||
case let .progress(progress):
|
||||
chatListController.updateStoryUploadProgress(progress)
|
||||
case .completed:
|
||||
case let .completed(id):
|
||||
if let id {
|
||||
moveStorySource(engine: context.engine, from: randomId, to: Int64(id))
|
||||
}
|
||||
Queue.mainQueue().after(0.2) {
|
||||
chatListController.updateStoryUploadProgress(nil)
|
||||
}
|
||||
@ -451,9 +455,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
}
|
||||
})
|
||||
Queue.mainQueue().justDispatch {
|
||||
commit({ [weak chatListController] in
|
||||
chatListController?.animateStoryUploadRipple()
|
||||
})
|
||||
commit({})
|
||||
}
|
||||
} else {
|
||||
commit({})
|
||||
|
@ -76,6 +76,7 @@ private enum ApplicationSpecificItemCacheCollectionIdValues: Int8 {
|
||||
case cachedImageRecognizedContent = 6
|
||||
case pendingInAppPurchaseState = 7
|
||||
case translationState = 10
|
||||
case storySource = 11
|
||||
}
|
||||
|
||||
public struct ApplicationSpecificItemCacheCollectionId {
|
||||
@ -88,6 +89,7 @@ public struct ApplicationSpecificItemCacheCollectionId {
|
||||
public static let cachedImageRecognizedContent = applicationSpecificItemCacheCollectionId(ApplicationSpecificItemCacheCollectionIdValues.cachedImageRecognizedContent.rawValue)
|
||||
public static let pendingInAppPurchaseState = applicationSpecificItemCacheCollectionId(ApplicationSpecificItemCacheCollectionIdValues.pendingInAppPurchaseState.rawValue)
|
||||
public static let translationState = applicationSpecificItemCacheCollectionId(ApplicationSpecificItemCacheCollectionIdValues.translationState.rawValue)
|
||||
public static let storySource = applicationSpecificItemCacheCollectionId(ApplicationSpecificItemCacheCollectionIdValues.storySource.rawValue)
|
||||
}
|
||||
|
||||
private enum ApplicationSpecificOrderedItemListCollectionIdValues: Int32 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user