Merge commit '73987dff5d4708433fe4e3527c4738a6d784915b'

# Conflicts:
#	submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift
This commit is contained in:
Ali 2023-06-08 00:43:51 +04:00
commit 780168d30b
35 changed files with 1237 additions and 538 deletions

View File

@ -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";

View File

@ -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
}
}

View File

@ -80,6 +80,7 @@ final class AttachmentContainer: ASDisplayNode, UIGestureRecognizerDelegate {
})
self.container.clipsToBounds = true
self.container.overflowInset = overflowInset
self.container.shouldAnimateDisappearance = true
super.init()

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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):

View File

@ -507,10 +507,10 @@ public final class EngineStorySubscriptions: Equatable {
public enum StoryUploadResult {
case progress(Float)
case completed
case completed(Int32?)
}
func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text: String, entities: [MessageTextEntity], pin: Bool, privacy: EngineStoryPrivacy, period: Int) -> Signal<StoryUploadResult, NoError> {
private func uploadedStoryContent(account: Account, media: EngineStoryInputMedia) -> (signal: Signal<PendingMessageUploadedContentResult?, NoError>, media: Media) {
let originalMedia: Media
let contentToUpload: MessageContentToUpload
@ -589,48 +589,60 @@ func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text:
contentSignal = signal
}
return contentSignal
|> map(Optional.init)
|> `catch` { _ -> Signal<PendingMessageUploadedContentResult?, NoError> in
return .single(nil)
return (
contentSignal
|> map(Optional.init)
|> `catch` { _ -> Signal<PendingMessageUploadedContentResult?, NoError> in
return .single(nil)
},
originalMedia
)
}
private func apiInputPrivacyRules(privacy: EngineStoryPrivacy, transaction: Transaction) -> [Api.InputPrivacyRule] {
var privacyRules: [Api.InputPrivacyRule]
switch privacy.base {
case .everyone:
privacyRules = [.inputPrivacyValueAllowAll]
case .contacts:
privacyRules = [.inputPrivacyValueAllowContacts]
case .closeFriends:
privacyRules = [.inputPrivacyValueAllowCloseFriends]
case .nobody:
privacyRules = [.inputPrivacyValueDisallowAll]
}
var privacyUsers: [Api.InputUser] = []
var privacyChats: [Int64] = []
for peerId in privacy.additionallyIncludePeers {
if let peer = transaction.getPeer(peerId) {
if let _ = peer as? TelegramUser {
if let inputUser = apiInputUser(peer) {
privacyUsers.append(inputUser)
}
} else if peer is TelegramGroup || peer is TelegramChannel {
privacyChats.append(peer.id.id._internalGetInt64Value())
}
}
}
if !privacyUsers.isEmpty {
privacyRules.append(.inputPrivacyValueAllowUsers(users: privacyUsers))
}
if !privacyChats.isEmpty {
privacyRules.append(.inputPrivacyValueAllowChatParticipants(chats: privacyChats))
}
return privacyRules
}
func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text: String, entities: [MessageTextEntity], pin: Bool, privacy: EngineStoryPrivacy, period: Int, randomId: Int64) -> Signal<StoryUploadResult, NoError> {
let (contentSignal, originalMedia) = uploadedStoryContent(account: account, media: media)
return contentSignal
|> mapToSignal { result -> Signal<StoryUploadResult, NoError> in
return account.postbox.transaction { transaction -> Signal<StoryUploadResult, NoError> in
switch result {
case let .progress(progress):
return .single(.progress(progress))
case let .content(content):
var privacyRules: [Api.InputPrivacyRule]
switch privacy.base {
case .everyone:
privacyRules = [.inputPrivacyValueAllowAll]
case .contacts:
privacyRules = [.inputPrivacyValueAllowContacts]
case .closeFriends:
privacyRules = [.inputPrivacyValueAllowCloseFriends]
case .nobody:
privacyRules = [.inputPrivacyValueDisallowAll]
}
var privacyUsers: [Api.InputUser] = []
var privacyChats: [Int64] = []
for peerId in privacy.additionallyIncludePeers {
if let peer = transaction.getPeer(peerId) {
if let _ = peer as? TelegramUser {
if let inputUser = apiInputUser(peer) {
privacyUsers.append(inputUser)
}
} else if peer is TelegramGroup || peer is TelegramChannel {
privacyChats.append(peer.id.id._internalGetInt64Value())
}
}
}
if !privacyUsers.isEmpty {
privacyRules.append(.inputPrivacyValueAllowUsers(users: privacyUsers))
}
if !privacyChats.isEmpty {
privacyRules.append(.inputPrivacyValueAllowChatParticipants(chats: privacyChats))
}
switch result {
case let .progress(progress):
return .single(.progress(progress))
case let .content(content):
return account.postbox.transaction { transaction -> Signal<StoryUploadResult, NoError> in
let privacyRules = apiInputPrivacyRules(privacy: privacy, transaction: transaction)
switch content.content {
case let .media(inputMedia, _):
var flags: Int32 = 0
@ -640,7 +652,6 @@ func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text:
if pin {
flags |= 1 << 2
}
if !text.isEmpty {
flags |= 1 << 0
apiCaption = text
@ -666,7 +677,7 @@ func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text:
caption: apiCaption,
entities: apiEntities,
privacyRules: privacyRules,
randomId: Int64.random(in: Int64.min ... Int64.max),
randomId: randomId,
period: Int32(period)
))
|> map(Optional.init)
@ -674,11 +685,13 @@ func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text:
return .single(nil)
}
|> mapToSignal { updates -> Signal<StoryUploadResult, NoError> in
var id: Int32?
if let updates = updates {
for update in updates.allUpdates {
if case let .updateStory(_, story) = update {
switch story {
case let .storyItem(_, _, _, _, _, _, media, _, _):
case let .storyItem(_, idValue, _, _, _, _, media, _, _):
id = idValue
let (parsedMedia, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, account.peerId)
if let parsedMedia = parsedMedia {
applyMediaResourceChanges(from: originalMedia, to: parsedMedia, postbox: account.postbox, force: false)
@ -692,13 +705,105 @@ func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text:
account.stateManager.addUpdates(updates)
}
return .single(.completed)
return .single(.completed(id))
}
default:
return .complete()
}
default:
return .complete()
}
|> switchToLatest
default:
return .complete()
}
}
}
func _internal_editStory(account: Account, media: EngineStoryInputMedia?, id: Int32, text: String, entities: [MessageTextEntity], privacy: EngineStoryPrivacy?) -> Signal<StoryUploadResult, NoError> {
let contentSignal: Signal<PendingMessageUploadedContentResult?, NoError>
let originalMedia: Media?
if let media = media {
(contentSignal, originalMedia) = uploadedStoryContent(account: account, media: media)
} else {
contentSignal = .single(nil)
originalMedia = nil
}
return contentSignal
|> mapToSignal { result -> Signal<StoryUploadResult, NoError> in
if let result = result, case let .progress(progress) = result {
return .single(.progress(progress))
}
let inputMedia: Api.InputMedia?
if let result = result, case let .content(uploadedContent) = result, case let .media(media, _) = uploadedContent.content {
inputMedia = media
} else {
inputMedia = nil
}
return account.postbox.transaction { transaction -> Signal<StoryUploadResult, NoError> in
var flags: Int32 = 0
var apiCaption: String?
var apiEntities: [Api.MessageEntity]?
var privacyRules: [Api.InputPrivacyRule]?
if let _ = inputMedia {
flags |= 1 << 0
}
if !text.isEmpty {
flags |= 1 << 1
apiCaption = text
if !entities.isEmpty {
flags |= 1 << 1
var associatedPeers: [PeerId: Peer] = [:]
for entity in entities {
for entityPeerId in entity.associatedPeerIds {
if let peer = transaction.getPeer(entityPeerId) {
associatedPeers[peer.id] = peer
}
}
}
apiEntities = apiEntitiesFromMessageTextEntities(entities, associatedPeers: SimpleDictionary(associatedPeers))
}
}
if let privacy = privacy {
privacyRules = apiInputPrivacyRules(privacy: privacy, transaction: transaction)
flags |= 1 << 2
}
return account.network.request(Api.functions.stories.editStory(
flags: flags,
id: id,
media: inputMedia,
caption: apiCaption,
entities: apiEntities,
privacyRules: privacyRules
))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
return .single(nil)
}
|> mapToSignal { updates -> Signal<StoryUploadResult, NoError> in
if let updates = updates {
for update in updates.allUpdates {
if case let .updateStory(_, story) = update {
switch story {
case let .storyItem(_, _, _, _, _, _, media, _, _):
let (parsedMedia, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, account.peerId)
if let parsedMedia = parsedMedia, let originalMedia = originalMedia {
applyMediaResourceChanges(from: originalMedia, to: parsedMedia, postbox: account.postbox, force: false)
}
default:
break
}
}
}
account.stateManager.addUpdates(updates)
}
return .single(.completed(id))
}
}
|> switchToLatest

View File

@ -918,8 +918,12 @@ public extension TelegramEngine {
}
}
public func uploadStory(media: EngineStoryInputMedia, text: String, entities: [MessageTextEntity], pin: Bool, privacy: EngineStoryPrivacy, period: Int) -> Signal<StoryUploadResult, NoError> {
return _internal_uploadStory(account: self.account, media: media, text: text, entities: entities, pin: pin, privacy: privacy, period: period)
public func uploadStory(media: EngineStoryInputMedia, text: String, entities: [MessageTextEntity], pin: Bool, privacy: EngineStoryPrivacy, period: Int, randomId: Int64) -> Signal<StoryUploadResult, NoError> {
return _internal_uploadStory(account: self.account, media: media, text: text, entities: entities, pin: pin, privacy: privacy, period: period, randomId: randomId)
}
public func editStory(media: EngineStoryInputMedia?, id: Int32, text: String, entities: [MessageTextEntity], privacy: EngineStoryPrivacy?) -> Signal<StoryUploadResult, NoError> {
return _internal_editStory(account: self.account, media: media, id: id, text: text, entities: entities, privacy: privacy)
}
public func deleteStory(id: Int32) -> Signal<Never, NoError> {

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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)
}

View File

@ -82,6 +82,11 @@ public final class MediaEditor {
}
set {
self.histogramCalculationPass.isEnabled = newValue
if newValue {
Queue.mainQueue().justDispatch {
self.updateRenderChain()
}
}
}
}

View File

@ -7,10 +7,6 @@ import MetalKit
import Display
import SwiftSignalKit
import TelegramCore
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import YuvConversion
import StickerResources
public func mediaEditorGenerateGradientImage(size: CGSize, colors: [UIColor]) -> UIImage? {
UIGraphicsBeginImageContextWithOptions(size, false, 1.0)
@ -280,298 +276,3 @@ private func makeEditorImageFrameComposition(inputImage: CIImage, gradientImage:
}
maybeFinalize()
}
private func composerEntityForDrawingEntity(account: Account, entity: DrawingEntity, colorSpace: CGColorSpace) -> MediaEditorComposerEntity? {
if let entity = entity as? DrawingStickerEntity {
let content: MediaEditorComposerStickerEntity.Content
switch entity.content {
case let .file(file):
content = .file(file)
case let .image(image):
content = .image(image)
}
return MediaEditorComposerStickerEntity(account: account, content: content, position: entity.position, scale: entity.scale, rotation: entity.rotation, baseSize: entity.baseSize, mirrored: entity.mirrored, colorSpace: colorSpace)
} else if let renderImage = entity.renderImage, let image = CIImage(image: renderImage, options: [.colorSpace: colorSpace]) {
if let entity = entity as? DrawingBubbleEntity {
return MediaEditorComposerStaticEntity(image: image, position: entity.position, scale: 1.0, rotation: entity.rotation, baseSize: entity.size, mirrored: false)
} else if let entity = entity as? DrawingSimpleShapeEntity {
return MediaEditorComposerStaticEntity(image: image, position: entity.position, scale: 1.0, rotation: entity.rotation, baseSize: entity.size, mirrored: false)
} else if let entity = entity as? DrawingVectorEntity {
return MediaEditorComposerStaticEntity(image: image, position: CGPoint(x: entity.drawingSize.width * 0.5, y: entity.drawingSize.height * 0.5), scale: 1.0, rotation: 0.0, baseSize: entity.drawingSize, mirrored: false)
} else if let entity = entity as? DrawingTextEntity {
return MediaEditorComposerStaticEntity(image: image, position: entity.position, scale: entity.scale, rotation: entity.rotation, baseSize: nil, mirrored: false)
}
}
return nil
}
private class MediaEditorComposerStaticEntity: MediaEditorComposerEntity {
let image: CIImage
let position: CGPoint
let scale: CGFloat
let rotation: CGFloat
let baseSize: CGSize?
let mirrored: Bool
init(image: CIImage, position: CGPoint, scale: CGFloat, rotation: CGFloat, baseSize: CGSize?, mirrored: Bool) {
self.image = image
self.position = position
self.scale = scale
self.rotation = rotation
self.baseSize = baseSize
self.mirrored = mirrored
}
func image(for time: CMTime, frameRate: Float, completion: @escaping (CIImage?) -> Void) {
completion(self.image)
}
}
private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity {
public enum Content {
case file(TelegramMediaFile)
case image(UIImage)
var file: TelegramMediaFile? {
if case let .file(file) = self {
return file
}
return nil
}
}
let content: Content
let position: CGPoint
let scale: CGFloat
let rotation: CGFloat
let baseSize: CGSize?
let mirrored: Bool
let colorSpace: CGColorSpace
var isAnimated: Bool
var source: AnimatedStickerNodeSource?
var frameSource = Promise<QueueLocalObject<AnimatedStickerDirectFrameSource>?>()
var frameCount: Int?
var frameRate: Int?
var currentFrameIndex: Int?
var totalDuration: Double?
let durationPromise = Promise<Double>()
let queue = Queue()
let disposables = DisposableSet()
var image: CIImage?
var imagePixelBuffer: CVPixelBuffer?
let imagePromise = Promise<UIImage>()
init(account: Account, content: Content, position: CGPoint, scale: CGFloat, rotation: CGFloat, baseSize: CGSize, mirrored: Bool, colorSpace: CGColorSpace) {
self.content = content
self.position = position
self.scale = scale
self.rotation = rotation
self.baseSize = baseSize
self.mirrored = mirrored
self.colorSpace = colorSpace
switch content {
case let .file(file):
if file.isAnimatedSticker || file.isVideoSticker || file.mimeType == "video/webm" {
self.isAnimated = true
self.source = AnimatedStickerResourceSource(account: account, resource: file.resource, isVideo: file.isVideoSticker || file.mimeType == "video/webm")
let pathPrefix = account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id)
if let source = self.source {
let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512)
let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 384, height: 384))
self.disposables.add((source.directDataPath(attemptSynchronously: true)
|> deliverOn(self.queue)).start(next: { [weak self] path in
if let strongSelf = self, let path {
if let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedRead]) {
let queue = strongSelf.queue
let frameSource = QueueLocalObject<AnimatedStickerDirectFrameSource>(queue: queue, generate: {
return AnimatedStickerDirectFrameSource(queue: queue, data: data, width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), cachePathPrefix: pathPrefix, useMetalCache: false, fitzModifier: nil)!
})
frameSource.syncWith { frameSource in
strongSelf.frameCount = frameSource.frameCount
strongSelf.frameRate = frameSource.frameRate
let duration = Double(frameSource.frameCount) / Double(frameSource.frameRate)
strongSelf.totalDuration = duration
strongSelf.durationPromise.set(.single(duration))
}
strongSelf.frameSource.set(.single(frameSource))
}
}
}))
}
} else {
self.isAnimated = false
self.disposables.add((chatMessageSticker(account: account, userLocation: .other, file: file, small: false, fetched: true, onlyFullSize: true, thumbnail: false, synchronousLoad: false, colorSpace: self.colorSpace)
|> deliverOn(self.queue)).start(next: { [weak self] generator in
if let self {
let context = generator(TransformImageArguments(corners: ImageCorners(), imageSize: baseSize, boundingSize: baseSize, intrinsicInsets: UIEdgeInsets()))
let image = context?.generateImage(colorSpace: self.colorSpace)
if let image {
self.imagePromise.set(.single(image))
}
}
}))
}
case let .image(image):
self.isAnimated = false
self.imagePromise.set(.single(image))
}
}
deinit {
self.disposables.dispose()
}
var tested = false
func image(for time: CMTime, frameRate: Float, completion: @escaping (CIImage?) -> Void) {
if self.isAnimated {
let currentTime = CMTimeGetSeconds(time)
var tintColor: UIColor?
if let file = self.content.file, file.isCustomTemplateEmoji {
tintColor = .white
}
self.disposables.add((self.frameSource.get()
|> take(1)
|> deliverOn(self.queue)).start(next: { [weak self] frameSource in
guard let strongSelf = self else {
completion(nil)
return
}
guard let frameSource, let duration = strongSelf.totalDuration, let frameCount = strongSelf.frameCount else {
completion(nil)
return
}
let relativeTime = currentTime - floor(currentTime / duration) * duration
var t = relativeTime / duration
t = max(0.0, t)
t = min(1.0, t)
let startFrame: Double = 0
let endFrame = Double(frameCount)
let frameOffset = Int(Double(startFrame) * (1.0 - t) + Double(endFrame - 1) * t)
let lowerBound: Int = 0
let upperBound = frameCount - 1
let frameIndex = max(lowerBound, min(upperBound, frameOffset))
let currentFrameIndex = strongSelf.currentFrameIndex
if currentFrameIndex != frameIndex {
let previousFrameIndex = currentFrameIndex
strongSelf.currentFrameIndex = frameIndex
var delta = 1
if let previousFrameIndex = previousFrameIndex {
delta = max(1, frameIndex - previousFrameIndex)
}
var frame: AnimatedStickerFrame?
frameSource.syncWith { frameSource in
for i in 0 ..< delta {
frame = frameSource.takeFrame(draw: i == delta - 1)
}
}
if let frame {
var imagePixelBuffer: CVPixelBuffer?
if let pixelBuffer = strongSelf.imagePixelBuffer {
imagePixelBuffer = pixelBuffer
} else {
let ioSurfaceProperties = NSMutableDictionary()
let options = NSMutableDictionary()
options.setObject(ioSurfaceProperties, forKey: kCVPixelBufferIOSurfacePropertiesKey as NSString)
var pixelBuffer: CVPixelBuffer?
CVPixelBufferCreate(
kCFAllocatorDefault,
frame.width,
frame.height,
kCVPixelFormatType_32BGRA,
options,
&pixelBuffer
)
imagePixelBuffer = pixelBuffer
strongSelf.imagePixelBuffer = pixelBuffer
}
if let imagePixelBuffer {
let image = render(width: frame.width, height: frame.height, bytesPerRow: frame.bytesPerRow, data: frame.data, type: frame.type, pixelBuffer: imagePixelBuffer, colorSpace: strongSelf.colorSpace, tintColor: tintColor)
strongSelf.image = image
}
completion(strongSelf.image)
} else {
completion(nil)
}
} else {
completion(strongSelf.image)
}
}))
} else {
var image: CIImage?
if let cachedImage = self.image {
image = cachedImage
completion(image)
} else {
let _ = (self.imagePromise.get()
|> take(1)
|> deliverOn(self.queue)).start(next: { [weak self] image in
if let self {
self.image = CIImage(image: image, options: [.colorSpace: self.colorSpace])
completion(self.image)
}
})
}
}
}
}
protocol MediaEditorComposerEntity {
var position: CGPoint { get }
var scale: CGFloat { get }
var rotation: CGFloat { get }
var baseSize: CGSize? { get }
var mirrored: Bool { get }
func image(for time: CMTime, frameRate: Float, completion: @escaping (CIImage?) -> Void)
}
private func render(width: Int, height: Int, bytesPerRow: Int, data: Data, type: AnimationRendererFrameType, pixelBuffer: CVPixelBuffer, colorSpace: CGColorSpace, tintColor: UIColor?) -> CIImage? {
//let calculatedBytesPerRow = (4 * Int(width) + 31) & (~31)
//assert(bytesPerRow == calculatedBytesPerRow)
CVPixelBufferLockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0))
let dest = CVPixelBufferGetBaseAddress(pixelBuffer)
switch type {
case .yuva:
data.withUnsafeBytes { buffer -> Void in
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
decodeYUVAToRGBA(bytes, dest, Int32(width), Int32(height), Int32(width * 4))
}
case .argb:
data.withUnsafeBytes { buffer -> Void in
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
memcpy(dest, bytes, data.count)
}
case .dct:
break
}
CVPixelBufferUnlockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0))
return CIImage(cvPixelBuffer: pixelBuffer, options: [.colorSpace: colorSpace])
}

