diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 97075e32bf..c5773b83cd 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -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"; diff --git a/submodules/AnimatedStickerNode/Sources/VideoStickerFrameSource.swift b/submodules/AnimatedStickerNode/Sources/VideoStickerFrameSource.swift index d599a9bb3a..df4c44a6fb 100644 --- a/submodules/AnimatedStickerNode/Sources/VideoStickerFrameSource.swift +++ b/submodules/AnimatedStickerNode/Sources/VideoStickerFrameSource.swift @@ -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 } } diff --git a/submodules/AttachmentUI/Sources/AttachmentContainer.swift b/submodules/AttachmentUI/Sources/AttachmentContainer.swift index 85916b1fc1..7d64e0d889 100644 --- a/submodules/AttachmentUI/Sources/AttachmentContainer.swift +++ b/submodules/AttachmentUI/Sources/AttachmentContainer.swift @@ -80,6 +80,7 @@ final class AttachmentContainer: ASDisplayNode, UIGestureRecognizerDelegate { }) self.container.clipsToBounds = true self.container.overflowInset = overflowInset + self.container.shouldAnimateDisappearance = true super.init() diff --git a/submodules/Display/Source/Navigation/NavigationContainer.swift b/submodules/Display/Source/Navigation/NavigationContainer.swift index 08d9e5a25b..32c5ab81a3 100644 --- a/submodules/Display/Source/Navigation/NavigationContainer.swift +++ b/submodules/Display/Source/Navigation/NavigationContainer.swift @@ -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) diff --git a/submodules/DrawingUI/Sources/DrawingScreen.swift b/submodules/DrawingUI/Sources/DrawingScreen.swift index 555862c2a7..74caa1b4d3 100644 --- a/submodules/DrawingUI/Sources/DrawingScreen.swift +++ b/submodules/DrawingUI/Sources/DrawingScreen.swift @@ -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) diff --git a/submodules/TelegramCore/Sources/ApiUtils/AdMessageAttribute.swift b/submodules/TelegramCore/Sources/ApiUtils/AdMessageAttribute.swift index 3044b8802d..09498984de 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/AdMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/AdMessageAttribute.swift @@ -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 diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift index 18e43864ca..a2a148ba3d 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift @@ -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): diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index 1cea3b2f8d..634f7d928d 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -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 { +private func uploadedStoryContent(account: Account, media: EngineStoryInputMedia) -> (signal: Signal, 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 in - return .single(nil) + return ( + contentSignal + |> map(Optional.init) + |> `catch` { _ -> Signal 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 { + let (contentSignal, originalMedia) = uploadedStoryContent(account: account, media: media) + return contentSignal |> mapToSignal { result -> Signal in - return account.postbox.transaction { transaction -> Signal 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 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 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 { + let contentSignal: Signal + 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 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 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 in + return .single(nil) + } + |> mapToSignal { updates -> Signal 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 diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index 596aa8d33e..8754438ac8 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -918,8 +918,12 @@ public extension TelegramEngine { } } - public func uploadStory(media: EngineStoryInputMedia, text: String, entities: [MessageTextEntity], pin: Bool, privacy: EngineStoryPrivacy, period: Int) -> Signal { - 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 { + 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 { + return _internal_editStory(account: self.account, media: media, id: id, text: text, entities: entities, privacy: privacy) } public func deleteStory(id: Int32) -> Signal { diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift index 852294e8de..0b8c7195d3 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift @@ -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 diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift index dbfea1dce2..e0478fd6bd 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift @@ -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 diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift index 4153994c1e..7fbdd67396 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift @@ -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, diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift index f78c8b485d..ec1c9d3ff7 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift @@ -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) } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift index e6629b7a8f..7d5554dcdc 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift @@ -82,6 +82,11 @@ public final class MediaEditor { } set { self.histogramCalculationPass.isEnabled = newValue + if newValue { + Queue.mainQueue().justDispatch { + self.updateRenderChain() + } + } } } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift index 7871390813..2f117a1741 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift @@ -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?>() - - var frameCount: Int? - var frameRate: Int? - var currentFrameIndex: Int? - var totalDuration: Double? - let durationPromise = Promise() - - let queue = Queue() - let disposables = DisposableSet() - - var image: CIImage? - var imagePixelBuffer: CVPixelBuffer? - let imagePromise = Promise() - - 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(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]) -} diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift new file mode 100644 index 0000000000..7c5c0f7455 --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift @@ -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?>() + var videoFrameSource = Promise?>() + var isVideo = false + + var frameCount: Int? + var frameRate: Int? + var currentFrameIndex: Int? + var totalDuration: Double? + let durationPromise = Promise() + + let queue = Queue() + let disposables = DisposableSet() + + var image: CIImage? + var imagePixelBuffer: CVPixelBuffer? + let imagePromise = Promise() + + 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(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(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]) +} diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift index 08d4a35874..a53b2c9272 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift @@ -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 diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD index 1910b4e0da..f7058eeb00 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD +++ b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD @@ -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", diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 186ad786c2..21b7787e2d 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -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, 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() 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 { diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaToolsScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaToolsScreen.swift index d2a066a61a..039f404d44 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaToolsScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaToolsScreen.swift @@ -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))) } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift index 2c48d81991..d270647c45 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift @@ -250,6 +250,7 @@ final class StoryPreviewComponent: Component { strings: presentationData.strings, style: .story, placeholder: "Reply Privately...", + alwaysDarkWhenHasText: false, presentController: { _ in }, sendMessageAction: { diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StorySource.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StorySource.swift new file mode 100644 index 0000000000..4a07354d0c --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StorySource.swift @@ -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 { + 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 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() +} diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/TintComponent.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/TintComponent.swift index cf8a3dcdcd..6f7fe7d810 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/TintComponent.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/TintComponent.swift @@ -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) } ) ), diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift index 60657da8a9..3bf6e9bc2b 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift @@ -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 diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD index e4cd9e8162..8d1b7d63ae 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD @@ -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", diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift index 9eeb44fddc..1ae7cdc108 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift @@ -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) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index b88a6377f7..7fe6ce2b2c 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -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 { diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index eaa5f5a4ad..1a2c70c56f 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -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 { diff --git a/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift index 5d5f0f75de..b0f69200ca 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAttachedContentNode.swift @@ -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 } diff --git a/submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift index c327b4be8c..15274b2d44 100644 --- a/submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageContactBubbleContentNode.swift @@ -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 { diff --git a/submodules/TelegramUI/Sources/ChatMessageUnsupportedBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageUnsupportedBubbleContentNode.swift index dd9a2204b6..1a3d365060 100644 --- a/submodules/TelegramUI/Sources/ChatMessageUnsupportedBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageUnsupportedBubbleContentNode.swift @@ -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 diff --git a/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift index 982390da9c..ab5d95c40e 100644 --- a/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift @@ -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 diff --git a/submodules/TelegramUI/Sources/FetchVideoMediaResource.swift b/submodules/TelegramUI/Sources/FetchVideoMediaResource.swift index a8b5699f2e..85c8a9a5ec 100644 --- a/submodules/TelegramUI/Sources/FetchVideoMediaResource.swift +++ b/submodules/TelegramUI/Sources/FetchVideoMediaResource.swift @@ -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? diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index 17e662f99e..6284bc00eb 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -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({}) diff --git a/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift b/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift index 8756172cbd..a88995398a 100644 --- a/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift +++ b/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift @@ -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 {