View File

@ -0,0 +1,329 @@
import Foundation
import AVFoundation
import UIKit
import CoreImage
import Metal
import MetalKit
import Display
import SwiftSignalKit
import TelegramCore
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import YuvConversion
import StickerResources
func composerEntityForDrawingEntity(account: Account, entity: DrawingEntity, colorSpace: CGColorSpace) -> MediaEditorComposerEntity? {
if let entity = entity as? DrawingStickerEntity {
let content: MediaEditorComposerStickerEntity.Content
switch entity.content {
case let .file(file):
content = .file(file)
case let .image(image):
content = .image(image)
}
return MediaEditorComposerStickerEntity(account: account, content: content, position: entity.position, scale: entity.scale, rotation: entity.rotation, baseSize: entity.baseSize, mirrored: entity.mirrored, colorSpace: colorSpace)
} else if let renderImage = entity.renderImage, let image = CIImage(image: renderImage, options: [.colorSpace: colorSpace]) {
if let entity = entity as? DrawingBubbleEntity {
return MediaEditorComposerStaticEntity(image: image, position: entity.position, scale: 1.0, rotation: entity.rotation, baseSize: entity.size, mirrored: false)
} else if let entity = entity as? DrawingSimpleShapeEntity {
return MediaEditorComposerStaticEntity(image: image, position: entity.position, scale: 1.0, rotation: entity.rotation, baseSize: entity.size, mirrored: false)
} else if let entity = entity as? DrawingVectorEntity {
return MediaEditorComposerStaticEntity(image: image, position: CGPoint(x: entity.drawingSize.width * 0.5, y: entity.drawingSize.height * 0.5), scale: 1.0, rotation: 0.0, baseSize: entity.drawingSize, mirrored: false)
} else if let entity = entity as? DrawingTextEntity {
return MediaEditorComposerStaticEntity(image: image, position: entity.position, scale: entity.scale, rotation: entity.rotation, baseSize: nil, mirrored: false)
}
}
return nil
}
private class MediaEditorComposerStaticEntity: MediaEditorComposerEntity {
let image: CIImage
let position: CGPoint
let scale: CGFloat
let rotation: CGFloat
let baseSize: CGSize?
let mirrored: Bool
init(image: CIImage, position: CGPoint, scale: CGFloat, rotation: CGFloat, baseSize: CGSize?, mirrored: Bool) {
self.image = image
self.position = position
self.scale = scale
self.rotation = rotation
self.baseSize = baseSize
self.mirrored = mirrored
}
func image(for time: CMTime, frameRate: Float, completion: @escaping (CIImage?) -> Void) {
completion(self.image)
}
}
private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity {
public enum Content {
case file(TelegramMediaFile)
case image(UIImage)
var file: TelegramMediaFile? {
if case let .file(file) = self {
return file
}
return nil
}
}
let content: Content
let position: CGPoint
let scale: CGFloat
let rotation: CGFloat
let baseSize: CGSize?
let mirrored: Bool
let colorSpace: CGColorSpace
var isAnimated: Bool
var source: AnimatedStickerNodeSource?
var frameSource = Promise<QueueLocalObject<AnimatedStickerDirectFrameSource>?>()
var videoFrameSource = Promise<QueueLocalObject<VideoStickerDirectFrameSource>?>()
var isVideo = false
var frameCount: Int?
var frameRate: Int?
var currentFrameIndex: Int?
var totalDuration: Double?
let durationPromise = Promise<Double>()
let queue = Queue()
let disposables = DisposableSet()
var image: CIImage?
var imagePixelBuffer: CVPixelBuffer?
let imagePromise = Promise<UIImage>()
init(account: Account, content: Content, position: CGPoint, scale: CGFloat, rotation: CGFloat, baseSize: CGSize, mirrored: Bool, colorSpace: CGColorSpace) {
self.content = content
self.position = position
self.scale = scale
self.rotation = rotation
self.baseSize = baseSize
self.mirrored = mirrored
self.colorSpace = colorSpace
switch content {
case let .file(file):
if file.isAnimatedSticker || file.isVideoSticker || file.mimeType == "video/webm" {
self.isAnimated = true
self.isVideo = file.isVideoSticker || file.mimeType == "video/webm"
self.source = AnimatedStickerResourceSource(account: account, resource: file.resource, isVideo: isVideo)
let pathPrefix = account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id)
if let source = self.source {
let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512)
let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 384, height: 384))
self.disposables.add((source.directDataPath(attemptSynchronously: true)
|> deliverOn(self.queue)).start(next: { [weak self] path in
if let strongSelf = self, let path {
if let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedRead]) {
let queue = strongSelf.queue
if strongSelf.isVideo {
let frameSource = QueueLocalObject<VideoStickerDirectFrameSource>(queue: queue, generate: {
return VideoStickerDirectFrameSource(queue: queue, path: path, width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), cachePathPrefix: pathPrefix, unpremultiplyAlpha: false)!
})
frameSource.syncWith { frameSource in
strongSelf.frameCount = frameSource.frameCount
strongSelf.frameRate = frameSource.frameRate
let duration = Double(frameSource.frameCount) / Double(frameSource.frameRate)
strongSelf.totalDuration = duration
strongSelf.durationPromise.set(.single(duration))
}
strongSelf.videoFrameSource.set(.single(frameSource))
} else {
let frameSource = QueueLocalObject<AnimatedStickerDirectFrameSource>(queue: queue, generate: {
return AnimatedStickerDirectFrameSource(queue: queue, data: data, width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), cachePathPrefix: pathPrefix, useMetalCache: false, fitzModifier: nil)!
})
frameSource.syncWith { frameSource in
strongSelf.frameCount = frameSource.frameCount
strongSelf.frameRate = frameSource.frameRate
let duration = Double(frameSource.frameCount) / Double(frameSource.frameRate)
strongSelf.totalDuration = duration
strongSelf.durationPromise.set(.single(duration))
}
strongSelf.frameSource.set(.single(frameSource))
}
}
}
}))
}
} else {
self.isAnimated = false
self.disposables.add((chatMessageSticker(account: account, userLocation: .other, file: file, small: false, fetched: true, onlyFullSize: true, thumbnail: false, synchronousLoad: false, colorSpace: self.colorSpace)
|> deliverOn(self.queue)).start(next: { [weak self] generator in
if let self {
let context = generator(TransformImageArguments(corners: ImageCorners(), imageSize: baseSize, boundingSize: baseSize, intrinsicInsets: UIEdgeInsets()))
let image = context?.generateImage(colorSpace: self.colorSpace)
if let image {
self.imagePromise.set(.single(image))
}
}
}))
}
case let .image(image):
self.isAnimated = false
self.imagePromise.set(.single(image))
}
}
deinit {
self.disposables.dispose()
}
var tested = false
func image(for time: CMTime, frameRate: Float, completion: @escaping (CIImage?) -> Void) {
if self.isAnimated {
let currentTime = CMTimeGetSeconds(time)
var tintColor: UIColor?
if let file = self.content.file, file.isCustomTemplateEmoji {
tintColor = .white
}
self.disposables.add((self.frameSource.get()
|> take(1)
|> deliverOn(self.queue)).start(next: { [weak self] frameSource in
guard let strongSelf = self else {
completion(nil)
return
}
guard let frameSource, let duration = strongSelf.totalDuration, let frameCount = strongSelf.frameCount else {
completion(nil)
return
}
let relativeTime = currentTime - floor(currentTime / duration) * duration
var t = relativeTime / duration
t = max(0.0, t)
t = min(1.0, t)
let startFrame: Double = 0
let endFrame = Double(frameCount)
let frameOffset = Int(Double(startFrame) * (1.0 - t) + Double(endFrame - 1) * t)
let lowerBound: Int = 0
let upperBound = frameCount - 1
let frameIndex = max(lowerBound, min(upperBound, frameOffset))
let currentFrameIndex = strongSelf.currentFrameIndex
if currentFrameIndex != frameIndex {
let previousFrameIndex = currentFrameIndex
strongSelf.currentFrameIndex = frameIndex
var delta = 1
if let previousFrameIndex = previousFrameIndex {
delta = max(1, frameIndex - previousFrameIndex)
}
var frame: AnimatedStickerFrame?
frameSource.syncWith { frameSource in
for i in 0 ..< delta {
frame = frameSource.takeFrame(draw: i == delta - 1)
}
}
if let frame {
var imagePixelBuffer: CVPixelBuffer?
if let pixelBuffer = strongSelf.imagePixelBuffer {
imagePixelBuffer = pixelBuffer
} else {
let ioSurfaceProperties = NSMutableDictionary()
let options = NSMutableDictionary()
options.setObject(ioSurfaceProperties, forKey: kCVPixelBufferIOSurfacePropertiesKey as NSString)
var pixelBuffer: CVPixelBuffer?
CVPixelBufferCreate(
kCFAllocatorDefault,
frame.width,
frame.height,
kCVPixelFormatType_32BGRA,
options,
&pixelBuffer
)
imagePixelBuffer = pixelBuffer
strongSelf.imagePixelBuffer = pixelBuffer
}
if let imagePixelBuffer {
let image = render(width: frame.width, height: frame.height, bytesPerRow: frame.bytesPerRow, data: frame.data, type: frame.type, pixelBuffer: imagePixelBuffer, tintColor: tintColor)
strongSelf.image = image
}
completion(strongSelf.image)
} else {
completion(nil)
}
} else {
completion(strongSelf.image)
}
}))
} else {
var image: CIImage?
if let cachedImage = self.image {
image = cachedImage
completion(image)
} else {
let _ = (self.imagePromise.get()
|> take(1)
|> deliverOn(self.queue)).start(next: { [weak self] image in
if let self {
self.image = CIImage(image: image, options: [.colorSpace: self.colorSpace])
completion(self.image)
}
})
}
}
}
}
protocol MediaEditorComposerEntity {
var position: CGPoint { get }
var scale: CGFloat { get }
var rotation: CGFloat { get }
var baseSize: CGSize? { get }
var mirrored: Bool { get }
func image(for time: CMTime, frameRate: Float, completion: @escaping (CIImage?) -> Void)
}
private func render(width: Int, height: Int, bytesPerRow: Int, data: Data, type: AnimationRendererFrameType, pixelBuffer: CVPixelBuffer, tintColor: UIColor?) -> CIImage? {
//let calculatedBytesPerRow = (4 * Int(width) + 31) & (~31)
//assert(bytesPerRow == calculatedBytesPerRow)
CVPixelBufferLockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0))
let dest = CVPixelBufferGetBaseAddress(pixelBuffer)
switch type {
case .yuva:
data.withUnsafeBytes { buffer -> Void in
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
decodeYUVAToRGBA(bytes, dest, Int32(width), Int32(height), Int32(width * 4))
}
case .argb:
data.withUnsafeBytes { buffer -> Void in
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
memcpy(dest, bytes, data.count)
}
case .dct:
break
}
CVPixelBufferUnlockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0))
return CIImage(cvPixelBuffer: pixelBuffer, options: [.colorSpace: deviceColorSpace])
}

View File

@ -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

View File

@ -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",

View File

@ -650,14 +650,28 @@ final class MediaEditorScreenComponent: Component {
}
let timeoutValue: Int32
var timeoutValue: String
let timeoutSelected: Bool
switch component.privacy {
case let .story(_, archive):
timeoutValue = 24
timeoutSelected = !archive
case let .story(_, timeout, archive):
switch timeout {
case 21600:
timeoutValue = "6"
case 43200:
timeoutValue = "12"
case 86400:
timeoutValue = "24"
case 172800:
timeoutValue = "2d"
default:
timeoutValue = "24"
}
if archive {
timeoutValue = ""
}
timeoutSelected = false
case let .message(_, timeout):
timeoutValue = timeout ?? 1
timeoutValue = "\(timeout ?? 1)"
timeoutSelected = timeout != nil
}
@ -671,6 +685,7 @@ final class MediaEditorScreenComponent: Component {
strings: environment.strings,
style: .editor,
placeholder: "Add a caption...",
alwaysDarkWhenHasText: false,
presentController: { [weak self] c in
guard let self, let _ = self.component else {
return
@ -693,13 +708,7 @@ final class MediaEditorScreenComponent: Component {
guard let self, let controller = self.environment?.controller() as? MediaEditorScreen else {
return
}
switch controller.state.privacy {
case let .story(privacy, archive):
controller.state.privacy = .story(privacy: privacy, archive: !archive)
controller.node.presentStoryArchiveTooltip(sourceView: view)
case .message:
controller.presentTimeoutSetup(sourceView: view)
}
controller.presentTimeoutSetup(sourceView: view)
},
audioRecorder: nil,
videoRecordingStatus: nil,
@ -741,7 +750,7 @@ final class MediaEditorScreenComponent: Component {
let privacyText: String
switch component.privacy {
case let .story(privacy, _):
case let .story(privacy, _, _):
switch privacy.base {
case .everyone:
privacyText = "Everyone"
@ -773,7 +782,7 @@ final class MediaEditorScreenComponent: Component {
),
action: {
if let controller = environment.controller() as? MediaEditorScreen {
controller.presentPrivacySettings()
controller.openPrivacySettings()
}
}
).tagged(privacyButtonTag)),
@ -1026,8 +1035,8 @@ private let storyDimensions = CGSize(width: 1080.0, height: 1920.0)
private let storyMaxVideoDuration: Double = 60.0
public enum MediaEditorResultPrivacy: Equatable {
case story(privacy: EngineStoryPrivacy, archive: Bool)
case message(peers: [EnginePeer.Id], timeout: Int32?)
case story(privacy: EngineStoryPrivacy, timeout: Int, archive: Bool)
case message(peers: [EnginePeer.Id], timeout: Int?)
}
public final class MediaEditorScreen: ViewController {
@ -1069,7 +1078,7 @@ public final class MediaEditorScreen: ViewController {
}
struct State {
var privacy: MediaEditorResultPrivacy = .story(privacy: EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []), archive: false)
var privacy: MediaEditorResultPrivacy = .story(privacy: EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []), timeout: 86400, archive: false)
}
var state = State() {
@ -1295,7 +1304,7 @@ public final class MediaEditorScreen: ViewController {
}
let initialValues: MediaEditorValues?
if case let .draft(draft) = subject {
if case let .draft(draft, _) = subject {
initialValues = draft.values
for entity in draft.values.entities {
@ -1567,6 +1576,10 @@ public final class MediaEditorScreen: ViewController {
}
}
}
} else {
if let view = self.componentHost.view as? MediaEditorScreenComponent.View {
view.animateIn(from: .camera)
}
}
// Queue.mainQueue().after(0.5) {
@ -1757,6 +1770,34 @@ public final class MediaEditorScreen: ViewController {
}
}
func updateEditProgress(_ progress: Float) {
guard let controller = self.controller else {
return
}
if let saveTooltip = self.saveTooltip {
if case .completion = saveTooltip.content {
saveTooltip.dismiss()
self.saveTooltip = nil
}
}
let text = "Uploading..."
if let tooltipController = self.saveTooltip {
tooltipController.content = .progress(text, progress)
} else {
let tooltipController = SaveProgressScreen(context: self.context, content: .progress(text, 0.0))
tooltipController.cancelled = { [weak self] in
if let self, let controller = self.controller {
controller.cancelVideoExport()
}
}
controller.present(tooltipController, in: .current)
self.saveTooltip = tooltipController
}
}
func updateVideoExportProgress(_ progress: Float) {
guard let controller = self.controller else {
return
@ -1787,7 +1828,7 @@ public final class MediaEditorScreen: ViewController {
private weak var storyArchiveTooltip: ViewController?
func presentStoryArchiveTooltip(sourceView: UIView) {
guard let controller = self.controller, case let .story(_, archive) = controller.state.privacy else {
guard let controller = self.controller, case let .story(_, _, archive) = controller.state.privacy else {
return
}
@ -1838,7 +1879,8 @@ public final class MediaEditorScreen: ViewController {
self.validLayout = layout
let previewSize = CGSize(width: layout.size.width, height: floorToScreenPixels(layout.size.width * 1.77778))
let topInset: CGFloat = floorToScreenPixels(layout.size.height - previewSize.height) / 2.0
let topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 12.0 //floorToScreenPixels(layout.size.height - previewSize.height) / 2.0
let bottomInset = layout.size.height - previewSize.height - topInset
let environment = ViewControllerComponentContainer.Environment(
statusBarHeight: layout.statusBarHeight ?? 0.0,
@ -1846,7 +1888,7 @@ public final class MediaEditorScreen: ViewController {
safeInsets: UIEdgeInsets(
top: topInset,
left: layout.safeInsets.left,
bottom: topInset,
bottom: bottomInset,
right: layout.safeInsets.right
),
inputHeight: layout.inputHeight ?? 0.0,
@ -2022,7 +2064,7 @@ public final class MediaEditorScreen: ViewController {
if self.entitiesView.selectedEntityView != nil || self.isDisplayingTool {
bottomInputOffset = inputHeight / 2.0
} else {
bottomInputOffset = inputHeight - topInset - 17.0
bottomInputOffset = inputHeight - bottomInset - 17.0
}
}
@ -2052,7 +2094,7 @@ public final class MediaEditorScreen: ViewController {
case image(UIImage, PixelDimensions)
case video(String, UIImage?, PixelDimensions)
case asset(PHAsset)
case draft(MediaEditorDraft)
case draft(MediaEditorDraft, Int64?)
var dimensions: PixelDimensions {
switch self {
@ -2060,7 +2102,7 @@ public final class MediaEditorScreen: ViewController {
return dimensions
case let .asset(asset):
return PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight))
case let .draft(draft):
case let .draft(draft, _):
return draft.dimensions
}
}
@ -2073,7 +2115,7 @@ public final class MediaEditorScreen: ViewController {
return .video(videoPath, transitionImage, dimensions)
case let .asset(asset):
return .asset(asset)
case let .draft(draft):
case let .draft(draft, _):
return .draft(draft)
}
}
@ -2086,7 +2128,7 @@ public final class MediaEditorScreen: ViewController {
return .video(videoPath, dimensions)
case let .asset(asset):
return .asset(asset)
case let .draft(draft):
case let .draft(draft, _):
return .image(draft.thumbnail, draft.dimensions)
}
}
@ -2108,7 +2150,7 @@ public final class MediaEditorScreen: ViewController {
fileprivate let transitionOut: (Bool) -> TransitionOut?
public var cancelled: (Bool) -> Void = { _ in }
public var completion: (MediaEditorScreen.Result, MediaEditorResultPrivacy, @escaping (@escaping () -> Void) -> Void) -> Void = { _, _, _ in }
public var completion: (Int64, MediaEditorScreen.Result, MediaEditorResultPrivacy, @escaping (@escaping () -> Void) -> Void) -> Void = { _, _, _, _ in }
public var dismissed: () -> Void = { }
public init(
@ -2116,7 +2158,7 @@ public final class MediaEditorScreen: ViewController {
subject: Signal<Subject?, NoError>,
transitionIn: TransitionIn?,
transitionOut: @escaping (Bool) -> TransitionOut?,
completion: @escaping (MediaEditorScreen.Result, MediaEditorResultPrivacy, @escaping (@escaping () -> Void) -> Void) -> Void
completion: @escaping (Int64, MediaEditorScreen.Result, MediaEditorResultPrivacy, @escaping (@escaping () -> Void) -> Void) -> Void
) {
self.context = context
self.subject = subject
@ -2147,9 +2189,9 @@ public final class MediaEditorScreen: ViewController {
super.displayNodeDidLoad()
}
func presentPrivacySettings() {
func openPrivacySettings() {
if case .message(_, _) = self.state.privacy {
self.presentSendAsMessage()
self.openSendAsMessage()
} else {
let stateContext = ShareWithPeersScreen.StateContext(context: self.context, subject: .stories)
let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in
@ -2157,9 +2199,13 @@ public final class MediaEditorScreen: ViewController {
return
}
var archive = true
var timeout: Int = 86400
let initialPrivacy: EngineStoryPrivacy
if case let .story(privacy, _) = self.state.privacy {
if case let .story(privacy, timeoutValue, archiveValue) = self.state.privacy {
initialPrivacy = privacy
timeout = timeoutValue
archive = archiveValue
} else {
initialPrivacy = EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: [])
}
@ -2173,25 +2219,25 @@ public final class MediaEditorScreen: ViewController {
guard let self else {
return
}
self.state.privacy = .story(privacy: privacy, archive: true)
self.state.privacy = .story(privacy: privacy, timeout: timeout, archive: archive)
},
editCategory: { [weak self] privacy in
guard let self else {
return
}
self.presentEditCategory(privacy: privacy, completion: { [weak self] privacy in
self.openEditCategory(privacy: privacy, completion: { [weak self] privacy in
guard let self else {
return
}
self.state.privacy = .story(privacy: privacy, archive: true)
self.presentPrivacySettings()
self.state.privacy = .story(privacy: privacy, timeout: timeout, archive: archive)
self.openPrivacySettings()
})
},
secondaryAction: { [weak self] in
guard let self else {
return
}
self.presentSendAsMessage()
self.openSendAsMessage()
}
)
)
@ -2199,7 +2245,7 @@ public final class MediaEditorScreen: ViewController {
}
}
private func presentEditCategory(privacy: EngineStoryPrivacy, completion: @escaping (EngineStoryPrivacy) -> Void) {
private func openEditCategory(privacy: EngineStoryPrivacy, completion: @escaping (EngineStoryPrivacy) -> Void) {
let stateContext = ShareWithPeersScreen.StateContext(context: self.context, subject: .contacts(privacy.base))
let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in
guard let self else {
@ -2225,14 +2271,14 @@ public final class MediaEditorScreen: ViewController {
guard let self else {
return
}
self.presentSendAsMessage()
self.openSendAsMessage()
}
)
)
})
}
private func presentSendAsMessage() {
private func openSendAsMessage() {
var initialPeerIds = Set<EnginePeer.Id>()
if case let .message(peers, _) = self.state.privacy {
initialPeerIds = Set(peers)
@ -2264,54 +2310,109 @@ public final class MediaEditorScreen: ViewController {
func presentTimeoutSetup(sourceView: UIView) {
var items: [ContextMenuItem] = []
let updateTimeout: (Int32?) -> Void = { [weak self] timeout in
let updateTimeout: (Int?, Bool) -> Void = { [weak self] timeout, archive in
guard let self else {
return
}
if case let .message(peers, _) = self.state.privacy {
switch self.state.privacy {
case let .story(privacy, _, _):
self.state.privacy = .story(privacy: privacy, timeout: timeout ?? 86400, archive: archive)
case let .message(peers, _):
self.state.privacy = .message(peers: peers, timeout: timeout)
}
}
var currentValue: Int?
var currentArchived = false
let emptyAction: ((ContextMenuActionItem.Action) -> Void)? = nil
items.append(.action(ContextMenuActionItem(text: "Choose how long the media will be kept after opening.", textLayout: .multiline, textFont: .small, icon: { _ in return nil }, action: emptyAction)))
let title: String
switch self.state.privacy {
case let .story(_, timeoutValue, archivedValue):
title = "Choose how long the story will be kept."
currentValue = timeoutValue
currentArchived = archivedValue
case let .message(_, timeoutValue):
title = "Choose how long the media will be kept after opening."
currentValue = timeoutValue
}
items.append(.action(ContextMenuActionItem(text: title, textLayout: .multiline, textFont: .small, icon: { _ in return nil }, action: emptyAction)))
switch self.state.privacy {
case .story:
items.append(.action(ContextMenuActionItem(text: "6 Hours", icon: { theme in
return currentValue == 3600 * 6 ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil
}, action: { _, a in
a(.default)
updateTimeout(3600 * 6, false)
})))
items.append(.action(ContextMenuActionItem(text: "12 Hours", icon: { theme in
return currentValue == 3600 * 12 ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil
}, action: { _, a in
a(.default)
updateTimeout(3600 * 12, false)
})))
items.append(.action(ContextMenuActionItem(text: "24 Hours", icon: { theme in
return currentValue == 86400 && !currentArchived ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil
}, action: { _, a in
a(.default)
updateTimeout(86400, false)
})))
items.append(.action(ContextMenuActionItem(text: "2 Days", icon: { theme in
return currentValue == 86400 * 2 ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil
}, action: { _, a in
a(.default)
updateTimeout(86400 * 2, false)
})))
items.append(.action(ContextMenuActionItem(text: "Forever", icon: { theme in
return currentArchived ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil
}, action: { _, a in
a(.default)
updateTimeout(86400, true)
})))
case .message:
items.append(.action(ContextMenuActionItem(text: "Until First View", icon: { _ in
return nil
}, action: { _, a in
a(.default)
updateTimeout(1, false)
})))
items.append(.action(ContextMenuActionItem(text: "3 Seconds", icon: { _ in
return nil
}, action: { _, a in
a(.default)
updateTimeout(3, false)
})))
items.append(.action(ContextMenuActionItem(text: "10 Seconds", icon: { _ in
return nil
}, action: { _, a in
a(.default)
updateTimeout(10, false)
})))
items.append(.action(ContextMenuActionItem(text: "1 Minute", icon: { _ in
return nil
}, action: { _, a in
a(.default)
updateTimeout(60, false)
})))
items.append(.action(ContextMenuActionItem(text: "Keep Always", icon: { _ in
return nil
}, action: { _, a in
a(.default)
updateTimeout(nil, false)
})))
}
items.append(.action(ContextMenuActionItem(text: "Until First View", icon: { _ in
return nil
}, action: { _, a in
a(.default)
updateTimeout(1)
})))
items.append(.action(ContextMenuActionItem(text: "3 Seconds", icon: { _ in
return nil
}, action: { _, a in
a(.default)
updateTimeout(3)
})))
items.append(.action(ContextMenuActionItem(text: "10 Seconds", icon: { _ in
return nil
}, action: { _, a in
a(.default)
updateTimeout(10)
})))
items.append(.action(ContextMenuActionItem(text: "1 Minute", icon: { _ in
return nil
}, action: { _, a in
a(.default)
updateTimeout(60)
})))
items.append(.action(ContextMenuActionItem(text: "Keep Always", icon: { _ in
return nil
}, action: { _, a in
a(.default)
updateTimeout(nil)
})))
let presentationData = self.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme)
let contextController = ContextController(account: self.context.account, presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: self, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: nil)
self.present(contextController, in: .window(.root))
@ -2365,11 +2466,11 @@ public final class MediaEditorScreen: ViewController {
self.dismissAllTooltips()
if saveDraft {
self.saveDraft()
self.saveDraft(id: nil)
} else {
if case let .draft(draft) = self.node.subject {
removeStoryDraft(engine: self.context.engine, path: draft.path, delete: true)
}
// if case let .draft(draft) = self.node.subject {
// removeStoryDraft(engine: self.context.engine, path: draft.path, delete: true)
// }
}
if let mediaEditor = self.node.mediaEditor {
@ -2384,7 +2485,7 @@ public final class MediaEditorScreen: ViewController {
})
}
private func saveDraft() {
private func saveDraft(id: Int64?) {
guard let subject = self.node.subject, let values = self.node.mediaEditor?.values else {
return
}
@ -2396,22 +2497,63 @@ public final class MediaEditorScreen: ViewController {
return
}
let fittedSize = resultImage.size.aspectFitted(CGSize(width: 128.0, height: 128.0))
if case let .image(image, dimensions) = subject {
let saveImageDraft: (UIImage, PixelDimensions) -> Void = { image, dimensions in
if let thumbnailImage = generateScaledImage(image: resultImage, size: fittedSize) {
let path = draftPath() + "\(Int64.random(in: .min ... .max)).jpg"
let path = draftPath() + "/\(Int64.random(in: .min ... .max)).jpg"
if let data = image.jpegData(compressionQuality: 0.87) {
try? data.write(to: URL(fileURLWithPath: path))
let draft = MediaEditorDraft(path: path, isVideo: false, thumbnail: thumbnailImage, dimensions: dimensions, values: values)
addStoryDraft(engine: self.context.engine, item: draft)
if let id {
saveStorySource(engine: self.context.engine, item: draft, id: id)
} else {
addStoryDraft(engine: self.context.engine, item: draft)
}
}
}
} else if case let .draft(draft) = subject {
}
let saveVideoDraft: (String, PixelDimensions) -> Void = { videoPath, dimensions in
if let thumbnailImage = generateScaledImage(image: resultImage, size: fittedSize) {
removeStoryDraft(engine: self.context.engine, path: draft.path, delete: false)
let draft = MediaEditorDraft(path: draft.path, isVideo: draft.isVideo, thumbnail: thumbnailImage, dimensions: draft.dimensions, values: values)
addStoryDraft(engine: self.context.engine, item: draft)
let path = draftPath() + "/\(Int64.random(in: .min ... .max)).mp4"
_ = thumbnailImage
_ = path
_ = videoPath
_ = dimensions
}
}
switch subject {
case let .image(image, dimensions):
saveImageDraft(image, dimensions)
case let .video(path, _, dimensions):
saveVideoDraft(path, dimensions)
case let .asset(asset):
if asset.mediaType == .video {
PHImageManager.default().requestAVAsset(forVideo: asset, options: nil) { avAsset, _, _ in
let _ = avAsset
}
} else {
let options = PHImageRequestOptions()
options.deliveryMode = .highQualityFormat
PHImageManager.default().requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .default, options: options) { image, _ in
if let image {
saveImageDraft(image, PixelDimensions(image.size))
}
}
}
case let .draft(draft, _):
if draft.isVideo {
} else if let image = UIImage(contentsOfFile: draft.path) {
saveImageDraft(image, PixelDimensions(image.size))
}
// if let thumbnailImage = generateScaledImage(image: resultImage, size: fittedSize) {
// removeStoryDraft(engine: self.context.engine, path: draft.path, delete: false)
// let draft = MediaEditorDraft(path: draft.path, isVideo: draft.isVideo, thumbnail: thumbnailImage, dimensions: draft.dimensions, values: values)
// addStoryDraft(engine: self.context.engine, item: draft)
// }
}
})
}
}
@ -2435,6 +2577,13 @@ public final class MediaEditorScreen: ViewController {
let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.node.entitiesView)
mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities)
let randomId: Int64
if case let .draft(_, id) = subject, let id {
randomId = id
} else {
randomId = Int64.random(in: .min ... .max)
}
if mediaEditor.resultIsVideo {
let videoResult: Result.VideoResult
let duration: Double
@ -2464,7 +2613,7 @@ public final class MediaEditorScreen: ViewController {
} else {
duration = 5.0
}
case let .draft(draft):
case let .draft(draft, _):
if draft.isVideo {
videoResult = .videoFile(path: draft.path)
if let videoTrimRange = mediaEditor.values.videoTrimRange {
@ -2477,7 +2626,7 @@ public final class MediaEditorScreen: ViewController {
duration = 5.0
}
}
self.completion(.video(video: videoResult, coverImage: nil, values: mediaEditor.values, duration: duration, dimensions: mediaEditor.values.resultDimensions, caption: caption), self.state.privacy, { [weak self] finished in
self.completion(randomId, .video(video: videoResult, coverImage: nil, values: mediaEditor.values, duration: duration, dimensions: mediaEditor.values.resultDimensions, caption: caption), self.state.privacy, { [weak self] finished in
self?.node.animateOut(finished: true, completion: { [weak self] in
self?.dismiss()
Queue.mainQueue().justDispatch {
@ -2486,14 +2635,16 @@ public final class MediaEditorScreen: ViewController {
})
})
if case let .draft(draft) = subject {
if case let .draft(draft, id) = subject, id == nil {
removeStoryDraft(engine: self.context.engine, path: draft.path, delete: true)
}
} else {
if let image = mediaEditor.resultImage {
makeEditorImageComposition(account: self.context.account, inputImage: image, dimensions: storyDimensions, values: mediaEditor.values, time: .zero, completion: { resultImage in
if let resultImage {
self.completion(.image(image: resultImage, dimensions: PixelDimensions(resultImage.size), caption: caption), self.state.privacy, { [weak self] finished in
self.saveDraft(id: randomId)
makeEditorImageComposition(account: self.context.account, inputImage: image, dimensions: storyDimensions, values: mediaEditor.values, time: .zero, completion: { [weak self] resultImage in
if let self, let resultImage {
self.completion(randomId, .image(image: resultImage, dimensions: PixelDimensions(resultImage.size), caption: caption), self.state.privacy, { [weak self] finished in
self?.node.animateOut(finished: true, completion: { [weak self] in
self?.dismiss()
Queue.mainQueue().justDispatch {
@ -2501,7 +2652,7 @@ public final class MediaEditorScreen: ViewController {
}
})
})
if case let .draft(draft) = subject {
if case let .draft(draft, id) = subject, id == nil {
removeStoryDraft(engine: self.context.engine, path: draft.path, delete: true)
}
}
@ -2581,7 +2732,7 @@ public final class MediaEditorScreen: ViewController {
}
return EmptyDisposable
}
case let .draft(draft):
case let .draft(draft, _):
if draft.isVideo {
let asset = AVURLAsset(url: NSURL(fileURLWithPath: draft.path) as URL)
exportSubject = .single(.video(asset))
@ -2665,6 +2816,10 @@ public final class MediaEditorScreen: ViewController {
}
}
public func updateEditProgress(_ progress: Float) {
self.node.updateEditProgress(progress)
}
private func dismissAllTooltips() {
self.window?.forEachController({ controller in
if let controller = controller as? TooltipScreen {

View File

@ -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)))
}

View File

@ -250,6 +250,7 @@ final class StoryPreviewComponent: Component {
strings: presentationData.strings,
style: .story,
placeholder: "Reply Privately...",
alwaysDarkWhenHasText: false,
presentController: { _ in
},
sendMessageAction: {

View File

@ -0,0 +1,48 @@
import Foundation
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramUIPreferences
import MediaEditor
public func saveStorySource(engine: TelegramEngine, item: MediaEditorDraft, id: Int64) {
let key = EngineDataBuffer(length: 8)
key.setInt64(0, value: id)
let _ = engine.itemCache.put(collectionId: ApplicationSpecificItemCacheCollectionId.storySource, id: key, item: item).start()
}
public func removeStorySource(engine: TelegramEngine, id: Int64) {
let key = EngineDataBuffer(length: 8)
key.setInt64(0, value: id)
let _ = engine.itemCache.remove(collectionId: ApplicationSpecificItemCacheCollectionId.storySource, id: key).start()
}
public func getStorySource(engine: TelegramEngine, id: Int64) -> Signal<MediaEditorDraft?, NoError> {
let key = EngineDataBuffer(length: 8)
key.setInt64(0, value: id)
return engine.data.get(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: ApplicationSpecificItemCacheCollectionId.storySource, id: key))
|> map { result -> MediaEditorDraft? in
return result?.get(MediaEditorDraft.self)
}
}
public func moveStorySource(engine: TelegramEngine, from fromId: Int64, to toId: Int64) {
let fromKey = EngineDataBuffer(length: 8)
fromKey.setInt64(0, value: fromId)
let toKey = EngineDataBuffer(length: 8)
toKey.setInt64(0, value: toId)
let _ = (engine.data.get(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: ApplicationSpecificItemCacheCollectionId.storySource, id: fromKey))
|> mapToSignal { item -> Signal<Never, NoError> in
if let item = item?.get(MediaEditorDraft.self) {
return engine.itemCache.put(collectionId: ApplicationSpecificItemCacheCollectionId.storySource, id: toKey, item: item)
|> then(
engine.itemCache.remove(collectionId: ApplicationSpecificItemCacheCollectionId.storySource, id: fromKey)
)
} else {
return .complete()
}
}).start()
}

View File

@ -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)
}
)
),

View File

@ -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

View File

@ -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",

View File

@ -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)

View File

@ -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 {

View File

@ -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 {

View File

@ -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
}

View File

@ -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 {

View File

@ -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

View File

@ -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

View File

@ -273,18 +273,6 @@ public func fetchVideoLibraryMediaResource(account: Account, resource: VideoLibr
TempBox.shared.dispose(tempFile)
subscriber.putNext(.moveTempFile(file: remuxedTempFile))
} else {
let tempVideoPath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).mp4"
if let _ = try? FileManager.default.copyItem(atPath: tempFile.path, toPath: tempVideoPath) {
PHPhotoLibrary.shared().performChanges({
PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: URL(fileURLWithPath: tempVideoPath))
}, completionHandler: { _, error in
if let error = error {
print("\(error)")
}
let _ = try? FileManager.default.removeItem(atPath: tempVideoPath)
})
}
TempBox.shared.dispose(remuxedTempFile)
if let data = try? Data(contentsOf: URL(fileURLWithPath: tempFile.path), options: [.mappedRead]) {
var range: Range<Int64>?

View File

@ -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({})

View File

@ -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 {