Various improvements
This commit is contained in:
parent
31eb1081df
commit
90f09a13e4
134 changed files with 8488 additions and 3171 deletions
BIN
Telegram/Telegram-iOS/Resources/Transcribe.tgs
Normal file
BIN
Telegram/Telegram-iOS/Resources/Transcribe.tgs
Normal file
Binary file not shown.
|
|
@ -10465,8 +10465,12 @@ Sorry for the inconvenience.";
|
|||
|
||||
"Channel.Info.Stats" = "Statistics and Boosts";
|
||||
|
||||
"Conversation.FreeTranscriptionLimitTooltip_1" = "You have **%@** free voice transcription left this month.";
|
||||
"Conversation.FreeTranscriptionLimitTooltip_any" = "You have **%@** free voice transcriptions left this month.";
|
||||
"Conversation.FreeTranscriptionLimitTooltip_1" = "You have **%@** free voice transcription left this week.";
|
||||
"Conversation.FreeTranscriptionLimitTooltip_any" = "You have **%@** free voice transcriptions left this week.";
|
||||
|
||||
"Conversation.FreeTranscriptionCooldownTooltip_1" = "You have used all your **%@** free transcription this week.";
|
||||
"Conversation.FreeTranscriptionCooldownTooltip_any" = "You have used all your **%@** free transcriptions this week.";
|
||||
"Conversation.FreeTranscriptionWaitOrSubscribe" = "Wait until **%@** to use it again or subscribe to [Telegram Premium]() now.";
|
||||
|
||||
"Notification.GiveawayResults_1" = "**%@** winner of the giveaway was randomly selected by Telegram and received their gift link in a private message.";
|
||||
"Notification.GiveawayResults_any" = "**%@** winners of the giveaway were randomly selected by Telegram and received their gift links in private messages.";
|
||||
|
|
@ -10483,6 +10487,8 @@ Sorry for the inconvenience.";
|
|||
"Chat.Giveaway.DeleteConfirmation.Text" = "Deleting this message won't cancel the giveaway - the winners will still be selected on **%@**.\n\nOnce deleted, the Giveaway Announcement cannot be recovered.";
|
||||
|
||||
"Chat.SimilarChannels" = "Similar Channels";
|
||||
"Chat.SimilarChannels.Join" = "Join";
|
||||
"Chat.SimilarChannels.JoinedChannel" = "You joined channel **%@**.";
|
||||
|
||||
"Wallpaper.ApplyForMe" = "Apply for Me";
|
||||
"Wallpaper.ApplyForBoth" = "Apply for Me and %@";
|
||||
|
|
@ -10514,3 +10520,19 @@ Sorry for the inconvenience.";
|
|||
"Share.RepostStory" = "Repost\nStory";
|
||||
|
||||
"PeerInfo.PaneRecommended" = "Similar Channels";
|
||||
|
||||
"Story.ViewList.ContextSortReposts" = "Reposts First";
|
||||
|
||||
"ShortTime.JustNow" = "now";
|
||||
"ShortTime.MinutesAgo_1" = "%@m";
|
||||
"ShortTime.MinutesAgo_any" = "%@m";
|
||||
"ShortTime.HoursAgo_1" = "%@h";
|
||||
"ShortTime.HoursAgo_any" = "%@h";
|
||||
"ShortTime.AtDate" = "%@";
|
||||
"ShortTime.AtPreciseDate" = "%@ at %@";
|
||||
|
||||
"Stats.Message.Reactions" = "Reactions";
|
||||
"Stats.ReactionsPerPost" = "Reactions Per Post";
|
||||
"Stats.ViewsPerStory" = "Views Per Story";
|
||||
"Stats.SharesPerStory" = "Shares Per Story";
|
||||
"Stats.ReactionsPerStory" = "Reactions Per Story";
|
||||
|
|
|
|||
|
|
@ -802,9 +802,26 @@ public struct StoryCameraTransitionInCoordinator {
|
|||
}
|
||||
}
|
||||
|
||||
public class MediaEditorTransitionOutExternalState {
|
||||
public var storyTarget: Stories.PendingTarget?
|
||||
public var isPeerArchived: Bool
|
||||
public var transitionOut: ((Stories.PendingTarget?, Bool) -> StoryCameraTransitionOut?)?
|
||||
|
||||
public init(storyTarget: Stories.PendingTarget?, isPeerArchived: Bool, transitionOut: ((Stories.PendingTarget?, Bool) -> StoryCameraTransitionOut?)?) {
|
||||
self.storyTarget = storyTarget
|
||||
self.isPeerArchived = isPeerArchived
|
||||
self.transitionOut = transitionOut
|
||||
}
|
||||
}
|
||||
|
||||
public protocol MediaEditorScreenResult {
|
||||
|
||||
}
|
||||
|
||||
public protocol TelegramRootControllerInterface: NavigationController {
|
||||
@discardableResult
|
||||
func openStoryCamera(customTarget: EnginePeer.Id?, transitionIn: StoryCameraTransitionIn?, transitionedIn: @escaping () -> Void, transitionOut: @escaping (Stories.PendingTarget?, Bool) -> StoryCameraTransitionOut?) -> StoryCameraTransitionInCoordinator?
|
||||
func proceedWithStoryUpload(target: Stories.PendingTarget, result: MediaEditorScreenResult, existingMedia: EngineMedia?, forwardInfo: Stories.PendingForwardInfo?, externalState: MediaEditorTransitionOutExternalState, commit: @escaping (@escaping () -> Void) -> Void)
|
||||
|
||||
func getContactsController() -> ViewController?
|
||||
func getChatsController() -> ViewController?
|
||||
|
|
@ -935,7 +952,7 @@ public protocol SharedAccountContext: AnyObject {
|
|||
|
||||
func makeChannelStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, peerId: EnginePeer.Id, boosts: Bool, boostStatus: ChannelBoostStatus?) -> ViewController
|
||||
func makeMessagesStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, messageId: EngineMessage.Id) -> ViewController
|
||||
func makeStoryStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, peerId: EnginePeer.Id, storyId: Int32) -> ViewController
|
||||
func makeStoryStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, peerId: EnginePeer.Id, storyId: Int32, storyItem: EngineStoryItem?) -> ViewController
|
||||
|
||||
func makeDebugSettingsController(context: AccountContext?) -> ViewController?
|
||||
|
||||
|
|
@ -1079,7 +1096,7 @@ public protocol AccountContext: AnyObject {
|
|||
|
||||
public struct PremiumConfiguration {
|
||||
public static var defaultValue: PremiumConfiguration {
|
||||
return PremiumConfiguration(isPremiumDisabled: false, showPremiumGiftInAttachMenu: false, showPremiumGiftInTextField: false, giveawayGiftsPurchaseAvailable: false, boostsPerGiftCount: 3, minChannelNameColorLevel: 5)
|
||||
return PremiumConfiguration(isPremiumDisabled: false, showPremiumGiftInAttachMenu: false, showPremiumGiftInTextField: false, giveawayGiftsPurchaseAvailable: false, boostsPerGiftCount: 3, minChannelNameColorLevel: 5, audioTransciptionTrialMaxDuration: 300, audioTransciptionTrialCount: 2)
|
||||
}
|
||||
|
||||
public let isPremiumDisabled: Bool
|
||||
|
|
@ -1088,14 +1105,18 @@ public struct PremiumConfiguration {
|
|||
public let giveawayGiftsPurchaseAvailable: Bool
|
||||
public let boostsPerGiftCount: Int32
|
||||
public let minChannelNameColorLevel: Int32
|
||||
public let audioTransciptionTrialMaxDuration: Int32
|
||||
public let audioTransciptionTrialCount: Int32
|
||||
|
||||
fileprivate init(isPremiumDisabled: Bool, showPremiumGiftInAttachMenu: Bool, showPremiumGiftInTextField: Bool, giveawayGiftsPurchaseAvailable: Bool, boostsPerGiftCount: Int32, minChannelNameColorLevel: Int32) {
|
||||
fileprivate init(isPremiumDisabled: Bool, showPremiumGiftInAttachMenu: Bool, showPremiumGiftInTextField: Bool, giveawayGiftsPurchaseAvailable: Bool, boostsPerGiftCount: Int32, minChannelNameColorLevel: Int32, audioTransciptionTrialMaxDuration: Int32, audioTransciptionTrialCount: Int32) {
|
||||
self.isPremiumDisabled = isPremiumDisabled
|
||||
self.showPremiumGiftInAttachMenu = showPremiumGiftInAttachMenu
|
||||
self.showPremiumGiftInTextField = showPremiumGiftInTextField
|
||||
self.giveawayGiftsPurchaseAvailable = giveawayGiftsPurchaseAvailable
|
||||
self.boostsPerGiftCount = boostsPerGiftCount
|
||||
self.minChannelNameColorLevel = minChannelNameColorLevel
|
||||
self.audioTransciptionTrialMaxDuration = audioTransciptionTrialMaxDuration
|
||||
self.audioTransciptionTrialCount = audioTransciptionTrialCount
|
||||
}
|
||||
|
||||
public static func with(appConfiguration: AppConfiguration) -> PremiumConfiguration {
|
||||
|
|
@ -1106,7 +1127,9 @@ public struct PremiumConfiguration {
|
|||
showPremiumGiftInTextField: data["premium_gift_text_field_icon"] as? Bool ?? false,
|
||||
giveawayGiftsPurchaseAvailable: data["giveaway_gifts_purchase_available"] as? Bool ?? false,
|
||||
boostsPerGiftCount: Int32(data["boosts_per_sent_gift"] as? Double ?? 3),
|
||||
minChannelNameColorLevel: Int32(data["channel_color_level_min"] as? Double ?? 5)
|
||||
minChannelNameColorLevel: Int32(data["channel_color_level_min"] as? Double ?? 5),
|
||||
audioTransciptionTrialMaxDuration: Int32(data["transcribe_audio_trial_duration_max"] as? Double ?? 300),
|
||||
audioTransciptionTrialCount: Int32(data["transcribe_audio_trial_weekly_number"] as? Double ?? 2)
|
||||
)
|
||||
} else {
|
||||
return .defaultValue
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ public final class ChatMessageItemAssociatedData: Equatable {
|
|||
public let translateToLanguage: String?
|
||||
public let maxReadStoryId: Int32?
|
||||
public let recommendedChannels: RecommendedChannels?
|
||||
public let audioTranscriptionTrial: AudioTranscription.TrialState
|
||||
|
||||
public init(
|
||||
automaticDownloadPeerType: MediaAutoDownloadPeerType,
|
||||
|
|
@ -75,7 +76,8 @@ public final class ChatMessageItemAssociatedData: Equatable {
|
|||
hasBots: Bool = false,
|
||||
translateToLanguage: String? = nil,
|
||||
maxReadStoryId: Int32? = nil,
|
||||
recommendedChannels: RecommendedChannels? = nil
|
||||
recommendedChannels: RecommendedChannels? = nil,
|
||||
audioTranscriptionTrial: AudioTranscription.TrialState = .defaultValue
|
||||
) {
|
||||
self.automaticDownloadPeerType = automaticDownloadPeerType
|
||||
self.automaticDownloadPeerId = automaticDownloadPeerId
|
||||
|
|
@ -100,6 +102,7 @@ public final class ChatMessageItemAssociatedData: Equatable {
|
|||
self.translateToLanguage = translateToLanguage
|
||||
self.maxReadStoryId = maxReadStoryId
|
||||
self.recommendedChannels = recommendedChannels
|
||||
self.audioTranscriptionTrial = audioTranscriptionTrial
|
||||
}
|
||||
|
||||
public static func == (lhs: ChatMessageItemAssociatedData, rhs: ChatMessageItemAssociatedData) -> Bool {
|
||||
|
|
@ -169,6 +172,9 @@ public final class ChatMessageItemAssociatedData: Equatable {
|
|||
if lhs.recommendedChannels != rhs.recommendedChannels {
|
||||
return false
|
||||
}
|
||||
if lhs.audioTranscriptionTrial != rhs.audioTranscriptionTrial {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -245,12 +245,9 @@ public final class AvatarBadgeView: UIImageView {
|
|||
var alpha: CGFloat = 0
|
||||
backgroundColor.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha)
|
||||
|
||||
if brightness > 0.5 {
|
||||
brightness = max(brightness - 0.2, 0)
|
||||
saturation = min(saturation + 0.2, 1)
|
||||
} else {
|
||||
brightness = min(brightness + 0.3, 1)
|
||||
saturation = max(saturation - 0.2, 0)
|
||||
if brightness > 0.7 {
|
||||
brightness = brightness * 0.9
|
||||
saturation = min(saturation + 0.1, 1)
|
||||
}
|
||||
|
||||
return UIColor(hue: hue, saturation: saturation, brightness: brightness, alpha: alpha)
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import DirectMediaImageCache
|
|||
private let deletedIcon = UIImage(bundleImageName: "Avatar/DeletedIcon")?.precomposed()
|
||||
private let phoneIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/PhoneIcon"), color: .white)
|
||||
public let savedMessagesIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/SavedMessagesIcon"), color: .white)
|
||||
public let repostStoryIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/RepostStoryIcon"), color: .white)
|
||||
private let archivedChatsIcon = UIImage(bundleImageName: "Avatar/ArchiveAvatarIcon")?.precomposed()
|
||||
private let repliesIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/RepliesMessagesIcon"), color: .white)
|
||||
|
||||
|
|
@ -87,6 +88,8 @@ private func calculateColors(context: AccountContext?, explicitColorIndex: Int?,
|
|||
colors = AvatarNode.grayscaleColors
|
||||
} else if case .savedMessagesIcon = icon {
|
||||
colors = AvatarNode.savedMessagesColors
|
||||
} else if case .repostIcon = icon {
|
||||
colors = AvatarNode.repostColors
|
||||
} else if case .repliesIcon = icon {
|
||||
colors = AvatarNode.savedMessagesColors
|
||||
} else if case .editAvatarIcon = icon, let theme {
|
||||
|
|
@ -173,6 +176,7 @@ private enum AvatarNodeIcon: Equatable {
|
|||
case editAvatarIcon
|
||||
case deletedIcon
|
||||
case phoneIcon
|
||||
case repostIcon
|
||||
}
|
||||
|
||||
public enum AvatarNodeImageOverride: Equatable {
|
||||
|
|
@ -184,6 +188,7 @@ public enum AvatarNodeImageOverride: Equatable {
|
|||
case editAvatarIcon(forceNone: Bool)
|
||||
case deletedIcon
|
||||
case phoneIcon
|
||||
case repostIcon
|
||||
}
|
||||
|
||||
public enum AvatarNodeColorOverride {
|
||||
|
|
@ -254,6 +259,10 @@ public final class AvatarNode: ASDisplayNode {
|
|||
UIColor(rgb: 0x2a9ef1), UIColor(rgb: 0x72d5fd)
|
||||
]
|
||||
|
||||
static let repostColors: [UIColor] = [
|
||||
UIColor(rgb: 0x34C76F), UIColor(rgb: 0x3DA1FD)
|
||||
]
|
||||
|
||||
public final class ContentNode: ASDisplayNode {
|
||||
private struct Params: Equatable {
|
||||
let peerId: EnginePeer.Id?
|
||||
|
|
@ -457,6 +466,9 @@ public final class AvatarNode: ASDisplayNode {
|
|||
case .savedMessagesIcon:
|
||||
representation = nil
|
||||
icon = .savedMessagesIcon
|
||||
case .repostIcon:
|
||||
representation = nil
|
||||
icon = .repostIcon
|
||||
case .repliesIcon:
|
||||
representation = nil
|
||||
icon = .repliesIcon
|
||||
|
|
@ -621,6 +633,9 @@ public final class AvatarNode: ASDisplayNode {
|
|||
case .savedMessagesIcon:
|
||||
representation = nil
|
||||
icon = .savedMessagesIcon
|
||||
case .repostIcon:
|
||||
representation = nil
|
||||
icon = .repostIcon
|
||||
case .repliesIcon:
|
||||
representation = nil
|
||||
icon = .repliesIcon
|
||||
|
|
@ -781,7 +796,11 @@ public final class AvatarNode: ASDisplayNode {
|
|||
let colorsArray: NSArray = colors.map(\.cgColor) as NSArray
|
||||
|
||||
var iconColor = UIColor.white
|
||||
var diagonal = false
|
||||
if let parameters = parameters as? AvatarNodeParameters, parameters.icon != .none {
|
||||
if case .repostIcon = parameters.icon {
|
||||
diagonal = true
|
||||
}
|
||||
if case let .archivedChatsIcon(hiddenByDefault) = parameters.icon, let theme = parameters.theme {
|
||||
if hiddenByDefault {
|
||||
iconColor = theme.chatList.unpinnedArchiveAvatarColor.foregroundColor
|
||||
|
|
@ -796,7 +815,11 @@ public final class AvatarNode: ASDisplayNode {
|
|||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
let gradient = CGGradient(colorsSpace: colorSpace, colors: colorsArray, locations: &locations)!
|
||||
|
||||
context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: bounds.size.height), options: CGGradientDrawingOptions())
|
||||
if diagonal {
|
||||
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: bounds.size.height), end: CGPoint(x: bounds.size.width, y: 0.0), options: CGGradientDrawingOptions())
|
||||
} else {
|
||||
context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: bounds.size.height), options: CGGradientDrawingOptions())
|
||||
}
|
||||
|
||||
context.setBlendMode(.normal)
|
||||
|
||||
|
|
@ -828,6 +851,15 @@ public final class AvatarNode: ASDisplayNode {
|
|||
if let savedMessagesIcon = savedMessagesIcon {
|
||||
context.draw(savedMessagesIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - savedMessagesIcon.size.width) / 2.0), y: floor((bounds.size.height - savedMessagesIcon.size.height) / 2.0)), size: savedMessagesIcon.size))
|
||||
}
|
||||
} else if case .repostIcon = parameters.icon {
|
||||
let factor = bounds.size.width / 60.0
|
||||
context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
|
||||
context.scaleBy(x: factor, y: -factor)
|
||||
context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0)
|
||||
|
||||
if let repostStoryIcon = repostStoryIcon {
|
||||
context.draw(repostStoryIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - repostStoryIcon.size.width) / 2.0), y: floor((bounds.size.height - repostStoryIcon.size.height) / 2.0)), size: repostStoryIcon.size))
|
||||
}
|
||||
} else if case .repliesIcon = parameters.icon {
|
||||
let factor = bounds.size.width / 60.0
|
||||
context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ final class CameraDeviceContext {
|
|||
self.output = CameraOutput(exclusive: exclusive)
|
||||
}
|
||||
|
||||
func configure(position: Camera.Position, previewView: CameraSimplePreviewView?, audio: Bool, photo: Bool, metadata: Bool) {
|
||||
func configure(position: Camera.Position, previewView: CameraSimplePreviewView?, audio: Bool, photo: Bool, metadata: Bool, preferWide: Bool = false) {
|
||||
guard let session = self.session else {
|
||||
return
|
||||
}
|
||||
|
|
@ -65,7 +65,7 @@ final class CameraDeviceContext {
|
|||
self.previewView = previewView
|
||||
|
||||
self.device.configure(for: session, position: position, dual: !exclusive || additional)
|
||||
self.device.configureDeviceFormat(maxDimensions: self.preferredMaxDimensions, maxFramerate: self.preferredMaxFrameRate)
|
||||
self.device.configureDeviceFormat(maxDimensions: self.maxDimensions(additional: self.additional, preferWide: preferWide), maxFramerate: self.preferredMaxFrameRate)
|
||||
self.input.configure(for: session, device: self.device, audio: audio)
|
||||
self.output.configure(for: session, device: self.device, input: self.input, previewView: previewView, audio: audio, photo: photo, metadata: metadata)
|
||||
|
||||
|
|
@ -82,8 +82,8 @@ final class CameraDeviceContext {
|
|||
self.input.invalidate(for: session)
|
||||
}
|
||||
|
||||
private var preferredMaxDimensions: CMVideoDimensions {
|
||||
if self.additional {
|
||||
private func maxDimensions(additional: Bool, preferWide: Bool) -> CMVideoDimensions {
|
||||
if additional || preferWide {
|
||||
return CMVideoDimensions(width: 1920, height: 1440)
|
||||
} else {
|
||||
return CMVideoDimensions(width: 1920, height: 1080)
|
||||
|
|
@ -336,7 +336,7 @@ private final class CameraContext {
|
|||
self.additionalDeviceContext = nil
|
||||
|
||||
self.mainDeviceContext = CameraDeviceContext(session: self.session, exclusive: true, additional: false)
|
||||
self.mainDeviceContext?.configure(position: self.positionValue, previewView: self.simplePreviewView, audio: self.initialConfiguration.audio, photo: self.initialConfiguration.photo, metadata: self.initialConfiguration.metadata)
|
||||
self.mainDeviceContext?.configure(position: self.positionValue, previewView: self.simplePreviewView, audio: self.initialConfiguration.audio, photo: self.initialConfiguration.photo, metadata: self.initialConfiguration.metadata, preferWide: self.initialConfiguration.preferWide)
|
||||
}
|
||||
self.mainDeviceContext?.output.processSampleBuffer = { [weak self] sampleBuffer, pixelBuffer, connection in
|
||||
guard let self, let mainDeviceContext = self.mainDeviceContext else {
|
||||
|
|
@ -481,25 +481,40 @@ private final class CameraContext {
|
|||
additionalDeviceContext.output.stopRecording()
|
||||
) |> mapToSignal { main, additional in
|
||||
if case let .finished(mainResult, _, duration, positionChangeTimestamps, _) = main, case let .finished(additionalResult, _, _, _, _) = additional {
|
||||
var additionalTransitionImage = additionalResult.1
|
||||
if let cgImage = additionalResult.1.cgImage {
|
||||
additionalTransitionImage = UIImage(cgImage: cgImage, scale: 1.0, orientation: .leftMirrored)
|
||||
var additionalThumbnailImage = additionalResult.thumbnail
|
||||
if let cgImage = additionalResult.thumbnail.cgImage {
|
||||
additionalThumbnailImage = UIImage(cgImage: cgImage, scale: 1.0, orientation: .leftMirrored)
|
||||
}
|
||||
return .single(.finished(mainResult, (additionalResult.0, additionalTransitionImage, true, additionalResult.3), duration, positionChangeTimestamps, CACurrentMediaTime()))
|
||||
|
||||
return .single(
|
||||
.finished(
|
||||
main: mainResult,
|
||||
additional: VideoCaptureResult.Result(path: additionalResult.path, thumbnail: additionalThumbnailImage, isMirrored: true, dimensions: additionalResult.dimensions),
|
||||
duration: duration,
|
||||
positionChangeTimestamps: positionChangeTimestamps,
|
||||
captureTimestamp: CACurrentMediaTime()
|
||||
)
|
||||
)
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mirror = self.positionValue == .front
|
||||
let isMirrored = self.positionValue == .front
|
||||
return mainDeviceContext.output.stopRecording()
|
||||
|> map { result -> VideoCaptureResult in
|
||||
if case let .finished(mainResult, _, duration, positionChangeTimestamps, time) = result {
|
||||
var transitionImage = mainResult.1
|
||||
if mirror, let cgImage = transitionImage.cgImage {
|
||||
transitionImage = UIImage(cgImage: cgImage, scale: 1.0, orientation: .leftMirrored)
|
||||
if case let .finished(mainResult, _, duration, positionChangeTimestamps, captureTimestamp) = result {
|
||||
var thumbnailImage = mainResult.thumbnail
|
||||
if isMirrored, let cgImage = thumbnailImage.cgImage {
|
||||
thumbnailImage = UIImage(cgImage: cgImage, scale: 1.0, orientation: .leftMirrored)
|
||||
}
|
||||
return .finished((mainResult.0, transitionImage, mirror, mainResult.3), nil, duration, positionChangeTimestamps, time)
|
||||
return .finished(
|
||||
main: VideoCaptureResult.Result(path: mainResult.path, thumbnail: thumbnailImage, isMirrored: isMirrored, dimensions: mainResult.dimensions),
|
||||
additional: nil,
|
||||
duration: duration,
|
||||
positionChangeTimestamps: positionChangeTimestamps,
|
||||
captureTimestamp: captureTimestamp
|
||||
)
|
||||
} else {
|
||||
return result
|
||||
}
|
||||
|
|
@ -548,8 +563,9 @@ public final class Camera {
|
|||
let photo: Bool
|
||||
let metadata: Bool
|
||||
let preferredFps: Double
|
||||
let preferWide: Bool
|
||||
|
||||
public init(preset: Preset, position: Position, isDualEnabled: Bool = false, audio: Bool, photo: Bool, metadata: Bool, preferredFps: Double) {
|
||||
public init(preset: Preset, position: Position, isDualEnabled: Bool = false, audio: Bool, photo: Bool, metadata: Bool, preferredFps: Double, preferWide: Bool = false) {
|
||||
self.preset = preset
|
||||
self.position = position
|
||||
self.isDualEnabled = isDualEnabled
|
||||
|
|
@ -557,6 +573,7 @@ public final class Camera {
|
|||
self.photo = photo
|
||||
self.metadata = metadata
|
||||
self.preferredFps = preferredFps
|
||||
self.preferWide = preferWide
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,14 @@ import VideoToolbox
|
|||
import TelegramCore
|
||||
|
||||
public enum VideoCaptureResult: Equatable {
|
||||
case finished((String, UIImage, Bool, CGSize), (String, UIImage, Bool, CGSize)?, Double, [(Bool, Double)], Double)
|
||||
public struct Result {
|
||||
public let path: String
|
||||
public let thumbnail: UIImage
|
||||
public let isMirrored: Bool
|
||||
public let dimensions: CGSize
|
||||
}
|
||||
|
||||
case finished(main: Result, additional: Result?, duration: Double, positionChangeTimestamps: [(Bool, Double)], captureTimestamp: Double)
|
||||
case failed
|
||||
|
||||
public static func == (lhs: VideoCaptureResult, rhs: VideoCaptureResult) -> Bool {
|
||||
|
|
@ -19,8 +26,8 @@ public enum VideoCaptureResult: Equatable {
|
|||
} else {
|
||||
return false
|
||||
}
|
||||
case let .finished(_, _, lhsDuration, lhsChangeTimestamps, lhsTime):
|
||||
if case let .finished(_, _, rhsDuration, rhsChangeTimestamps, rhsTime) = rhs, lhsDuration == rhsDuration, lhsTime == rhsTime {
|
||||
case let .finished(_, _, lhsDuration, lhsChangeTimestamps, lhsTimestamp):
|
||||
if case let .finished(_, _, rhsDuration, rhsChangeTimestamps, rhsTimestamp) = rhs, lhsDuration == rhsDuration, lhsTimestamp == rhsTimestamp {
|
||||
if lhsChangeTimestamps.count != rhsChangeTimestamps.count {
|
||||
return false
|
||||
}
|
||||
|
|
@ -302,10 +309,26 @@ final class CameraOutput: NSObject {
|
|||
let outputFileURL = URL(fileURLWithPath: outputFilePath)
|
||||
|
||||
let videoRecorder = VideoRecorder(configuration: VideoRecorder.Configuration(videoSettings: videoSettings, audioSettings: audioSettings), orientation: orientation, fileUrl: outputFileURL, completion: { [weak self] result in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if case let .success(transitionImage, duration, positionChangeTimestamps) = result {
|
||||
self?.recordingCompletionPipe.putNext(.finished((outputFilePath, transitionImage ?? UIImage(), false, dimensions), nil, duration, positionChangeTimestamps.map { ($0 == .front, $1) }, CACurrentMediaTime()))
|
||||
self.recordingCompletionPipe.putNext(
|
||||
.finished(
|
||||
main: VideoCaptureResult.Result(
|
||||
path: outputFilePath,
|
||||
thumbnail: transitionImage ?? UIImage(),
|
||||
isMirrored: false,
|
||||
dimensions: dimensions
|
||||
),
|
||||
additional: nil,
|
||||
duration: duration,
|
||||
positionChangeTimestamps: positionChangeTimestamps.map { ($0 == .front, $1) },
|
||||
captureTimestamp: CACurrentMediaTime()
|
||||
)
|
||||
)
|
||||
} else {
|
||||
self?.recordingCompletionPipe.putNext(.failed)
|
||||
self.recordingCompletionPipe.putNext(.failed)
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ private enum DebugControllerSection: Int32 {
|
|||
case sticker
|
||||
case logs
|
||||
case logging
|
||||
case web
|
||||
case experiments
|
||||
case translation
|
||||
case videoExperiments
|
||||
|
|
@ -86,6 +87,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
|||
case resetCacheIndex
|
||||
case reindexCache
|
||||
case resetBiometricsData(PresentationTheme)
|
||||
case webViewInspection(Bool)
|
||||
case resetWebViewCache(PresentationTheme)
|
||||
case optimizeDatabase(PresentationTheme)
|
||||
case photoPreview(PresentationTheme, Bool)
|
||||
|
|
@ -121,9 +123,11 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
|||
return DebugControllerSection.logs.rawValue
|
||||
case .logToFile, .logToConsole, .redactSensitiveData:
|
||||
return DebugControllerSection.logging.rawValue
|
||||
case .webViewInspection, .resetWebViewCache:
|
||||
return DebugControllerSection.web.rawValue
|
||||
case .keepChatNavigationStack, .skipReadHistory, .unidirectionalSwipeToReply, .dustEffect, .callUIV2, .crashOnSlowQueries, .crashOnMemoryPressure:
|
||||
return DebugControllerSection.experiments.rawValue
|
||||
case .clearTips, .resetNotifications, .crash, .resetData, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .resetWebViewCache, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .storiesExperiment, .storiesJpegExperiment, .playlistPlayback, .enableQuickReactionSwitch, .voiceConference, .experimentalCompatibility, .enableDebugDataDisplay, .acceleratedStickers, .inlineForums, .localTranscription, .enableReactionOverrides, .restorePurchases:
|
||||
case .clearTips, .resetNotifications, .crash, .resetData, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .storiesExperiment, .storiesJpegExperiment, .playlistPlayback, .enableQuickReactionSwitch, .voiceConference, .experimentalCompatibility, .enableDebugDataDisplay, .acceleratedStickers, .inlineForums, .localTranscription, .enableReactionOverrides, .restorePurchases:
|
||||
return DebugControllerSection.experiments.rawValue
|
||||
case .logTranslationRecognition, .resetTranslationStates:
|
||||
return DebugControllerSection.translation.rawValue
|
||||
|
|
@ -164,43 +168,45 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
|||
return 11
|
||||
case .redactSensitiveData:
|
||||
return 12
|
||||
case .keepChatNavigationStack:
|
||||
return 14
|
||||
case .skipReadHistory:
|
||||
return 15
|
||||
case .unidirectionalSwipeToReply:
|
||||
return 16
|
||||
case .dustEffect:
|
||||
return 17
|
||||
case .callUIV2:
|
||||
return 18
|
||||
case .crashOnSlowQueries:
|
||||
return 19
|
||||
case .crashOnMemoryPressure:
|
||||
return 20
|
||||
case .clearTips:
|
||||
return 21
|
||||
case .resetNotifications:
|
||||
return 22
|
||||
case .crash:
|
||||
return 23
|
||||
case .resetData:
|
||||
return 24
|
||||
case .resetDatabase:
|
||||
return 25
|
||||
case .resetDatabaseAndCache:
|
||||
return 26
|
||||
case .resetHoles:
|
||||
return 27
|
||||
case .reindexUnread:
|
||||
return 28
|
||||
case .resetCacheIndex:
|
||||
return 29
|
||||
case .reindexCache:
|
||||
return 30
|
||||
case .resetBiometricsData:
|
||||
return 31
|
||||
case .webViewInspection:
|
||||
return 13
|
||||
case .resetWebViewCache:
|
||||
return 14
|
||||
case .keepChatNavigationStack:
|
||||
return 15
|
||||
case .skipReadHistory:
|
||||
return 16
|
||||
case .unidirectionalSwipeToReply:
|
||||
return 17
|
||||
case .dustEffect:
|
||||
return 18
|
||||
case .callUIV2:
|
||||
return 19
|
||||
case .crashOnSlowQueries:
|
||||
return 20
|
||||
case .crashOnMemoryPressure:
|
||||
return 21
|
||||
case .clearTips:
|
||||
return 22
|
||||
case .resetNotifications:
|
||||
return 23
|
||||
case .crash:
|
||||
return 24
|
||||
case .resetData:
|
||||
return 25
|
||||
case .resetDatabase:
|
||||
return 26
|
||||
case .resetDatabaseAndCache:
|
||||
return 27
|
||||
case .resetHoles:
|
||||
return 28
|
||||
case .reindexUnread:
|
||||
return 29
|
||||
case .resetCacheIndex:
|
||||
return 30
|
||||
case .reindexCache:
|
||||
return 31
|
||||
case .resetBiometricsData:
|
||||
return 32
|
||||
case .optimizeDatabase:
|
||||
return 33
|
||||
|
|
@ -1163,6 +1169,14 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
|||
return settings.withUpdatedBiometricsDomainState(nil).withUpdatedShareBiometricsDomainState(nil)
|
||||
}).start()
|
||||
})
|
||||
case let .webViewInspection(value):
|
||||
return ItemListSwitchItem(presentationData: presentationData, title: "Allow Web View Inspection", value: value, sectionId: self.section, style: .blocks, updated: { value in
|
||||
let _ = updateExperimentalUISettingsInteractively(accountManager: arguments.sharedContext.accountManager, { settings in
|
||||
var settings = settings
|
||||
settings.allowWebViewInspection = value
|
||||
return settings
|
||||
}).start()
|
||||
})
|
||||
case .resetWebViewCache:
|
||||
return ItemListActionItem(presentationData: presentationData, title: "Clear Web View Cache", kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: {
|
||||
WKWebsiteDataStore.default().removeData(ofTypes: [WKWebsiteDataTypeDiskCache, WKWebsiteDataTypeMemoryCache], modifiedSince: Date(timeIntervalSince1970: 0), completionHandler:{ })
|
||||
|
|
@ -1404,6 +1418,9 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present
|
|||
entries.append(.redactSensitiveData(presentationData.theme, loggingSettings.redactSensitiveData))
|
||||
|
||||
if isMainApp {
|
||||
entries.append(.webViewInspection(experimentalSettings.allowWebViewInspection))
|
||||
entries.append(.resetWebViewCache(presentationData.theme))
|
||||
|
||||
entries.append(.keepChatNavigationStack(presentationData.theme, experimentalSettings.keepChatNavigationStack))
|
||||
#if DEBUG
|
||||
entries.append(.skipReadHistory(presentationData.theme, experimentalSettings.skipReadHistory))
|
||||
|
|
@ -1427,7 +1444,6 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present
|
|||
entries.append(.reindexUnread(presentationData.theme))
|
||||
entries.append(.resetCacheIndex)
|
||||
entries.append(.reindexCache)
|
||||
entries.append(.resetWebViewCache(presentationData.theme))
|
||||
}
|
||||
entries.append(.optimizeDatabase(presentationData.theme))
|
||||
if isMainApp {
|
||||
|
|
|
|||
|
|
@ -153,129 +153,6 @@ open class ContextControllerSourceNode: ContextReferenceContentNode {
|
|||
}
|
||||
}
|
||||
|
||||
/*open class ContextControllerSourceNode: ASDisplayNode {
|
||||
private var viewImpl: ContextControllerSourceView {
|
||||
return self.view as! ContextControllerSourceView
|
||||
}
|
||||
|
||||
public var contextGesture: ContextGesture? {
|
||||
if self.isNodeLoaded {
|
||||
return self.viewImpl.contextGesture
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public var isGestureEnabled: Bool = true {
|
||||
didSet {
|
||||
if self.isNodeLoaded {
|
||||
self.viewImpl.isGestureEnabled = self.isGestureEnabled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var beginDelay: Double = 0.12 {
|
||||
didSet {
|
||||
if self.isNodeLoaded {
|
||||
self.viewImpl.beginDelay = self.beginDelay
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var animateScale: Bool = true {
|
||||
didSet {
|
||||
if self.isNodeLoaded {
|
||||
self.viewImpl.animateScale = self.animateScale
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var activated: ((ContextGesture, CGPoint) -> Void)? {
|
||||
didSet {
|
||||
if self.isNodeLoaded {
|
||||
self.viewImpl.activated = self.activated
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var shouldBegin: ((CGPoint) -> Bool)? {
|
||||
didSet {
|
||||
if self.isNodeLoaded {
|
||||
self.viewImpl.shouldBegin = self.shouldBegin
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var customActivationProgress: ((CGFloat, ContextGestureTransition) -> Void)? {
|
||||
didSet {
|
||||
if self.isNodeLoaded {
|
||||
self.viewImpl.customActivationProgress = self.customActivationProgress
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public weak var additionalActivationProgressLayer: CALayer? {
|
||||
didSet {
|
||||
if self.isNodeLoaded {
|
||||
self.viewImpl.additionalActivationProgressLayer = self.additionalActivationProgressLayer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var targetNodeForActivationProgress: ASDisplayNode? {
|
||||
didSet {
|
||||
if self.isNodeLoaded {
|
||||
self.viewImpl.targetNodeForActivationProgress = self.targetNodeForActivationProgress
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var targetViewForActivationProgress: UIView? {
|
||||
didSet {
|
||||
if self.isNodeLoaded {
|
||||
self.viewImpl.targetViewForActivationProgress = self.targetViewForActivationProgress
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var targetNodeForActivationProgressContentRect: CGRect? {
|
||||
didSet {
|
||||
if self.isNodeLoaded {
|
||||
self.viewImpl.targetNodeForActivationProgressContentRect = self.targetNodeForActivationProgressContentRect
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override public init() {
|
||||
super.init()
|
||||
|
||||
self.setViewBlock({
|
||||
return ContextControllerSourceView(frame: CGRect())
|
||||
})
|
||||
}
|
||||
|
||||
override open func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.viewImpl.isGestureEnabled = self.isGestureEnabled
|
||||
self.viewImpl.beginDelay = self.beginDelay
|
||||
self.viewImpl.animateScale = self.animateScale
|
||||
self.viewImpl.activated = self.activated
|
||||
self.viewImpl.shouldBegin = self.shouldBegin
|
||||
self.viewImpl.customActivationProgress = self.customActivationProgress
|
||||
self.viewImpl.additionalActivationProgressLayer = self.additionalActivationProgressLayer
|
||||
self.viewImpl.targetNodeForActivationProgress = self.targetNodeForActivationProgress
|
||||
self.viewImpl.targetViewForActivationProgress = self.targetViewForActivationProgress
|
||||
self.viewImpl.targetNodeForActivationProgressContentRect = self.targetNodeForActivationProgressContentRect
|
||||
}
|
||||
|
||||
public func cancelGesture() {
|
||||
if self.isNodeLoaded {
|
||||
self.viewImpl.cancelGesture()
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
open class ContextControllerSourceView: UIView {
|
||||
public private(set) var contextGesture: ContextGesture?
|
||||
|
||||
|
|
|
|||
|
|
@ -104,6 +104,7 @@ swift_library(
|
|||
"//submodules/TelegramUI/Components/CameraButtonComponent",
|
||||
"//submodules/ReactionSelectionNode",
|
||||
"//submodules/TelegramUI/Components/EntityKeyboard",
|
||||
"//submodules/Camera",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -15,7 +15,11 @@ private func makeEntityView(context: AccountContext, entity: DrawingEntity) -> D
|
|||
} else if let entity = entity as? DrawingSimpleShapeEntity {
|
||||
return DrawingSimpleShapeEntityView(context: context, entity: entity)
|
||||
} else if let entity = entity as? DrawingStickerEntity {
|
||||
return DrawingStickerEntityView(context: context, entity: entity)
|
||||
if case let .file(_, type) = entity.content, case .reaction = type {
|
||||
return DrawingReactionEntityView(context: context, entity: entity)
|
||||
} else {
|
||||
return DrawingStickerEntityView(context: context, entity: entity)
|
||||
}
|
||||
} else if let entity = entity as? DrawingTextEntity {
|
||||
return DrawingTextEntityView(context: context, entity: entity)
|
||||
} else if let entity = entity as? DrawingVectorEntity {
|
||||
|
|
|
|||
347
submodules/DrawingUI/Sources/DrawingReactionView.swift
Normal file
347
submodules/DrawingUI/Sources/DrawingReactionView.swift
Normal file
|
|
@ -0,0 +1,347 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import AVFoundation
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import AnimatedStickerNode
|
||||
import TelegramAnimatedStickerNode
|
||||
import StickerResources
|
||||
import AccountContext
|
||||
import MediaEditor
|
||||
import TelegramPresentationData
|
||||
import ReactionSelectionNode
|
||||
import UndoUI
|
||||
import EntityKeyboard
|
||||
import ComponentFlow
|
||||
|
||||
public class DrawingReactionEntityView: DrawingStickerEntityView {
|
||||
private var backgroundView: UIImageView
|
||||
private var outlineView: UIImageView
|
||||
|
||||
override init(context: AccountContext, entity: DrawingStickerEntity) {
|
||||
let backgroundView = UIImageView(image: UIImage(bundleImageName: "Stories/ReactionShadow"))
|
||||
backgroundView.layer.zPosition = -1000.0
|
||||
|
||||
let outlineView = UIImageView(image: UIImage(bundleImageName: "Stories/ReactionOutline"))
|
||||
outlineView.tintColor = .white
|
||||
backgroundView.addSubview(outlineView)
|
||||
|
||||
self.backgroundView = backgroundView
|
||||
self.outlineView = outlineView
|
||||
|
||||
super.init(context: context, entity: entity)
|
||||
|
||||
self.insertSubview(backgroundView, at: 0)
|
||||
|
||||
self.setup()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override var isReaction: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
override func animateInsertion() {
|
||||
super.animateInsertion()
|
||||
|
||||
Queue.mainQueue().after(0.2) {
|
||||
let _ = self.selectedTapAction()
|
||||
}
|
||||
}
|
||||
|
||||
override func onSelection() {
|
||||
self.presentReactionSelection()
|
||||
}
|
||||
|
||||
override func onDeselection() {
|
||||
let _ = self.dismissReactionSelection()
|
||||
}
|
||||
|
||||
public override func update(animated: Bool) {
|
||||
super.update(animated: animated)
|
||||
|
||||
if case let .file(_, type) = self.stickerEntity.content, case let .reaction(_, style) = type {
|
||||
switch style {
|
||||
case .white:
|
||||
self.outlineView.tintColor = .white
|
||||
case .black:
|
||||
self.outlineView.tintColor = UIColor(rgb: 0x000000, alpha: 0.5)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func updateMirroring(animated: Bool) {
|
||||
let staticTransform = CATransform3DMakeScale(self.stickerEntity.mirrored ? -1.0 : 1.0, 1.0, 1.0)
|
||||
if animated {
|
||||
let isCurrentlyMirrored = ((self.backgroundView.layer.value(forKeyPath: "transform.scale.y") as? NSNumber)?.floatValue ?? 1.0) < 0.0
|
||||
var animationSourceTransform = CATransform3DIdentity
|
||||
var animationTargetTransform = CATransform3DIdentity
|
||||
if isCurrentlyMirrored {
|
||||
animationSourceTransform = CATransform3DRotate(animationSourceTransform, .pi, 0.0, 1.0, 0.0)
|
||||
animationSourceTransform.m34 = -1.0 / self.imageNode.frame.width
|
||||
}
|
||||
if self.stickerEntity.mirrored {
|
||||
animationTargetTransform = CATransform3DRotate(animationTargetTransform, .pi, 0.0, 1.0, 0.0)
|
||||
animationTargetTransform.m34 = -1.0 / self.imageNode.frame.width
|
||||
}
|
||||
self.backgroundView.layer.transform = animationSourceTransform
|
||||
|
||||
let values = [1.0, 0.01, 1.0]
|
||||
let keyTimes = [0.0, 0.5, 1.0]
|
||||
self.animationNode?.layer.animateKeyframes(values: values as [NSNumber], keyTimes: keyTimes as [NSNumber], duration: 0.25, keyPath: "transform.scale.x", timingFunction: CAMediaTimingFunctionName.linear.rawValue)
|
||||
|
||||
UIView.animate(withDuration: 0.25, animations: {
|
||||
self.backgroundView.layer.transform = animationTargetTransform
|
||||
}, completion: { finished in
|
||||
self.backgroundView.layer.transform = staticTransform
|
||||
})
|
||||
} else {
|
||||
CATransaction.begin()
|
||||
CATransaction.setDisableActions(true)
|
||||
self.backgroundView.layer.transform = staticTransform
|
||||
CATransaction.commit()
|
||||
}
|
||||
}
|
||||
|
||||
private weak var reactionContextNode: ReactionContextNode?
|
||||
fileprivate func presentReactionSelection() {
|
||||
guard let containerView = self.containerView, let superview = containerView.superview?.superview?.superview?.superview, self.reactionContextNode == nil else {
|
||||
return
|
||||
}
|
||||
|
||||
let availableSize = superview.frame.size
|
||||
let reactionItems = containerView.getAvailableReactions()
|
||||
|
||||
let insets = UIEdgeInsets(top: 64.0, left: 0.0, bottom: 64.0, right: 0.0)
|
||||
|
||||
let layout: (ContainedViewLayoutTransition) -> Void = { [weak self, weak superview] transition in
|
||||
guard let self, let superview, let reactionContextNode = self.reactionContextNode else {
|
||||
return
|
||||
}
|
||||
let anchorRect = self.convert(self.bounds, to: superview).offsetBy(dx: 0.0, dy: -20.0)
|
||||
reactionContextNode.updateLayout(size: availableSize, insets: insets, anchorRect: anchorRect, centerAligned: true, isCoveredByInput: false, isAnimatingOut: false, transition: transition)
|
||||
}
|
||||
|
||||
let reactionContextNodeTransition: Transition = .immediate
|
||||
let reactionContextNode: ReactionContextNode
|
||||
reactionContextNode = ReactionContextNode(
|
||||
context: self.context,
|
||||
animationCache: self.context.animationCache,
|
||||
presentationData: self.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme),
|
||||
items: reactionItems.map(ReactionContextItem.reaction),
|
||||
selectedItems: Set(),
|
||||
title: nil,
|
||||
getEmojiContent: { [weak self] animationCache, animationRenderer in
|
||||
guard let self else {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
let mappedReactionItems: [EmojiComponentReactionItem] = reactionItems.map { reaction -> EmojiComponentReactionItem in
|
||||
return EmojiComponentReactionItem(reaction: reaction.reaction.rawValue, file: reaction.stillAnimation)
|
||||
}
|
||||
|
||||
return EmojiPagerContentComponent.emojiInputData(
|
||||
context: self.context,
|
||||
animationCache: animationCache,
|
||||
animationRenderer: animationRenderer,
|
||||
isStandalone: false,
|
||||
subject: .reaction,
|
||||
hasTrending: false,
|
||||
topReactionItems: mappedReactionItems,
|
||||
areUnicodeEmojiEnabled: false,
|
||||
areCustomEmojiEnabled: true,
|
||||
chatPeerId: self.context.account.peerId,
|
||||
selectedItems: Set(),
|
||||
premiumIfSavedMessages: false
|
||||
)
|
||||
},
|
||||
isExpandedUpdated: { transition in
|
||||
layout(transition)
|
||||
},
|
||||
requestLayout: { transition in
|
||||
layout(transition)
|
||||
},
|
||||
requestUpdateOverlayWantsToBeBelowKeyboard: { transition in
|
||||
layout(transition)
|
||||
}
|
||||
)
|
||||
reactionContextNode.displayTail = true
|
||||
reactionContextNode.forceTailToRight = true
|
||||
reactionContextNode.forceDark = true
|
||||
self.reactionContextNode = reactionContextNode
|
||||
|
||||
reactionContextNode.reactionSelected = { [weak self] updateReaction, _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
let continueWithAnimationFile: (TelegramMediaFile) -> Void = { [weak self] animation in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
if case let .file(_, type) = self.stickerEntity.content, case let .reaction(_, style) = type {
|
||||
self.stickerEntity.content = .file(animation, .reaction(updateReaction.reaction, style))
|
||||
}
|
||||
|
||||
var nodeToTransitionOut: ASDisplayNode?
|
||||
if let animationNode = self.animationNode {
|
||||
nodeToTransitionOut = animationNode
|
||||
} else if !self.imageNode.isHidden {
|
||||
nodeToTransitionOut = self.imageNode
|
||||
}
|
||||
|
||||
if let nodeToTransitionOut, let snapshot = nodeToTransitionOut.view.snapshotView(afterScreenUpdates: false) {
|
||||
snapshot.frame = nodeToTransitionOut.frame
|
||||
snapshot.layer.transform = nodeToTransitionOut.transform
|
||||
snapshot.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
|
||||
snapshot.removeFromSuperview()
|
||||
})
|
||||
snapshot.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2)
|
||||
self.addSubview(snapshot)
|
||||
}
|
||||
|
||||
self.animationNode?.removeFromSupernode()
|
||||
self.animationNode = nil
|
||||
self.didSetUpAnimationNode = false
|
||||
self.isPlaying = false
|
||||
self.currentSize = nil
|
||||
|
||||
self.setup()
|
||||
self.applyVisibility()
|
||||
self.setNeedsLayout()
|
||||
|
||||
let nodeToTransitionIn: ASDisplayNode?
|
||||
if let animationNode = self.animationNode {
|
||||
nodeToTransitionIn = animationNode
|
||||
} else {
|
||||
nodeToTransitionIn = self.imageNode
|
||||
}
|
||||
|
||||
if let nodeToTransitionIn {
|
||||
nodeToTransitionIn.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
nodeToTransitionIn.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
|
||||
}
|
||||
|
||||
let _ = self.dismissReactionSelection()
|
||||
}
|
||||
|
||||
switch updateReaction {
|
||||
case .builtin:
|
||||
let _ = (self.context.engine.stickers.availableReactions()
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { availableReactions in
|
||||
guard let availableReactions else {
|
||||
return
|
||||
}
|
||||
var animation: TelegramMediaFile?
|
||||
for reaction in availableReactions.reactions {
|
||||
if reaction.value == updateReaction.reaction {
|
||||
animation = reaction.selectAnimation
|
||||
break
|
||||
}
|
||||
}
|
||||
if let animation {
|
||||
continueWithAnimationFile(animation)
|
||||
}
|
||||
})
|
||||
case let .custom(fileId, file):
|
||||
if let file {
|
||||
continueWithAnimationFile(file)
|
||||
} else {
|
||||
let _ = (self.context.engine.stickers.resolveInlineStickers(fileIds: [fileId])
|
||||
|> deliverOnMainQueue).start(next: { files in
|
||||
if let itemFile = files[fileId] {
|
||||
continueWithAnimationFile(itemFile)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reactionContextNode.premiumReactionsSelected = { [weak self] file in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
if let file {
|
||||
let context = self.context
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
let controller = UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, loop: true, title: nil, text: presentationData.strings.Story_Editor_TooltipPremiumReaction, undoText: nil, customAction: nil), elevatedLayout: true, animateInAsReplacement: false, blurred: true, action: { [weak self] action in
|
||||
if case .info = action, let self {
|
||||
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .storiesExpirationDurations, forceDark: true, dismissed: nil)
|
||||
self.containerView?.push(controller)
|
||||
}
|
||||
return false
|
||||
})
|
||||
self.containerView?.present(controller)
|
||||
} else {
|
||||
let controller = self.context.sharedContext.makePremiumIntroController(context: self.context, source: .storiesExpirationDurations, forceDark: true, dismissed: nil)
|
||||
self.containerView?.push(controller)
|
||||
}
|
||||
}
|
||||
|
||||
let anchorRect = self.convert(self.bounds, to: superview).offsetBy(dx: 0.0, dy: -20.0)
|
||||
reactionContextNodeTransition.setFrame(view: reactionContextNode.view, frame: CGRect(origin: CGPoint(), size: availableSize))
|
||||
reactionContextNode.updateLayout(size: availableSize, insets: insets, anchorRect: anchorRect, centerAligned: true, isCoveredByInput: false, isAnimatingOut: false, transition: reactionContextNodeTransition.containedViewLayoutTransition)
|
||||
|
||||
superview.addSubnode(reactionContextNode)
|
||||
reactionContextNode.animateIn(from: anchorRect)
|
||||
}
|
||||
|
||||
fileprivate func dismissReactionSelection() -> Bool {
|
||||
if let reactionContextNode = self.reactionContextNode {
|
||||
reactionContextNode.animateOut(to: nil, animatingOutToReaction: false)
|
||||
self.reactionContextNode = nil
|
||||
|
||||
Queue.mainQueue().after(0.35) {
|
||||
reactionContextNode.view.removeFromSuperview()
|
||||
}
|
||||
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
override func selectedTapAction() -> Bool {
|
||||
if case let .file(file, type) = self.stickerEntity.content, case let .reaction(reaction, style) = type {
|
||||
guard self.reactionContextNode == nil else {
|
||||
let values = [self.entity.scale, self.entity.scale * 0.93, self.entity.scale]
|
||||
let keyTimes = [0.0, 0.33, 1.0]
|
||||
self.layer.animateKeyframes(values: values as [NSNumber], keyTimes: keyTimes as [NSNumber], duration: 0.3, keyPath: "transform.scale")
|
||||
|
||||
let updatedStyle: DrawingStickerEntity.Content.FileType.ReactionStyle
|
||||
switch style {
|
||||
case .white:
|
||||
updatedStyle = .black
|
||||
case .black:
|
||||
updatedStyle = .white
|
||||
}
|
||||
self.stickerEntity.content = .file(file, .reaction(reaction, updatedStyle))
|
||||
|
||||
self.update(animated: false)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
self.presentReactionSelection()
|
||||
|
||||
return true
|
||||
} else {
|
||||
return super.selectedTapAction()
|
||||
}
|
||||
}
|
||||
|
||||
override func innerLayoutSubview(boundingSize: CGSize) -> CGSize {
|
||||
self.backgroundView.frame = CGRect(origin: .zero, size: boundingSize).insetBy(dx: -5.0, dy: -5.0)
|
||||
self.outlineView.frame = backgroundView.bounds
|
||||
return CGSize(width: floor(boundingSize.width * 0.63), height: floor(boundingSize.width * 0.63))
|
||||
}
|
||||
}
|
||||
|
|
@ -13,54 +13,36 @@ import MediaEditor
|
|||
import UniversalMediaPlayer
|
||||
import TelegramPresentationData
|
||||
import TelegramUniversalVideoContent
|
||||
import ReactionSelectionNode
|
||||
import UndoUI
|
||||
import EntityKeyboard
|
||||
import ComponentFlow
|
||||
|
||||
public final class DrawingStickerEntityView: DrawingEntityView {
|
||||
private var stickerEntity: DrawingStickerEntity {
|
||||
public class DrawingStickerEntityView: DrawingEntityView {
|
||||
var stickerEntity: DrawingStickerEntity {
|
||||
return self.entity as! DrawingStickerEntity
|
||||
}
|
||||
|
||||
var started: ((Double) -> Void)?
|
||||
let imageNode: TransformImageNode
|
||||
var animationNode: DefaultAnimatedStickerNodeImpl?
|
||||
var videoNode: UniversalVideoNode?
|
||||
var cameraPreviewView: UIView?
|
||||
|
||||
public var updated: () -> Void = {}
|
||||
let progressDisposable = MetaDisposable()
|
||||
let progressLayer = CAShapeLayer()
|
||||
|
||||
private var currentSize: CGSize?
|
||||
|
||||
private var backgroundView: UIImageView?
|
||||
private var outlineView: UIImageView?
|
||||
|
||||
private let imageNode: TransformImageNode
|
||||
private var animationNode: DefaultAnimatedStickerNodeImpl?
|
||||
private var videoNode: UniversalVideoNode?
|
||||
|
||||
private var didSetUpAnimationNode = false
|
||||
var didSetUpAnimationNode = false
|
||||
private let stickerFetchedDisposable = MetaDisposable()
|
||||
private let cachedDisposable = MetaDisposable()
|
||||
|
||||
private var isVisible = true
|
||||
private var isPlaying = false
|
||||
var isPlaying = false
|
||||
var started: ((Double) -> Void)?
|
||||
|
||||
var currentSize: CGSize?
|
||||
public var updated: () -> Void = {}
|
||||
|
||||
init(context: AccountContext, entity: DrawingStickerEntity) {
|
||||
self.imageNode = TransformImageNode()
|
||||
|
||||
super.init(context: context, entity: entity)
|
||||
|
||||
if case .file(_, .reaction) = entity.content {
|
||||
let backgroundView = UIImageView(image: UIImage(bundleImageName: "Stories/ReactionShadow"))
|
||||
backgroundView.layer.zPosition = -1000.0
|
||||
|
||||
let outlineView = UIImageView(image: UIImage(bundleImageName: "Stories/ReactionOutline"))
|
||||
outlineView.tintColor = .white
|
||||
backgroundView.addSubview(outlineView)
|
||||
|
||||
self.addSubview(backgroundView)
|
||||
self.backgroundView = backgroundView
|
||||
self.outlineView = outlineView
|
||||
}
|
||||
|
||||
self.addSubview(self.imageNode.view)
|
||||
|
||||
self.setup()
|
||||
|
|
@ -73,6 +55,7 @@ public final class DrawingStickerEntityView: DrawingEntityView {
|
|||
deinit {
|
||||
self.stickerFetchedDisposable.dispose()
|
||||
self.cachedDisposable.dispose()
|
||||
self.progressDisposable.dispose()
|
||||
}
|
||||
|
||||
private var file: TelegramMediaFile? {
|
||||
|
|
@ -103,7 +86,6 @@ public final class DrawingStickerEntityView: DrawingEntityView {
|
|||
return image
|
||||
}
|
||||
|
||||
|
||||
private var video: TelegramMediaFile? {
|
||||
if case let .video(file) = self.stickerEntity.content {
|
||||
return file
|
||||
|
|
@ -143,7 +125,7 @@ public final class DrawingStickerEntityView: DrawingEntityView {
|
|||
self.animationNode?.dynamicColor = color
|
||||
}
|
||||
|
||||
private func setup() {
|
||||
func setup() {
|
||||
if let file = self.file {
|
||||
if let dimensions = file.dimensions {
|
||||
if file.isAnimatedSticker || file.isVideoSticker || file.mimeType == "video/webm" {
|
||||
|
|
@ -308,7 +290,7 @@ public final class DrawingStickerEntityView: DrawingEntityView {
|
|||
self.applyVisibility()
|
||||
}
|
||||
|
||||
private func applyVisibility() {
|
||||
func applyVisibility() {
|
||||
let isPlaying = self.isVisible
|
||||
if self.isPlaying != isPlaying {
|
||||
self.isPlaying = isPlaying
|
||||
|
|
@ -331,6 +313,41 @@ public final class DrawingStickerEntityView: DrawingEntityView {
|
|||
}
|
||||
}
|
||||
|
||||
public func setupCameraPreviewView(_ cameraPreviewView: UIView, progress: Signal<Float, NoError>) {
|
||||
self.addSubview(cameraPreviewView)
|
||||
self.cameraPreviewView = cameraPreviewView
|
||||
|
||||
self.progressLayer.transform = CATransform3DMakeRotation(-.pi / 2.0, 0.0, 0.0, 1.0)
|
||||
self.progressLayer.fillColor = UIColor.clear.cgColor
|
||||
self.progressLayer.strokeColor = UIColor(rgb: 0xffffff, alpha: 0.5).cgColor
|
||||
self.progressLayer.lineWidth = 3.0
|
||||
self.progressLayer.lineCap = .round
|
||||
self.progressLayer.strokeEnd = 0.0
|
||||
self.layer.addSublayer(self.progressLayer)
|
||||
|
||||
self.setNeedsLayout()
|
||||
|
||||
self.progressDisposable.set((progress
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] progress in
|
||||
if let self {
|
||||
self.progressLayer.strokeEnd = CGFloat(progress)
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
public func invalidateCameraPreviewView() {
|
||||
guard let cameraPreviewView = self.cameraPreviewView else {
|
||||
return
|
||||
}
|
||||
Queue.mainQueue().after(0.3, {
|
||||
self.cameraPreviewView = nil
|
||||
cameraPreviewView.removeFromSuperview()
|
||||
})
|
||||
self.progressLayer.removeFromSuperlayer()
|
||||
self.progressLayer.path = nil
|
||||
self.progressDisposable.set(nil)
|
||||
}
|
||||
|
||||
private var didApplyVisibility = false
|
||||
public override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
|
@ -341,14 +358,8 @@ public final class DrawingStickerEntityView: DrawingEntityView {
|
|||
self.currentSize = size
|
||||
|
||||
let sideSize: CGFloat = max(size.width, size.height)
|
||||
var boundingSize = CGSize(width: sideSize, height: sideSize)
|
||||
|
||||
if let backgroundView = self.backgroundView, let outlineView = self.outlineView {
|
||||
backgroundView.frame = CGRect(origin: .zero, size: boundingSize).insetBy(dx: -5.0, dy: -5.0)
|
||||
outlineView.frame = backgroundView.bounds
|
||||
boundingSize = CGSize(width: floor(sideSize * 0.63), height: floor(sideSize * 0.63))
|
||||
}
|
||||
|
||||
let boundingSize = self.innerLayoutSubview(boundingSize: CGSize(width: sideSize, height: sideSize))
|
||||
|
||||
let imageSize = self.dimensions.aspectFitted(boundingSize)
|
||||
let imageFrame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: (size.height - imageSize.height) / 2.0), size: imageSize)
|
||||
|
||||
|
|
@ -373,271 +384,32 @@ public final class DrawingStickerEntityView: DrawingEntityView {
|
|||
videoNode.updateLayout(size: imageSize, transition: .immediate)
|
||||
}
|
||||
|
||||
if let cameraPreviewView = self.cameraPreviewView {
|
||||
cameraPreviewView.layer.cornerRadius = imageSize.width / 2.0
|
||||
cameraPreviewView.frame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) * 0.5), y: floor((size.height - imageSize.height) * 0.5)), size: imageSize)
|
||||
self.progressLayer.frame = cameraPreviewView.frame
|
||||
|
||||
if self.progressLayer.path == nil {
|
||||
self.progressLayer.path = CGPath(ellipseIn: cameraPreviewView.frame.insetBy(dx: 6.0, dy: 6.0), transform: nil)
|
||||
}
|
||||
}
|
||||
|
||||
self.update(animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
private var isReaction: Bool {
|
||||
if case let .file(_, type) = self.stickerEntity.content, case .reaction = type {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
override func animateInsertion() {
|
||||
super.animateInsertion()
|
||||
|
||||
if self.isReaction {
|
||||
Queue.mainQueue().after(0.2) {
|
||||
let _ = self.selectedTapAction()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func onSelection() {
|
||||
if self.isReaction {
|
||||
self.presentReactionSelection()
|
||||
}
|
||||
var isReaction: Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func onDeselection() {
|
||||
if self.isReaction {
|
||||
let _ = self.dismissReactionSelection()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private weak var reactionContextNode: ReactionContextNode?
|
||||
fileprivate func presentReactionSelection() {
|
||||
guard let containerView = self.containerView, let superview = containerView.superview?.superview?.superview?.superview, self.reactionContextNode == nil else {
|
||||
return
|
||||
}
|
||||
|
||||
let availableSize = superview.frame.size
|
||||
let reactionItems = containerView.getAvailableReactions()
|
||||
|
||||
let insets = UIEdgeInsets(top: 64.0, left: 0.0, bottom: 64.0, right: 0.0)
|
||||
|
||||
let layout: (ContainedViewLayoutTransition) -> Void = { [weak self, weak superview] transition in
|
||||
guard let self, let superview, let reactionContextNode = self.reactionContextNode else {
|
||||
return
|
||||
}
|
||||
let anchorRect = self.convert(self.bounds, to: superview).offsetBy(dx: 0.0, dy: -20.0)
|
||||
reactionContextNode.updateLayout(size: availableSize, insets: insets, anchorRect: anchorRect, centerAligned: true, isCoveredByInput: false, isAnimatingOut: false, transition: transition)
|
||||
}
|
||||
|
||||
let reactionContextNodeTransition: Transition = .immediate
|
||||
let reactionContextNode: ReactionContextNode
|
||||
reactionContextNode = ReactionContextNode(
|
||||
context: self.context,
|
||||
animationCache: self.context.animationCache,
|
||||
presentationData: self.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme),
|
||||
items: reactionItems.map(ReactionContextItem.reaction),
|
||||
selectedItems: Set(),
|
||||
title: nil,
|
||||
getEmojiContent: { [weak self] animationCache, animationRenderer in
|
||||
guard let self else {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
let mappedReactionItems: [EmojiComponentReactionItem] = reactionItems.map { reaction -> EmojiComponentReactionItem in
|
||||
return EmojiComponentReactionItem(reaction: reaction.reaction.rawValue, file: reaction.stillAnimation)
|
||||
}
|
||||
|
||||
return EmojiPagerContentComponent.emojiInputData(
|
||||
context: self.context,
|
||||
animationCache: animationCache,
|
||||
animationRenderer: animationRenderer,
|
||||
isStandalone: false,
|
||||
subject: .reaction,
|
||||
hasTrending: false,
|
||||
topReactionItems: mappedReactionItems,
|
||||
areUnicodeEmojiEnabled: false,
|
||||
areCustomEmojiEnabled: true,
|
||||
chatPeerId: self.context.account.peerId,
|
||||
selectedItems: Set(),
|
||||
premiumIfSavedMessages: false
|
||||
)
|
||||
},
|
||||
isExpandedUpdated: { transition in
|
||||
layout(transition)
|
||||
},
|
||||
requestLayout: { transition in
|
||||
layout(transition)
|
||||
},
|
||||
requestUpdateOverlayWantsToBeBelowKeyboard: { transition in
|
||||
layout(transition)
|
||||
}
|
||||
)
|
||||
reactionContextNode.displayTail = true
|
||||
reactionContextNode.forceTailToRight = true
|
||||
reactionContextNode.forceDark = true
|
||||
self.reactionContextNode = reactionContextNode
|
||||
|
||||
reactionContextNode.reactionSelected = { [weak self] updateReaction, _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
let continueWithAnimationFile: (TelegramMediaFile) -> Void = { [weak self] animation in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
if case let .file(_, type) = self.stickerEntity.content, case let .reaction(_, style) = type {
|
||||
self.stickerEntity.content = .file(animation, .reaction(updateReaction.reaction, style))
|
||||
}
|
||||
|
||||
var nodeToTransitionOut: ASDisplayNode?
|
||||
if let animationNode = self.animationNode {
|
||||
nodeToTransitionOut = animationNode
|
||||
} else if !self.imageNode.isHidden {
|
||||
nodeToTransitionOut = self.imageNode
|
||||
}
|
||||
|
||||
if let nodeToTransitionOut, let snapshot = nodeToTransitionOut.view.snapshotView(afterScreenUpdates: false) {
|
||||
snapshot.frame = nodeToTransitionOut.frame
|
||||
snapshot.layer.transform = nodeToTransitionOut.transform
|
||||
snapshot.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
|
||||
snapshot.removeFromSuperview()
|
||||
})
|
||||
snapshot.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2)
|
||||
self.addSubview(snapshot)
|
||||
}
|
||||
|
||||
self.animationNode?.removeFromSupernode()
|
||||
self.animationNode = nil
|
||||
self.didSetUpAnimationNode = false
|
||||
self.isPlaying = false
|
||||
self.currentSize = nil
|
||||
|
||||
self.setup()
|
||||
self.applyVisibility()
|
||||
self.setNeedsLayout()
|
||||
|
||||
let nodeToTransitionIn: ASDisplayNode?
|
||||
if let animationNode = self.animationNode {
|
||||
nodeToTransitionIn = animationNode
|
||||
} else {
|
||||
nodeToTransitionIn = self.imageNode
|
||||
}
|
||||
|
||||
if let nodeToTransitionIn {
|
||||
nodeToTransitionIn.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
nodeToTransitionIn.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
|
||||
}
|
||||
|
||||
let _ = self.dismissReactionSelection()
|
||||
}
|
||||
|
||||
switch updateReaction {
|
||||
case .builtin:
|
||||
let _ = (self.context.engine.stickers.availableReactions()
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { availableReactions in
|
||||
guard let availableReactions else {
|
||||
return
|
||||
}
|
||||
var animation: TelegramMediaFile?
|
||||
for reaction in availableReactions.reactions {
|
||||
if reaction.value == updateReaction.reaction {
|
||||
animation = reaction.selectAnimation
|
||||
break
|
||||
}
|
||||
}
|
||||
if let animation {
|
||||
continueWithAnimationFile(animation)
|
||||
}
|
||||
})
|
||||
case let .custom(fileId, file):
|
||||
if let file {
|
||||
continueWithAnimationFile(file)
|
||||
} else {
|
||||
let _ = (self.context.engine.stickers.resolveInlineStickers(fileIds: [fileId])
|
||||
|> deliverOnMainQueue).start(next: { files in
|
||||
if let itemFile = files[fileId] {
|
||||
continueWithAnimationFile(itemFile)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reactionContextNode.premiumReactionsSelected = { [weak self] file in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
if let file {
|
||||
let context = self.context
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
let controller = UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, loop: true, title: nil, text: presentationData.strings.Story_Editor_TooltipPremiumReaction, undoText: nil, customAction: nil), elevatedLayout: true, animateInAsReplacement: false, blurred: true, action: { [weak self] action in
|
||||
if case .info = action, let self {
|
||||
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .storiesExpirationDurations, forceDark: true, dismissed: nil)
|
||||
self.containerView?.push(controller)
|
||||
}
|
||||
return false
|
||||
})
|
||||
self.containerView?.present(controller)
|
||||
} else {
|
||||
let controller = self.context.sharedContext.makePremiumIntroController(context: self.context, source: .storiesExpirationDurations, forceDark: true, dismissed: nil)
|
||||
self.containerView?.push(controller)
|
||||
}
|
||||
}
|
||||
|
||||
let anchorRect = self.convert(self.bounds, to: superview).offsetBy(dx: 0.0, dy: -20.0)
|
||||
reactionContextNodeTransition.setFrame(view: reactionContextNode.view, frame: CGRect(origin: CGPoint(), size: availableSize))
|
||||
reactionContextNode.updateLayout(size: availableSize, insets: insets, anchorRect: anchorRect, centerAligned: true, isCoveredByInput: false, isAnimatingOut: false, transition: reactionContextNodeTransition.containedViewLayoutTransition)
|
||||
|
||||
superview.addSubnode(reactionContextNode)
|
||||
reactionContextNode.animateIn(from: anchorRect)
|
||||
func innerLayoutSubview(boundingSize: CGSize) -> CGSize {
|
||||
return boundingSize
|
||||
}
|
||||
|
||||
fileprivate func dismissReactionSelection() -> Bool {
|
||||
if let reactionContextNode = self.reactionContextNode {
|
||||
reactionContextNode.animateOut(to: nil, animatingOutToReaction: false)
|
||||
self.reactionContextNode = nil
|
||||
|
||||
Queue.mainQueue().after(0.35) {
|
||||
reactionContextNode.view.removeFromSuperview()
|
||||
}
|
||||
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
override func selectedTapAction() -> Bool {
|
||||
if case let .file(file, type) = self.stickerEntity.content, case let .reaction(reaction, style) = type {
|
||||
guard self.reactionContextNode == nil else {
|
||||
let values = [self.entity.scale, self.entity.scale * 0.93, self.entity.scale]
|
||||
let keyTimes = [0.0, 0.33, 1.0]
|
||||
self.layer.animateKeyframes(values: values as [NSNumber], keyTimes: keyTimes as [NSNumber], duration: 0.3, keyPath: "transform.scale")
|
||||
|
||||
let updatedStyle: DrawingStickerEntity.Content.FileType.ReactionStyle
|
||||
switch style {
|
||||
case .white:
|
||||
updatedStyle = .black
|
||||
case .black:
|
||||
updatedStyle = .white
|
||||
}
|
||||
self.stickerEntity.content = .file(file, .reaction(reaction, updatedStyle))
|
||||
|
||||
self.update(animated: false)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
self.presentReactionSelection()
|
||||
|
||||
return true
|
||||
} else {
|
||||
return super.selectedTapAction()
|
||||
}
|
||||
}
|
||||
|
||||
public override func update(animated: Bool) {
|
||||
self.center = self.stickerEntity.position
|
||||
|
||||
|
|
@ -646,24 +418,19 @@ public final class DrawingStickerEntityView: DrawingEntityView {
|
|||
self.bounds = CGRect(origin: .zero, size: self.dimensions.aspectFitted(size))
|
||||
self.transform = CGAffineTransformScale(CGAffineTransformMakeRotation(self.stickerEntity.rotation), self.stickerEntity.scale, self.stickerEntity.scale)
|
||||
|
||||
if case let .file(_, type) = self.stickerEntity.content, case let .reaction(_, style) = type {
|
||||
switch style {
|
||||
case .white:
|
||||
self.outlineView?.tintColor = .white
|
||||
case .black:
|
||||
self.outlineView?.tintColor = UIColor(rgb: 0x000000, alpha: 0.5)
|
||||
}
|
||||
}
|
||||
self.updateAnimationColor()
|
||||
|
||||
let isReaction = self.isReaction
|
||||
let staticTransform = CATransform3DMakeScale(self.stickerEntity.mirrored ? -1.0 : 1.0, 1.0, 1.0)
|
||||
self.updateMirroring(animated: animated)
|
||||
|
||||
self.updated()
|
||||
|
||||
super.update(animated: animated)
|
||||
}
|
||||
|
||||
func updateMirroring(animated: Bool) {
|
||||
let staticTransform = CATransform3DMakeScale(self.stickerEntity.mirrored ? -1.0 : 1.0, 1.0, 1.0)
|
||||
if animated {
|
||||
var isCurrentlyMirrored = ((self.imageNode.layer.value(forKeyPath: "transform.scale.y") as? NSNumber)?.floatValue ?? 1.0) < 0.0
|
||||
if isReaction {
|
||||
isCurrentlyMirrored = ((self.backgroundView?.layer.value(forKeyPath: "transform.scale.y") as? NSNumber)?.floatValue ?? 1.0) < 0.0
|
||||
}
|
||||
let isCurrentlyMirrored = ((self.imageNode.layer.value(forKeyPath: "transform.scale.y") as? NSNumber)?.floatValue ?? 1.0) < 0.0
|
||||
var animationSourceTransform = CATransform3DIdentity
|
||||
var animationTargetTransform = CATransform3DIdentity
|
||||
if isCurrentlyMirrored {
|
||||
|
|
@ -674,50 +441,28 @@ public final class DrawingStickerEntityView: DrawingEntityView {
|
|||
animationTargetTransform = CATransform3DRotate(animationTargetTransform, .pi, 0.0, 1.0, 0.0)
|
||||
animationTargetTransform.m34 = -1.0 / self.imageNode.frame.width
|
||||
}
|
||||
if isReaction {
|
||||
self.backgroundView?.layer.transform = animationSourceTransform
|
||||
|
||||
let values = [1.0, 0.01, 1.0]
|
||||
let keyTimes = [0.0, 0.5, 1.0]
|
||||
self.animationNode?.layer.animateKeyframes(values: values as [NSNumber], keyTimes: keyTimes as [NSNumber], duration: 0.25, keyPath: "transform.scale.x", timingFunction: CAMediaTimingFunctionName.linear.rawValue)
|
||||
} else {
|
||||
self.imageNode.transform = animationSourceTransform
|
||||
self.animationNode?.transform = animationSourceTransform
|
||||
self.videoNode?.transform = animationSourceTransform
|
||||
}
|
||||
|
||||
self.imageNode.transform = animationSourceTransform
|
||||
self.animationNode?.transform = animationSourceTransform
|
||||
self.videoNode?.transform = animationSourceTransform
|
||||
|
||||
UIView.animate(withDuration: 0.25, animations: {
|
||||
if isReaction {
|
||||
self.backgroundView?.layer.transform = animationTargetTransform
|
||||
} else {
|
||||
self.imageNode.transform = animationTargetTransform
|
||||
self.animationNode?.transform = animationTargetTransform
|
||||
self.videoNode?.transform = animationTargetTransform
|
||||
}
|
||||
self.imageNode.transform = animationTargetTransform
|
||||
self.animationNode?.transform = animationTargetTransform
|
||||
self.videoNode?.transform = animationTargetTransform
|
||||
}, completion: { finished in
|
||||
if isReaction {
|
||||
self.backgroundView?.layer.transform = staticTransform
|
||||
} else {
|
||||
self.imageNode.transform = staticTransform
|
||||
self.animationNode?.transform = staticTransform
|
||||
self.videoNode?.transform = staticTransform
|
||||
}
|
||||
self.imageNode.transform = staticTransform
|
||||
self.animationNode?.transform = staticTransform
|
||||
self.videoNode?.transform = staticTransform
|
||||
})
|
||||
} else {
|
||||
CATransaction.begin()
|
||||
CATransaction.setDisableActions(true)
|
||||
if isReaction {
|
||||
self.backgroundView?.layer.transform = staticTransform
|
||||
} else {
|
||||
self.imageNode.transform = staticTransform
|
||||
self.animationNode?.transform = staticTransform
|
||||
self.videoNode?.transform = staticTransform
|
||||
}
|
||||
self.imageNode.transform = staticTransform
|
||||
self.animationNode?.transform = staticTransform
|
||||
self.videoNode?.transform = staticTransform
|
||||
CATransaction.commit()
|
||||
}
|
||||
|
||||
self.updated()
|
||||
|
||||
super.update(animated: animated)
|
||||
}
|
||||
|
||||
override func updateSelectionView() {
|
||||
|
|
@ -838,7 +583,7 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView {
|
|||
|
||||
self.snapTool.maybeSkipFromStart(entityView: entityView, position: entity.position)
|
||||
|
||||
let _ = entityView.dismissReactionSelection()
|
||||
entityView.onDeselection()
|
||||
|
||||
if let sublayers = self.layer.sublayers {
|
||||
for layer in sublayers {
|
||||
|
|
@ -927,7 +672,7 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView {
|
|||
|
||||
switch gestureRecognizer.state {
|
||||
case .began, .changed:
|
||||
let _ = entityView.dismissReactionSelection()
|
||||
entityView.onDeselection()
|
||||
|
||||
if case .began = gestureRecognizer.state {
|
||||
entityView.onInteractionUpdated(true)
|
||||
|
|
@ -961,7 +706,7 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView {
|
|||
|
||||
switch gestureRecognizer.state {
|
||||
case .began:
|
||||
let _ = entityView.dismissReactionSelection()
|
||||
entityView.onDeselection()
|
||||
|
||||
self.snapTool.maybeSkipFromStart(entityView: entityView, rotation: entity.rotation)
|
||||
entityView.onInteractionUpdated(true)
|
||||
|
|
@ -80,10 +80,6 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate
|
|||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.displayLink?.invalidate()
|
||||
}
|
||||
|
||||
override func animateInsertion() {
|
||||
|
||||
}
|
||||
|
|
@ -516,50 +512,6 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate
|
|||
self.updateTextAnimations()
|
||||
}
|
||||
}
|
||||
|
||||
private var previousDisplayLinkTime: Double?
|
||||
|
||||
private var displayLinkStart: Double?
|
||||
private var displayLink: SharedDisplayLinkDriver.Link?
|
||||
|
||||
private var pendingImage: (Double, UIImage)?
|
||||
private var cachedFrames: [DrawingTextEntity.AnimationFrame] = []
|
||||
|
||||
private func setupRecorder(delta: Double, duration: Double) {
|
||||
self.cachedFrames.removeAll()
|
||||
|
||||
self.displayLink?.invalidate()
|
||||
self.displayLink = nil
|
||||
|
||||
self.previousDisplayLinkTime = nil
|
||||
let displayLinkStart = CACurrentMediaTime()
|
||||
self.displayLinkStart = displayLinkStart
|
||||
|
||||
self.displayLink = SharedDisplayLinkDriver.shared.add { [weak self] _ in
|
||||
if let strongSelf = self {
|
||||
let currentTime = CACurrentMediaTime()
|
||||
if let previousDisplayLinkTime = strongSelf.previousDisplayLinkTime, currentTime < previousDisplayLinkTime + delta {
|
||||
return
|
||||
}
|
||||
if currentTime >= displayLinkStart + duration {
|
||||
strongSelf.displayLink?.invalidate()
|
||||
strongSelf.displayLink = nil
|
||||
}
|
||||
if let (timestamp, image) = strongSelf.pendingImage, let previousDisplayLinkTime = strongSelf.previousDisplayLinkTime {
|
||||
strongSelf.cachedFrames.append(DrawingTextEntity.AnimationFrame(timestamp: timestamp - displayLinkStart, duration: currentTime - previousDisplayLinkTime, image: image))
|
||||
}
|
||||
if let image = strongSelf.getPresentationRenderImage() {
|
||||
strongSelf.pendingImage = (currentTime, image)
|
||||
}
|
||||
if strongSelf.previousDisplayLinkTime == nil {
|
||||
strongSelf.previousDisplayLinkTime = displayLinkStart
|
||||
} else {
|
||||
strongSelf.previousDisplayLinkTime = currentTime
|
||||
}
|
||||
}
|
||||
}
|
||||
self.displayLink?.isPaused = false
|
||||
}
|
||||
|
||||
func updateTextAnimations() {
|
||||
for layer in self.textView.characterLayers {
|
||||
|
|
@ -587,7 +539,6 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate
|
|||
layer.add(animation, forKey: "opacity")
|
||||
offset += delta
|
||||
}
|
||||
self.setupRecorder(delta: delta, duration: duration)
|
||||
case .wiggle:
|
||||
for layer in self.textView.characterLayers {
|
||||
let animation = CABasicAnimation(keyPath: "transform.rotation.z")
|
||||
|
|
@ -599,7 +550,6 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate
|
|||
animation.repeatCount = .infinity
|
||||
layer.add(animation, forKey: "transform.rotation.z")
|
||||
}
|
||||
self.setupRecorder(delta: 0.033, duration: 1.2)
|
||||
case .zoomIn:
|
||||
let animation = CABasicAnimation(keyPath: "transform.scale")
|
||||
animation.fromValue = 0.001 as NSNumber
|
||||
|
|
@ -766,7 +716,7 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate
|
|||
}
|
||||
|
||||
func getRenderAnimationFrames() -> [DrawingTextEntity.AnimationFrame]? {
|
||||
return self.cachedFrames
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -538,6 +538,9 @@ public class StickerPickerScreen: ViewController {
|
|||
self.storyStickersContentView?.reactionAction = { [weak self] in
|
||||
self?.controller?.addReaction()
|
||||
}
|
||||
self.storyStickersContentView?.cameraAction = { [weak self] in
|
||||
self?.controller?.addCamera()
|
||||
}
|
||||
|
||||
let gifItems: Signal<EntityKeyboardGifContent?, NoError>
|
||||
if controller.hasGifs {
|
||||
|
|
@ -1966,6 +1969,7 @@ public class StickerPickerScreen: ViewController {
|
|||
public var presentLocationPicker: () -> Void = { }
|
||||
public var presentAudioPicker: () -> Void = { }
|
||||
public var addReaction: () -> Void = { }
|
||||
public var addCamera: () -> Void = { }
|
||||
|
||||
public init(context: AccountContext, inputData: Signal<StickerPickerInputData, NoError>, defaultToEmoji: Bool = false, hasGifs: Bool = false) {
|
||||
self.context = context
|
||||
|
|
@ -2194,14 +2198,7 @@ private final class InteractiveReactionButtonContent: Component {
|
|||
}
|
||||
|
||||
func update(component: InteractiveReactionButtonContent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
// if component.useOpaqueTheme {
|
||||
// self.backgroundLayer.backgroundColor = component.theme.chat.inputMediaPanel.panelContentControlOpaqueSelectionColor.cgColor
|
||||
// self.tintBackgroundLayer.backgroundColor = UIColor.white.cgColor
|
||||
// } else {
|
||||
// self.backgroundLayer.backgroundColor = component.theme.chat.inputMediaPanel.panelContentControlVibrantSelectionColor.cgColor
|
||||
// self.tintBackgroundLayer.backgroundColor = UIColor(white: 1.0, alpha: 0.2).cgColor
|
||||
// }
|
||||
|
||||
let bounds = CGRect(origin: .zero, size: CGSize(width: 54.0, height: 54.0))
|
||||
let iconSize = self.icon.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(BundleIconComponent(
|
||||
|
|
@ -2217,10 +2214,89 @@ private final class InteractiveReactionButtonContent: Component {
|
|||
if view.superview == nil {
|
||||
self.addSubview(view)
|
||||
}
|
||||
transition.setFrame(view: view, frame: CGRect(origin: .zero, size: iconSize))
|
||||
transition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: 2.0, y: 0.0), size: iconSize))
|
||||
}
|
||||
|
||||
return iconSize
|
||||
return bounds.size
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
private final class RoundVideoButtonContent: Component {
|
||||
let theme: PresentationTheme
|
||||
|
||||
public init(
|
||||
theme: PresentationTheme
|
||||
) {
|
||||
self.theme = theme
|
||||
}
|
||||
|
||||
public static func ==(lhs: RoundVideoButtonContent, rhs: RoundVideoButtonContent) -> Bool {
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
override public static var layerClass: AnyClass {
|
||||
return PassthroughLayer.self
|
||||
}
|
||||
|
||||
private let backgroundLayer = SimpleLayer()
|
||||
private var icon: ComponentView<Empty>
|
||||
|
||||
private var component: InteractiveReactionButtonContent?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.icon = ComponentView<Empty>()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.isExclusiveTouch = true
|
||||
|
||||
self.layer.addSublayer(self.backgroundLayer)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(component: RoundVideoButtonContent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
self.backgroundLayer.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.11).cgColor
|
||||
|
||||
let bounds = CGRect(origin: .zero, size: CGSize(width: 54.0, height: 54.0))
|
||||
let backgroundSize = CGSize(width: 50.0, height: 50.0)
|
||||
self.backgroundLayer.frame = backgroundSize.centered(in: bounds)
|
||||
self.backgroundLayer.cornerRadius = backgroundSize.width / 2.0
|
||||
|
||||
let iconSize = self.icon.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(BundleIconComponent(
|
||||
name: "Chat List/Tabs/IconCamera",
|
||||
tintColor: nil,
|
||||
maxSize: CGSize(width: 30.0, height: 30.0)
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
|
||||
if let view = self.icon.view {
|
||||
if view.superview == nil {
|
||||
self.addSubview(view)
|
||||
}
|
||||
transition.setFrame(view: view, frame: iconSize.centered(in: bounds))
|
||||
}
|
||||
|
||||
return bounds.size
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2352,7 +2428,8 @@ final class StoryStickersContentView: UIView, EmojiCustomContentView {
|
|||
var locationAction: () -> Void = {}
|
||||
var audioAction: () -> Void = {}
|
||||
var reactionAction: () -> Void = {}
|
||||
|
||||
var cameraAction: () -> Void = {}
|
||||
|
||||
func update(theme: PresentationTheme, strings: PresentationStrings, useOpaqueTheme: Bool, availableSize: CGSize, transition: Transition) -> CGSize {
|
||||
let padding: CGFloat = 22.0
|
||||
let size = self.container.update(
|
||||
|
|
|
|||
173
submodules/DrawingUI/Sources/VideoRecorder.swift
Normal file
173
submodules/DrawingUI/Sources/VideoRecorder.swift
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import SwiftSignalKit
|
||||
import Camera
|
||||
import MediaEditor
|
||||
|
||||
public final class EntityVideoRecorder {
|
||||
private weak var mediaEditor: MediaEditor?
|
||||
private weak var entitiesView: DrawingEntitiesView?
|
||||
|
||||
private let maxDuration: Double
|
||||
|
||||
private let camera: Camera
|
||||
private let previewView: CameraSimplePreviewView
|
||||
private let entity: DrawingStickerEntity
|
||||
|
||||
private var recordingDisposable = MetaDisposable()
|
||||
private let durationPromise = ValuePromise<Double>()
|
||||
private let micLevelPromise = Promise<Float>()
|
||||
|
||||
public var duration: Signal<Double, NoError> {
|
||||
return self.durationPromise.get()
|
||||
}
|
||||
|
||||
public var micLevel: Signal<Float, NoError> {
|
||||
return self.micLevelPromise.get()
|
||||
}
|
||||
|
||||
public var onAutomaticStop: () -> Void = {}
|
||||
|
||||
public init(mediaEditor: MediaEditor, entitiesView: DrawingEntitiesView) {
|
||||
self.mediaEditor = mediaEditor
|
||||
self.entitiesView = entitiesView
|
||||
|
||||
self.maxDuration = mediaEditor.duration ?? 60.0
|
||||
self.previewView = CameraSimplePreviewView(frame: .zero, main: true)
|
||||
|
||||
self.entity = DrawingStickerEntity(content: .dualVideoReference)
|
||||
|
||||
self.camera = Camera(
|
||||
configuration: Camera.Configuration(
|
||||
preset: .hd1920x1080,
|
||||
position: .front,
|
||||
isDualEnabled: false,
|
||||
audio: true,
|
||||
photo: false,
|
||||
metadata: false,
|
||||
preferredFps: 60.0,
|
||||
preferWide: true
|
||||
),
|
||||
previewView: self.previewView,
|
||||
secondaryPreviewView: nil
|
||||
)
|
||||
self.camera.startCapture()
|
||||
|
||||
let action = { [weak self] in
|
||||
self?.previewView.removePlaceholder(delay: 0.15)
|
||||
Queue.mainQueue().after(0.1) {
|
||||
self?.startRecording()
|
||||
self?.mediaEditor?.play()
|
||||
}
|
||||
}
|
||||
if #available(iOS 13.0, *) {
|
||||
let _ = (self.previewView.isPreviewing
|
||||
|> filter { $0 }
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).startStandalone(next: { _ in
|
||||
action()
|
||||
})
|
||||
} else {
|
||||
Queue.mainQueue().after(0.35) {
|
||||
action()
|
||||
}
|
||||
}
|
||||
|
||||
self.micLevelPromise.set(.single(0.0))
|
||||
|
||||
self.mediaEditor?.stop()
|
||||
self.mediaEditor?.seek(0.0, andPlay: false)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.recordingDisposable.dispose()
|
||||
}
|
||||
|
||||
public func setup(
|
||||
referenceDrawingSize: CGSize,
|
||||
scale: CGFloat,
|
||||
position: CGPoint
|
||||
) {
|
||||
self.entity.referenceDrawingSize = referenceDrawingSize
|
||||
self.entity.scale = scale
|
||||
self.entity.position = position
|
||||
self.entitiesView?.add(self.entity)
|
||||
|
||||
if let entityView = self.entitiesView?.getView(for: self.entity.uuid) as? DrawingStickerEntityView {
|
||||
let maxDuration = self.maxDuration
|
||||
entityView.setupCameraPreviewView(
|
||||
self.previewView,
|
||||
progress: self.durationPromise.get() |> map {
|
||||
Float(max(0.0, min(1.0, $0 / maxDuration)))
|
||||
}
|
||||
)
|
||||
self.previewView.resetPlaceholder(front: true)
|
||||
entityView.animateInsertion()
|
||||
}
|
||||
}
|
||||
|
||||
var start: Double = 0.0
|
||||
private func startRecording() {
|
||||
self.start = CACurrentMediaTime()
|
||||
self.recordingDisposable.set((self.camera.startRecording()
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] duration in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.durationPromise.set(duration)
|
||||
if duration >= self.maxDuration {
|
||||
let onAutomaticStop = self.onAutomaticStop
|
||||
self.stopRecording(save: true, completion: {
|
||||
onAutomaticStop()
|
||||
})
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
public func stopRecording(save: Bool, completion: @escaping () -> Void = {}) {
|
||||
var save = save
|
||||
var remove = false
|
||||
let duration = CACurrentMediaTime() - self.start
|
||||
if duration < 0.2 {
|
||||
save = false
|
||||
remove = true
|
||||
}
|
||||
self.recordingDisposable.set((self.camera.stopRecording()
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] result in
|
||||
guard let self, let mediaEditor = self.mediaEditor, let entitiesView = self.entitiesView, case let .finished(mainResult, _, duration, _, _) = result else {
|
||||
return
|
||||
}
|
||||
if save {
|
||||
mediaEditor.setAdditionalVideo(mainResult.path, positionChanges: [])
|
||||
mediaEditor.setAdditionalVideoTrimRange(0..<duration, apply: true)
|
||||
mediaEditor.seek(0.0, andPlay: true)
|
||||
if let entityView = entitiesView.getView(for: self.entity.uuid) as? DrawingStickerEntityView {
|
||||
entityView.invalidateCameraPreviewView()
|
||||
|
||||
let entity = self.entity
|
||||
let update = { [weak mediaEditor, weak entity] in
|
||||
if let mediaEditor, let entity {
|
||||
mediaEditor.setAdditionalVideoPosition(entity.position, scale: entity.scale, rotation: entity.rotation)
|
||||
}
|
||||
}
|
||||
entityView.updated = {
|
||||
update()
|
||||
}
|
||||
update()
|
||||
}
|
||||
} else {
|
||||
self.entitiesView?.remove(uuid: self.entity.uuid, animated: true)
|
||||
if remove {
|
||||
mediaEditor.setAdditionalVideo(nil, positionChanges: [])
|
||||
}
|
||||
}
|
||||
self.camera.stopCapture(invalidate: true)
|
||||
|
||||
completion()
|
||||
}))
|
||||
}
|
||||
|
||||
public func togglePosition() {
|
||||
self.camera.togglePosition()
|
||||
}
|
||||
}
|
||||
|
|
@ -497,7 +497,7 @@ final class MediaPickerGridItemNode: GridItemNode {
|
|||
self.imageNode.contentMode = .scaleAspectFit
|
||||
Queue.concurrentDefaultQueue().async {
|
||||
let colors = mediaEditorGetGradientColors(from: image)
|
||||
let gradientImage = mediaEditorGenerateGradientImage(size: CGSize(width: 3.0, height: 128.0), colors: [colors.0, colors.1])
|
||||
let gradientImage = mediaEditorGenerateGradientImage(size: CGSize(width: 3.0, height: 128.0), colors: colors.array)
|
||||
Queue.mainQueue().async {
|
||||
self.backgroundNode.image = gradientImage
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1068,7 +1068,7 @@ final class MediaPickerSelectedListNode: ASDisplayNode, UIScrollViewDelegate, UI
|
|||
}
|
||||
|
||||
let inset: CGFloat = insets.left == 70 ? insets.left : 0.0
|
||||
self.wallpaperBackgroundNode.update(wallpaper: wallpaper)
|
||||
self.wallpaperBackgroundNode.update(wallpaper: wallpaper, animated: false)
|
||||
self.wallpaperBackgroundNode.updateBubbleTheme(bubbleTheme: theme, bubbleCorners: bubbleCorners)
|
||||
transition.updateFrame(node: self.wallpaperBackgroundNode, frame: CGRect(origin: CGPoint(x: inset, y: 0.0), size: CGSize(width: size.width - inset * 2.0, height: size.height)))
|
||||
self.wallpaperBackgroundNode.updateLayout(size: CGSize(width: size.width - inset * 2.0, height: size.height), displayMode: .aspectFill, transition: transition)
|
||||
|
|
|
|||
|
|
@ -171,6 +171,15 @@ public final class SelectablePeerNode: ASDisplayNode {
|
|||
)
|
||||
}
|
||||
|
||||
public func setupStoryRepost(accountPeerId: EnginePeer.Id, postbox: Postbox, network: Network, theme: PresentationTheme, strings: PresentationStrings, synchronousLoad: Bool) {
|
||||
self.peer = nil
|
||||
|
||||
self.textNode.maximumNumberOfLines = 2
|
||||
self.textNode.attributedText = NSAttributedString(string: strings.Share_RepostStory, font: textFont, textColor: self.theme.textColor, paragraphAlignment: .center)
|
||||
self.avatarNode.setPeer(accountPeerId: accountPeerId, postbox: postbox, network: network, contentSettings: ContentSettings.default, theme: theme, peer: nil, overrideImage: .repostIcon, emptyColor: self.theme.avatarPlaceholderColor, clipStyle: .round, synchronousLoad: synchronousLoad)
|
||||
|
||||
}
|
||||
|
||||
public func setup(accountPeerId: EnginePeer.Id, postbox: Postbox, network: Network, energyUsageSettings: EnergyUsageSettings, contentSettings: ContentSettings, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, resolveInlineStickers: @escaping ([Int64]) -> Signal<[Int64: TelegramMediaFile], NoError>, theme: PresentationTheme, strings: PresentationStrings, peer: EngineRenderedPeer, customTitle: String? = nil, iconId: Int64? = nil, iconColor: Int32? = nil, online: Bool = false, numberOfLines: Int = 2, synchronousLoad: Bool) {
|
||||
let isFirstTime = self.peer == nil
|
||||
self.peer = peer
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ private final class BubbleSettingsControllerNode: ASDisplayNode, UIScrollViewDel
|
|||
self.messagesContainerNode.clipsToBounds = true
|
||||
self.messagesContainerNode.transform = CATransform3DMakeScale(1.0, -1.0, 1.0)
|
||||
|
||||
self.chatBackgroundNode.update(wallpaper: self.presentationData.chatWallpaper)
|
||||
self.chatBackgroundNode.update(wallpaper: self.presentationData.chatWallpaper, animated: false)
|
||||
self.chatBackgroundNode.updateBubbleTheme(bubbleTheme: self.presentationData.theme, bubbleCorners: self.presentationData.chatBubbleCorners)
|
||||
|
||||
self.toolbarNode = BubbleSettingsToolbarNode(presentationThemeSettings: self.presentationThemeSettings, presentationData: self.presentationData)
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ class ForwardPrivacyChatPreviewItemNode: ListViewItemNode {
|
|||
return { item, params, neighbors in
|
||||
if currentBackgroundNode == nil {
|
||||
currentBackgroundNode = createWallpaperBackgroundNode(context: item.context, forChatDisplay: false)
|
||||
currentBackgroundNode?.update(wallpaper: item.wallpaper)
|
||||
currentBackgroundNode?.update(wallpaper: item.wallpaper, animated: false)
|
||||
currentBackgroundNode?.updateBubbleTheme(bubbleTheme: item.theme, bubbleCorners: item.chatBubbleCorners)
|
||||
}
|
||||
|
||||
|
|
@ -190,7 +190,7 @@ class ForwardPrivacyChatPreviewItemNode: ListViewItemNode {
|
|||
strongSelf.item = item
|
||||
|
||||
if let currentBackgroundNode {
|
||||
currentBackgroundNode.update(wallpaper: item.wallpaper)
|
||||
currentBackgroundNode.update(wallpaper: item.wallpaper, animated: false)
|
||||
currentBackgroundNode.updateBubbleTheme(bubbleTheme: item.theme, bubbleCorners: item.chatBubbleCorners)
|
||||
}
|
||||
|
||||
|
|
@ -268,7 +268,7 @@ class ForwardPrivacyChatPreviewItemNode: ListViewItemNode {
|
|||
|
||||
if let backgroundNode = strongSelf.backgroundNode {
|
||||
backgroundNode.frame = backgroundFrame.insetBy(dx: 0.0, dy: -100.0)
|
||||
backgroundNode.update(wallpaper: item.wallpaper)
|
||||
backgroundNode.update(wallpaper: item.wallpaper, animated: false)
|
||||
backgroundNode.updateBubbleTheme(bubbleTheme: item.theme, bubbleCorners: item.chatBubbleCorners)
|
||||
backgroundNode.updateLayout(size: backgroundNode.bounds.size, displayMode: displayMode, transition: .immediate)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -261,7 +261,7 @@ class ReactionChatPreviewItemNode: ListViewItemNode {
|
|||
return { item, params, neighbors in
|
||||
if currentBackgroundNode == nil {
|
||||
currentBackgroundNode = createWallpaperBackgroundNode(context: item.context, forChatDisplay: false)
|
||||
currentBackgroundNode?.update(wallpaper: item.wallpaper)
|
||||
currentBackgroundNode?.update(wallpaper: item.wallpaper, animated: false)
|
||||
currentBackgroundNode?.updateBubbleTheme(bubbleTheme: item.theme, bubbleCorners: item.chatBubbleCorners)
|
||||
}
|
||||
|
||||
|
|
@ -331,7 +331,7 @@ class ReactionChatPreviewItemNode: ListViewItemNode {
|
|||
strongSelf.item = item
|
||||
|
||||
if let currentBackgroundNode {
|
||||
currentBackgroundNode.update(wallpaper: item.wallpaper)
|
||||
currentBackgroundNode.update(wallpaper: item.wallpaper, animated: false)
|
||||
currentBackgroundNode.updateBubbleTheme(bubbleTheme: item.theme, bubbleCorners: item.chatBubbleCorners)
|
||||
}
|
||||
|
||||
|
|
@ -414,7 +414,7 @@ class ReactionChatPreviewItemNode: ListViewItemNode {
|
|||
|
||||
if let backgroundNode = strongSelf.backgroundNode {
|
||||
backgroundNode.frame = backgroundFrame.insetBy(dx: 0.0, dy: -100.0)
|
||||
backgroundNode.update(wallpaper: item.wallpaper)
|
||||
backgroundNode.update(wallpaper: item.wallpaper, animated: false)
|
||||
backgroundNode.updateBubbleTheme(bubbleTheme: item.theme, bubbleCorners: item.chatBubbleCorners)
|
||||
backgroundNode.updateLayout(size: backgroundNode.bounds.size, displayMode: displayMode, transition: .immediate)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, UIScrollView
|
|||
self.messagesContainerNode.clipsToBounds = true
|
||||
self.messagesContainerNode.transform = CATransform3DMakeScale(1.0, -1.0, 1.0)
|
||||
|
||||
self.chatBackgroundNode.update(wallpaper: self.presentationData.chatWallpaper)
|
||||
self.chatBackgroundNode.update(wallpaper: self.presentationData.chatWallpaper, animated: false)
|
||||
self.chatBackgroundNode.updateBubbleTheme(bubbleTheme: self.presentationData.theme, bubbleCorners: self.presentationData.chatBubbleCorners)
|
||||
|
||||
self.toolbarNode = TextSelectionToolbarNode(presentationThemeSettings: self.presentationThemeSettings, presentationData: self.presentationData)
|
||||
|
|
|
|||
|
|
@ -454,7 +454,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate
|
|||
}
|
||||
}
|
||||
|
||||
self.backgroundNode.update(wallpaper: self.wallpaper)
|
||||
self.backgroundNode.update(wallpaper: self.wallpaper, animated: false)
|
||||
self.backgroundNode.updateBubbleTheme(bubbleTheme: self.theme, bubbleCorners: self.presentationData.chatBubbleCorners)
|
||||
|
||||
self.stateDisposable = (self.statePromise.get()
|
||||
|
|
@ -551,7 +551,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate
|
|||
strongSelf.serviceBackgroundColor = serviceBackgroundColor
|
||||
strongSelf.serviceBackgroundColorPromise.set(.single(serviceBackgroundColor))
|
||||
|
||||
strongSelf.backgroundNode.update(wallpaper: wallpaper)
|
||||
strongSelf.backgroundNode.update(wallpaper: wallpaper, animated: false)
|
||||
strongSelf.backgroundNode.updateBubbleTheme(bubbleTheme: strongSelf.theme, bubbleCorners: strongSelf.presentationData.chatBubbleCorners)
|
||||
|
||||
strongSelf.ready.set(.single(true))
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
|||
self.instantChatBackgroundNode.displaysAsynchronously = false
|
||||
|
||||
self.ready.set(.single(true))
|
||||
self.instantChatBackgroundNode.update(wallpaper: wallpaper)
|
||||
self.instantChatBackgroundNode.update(wallpaper: wallpaper, animated: false)
|
||||
|
||||
self.instantChatBackgroundNode.view.contentMode = .scaleAspectFill
|
||||
|
||||
|
|
@ -207,7 +207,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
|||
self.chatContainerNode.insertSubnode(self.wallpaperNode, belowSubnode: self.messagesContainerNode)
|
||||
}
|
||||
|
||||
self.wallpaperNode.update(wallpaper: self.wallpaper)
|
||||
self.wallpaperNode.update(wallpaper: self.wallpaper, animated: false)
|
||||
self.wallpaperNode.updateBubbleTheme(bubbleTheme: self.previewTheme, bubbleCorners: self.presentationData.chatBubbleCorners)
|
||||
|
||||
self.remoteChatBackgroundNode.imageUpdated = { [weak self] image in
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ class ThemeSettingsChatPreviewItemNode: ListViewItemNode {
|
|||
if currentBackgroundNode == nil {
|
||||
currentBackgroundNode = createWallpaperBackgroundNode(context: item.context, forChatDisplay: false)
|
||||
}
|
||||
currentBackgroundNode?.update(wallpaper: item.wallpaper)
|
||||
currentBackgroundNode?.update(wallpaper: item.wallpaper, animated: false)
|
||||
currentBackgroundNode?.updateBubbleTheme(bubbleTheme: item.componentTheme, bubbleCorners: item.chatBubbleCorners)
|
||||
|
||||
let insets: UIEdgeInsets
|
||||
|
|
|
|||
|
|
@ -636,7 +636,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode {
|
|||
switch entry {
|
||||
case let .wallpaper(wallpaper, _):
|
||||
Queue.mainQueue().justDispatch {
|
||||
self.nativeNode.update(wallpaper: wallpaper)
|
||||
self.nativeNode.update(wallpaper: wallpaper, animated: false)
|
||||
}
|
||||
|
||||
if case let .file(file) = wallpaper, file.isPattern {
|
||||
|
|
@ -651,7 +651,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode {
|
|||
isColor = true
|
||||
} else if case let .gradient(gradient) = wallpaper {
|
||||
self.nativeNode.isHidden = false
|
||||
self.nativeNode.update(wallpaper: wallpaper)
|
||||
self.nativeNode.update(wallpaper: wallpaper, animated: false)
|
||||
self.patternButtonNode.isSelected = false
|
||||
|
||||
if gradient.colors.count >= 3 {
|
||||
|
|
@ -662,7 +662,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode {
|
|||
isColor = true
|
||||
} else if case .color = wallpaper {
|
||||
self.nativeNode.isHidden = false
|
||||
self.nativeNode.update(wallpaper: wallpaper)
|
||||
self.nativeNode.update(wallpaper: wallpaper, animated: false)
|
||||
self.patternButtonNode.isSelected = false
|
||||
isColor = true
|
||||
} else {
|
||||
|
|
@ -989,7 +989,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode {
|
|||
strongSelf.context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data, synchronous: true)
|
||||
|
||||
let wallpaper: TelegramWallpaper = .image([TelegramMediaImageRepresentation(dimensions: PixelDimensions(image.size), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)], WallpaperSettings())
|
||||
strongSelf.nativeNode.update(wallpaper: wallpaper)
|
||||
strongSelf.nativeNode.update(wallpaper: wallpaper, animated: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -465,6 +465,8 @@ public final class ShareController: ViewController {
|
|||
}
|
||||
|
||||
public var openShareAsImage: (([Message]) -> Void)?
|
||||
|
||||
public var shareStory: (() -> Void)?
|
||||
|
||||
public var debugAction: (() -> Void)?
|
||||
|
||||
|
|
@ -700,7 +702,7 @@ public final class ShareController: ViewController {
|
|||
return
|
||||
}
|
||||
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: title, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
}, externalShare: self.externalShare, immediateExternalShare: self.immediateExternalShare, immediatePeerId: self.immediatePeerId, fromForeignApp: self.fromForeignApp, forceTheme: self.forceTheme, fromPublicChannel: fromPublicChannel, segmentedValues: self.segmentedValues)
|
||||
}, externalShare: self.externalShare, immediateExternalShare: self.immediateExternalShare, immediatePeerId: self.immediatePeerId, fromForeignApp: self.fromForeignApp, forceTheme: self.forceTheme, fromPublicChannel: fromPublicChannel, segmentedValues: self.segmentedValues, shareStory: self.shareStory)
|
||||
self.controllerNode.completed = self.completed
|
||||
self.controllerNode.present = { [weak self] c in
|
||||
self?.presentInGlobalOverlay(c)
|
||||
|
|
@ -2471,134 +2473,6 @@ public final class ShareController: ViewController {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
final class MessageStoryRenderer {
|
||||
private let context: AccountContext
|
||||
private let presentationData: PresentationData
|
||||
private let messages: [Message]
|
||||
|
||||
let containerNode: ASDisplayNode
|
||||
private let instantChatBackgroundNode: WallpaperBackgroundNode
|
||||
private let messagesContainerNode: ASDisplayNode
|
||||
private var dateHeaderNode: ListViewItemHeaderNode?
|
||||
private var messageNodes: [ListViewItemNode]?
|
||||
private let addressNode: ImmediateTextNode
|
||||
|
||||
init(context: AccountContext, messages: [Message]) {
|
||||
self.context = context
|
||||
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
self.messages = messages
|
||||
|
||||
self.containerNode = ASDisplayNode()
|
||||
|
||||
self.instantChatBackgroundNode = createWallpaperBackgroundNode(context: context, forChatDisplay: false)
|
||||
self.instantChatBackgroundNode.displaysAsynchronously = false
|
||||
|
||||
self.messagesContainerNode = ASDisplayNode()
|
||||
self.messagesContainerNode.clipsToBounds = true
|
||||
self.messagesContainerNode.transform = CATransform3DMakeScale(1.0, -1.0, 1.0)
|
||||
|
||||
let message = messages.first!
|
||||
let addressName = message.peers[message.id.peerId]?.addressName ?? ""
|
||||
|
||||
self.addressNode = ImmediateTextNode()
|
||||
self.addressNode.displaysAsynchronously = false
|
||||
self.addressNode.attributedText = NSAttributedString(string: "t.me/\(addressName)/\(message.id.id)", font: Font.medium(14.0), textColor: UIColor(rgb: 0xffffff))
|
||||
self.addressNode.textShadowColor = UIColor(rgb: 0x929292, alpha: 0.8)
|
||||
|
||||
self.containerNode.addSubnode(self.instantChatBackgroundNode)
|
||||
self.containerNode.addSubnode(self.messagesContainerNode)
|
||||
self.containerNode.addSubnode(self.addressNode)
|
||||
}
|
||||
|
||||
func update(layout: ContainerViewLayout, completion: @escaping (UIImage?) -> Void) {
|
||||
self.updateMessagesLayout(layout: layout)
|
||||
|
||||
Queue.mainQueue().after(0.01) {
|
||||
UIGraphicsBeginImageContextWithOptions(layout.size, false, 3.0)
|
||||
self.containerNode.view.drawHierarchy(in: CGRect(origin: CGPoint(), size: layout.size), afterScreenUpdates: true)
|
||||
let img = UIGraphicsGetImageFromCurrentImageContext()
|
||||
UIGraphicsEndImageContext()
|
||||
completion(img)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateMessagesLayout(layout: ContainerViewLayout) {
|
||||
let size = layout.size
|
||||
self.containerNode.frame = CGRect(origin: CGPoint(), size: layout.size)
|
||||
self.instantChatBackgroundNode.frame = CGRect(origin: CGPoint(), size: layout.size)
|
||||
self.instantChatBackgroundNode.updateLayout(size: size, displayMode: .aspectFill, transition: .immediate)
|
||||
self.messagesContainerNode.frame = CGRect(origin: CGPoint(), size: layout.size)
|
||||
|
||||
let addressLayout = self.addressNode.updateLayout(size)
|
||||
|
||||
let theme = self.presentationData.theme.withUpdated(preview: true)
|
||||
let headerItem = self.context.sharedContext.makeChatMessageDateHeaderItem(context: self.context, timestamp: self.messages.first?.timestamp ?? 0, theme: theme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder)
|
||||
|
||||
let items: [ListViewItem] = [self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: self.messages, theme: theme, strings: self.presentationData.strings, wallpaper: self.presentationData.theme.chat.defaultWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: nil, availableReactions: nil, isCentered: false)]
|
||||
|
||||
let inset: CGFloat = 16.0
|
||||
let width = layout.size.width - inset * 2.0
|
||||
let params = ListViewItemLayoutParams(width: width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, availableHeight: layout.size.height)
|
||||
if let messageNodes = self.messageNodes {
|
||||
for i in 0 ..< items.count {
|
||||
let itemNode = messageNodes[i]
|
||||
items[i].updateNode(async: { $0() }, node: {
|
||||
return itemNode
|
||||
}, params: params, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], animation: .None, completion: { (layout, apply) in
|
||||
let nodeFrame = CGRect(origin: CGPoint(x: 0.0, y: floor((size.height - layout.size.height) / 2.0)), size: CGSize(width: width, height: layout.size.height))
|
||||
|
||||
itemNode.contentSize = layout.contentSize
|
||||
itemNode.insets = layout.insets
|
||||
itemNode.frame = nodeFrame
|
||||
itemNode.isUserInteractionEnabled = false
|
||||
|
||||
apply(ListViewItemApply(isOnScreen: true))
|
||||
})
|
||||
}
|
||||
} else {
|
||||
var messageNodes: [ListViewItemNode] = []
|
||||
for i in 0 ..< items.count {
|
||||
var itemNode: ListViewItemNode?
|
||||
items[i].nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: true, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], completion: { node, apply in
|
||||
itemNode = node
|
||||
apply().1(ListViewItemApply(isOnScreen: true))
|
||||
})
|
||||
itemNode!.subnodeTransform = CATransform3DMakeScale(-1.0, 1.0, 1.0)
|
||||
itemNode!.isUserInteractionEnabled = false
|
||||
messageNodes.append(itemNode!)
|
||||
self.messagesContainerNode.addSubnode(itemNode!)
|
||||
}
|
||||
self.messageNodes = messageNodes
|
||||
}
|
||||
|
||||
var bottomOffset: CGFloat = 0.0
|
||||
if let messageNodes = self.messageNodes {
|
||||
for itemNode in messageNodes {
|
||||
itemNode.frame = CGRect(origin: CGPoint(x: inset, y: floor((size.height - itemNode.frame.height) / 2.0)), size: itemNode.frame.size)
|
||||
bottomOffset += itemNode.frame.maxY
|
||||
itemNode.updateFrame(itemNode.frame, within: layout.size)
|
||||
}
|
||||
}
|
||||
|
||||
self.addressNode.frame = CGRect(origin: CGPoint(x: inset + 16.0, y: bottomOffset + 3.0), size: CGSize(width: addressLayout.width, height: addressLayout.height + 3.0))
|
||||
|
||||
let dateHeaderNode: ListViewItemHeaderNode
|
||||
if let currentDateHeaderNode = self.dateHeaderNode {
|
||||
dateHeaderNode = currentDateHeaderNode
|
||||
headerItem.updateNode(dateHeaderNode, previous: nil, next: headerItem)
|
||||
} else {
|
||||
dateHeaderNode = headerItem.node(synchronousLoad: true)
|
||||
dateHeaderNode.subnodeTransform = CATransform3DMakeScale(-1.0, 1.0, 1.0)
|
||||
self.messagesContainerNode.addSubnode(dateHeaderNode)
|
||||
self.dateHeaderNode = dateHeaderNode
|
||||
}
|
||||
|
||||
dateHeaderNode.frame = CGRect(origin: CGPoint(x: 0.0, y: bottomOffset), size: CGSize(width: layout.size.width, height: headerItem.height))
|
||||
dateHeaderNode.updateLayout(size: self.containerNode.frame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right)
|
||||
}
|
||||
}
|
||||
|
||||
public class ShareToInstagramActivity: UIActivity {
|
||||
private let context: AccountContext
|
||||
private var activityItems = [Any]()
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
|
|||
|
||||
private let showNames = ValuePromise<Bool>(true)
|
||||
|
||||
init(environment: ShareControllerEnvironment, presentationData: PresentationData, presetText: String?, defaultAction: ShareControllerAction?, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void, presentError: @escaping (String?, String) -> Void, externalShare: Bool, immediateExternalShare: Bool, immediatePeerId: PeerId?, fromForeignApp: Bool, forceTheme: PresentationTheme?, fromPublicChannel: Bool, segmentedValues: [ShareControllerSegmentedValue]?) {
|
||||
init(environment: ShareControllerEnvironment, presentationData: PresentationData, presetText: String?, defaultAction: ShareControllerAction?, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void, presentError: @escaping (String?, String) -> Void, externalShare: Bool, immediateExternalShare: Bool, immediatePeerId: PeerId?, fromForeignApp: Bool, forceTheme: PresentationTheme?, fromPublicChannel: Bool, segmentedValues: [ShareControllerSegmentedValue]?, shareStory: (() -> Void)?) {
|
||||
self.environment = environment
|
||||
self.presentationData = presentationData
|
||||
self.forceTheme = forceTheme
|
||||
|
|
@ -318,6 +318,13 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
|
|||
strongSelf.closePeerTopics(peer.peerId, selected: true)
|
||||
})
|
||||
}
|
||||
}, shareStory: shareStory.flatMap { shareStory in
|
||||
return { [weak self] in
|
||||
self?.animateOut(shared: false, completion: { [weak self] in
|
||||
self?.dismiss?(false)
|
||||
})
|
||||
shareStory()
|
||||
}
|
||||
})
|
||||
|
||||
self.backgroundColor = nil
|
||||
|
|
|
|||
|
|
@ -20,10 +20,12 @@ final class ShareControllerInteraction {
|
|||
|
||||
let togglePeer: (EngineRenderedPeer, Bool) -> Void
|
||||
let selectTopic: (EngineRenderedPeer, Int64, MessageHistoryThreadData) -> Void
|
||||
|
||||
init(togglePeer: @escaping (EngineRenderedPeer, Bool) -> Void, selectTopic: @escaping (EngineRenderedPeer, Int64, MessageHistoryThreadData) -> Void) {
|
||||
let shareStory: (() -> Void)?
|
||||
|
||||
init(togglePeer: @escaping (EngineRenderedPeer, Bool) -> Void, selectTopic: @escaping (EngineRenderedPeer, Int64, MessageHistoryThreadData) -> Void, shareStory: (() -> Void)?) {
|
||||
self.togglePeer = togglePeer
|
||||
self.selectTopic = selectTopic
|
||||
self.shareStory = shareStory
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -91,28 +93,35 @@ final class ShareControllerGridSectionNode: ASDisplayNode {
|
|||
}
|
||||
|
||||
final class ShareControllerPeerGridItem: GridItem {
|
||||
enum ShareItem: Equatable {
|
||||
case peer(peer: EngineRenderedPeer, presence: EnginePeer.Presence?, topicId: Int64?, threadData: MessageHistoryThreadData?)
|
||||
case story
|
||||
|
||||
var peerId: EnginePeer.Id? {
|
||||
if case let .peer(peer, _, _, _) = self {
|
||||
return peer.peerId
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let environment: ShareControllerEnvironment
|
||||
let context: ShareControllerAccountContext
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let peer: EngineRenderedPeer?
|
||||
let presence: EnginePeer.Presence?
|
||||
let topicId: Int64?
|
||||
let threadData: MessageHistoryThreadData?
|
||||
let item: ShareItem?
|
||||
let controllerInteraction: ShareControllerInteraction
|
||||
let search: Bool
|
||||
|
||||
let section: GridSection?
|
||||
|
||||
init(environment: ShareControllerEnvironment, context: ShareControllerAccountContext, theme: PresentationTheme, strings: PresentationStrings, peer: EngineRenderedPeer?, presence: EnginePeer.Presence?, topicId: Int64?, threadData: MessageHistoryThreadData?, controllerInteraction: ShareControllerInteraction, sectionTitle: String? = nil, search: Bool = false) {
|
||||
init(environment: ShareControllerEnvironment, context: ShareControllerAccountContext, theme: PresentationTheme, strings: PresentationStrings, item: ShareItem?, controllerInteraction: ShareControllerInteraction, sectionTitle: String? = nil, search: Bool = false) {
|
||||
self.environment = environment
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.peer = peer
|
||||
self.presence = presence
|
||||
self.topicId = topicId
|
||||
self.threadData = threadData
|
||||
self.item = item
|
||||
self.controllerInteraction = controllerInteraction
|
||||
self.search = search
|
||||
|
||||
|
|
@ -126,7 +135,7 @@ final class ShareControllerPeerGridItem: GridItem {
|
|||
func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode {
|
||||
let node = ShareControllerPeerGridItemNode()
|
||||
node.controllerInteraction = self.controllerInteraction
|
||||
node.setup(environment: self.environment, context: self.context, theme: self.theme, strings: self.strings, peer: self.peer, presence: self.presence, topicId: self.topicId, threadData: self.threadData, search: self.search, synchronousLoad: synchronousLoad, force: false)
|
||||
node.setup(environment: self.environment, context: self.context, theme: self.theme, strings: self.strings, item: self.item, search: self.search, synchronousLoad: synchronousLoad, force: false)
|
||||
return node
|
||||
}
|
||||
|
||||
|
|
@ -136,12 +145,12 @@ final class ShareControllerPeerGridItem: GridItem {
|
|||
return
|
||||
}
|
||||
node.controllerInteraction = self.controllerInteraction
|
||||
node.setup(environment: self.environment, context: self.context, theme: self.theme, strings: self.strings, peer: self.peer, presence: self.presence, topicId: self.topicId, threadData: self.threadData, search: self.search, synchronousLoad: false, force: false)
|
||||
node.setup(environment: self.environment, context: self.context, theme: self.theme, strings: self.strings, item: self.item, search: self.search, synchronousLoad: false, force: false)
|
||||
}
|
||||
}
|
||||
|
||||
final class ShareControllerPeerGridItemNode: GridItemNode {
|
||||
private var currentState: (ShareControllerEnvironment, ShareControllerAccountContext, PresentationTheme, PresentationStrings, EngineRenderedPeer?, Bool, EnginePeer.Presence?, Int64?, MessageHistoryThreadData?)?
|
||||
private var currentState: (environment: ShareControllerEnvironment, accountContext: ShareControllerAccountContext, theme: PresentationTheme, strings: PresentationStrings, item: ShareControllerPeerGridItem.ShareItem?, search: Bool)?
|
||||
private let peerNode: SelectablePeerNode
|
||||
private var presenceManager: PeerPresenceStatusManager?
|
||||
|
||||
|
|
@ -151,7 +160,11 @@ final class ShareControllerPeerGridItemNode: GridItemNode {
|
|||
private var absoluteLocation: (CGRect, CGSize)?
|
||||
|
||||
var peerId: EnginePeer.Id? {
|
||||
return self.currentState?.4?.peerId
|
||||
if let item = self.currentState?.item, case let .peer(peer, _, _, _) = item {
|
||||
return peer.peerId
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
override init() {
|
||||
|
|
@ -161,9 +174,11 @@ final class ShareControllerPeerGridItemNode: GridItemNode {
|
|||
|
||||
self.peerNode.toggleSelection = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
if let (_, _, _, _, maybePeer, search, _, _, _) = strongSelf.currentState, let peer = maybePeer {
|
||||
if let _ = peer.peers[peer.peerId] {
|
||||
if let (_, _, _, _, maybeItem, search) = strongSelf.currentState, let item = maybeItem {
|
||||
if case let .peer(peer, _, _, _) = item, let _ = peer.peers[peer.peerId] {
|
||||
strongSelf.controllerInteraction?.togglePeer(peer, search)
|
||||
} else if case .story = item {
|
||||
strongSelf.controllerInteraction?.shareStory?()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -173,7 +188,7 @@ final class ShareControllerPeerGridItemNode: GridItemNode {
|
|||
guard let strongSelf = self, let currentState = strongSelf.currentState else {
|
||||
return
|
||||
}
|
||||
strongSelf.setup(environment: currentState.0, context: currentState.1, theme: currentState.2, strings: currentState.3, peer: currentState.4, presence: currentState.6, topicId: currentState.7, threadData: currentState.8, search: currentState.5, synchronousLoad: false, force: true)
|
||||
strongSelf.setup(environment: currentState.0, context: currentState.1, theme: currentState.2, strings: currentState.3, item: currentState.4, search: currentState.5, synchronousLoad: false, force: true)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -185,21 +200,27 @@ final class ShareControllerPeerGridItemNode: GridItemNode {
|
|||
}
|
||||
}
|
||||
|
||||
func setup(environment: ShareControllerEnvironment, context: ShareControllerAccountContext, theme: PresentationTheme, strings: PresentationStrings, peer: EngineRenderedPeer?, presence: EnginePeer.Presence?, topicId: Int64?, threadData: MessageHistoryThreadData?, search: Bool, synchronousLoad: Bool, force: Bool) {
|
||||
if force || self.currentState == nil || self.currentState!.1 !== context || self.currentState!.3 !== theme || self.currentState!.4 != peer || self.currentState!.6 != presence || self.currentState!.7 != topicId {
|
||||
func setup(environment: ShareControllerEnvironment, context: ShareControllerAccountContext, theme: PresentationTheme, strings: PresentationStrings, item: ShareControllerPeerGridItem.ShareItem?, search: Bool, synchronousLoad: Bool, force: Bool) {
|
||||
if force || self.currentState == nil || self.currentState!.1 !== context || self.currentState!.3 !== theme || self.currentState!.item != item {
|
||||
let itemTheme = SelectablePeerNodeTheme(textColor: theme.actionSheet.primaryTextColor, secretTextColor: theme.chatList.secretTitleColor, selectedTextColor: theme.actionSheet.controlAccentColor, checkBackgroundColor: theme.actionSheet.opaqueItemBackgroundColor, checkFillColor: theme.actionSheet.controlAccentColor, checkColor: theme.actionSheet.checkContentColor, avatarPlaceholderColor: theme.list.mediaPlaceholderColor)
|
||||
|
||||
var effectivePresence: EnginePeer.Presence?
|
||||
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
|
||||
var online = false
|
||||
if case let .user(peer) = peer?.peer, let presence = presence, !isServicePeer(peer) && !peer.flags.contains(.isSupport) && peer.id != context.accountPeerId {
|
||||
let relativeStatus = relativeUserPresenceStatus(presence, relativeTo: timestamp)
|
||||
if case .online = relativeStatus {
|
||||
online = true
|
||||
}
|
||||
}
|
||||
|
||||
self.peerNode.theme = itemTheme
|
||||
if let peer = peer {
|
||||
if let item, case let .peer(renderedPeer, presence, _, threadData) = item, let peer = renderedPeer.peer {
|
||||
effectivePresence = presence
|
||||
var isOnline = false
|
||||
var isSupport = false
|
||||
if case let .user(user) = peer, user.flags.contains(.isSupport) {
|
||||
isSupport = true
|
||||
}
|
||||
if let presence, !peer.isService && !isSupport && peer.id != context.accountPeerId {
|
||||
let relativeStatus = relativeUserPresenceStatus(presence, relativeTo: timestamp)
|
||||
if case .online = relativeStatus {
|
||||
isOnline = true
|
||||
}
|
||||
}
|
||||
|
||||
let resolveInlineStickers = context.resolveInlineStickers
|
||||
self.peerNode.setup(
|
||||
accountPeerId: context.accountPeerId,
|
||||
|
|
@ -214,17 +235,26 @@ final class ShareControllerPeerGridItemNode: GridItemNode {
|
|||
},
|
||||
theme: theme,
|
||||
strings: strings,
|
||||
peer: peer,
|
||||
peer: renderedPeer,
|
||||
customTitle: threadData?.info.title,
|
||||
iconId: threadData?.info.icon,
|
||||
iconColor: threadData?.info.iconColor ?? 0,
|
||||
online: online,
|
||||
online: isOnline,
|
||||
synchronousLoad: synchronousLoad
|
||||
)
|
||||
if let shimmerNode = self.placeholderNode {
|
||||
self.placeholderNode = nil
|
||||
shimmerNode.removeFromSupernode()
|
||||
}
|
||||
} else if let item, case .story = item {
|
||||
self.peerNode.setupStoryRepost(
|
||||
accountPeerId: context.accountPeerId,
|
||||
postbox: context.stateManager.postbox,
|
||||
network: context.stateManager.network,
|
||||
theme: theme,
|
||||
strings: strings,
|
||||
synchronousLoad: synchronousLoad
|
||||
)
|
||||
} else {
|
||||
let shimmerNode: ShimmerEffectNode
|
||||
if let current = self.placeholderNode {
|
||||
|
|
@ -252,10 +282,10 @@ final class ShareControllerPeerGridItemNode: GridItemNode {
|
|||
|
||||
shimmerNode.update(backgroundColor: theme.list.itemBlocksBackgroundColor, foregroundColor: theme.list.mediaPlaceholderColor, shimmeringColor: theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: shapes, horizontal: true, size: self.bounds.size)
|
||||
}
|
||||
self.currentState = (environment, context, theme, strings, peer, search, presence, topicId, threadData)
|
||||
self.currentState = (environment, context, theme, strings, item, search)
|
||||
self.setNeedsLayout()
|
||||
if let presence = presence {
|
||||
self.presenceManager?.reset(presence: presence)
|
||||
if let effectivePresence {
|
||||
self.presenceManager?.reset(presence: effectivePresence)
|
||||
}
|
||||
}
|
||||
self.updateSelection(animated: false)
|
||||
|
|
@ -263,8 +293,10 @@ final class ShareControllerPeerGridItemNode: GridItemNode {
|
|||
|
||||
func updateSelection(animated: Bool) {
|
||||
var selected = false
|
||||
if let controllerInteraction = self.controllerInteraction, let (_, _, _, _, maybePeer, _, _, _, _) = self.currentState, let peer = maybePeer {
|
||||
selected = controllerInteraction.selectedPeerIds.contains(peer.peerId)
|
||||
if let controllerInteraction = self.controllerInteraction, let (_, _, _, _, maybeItem, _) = self.currentState, let item = maybeItem {
|
||||
if case let .peer(peer, _, _, _) = item {
|
||||
selected = controllerInteraction.selectedPeerIds.contains(peer.peerId)
|
||||
}
|
||||
}
|
||||
|
||||
self.peerNode.updateSelection(selected: selected, animated: animated)
|
||||
|
|
@ -277,7 +309,7 @@ final class ShareControllerPeerGridItemNode: GridItemNode {
|
|||
self.peerNode.frame = bounds
|
||||
self.placeholderNode?.frame = bounds
|
||||
|
||||
if let (_, _, theme, _, _, _, _, _, _) = self.currentState, let shimmerNode = self.placeholderNode {
|
||||
if let theme = self.currentState?.theme, let shimmerNode = self.placeholderNode {
|
||||
var shapes: [ShimmerEffectNode.Shape] = []
|
||||
|
||||
let titleLineWidth: CGFloat = 56.0
|
||||
|
|
|
|||
|
|
@ -37,31 +37,24 @@ extension CGPoint {
|
|||
|
||||
private struct SharePeerEntry: Comparable, Identifiable {
|
||||
let index: Int32
|
||||
let peer: EngineRenderedPeer
|
||||
let presence: EnginePeer.Presence?
|
||||
let threadId: Int64?
|
||||
let threadData: MessageHistoryThreadData?
|
||||
let item: ShareControllerPeerGridItem.ShareItem
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
|
||||
var stableId: Int64 {
|
||||
return self.peer.peerId.toInt64()
|
||||
switch self.item {
|
||||
case let .peer(peer, _, _, _):
|
||||
return peer.peerId.toInt64()
|
||||
case .story:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
static func ==(lhs: SharePeerEntry, rhs: SharePeerEntry) -> Bool {
|
||||
if lhs.index != rhs.index {
|
||||
return false
|
||||
}
|
||||
if lhs.peer != rhs.peer {
|
||||
return false
|
||||
}
|
||||
if lhs.presence != rhs.presence {
|
||||
return false
|
||||
}
|
||||
if lhs.threadId != rhs.threadId {
|
||||
return false
|
||||
}
|
||||
if lhs.threadData != rhs.threadData {
|
||||
if lhs.item != rhs.item {
|
||||
return false
|
||||
}
|
||||
if lhs.theme !== rhs.theme {
|
||||
|
|
@ -76,7 +69,7 @@ private struct SharePeerEntry: Comparable, Identifiable {
|
|||
}
|
||||
|
||||
func item(environment: ShareControllerEnvironment, context: ShareControllerAccountContext, interfaceInteraction: ShareControllerInteraction) -> GridItem {
|
||||
return ShareControllerPeerGridItem(environment: environment, context: context, theme: self.theme, strings: self.strings, peer: self.peer, presence: self.presence, topicId: self.threadId, threadData: self.threadData, controllerInteraction: interfaceInteraction, search: false)
|
||||
return ShareControllerPeerGridItem(environment: environment, context: context, theme: self.theme, strings: self.strings, item: self.item, controllerInteraction: interfaceInteraction, search: false)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -169,19 +162,26 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
|
|||
|
||||
self.peersValue.set(.single(peers))
|
||||
|
||||
let canShareStory = controllerInteraction.shareStory != nil
|
||||
|
||||
let items: Signal<[SharePeerEntry], NoError> = combineLatest(self.peersValue.get(), self.foundPeers.get(), self.tick.get(), self.themePromise.get())
|
||||
|> map { [weak controllerInteraction] initialPeers, foundPeers, _, theme -> [SharePeerEntry] in
|
||||
var entries: [SharePeerEntry] = []
|
||||
var index: Int32 = 0
|
||||
|
||||
if canShareStory {
|
||||
entries.append(SharePeerEntry(index: index, item: .story, theme: theme, strings: strings))
|
||||
index += 1
|
||||
}
|
||||
|
||||
var existingPeerIds: Set<EnginePeer.Id> = Set()
|
||||
entries.append(SharePeerEntry(index: index, peer: EngineRenderedPeer(peer: accountPeer), presence: nil, threadId: nil, threadData: nil, theme: theme, strings: strings))
|
||||
entries.append(SharePeerEntry(index: index, item: .peer(peer: EngineRenderedPeer(peer: accountPeer), presence: nil, topicId: nil, threadData: nil), theme: theme, strings: strings))
|
||||
existingPeerIds.insert(accountPeer.id)
|
||||
index += 1
|
||||
|
||||
for peer in foundPeers.reversed() {
|
||||
if !existingPeerIds.contains(peer.peerId) {
|
||||
entries.append(SharePeerEntry(index: index, peer: peer, presence: nil, threadId: nil, threadData: nil, theme: theme, strings: strings))
|
||||
entries.append(SharePeerEntry(index: index, item: .peer(peer: peer, presence: nil, topicId: nil, threadData: nil), theme: theme, strings: strings))
|
||||
existingPeerIds.insert(peer.peerId)
|
||||
index += 1
|
||||
}
|
||||
|
|
@ -190,7 +190,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
|
|||
for (peer, presence) in initialPeers {
|
||||
if !existingPeerIds.contains(peer.peerId) {
|
||||
let thread = controllerInteraction?.selectedTopics[peer.peerId]
|
||||
entries.append(SharePeerEntry(index: index, peer: peer, presence: presence, threadId: thread?.0, threadData: thread?.1, theme: theme, strings: strings))
|
||||
entries.append(SharePeerEntry(index: index, item: .peer(peer: peer, presence: presence, topicId: thread?.0, threadData: thread?.1), theme: theme, strings: strings))
|
||||
existingPeerIds.insert(peer.peerId)
|
||||
index += 1
|
||||
}
|
||||
|
|
@ -568,7 +568,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
|
|||
var scrollToItem: GridNodeScrollToItem?
|
||||
if let ensurePeerVisibleOnLayout = self.ensurePeerVisibleOnLayout {
|
||||
self.ensurePeerVisibleOnLayout = nil
|
||||
if let index = self.entries.firstIndex(where: { $0.peer.peerId == ensurePeerVisibleOnLayout }) {
|
||||
if let index = self.entries.firstIndex(where: { $0.item.peerId == ensurePeerVisibleOnLayout }) {
|
||||
scrollToItem = GridNodeScrollToItem(index: index, position: .visible, transition: transition, directionHint: .up, adjustForSection: false)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,15 +85,15 @@ private enum ShareSearchRecentEntry: Comparable, Identifiable {
|
|||
|
||||
func item(environment: ShareControllerEnvironment, context: ShareControllerAccountContext, interfaceInteraction: ShareControllerInteraction) -> GridItem {
|
||||
switch self {
|
||||
case let .topPeers(theme, strings):
|
||||
return ShareControllerRecentPeersGridItem(environment: environment, context: context, theme: theme, strings: strings, controllerInteraction: interfaceInteraction)
|
||||
case let .peer(_, theme, peer, associatedPeer, presence, strings):
|
||||
var peers: [EnginePeer.Id: EnginePeer] = [peer.id: peer]
|
||||
if let associatedPeer = associatedPeer {
|
||||
peers[associatedPeer.id] = associatedPeer
|
||||
}
|
||||
let peer = EngineRenderedPeer(peerId: peer.id, peers: peers, associatedMedia: [:])
|
||||
return ShareControllerPeerGridItem(environment: environment, context: context, theme: theme, strings: strings, peer: peer, presence: presence, topicId: nil, threadData: nil, controllerInteraction: interfaceInteraction, sectionTitle: strings.DialogList_SearchSectionRecent, search: true)
|
||||
case let .topPeers(theme, strings):
|
||||
return ShareControllerRecentPeersGridItem(environment: environment, context: context, theme: theme, strings: strings, controllerInteraction: interfaceInteraction)
|
||||
case let .peer(_, theme, peer, associatedPeer, presence, strings):
|
||||
var peers: [EnginePeer.Id: EnginePeer] = [peer.id: peer]
|
||||
if let associatedPeer = associatedPeer {
|
||||
peers[associatedPeer.id] = associatedPeer
|
||||
}
|
||||
let peer = EngineRenderedPeer(peerId: peer.id, peers: peers, associatedMedia: [:])
|
||||
return ShareControllerPeerGridItem(environment: environment, context: context, theme: theme, strings: strings, item: .peer(peer: peer, presence: presence, topicId: nil, threadData: nil), controllerInteraction: interfaceInteraction, sectionTitle: strings.DialogList_SearchSectionRecent, search: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -131,7 +131,9 @@ private struct ShareSearchPeerEntry: Comparable, Identifiable {
|
|||
}
|
||||
|
||||
func item(environment: ShareControllerEnvironment, context: ShareControllerAccountContext, interfaceInteraction: ShareControllerInteraction) -> GridItem {
|
||||
return ShareControllerPeerGridItem(environment: environment, context: context, theme: self.theme, strings: self.strings, peer: self.peer, presence: self.presence, topicId: nil, threadData: nil, controllerInteraction: interfaceInteraction, search: true)
|
||||
// let item: ShareControllerPeerGridItem.ShareItem
|
||||
// item = self.peer.flatMap { .peer(peer: $0, presence: self.presence, topicId: nil, threadData: nil) }
|
||||
return ShareControllerPeerGridItem(environment: environment, context: context, theme: self.theme, strings: self.strings, item: self.peer.flatMap({ .peer(peer: $0, presence: self.presence, topicId: nil, threadData: nil) }), controllerInteraction: interfaceInteraction, search: true)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ swift_library(
|
|||
"//submodules/InviteLinksUI:InviteLinksUI",
|
||||
"//submodules/ShareController:ShareController",
|
||||
"//submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent",
|
||||
"//submodules/TelegramUI/Components/Stories/StoryContainerScreen",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import UndoUI
|
|||
import ShareController
|
||||
import ItemListPeerActionItem
|
||||
import PremiumUI
|
||||
import StoryContainerScreen
|
||||
|
||||
private let initialBoostersDisplayedLimit: Int32 = 5
|
||||
|
||||
|
|
@ -28,6 +29,7 @@ private final class ChannelStatsControllerArguments {
|
|||
let context: AccountContext
|
||||
let loadDetailedGraph: (StatsGraph, Int64) -> Signal<StatsGraph?, NoError>
|
||||
let openPostStats: (EnginePeer, StatsPostItem) -> Void
|
||||
let openStory: (EngineStoryItem, UIView) -> Void
|
||||
let contextAction: (MessageId, ASDisplayNode, ContextGesture?) -> Void
|
||||
let copyBoostLink: (String) -> Void
|
||||
let shareBoostLink: (String) -> Void
|
||||
|
|
@ -37,10 +39,11 @@ private final class ChannelStatsControllerArguments {
|
|||
let createPrepaidGiveaway: (PrepaidGiveaway) -> Void
|
||||
let updateGiftsSelected: (Bool) -> Void
|
||||
|
||||
init(context: AccountContext, loadDetailedGraph: @escaping (StatsGraph, Int64) -> Signal<StatsGraph?, NoError>, openPostStats: @escaping (EnginePeer, StatsPostItem) -> Void, contextAction: @escaping (MessageId, ASDisplayNode, ContextGesture?) -> Void, copyBoostLink: @escaping (String) -> Void, shareBoostLink: @escaping (String) -> Void, openBoost: @escaping (ChannelBoostersContext.State.Boost) -> Void, expandBoosters: @escaping () -> Void, openGifts: @escaping () -> Void, createPrepaidGiveaway: @escaping (PrepaidGiveaway) -> Void, updateGiftsSelected: @escaping (Bool) -> Void) {
|
||||
init(context: AccountContext, loadDetailedGraph: @escaping (StatsGraph, Int64) -> Signal<StatsGraph?, NoError>, openPostStats: @escaping (EnginePeer, StatsPostItem) -> Void, openStory: @escaping (EngineStoryItem, UIView) -> Void, contextAction: @escaping (MessageId, ASDisplayNode, ContextGesture?) -> Void, copyBoostLink: @escaping (String) -> Void, shareBoostLink: @escaping (String) -> Void, openBoost: @escaping (ChannelBoostersContext.State.Boost) -> Void, expandBoosters: @escaping () -> Void, openGifts: @escaping () -> Void, createPrepaidGiveaway: @escaping (PrepaidGiveaway) -> Void, updateGiftsSelected: @escaping (Bool) -> Void) {
|
||||
self.context = context
|
||||
self.loadDetailedGraph = loadDetailedGraph
|
||||
self.openPostStats = openPostStats
|
||||
self.openStory = openStory
|
||||
self.contextAction = contextAction
|
||||
self.copyBoostLink = copyBoostLink
|
||||
self.shareBoostLink = shareBoostLink
|
||||
|
|
@ -652,6 +655,10 @@ private enum StatsEntry: ItemListNodeEntry {
|
|||
case let .post(_, _, _, _, peer, post, interactions):
|
||||
return StatsMessageItem(context: arguments.context, presentationData: presentationData, peer: peer, item: post, views: interactions.views, reactions: interactions.reactions, forwards: interactions.forwards, sectionId: self.section, style: .blocks, action: {
|
||||
arguments.openPostStats(EnginePeer(peer), post)
|
||||
}, openStory: { sourceView in
|
||||
if case let .story(story) = post {
|
||||
arguments.openStory(story, sourceView)
|
||||
}
|
||||
}, contextAction: !post.isStory ? { node, gesture in
|
||||
if case let .message(message) = post {
|
||||
arguments.contextAction(message.id, node, gesture)
|
||||
|
|
@ -1048,6 +1055,7 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
|
|||
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
|
||||
|
||||
var openPostStatsImpl: ((EnginePeer, StatsPostItem) -> Void)?
|
||||
var openStoryImpl: ((EngineStoryItem, UIView) -> Void)?
|
||||
var contextActionImpl: ((MessageId, ASDisplayNode, ContextGesture?) -> Void)?
|
||||
|
||||
let actionsDisposable = DisposableSet()
|
||||
|
|
@ -1097,6 +1105,8 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
|
|||
return statsContext.loadDetailedGraph(graph, x: x)
|
||||
}, openPostStats: { peer, item in
|
||||
openPostStatsImpl?(peer, item)
|
||||
}, openStory: { story, sourceView in
|
||||
openStoryImpl?(story, sourceView)
|
||||
}, contextAction: { messageId, node, gesture in
|
||||
contextActionImpl?(messageId, node, gesture)
|
||||
}, copyBoostLink: { link in
|
||||
|
|
@ -1292,10 +1302,70 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
|
|||
case let .message(message):
|
||||
subject = .message(id: message.id)
|
||||
case let .story(story):
|
||||
subject = .story(peerId: peerId, id: story.id)
|
||||
subject = .story(peerId: peerId, id: story.id, item: story)
|
||||
}
|
||||
controller?.push(messageStatsController(context: context, subject: subject))
|
||||
}
|
||||
openStoryImpl = { [weak controller] story, sourceView in
|
||||
let storyContent = SingleStoryContentContextImpl(context: context, storyId: StoryId(peerId: peerId, id: story.id), storyItem: story, readGlobally: false)
|
||||
let _ = (storyContent.state
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).startStandalone(next: { [weak controller, weak sourceView] _ in
|
||||
guard let controller, let sourceView else {
|
||||
return
|
||||
}
|
||||
let transitionIn = StoryContainerScreen.TransitionIn(
|
||||
sourceView: sourceView,
|
||||
sourceRect: sourceView.bounds,
|
||||
sourceCornerRadius: sourceView.bounds.width * 0.5,
|
||||
sourceIsAvatar: false
|
||||
)
|
||||
|
||||
let storyContainerScreen = StoryContainerScreen(
|
||||
context: context,
|
||||
content: storyContent,
|
||||
transitionIn: transitionIn,
|
||||
transitionOut: { [weak sourceView] peerId, storyIdValue in
|
||||
if let sourceView {
|
||||
let destinationView = sourceView
|
||||
return StoryContainerScreen.TransitionOut(
|
||||
destinationView: destinationView,
|
||||
transitionView: StoryContainerScreen.TransitionView(
|
||||
makeView: { [weak destinationView] in
|
||||
let parentView = UIView()
|
||||
if let copyView = destinationView?.snapshotContentTree(unhide: true) {
|
||||
parentView.addSubview(copyView)
|
||||
}
|
||||
return parentView
|
||||
},
|
||||
updateView: { copyView, state, transition in
|
||||
guard let view = copyView.subviews.first else {
|
||||
return
|
||||
}
|
||||
let size = state.sourceSize.interpolate(to: state.destinationSize, amount: state.progress)
|
||||
transition.setPosition(view: view, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5))
|
||||
transition.setScale(view: view, scale: size.width / state.destinationSize.width)
|
||||
},
|
||||
insertCloneTransitionView: nil
|
||||
),
|
||||
destinationRect: destinationView.bounds,
|
||||
destinationCornerRadius: destinationView.bounds.width * 0.5,
|
||||
destinationIsAvatar: false,
|
||||
completed: { [weak sourceView] in
|
||||
guard let sourceView else {
|
||||
return
|
||||
}
|
||||
sourceView.isHidden = false
|
||||
}
|
||||
)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
)
|
||||
controller.push(storyContainerScreen)
|
||||
})
|
||||
}
|
||||
contextActionImpl = { [weak controller] messageId, sourceNode, gesture in
|
||||
guard let controller = controller, let sourceNode = sourceNode as? ContextExtractedContentContainingNode else {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@ private enum StatsEntry: ItemListNodeEntry {
|
|||
let .publicForwardsTitle(_, text):
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||
case let .overview(_, stats, publicShares):
|
||||
return MessageStatsOverviewItem(presentationData: presentationData, stats: stats, publicShares: publicShares, sectionId: self.section, style: .blocks)
|
||||
return StatsOverviewItem(presentationData: presentationData, stats: stats as! Stats, publicShares: publicShares, sectionId: self.section, style: .blocks)
|
||||
case let .interactionsGraph(_, _, _, graph, type), let .reactionsGraph(_, _, _, graph, type):
|
||||
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, getDetailsData: { date, completion in
|
||||
let _ = arguments.loadDetailedGraph(graph, Int64(date.timeIntervalSince1970) * 1000).start(next: { graph in
|
||||
|
|
@ -221,7 +221,7 @@ private func messageStatsControllerEntries(data: PostStats?, messages: SearchMes
|
|||
|
||||
public enum StatsSubject {
|
||||
case message(id: EngineMessage.Id)
|
||||
case story(peerId: EnginePeer.Id, id: Int32)
|
||||
case story(peerId: EnginePeer.Id, id: Int32, item: EngineStoryItem?)
|
||||
}
|
||||
|
||||
protocol PostStats {
|
||||
|
|
@ -252,7 +252,7 @@ public func messageStatsController(context: AccountContext, updatedPresentationD
|
|||
var loadDetailedGraphImpl: ((StatsGraph, Int64) -> Signal<StatsGraph?, NoError>)?
|
||||
switch subject {
|
||||
case let .message(id):
|
||||
let statsContext = MessageStatsContext(postbox: context.account.postbox, network: context.account.network, messageId: id)
|
||||
let statsContext = MessageStatsContext(account: context.account, messageId: id)
|
||||
loadDetailedGraphImpl = { [weak statsContext] graph, x in
|
||||
return statsContext?.loadDetailedGraph(graph, x: x) ?? .single(nil)
|
||||
}
|
||||
|
|
@ -262,8 +262,8 @@ public func messageStatsController(context: AccountContext, updatedPresentationD
|
|||
}
|
||||
dataPromise.set(.single(nil) |> then(dataSignal))
|
||||
anyStatsContext = statsContext
|
||||
case let .story(peerId, id):
|
||||
let statsContext = StoryStatsContext(postbox: context.account.postbox, network: context.account.network, peerId: peerId, storyId: id)
|
||||
case let .story(peerId, id, _):
|
||||
let statsContext = StoryStatsContext(account: context.account, peerId: peerId, storyId: id)
|
||||
loadDetailedGraphImpl = { [weak statsContext] graph, x in
|
||||
return statsContext?.loadDetailedGraph(graph, x: x) ?? .single(nil)
|
||||
}
|
||||
|
|
@ -303,16 +303,15 @@ public func messageStatsController(context: AccountContext, updatedPresentationD
|
|||
}
|
||||
|
||||
let iconNodePromise = Promise<ASDisplayNode?>()
|
||||
if case let .story(peerId, id) = subject {
|
||||
if case let .story(peerId, id, storyItem) = subject, let storyItem {
|
||||
let _ = id
|
||||
iconNodePromise.set(
|
||||
context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|
||||
|> deliverOnMainQueue
|
||||
|> map { peer -> ASDisplayNode? in
|
||||
if let _ = peer?._asPeer() {
|
||||
// let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
// return StoryIconNode(context: context, theme: presentationData.theme, peer: peer, storyItem: storyItem)
|
||||
return nil
|
||||
if let peer = peer?._asPeer() {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
return StoryIconNode(context: context, theme: presentationData.theme, peer: peer, storyItem: storyItem)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,17 +8,19 @@ import TelegramPresentationData
|
|||
import ItemListUI
|
||||
import PresentationDataUtils
|
||||
|
||||
class MessageStatsOverviewItem: ListViewItem, ItemListItem {
|
||||
final class MessageStatsOverviewItem: ListViewItem, ItemListItem {
|
||||
let presentationData: ItemListPresentationData
|
||||
let stats: PostStats
|
||||
let publicShares: Int32?
|
||||
let reactions: Int32
|
||||
let sectionId: ItemListSectionId
|
||||
let style: ItemListStyle
|
||||
|
||||
init(presentationData: ItemListPresentationData, stats: PostStats, publicShares: Int32?, sectionId: ItemListSectionId, style: ItemListStyle) {
|
||||
init(presentationData: ItemListPresentationData, stats: PostStats, publicShares: Int32?, reactions: Int32, sectionId: ItemListSectionId, style: ItemListStyle) {
|
||||
self.presentationData = presentationData
|
||||
self.stats = stats
|
||||
self.publicShares = publicShares
|
||||
self.reactions = reactions
|
||||
self.sectionId = sectionId
|
||||
self.style = style
|
||||
}
|
||||
|
|
@ -161,9 +163,19 @@ class MessageStatsOverviewItemNode: ListViewItemNode {
|
|||
let rightTitleLabelLayoutAndApply: ((Display.TextNodeLayout, () -> Display.TextNode))?
|
||||
let centerTitleLabelLayoutAndApply: ((Display.TextNodeLayout, () -> Display.TextNode))?
|
||||
|
||||
let centerTitle: String
|
||||
let centerValue: String
|
||||
if let _ = item.stats as? StoryStats {
|
||||
centerTitle = "Reactions"
|
||||
centerValue = compactNumericCountString(Int(item.reactions))
|
||||
} else {
|
||||
centerTitle = item.presentationData.strings.Stats_Message_PublicShares
|
||||
centerValue = item.publicShares.flatMap { compactNumericCountString(Int($0)) } ?? "–"
|
||||
}
|
||||
|
||||
leftValueLabelLayoutAndApply = makeLeftValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: compactNumericCountString(item.stats.views), font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
centerValueLabelLayoutAndApply = makeCenterValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.publicShares.flatMap { compactNumericCountString(Int($0)) } ?? "–", font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
centerValueLabelLayoutAndApply = makeCenterValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: centerValue, font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
rightValueLabelLayoutAndApply = makeRightValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.publicShares.flatMap { "≈\( compactNumericCountString(max(0, item.stats.forwards - Int($0))))" } ?? "–", font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
|
|
@ -173,12 +185,7 @@ class MessageStatsOverviewItemNode: ListViewItemNode {
|
|||
leftTitleLabelLayoutAndApply = makeLeftTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stats_Message_Views, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: min(maxItemWidth, remainingWidth), height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
remainingWidth -= leftTitleLabelLayoutAndApply!.0.size.width - 4.0
|
||||
|
||||
let centerTitle: String
|
||||
if let _ = item.stats as? StoryStats {
|
||||
centerTitle = "Reactions"
|
||||
} else {
|
||||
centerTitle = item.presentationData.strings.Stats_Message_PublicShares
|
||||
}
|
||||
|
||||
|
||||
centerTitleLabelLayoutAndApply = makeCenterTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: centerTitle, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: min(maxItemWidth, remainingWidth), height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
remainingWidth -= centerTitleLabelLayoutAndApply!.0.size.width - 4.0
|
||||
|
|
|
|||
|
|
@ -25,9 +25,10 @@ public class StatsMessageItem: ListViewItem, ItemListItem {
|
|||
public let sectionId: ItemListSectionId
|
||||
let style: ItemListStyle
|
||||
let action: (() -> Void)?
|
||||
let openStory: (UIView) -> Void
|
||||
let contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?
|
||||
|
||||
init(context: AccountContext, presentationData: ItemListPresentationData, peer: Peer, item: StatsPostItem, views: Int32, reactions: Int32, forwards: Int32, sectionId: ItemListSectionId, style: ItemListStyle, action: (() -> Void)?, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?) {
|
||||
init(context: AccountContext, presentationData: ItemListPresentationData, peer: Peer, item: StatsPostItem, views: Int32, reactions: Int32, forwards: Int32, sectionId: ItemListSectionId, style: ItemListStyle, action: (() -> Void)?, openStory: @escaping (UIView) -> Void, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?) {
|
||||
self.context = context
|
||||
self.presentationData = presentationData
|
||||
self.peer = peer
|
||||
|
|
@ -38,6 +39,7 @@ public class StatsMessageItem: ListViewItem, ItemListItem {
|
|||
self.sectionId = sectionId
|
||||
self.style = style
|
||||
self.action = action
|
||||
self.openStory = openStory
|
||||
self.contextAction = contextAction
|
||||
}
|
||||
|
||||
|
|
@ -103,6 +105,8 @@ final class StatsMessageItemNode: ListViewItemNode, ItemListItemNode {
|
|||
|
||||
let contentImageNode: TransformImageNode
|
||||
var storyIndicator: ComponentView<Empty>?
|
||||
var storyButton: HighlightTrackingButton?
|
||||
|
||||
let titleNode: TextNode
|
||||
let labelNode: TextNode
|
||||
let viewsNode: TextNode
|
||||
|
|
@ -146,7 +150,7 @@ final class StatsMessageItemNode: ListViewItemNode, ItemListItemNode {
|
|||
self.extractedBackgroundImageNode.alpha = 0.0
|
||||
|
||||
self.contentImageNode = TransformImageNode()
|
||||
self.contentImageNode.isLayerBacked = true
|
||||
self.contentImageNode.isLayerBacked = false
|
||||
|
||||
self.offsetContainerNode = ASDisplayNode()
|
||||
self.countersContainerNode = ASDisplayNode()
|
||||
|
|
@ -234,6 +238,18 @@ final class StatsMessageItemNode: ListViewItemNode, ItemListItemNode {
|
|||
}
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
let result = super.hitTest(point, with: event)
|
||||
return result
|
||||
}
|
||||
|
||||
@objc private func storyPressed() {
|
||||
guard let item = self.item else {
|
||||
return
|
||||
}
|
||||
item.openStory(self.contentImageNode.view)
|
||||
}
|
||||
|
||||
public func asyncLayout() -> (_ item: StatsMessageItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
||||
let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
|
||||
|
|
@ -603,17 +619,34 @@ final class StatsMessageItemNode: ListViewItemNode, ItemListItemNode {
|
|||
environment: {},
|
||||
containerSize: indicatorSize
|
||||
)
|
||||
let storyIndicatorFrame = CGRect(origin: CGPoint(x: contentImageFrame.midX - indicatorSize.width / 2.0, y: contentImageFrame.midY - indicatorSize.height / 2.0), size: indicatorSize)
|
||||
if let storyIndicatorView = storyIndicator.view {
|
||||
if storyIndicatorView.superview == nil {
|
||||
strongSelf.offsetContainerNode.view.addSubview(storyIndicatorView)
|
||||
}
|
||||
indicatorTransition.setFrame(view: storyIndicatorView, frame: CGRect(origin: CGPoint(x: contentImageFrame.midX - indicatorSize.width / 2.0, y: contentImageFrame.midY - indicatorSize.height / 2.0), size: indicatorSize))
|
||||
indicatorTransition.setFrame(view: storyIndicatorView, frame: storyIndicatorFrame)
|
||||
}
|
||||
|
||||
let storyButton: HighlightTrackingButton
|
||||
if let current = strongSelf.storyButton {
|
||||
storyButton = current
|
||||
} else {
|
||||
storyButton = HighlightTrackingButton()
|
||||
storyButton.addTarget(strongSelf, action: #selector(strongSelf.storyPressed), for: .touchUpInside)
|
||||
strongSelf.view.addSubview(storyButton)
|
||||
strongSelf.storyButton = storyButton
|
||||
}
|
||||
storyButton.frame = storyIndicatorFrame
|
||||
} else if let storyIndicator = strongSelf.storyIndicator {
|
||||
if let storyIndicatorView = storyIndicator.view {
|
||||
storyIndicatorView.removeFromSuperview()
|
||||
}
|
||||
strongSelf.storyIndicator = nil
|
||||
|
||||
if let storyButton = strongSelf.storyButton {
|
||||
storyButton.removeFromSuperview()
|
||||
strongSelf.storyButton = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
@ -623,6 +656,11 @@ final class StatsMessageItemNode: ListViewItemNode, ItemListItemNode {
|
|||
override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
|
||||
super.setHighlighted(highlighted, at: point, animated: animated)
|
||||
|
||||
var highlighted = highlighted
|
||||
if let avatarButton = self.storyButton, avatarButton.bounds.contains(self.view.convert(point, to: storyButton)) {
|
||||
highlighted = false
|
||||
}
|
||||
|
||||
if highlighted {
|
||||
self.highlightedBackgroundNode.alpha = 1.0
|
||||
if self.highlightedBackgroundNode.supernode == nil {
|
||||
|
|
@ -670,4 +708,3 @@ final class StatsMessageItemNode: ListViewItemNode, ItemListItemNode {
|
|||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,31 +8,41 @@ import TelegramPresentationData
|
|||
import ItemListUI
|
||||
import PresentationDataUtils
|
||||
|
||||
protocol PeerStats {
|
||||
protocol Stats {
|
||||
|
||||
}
|
||||
|
||||
extension ChannelStats: PeerStats {
|
||||
extension ChannelStats: Stats {
|
||||
|
||||
}
|
||||
|
||||
extension GroupStats: PeerStats {
|
||||
extension GroupStats: Stats {
|
||||
|
||||
}
|
||||
|
||||
extension ChannelBoostStatus: PeerStats {
|
||||
extension ChannelBoostStatus: Stats {
|
||||
|
||||
}
|
||||
|
||||
extension MessageStats: Stats {
|
||||
|
||||
}
|
||||
|
||||
extension StoryStats: Stats {
|
||||
|
||||
}
|
||||
|
||||
class StatsOverviewItem: ListViewItem, ItemListItem {
|
||||
let presentationData: ItemListPresentationData
|
||||
let stats: PeerStats
|
||||
let stats: Stats
|
||||
let publicShares: Int32?
|
||||
let sectionId: ItemListSectionId
|
||||
let style: ItemListStyle
|
||||
|
||||
init(presentationData: ItemListPresentationData, stats: PeerStats, sectionId: ItemListSectionId, style: ItemListStyle) {
|
||||
init(presentationData: ItemListPresentationData, stats: Stats, publicShares: Int32? = nil, sectionId: ItemListSectionId, style: ItemListStyle) {
|
||||
self.presentationData = presentationData
|
||||
self.stats = stats
|
||||
self.publicShares = publicShares
|
||||
self.sectionId = sectionId
|
||||
self.style = style
|
||||
}
|
||||
|
|
@ -73,26 +83,133 @@ class StatsOverviewItem: ListViewItem, ItemListItem {
|
|||
var selectable: Bool = false
|
||||
}
|
||||
|
||||
private final class ValueItemNode: ASDisplayNode {
|
||||
enum DeltaColor {
|
||||
case generic
|
||||
case positive
|
||||
case negative
|
||||
}
|
||||
|
||||
private let valueNode: TextNode
|
||||
private let titleNode: TextNode
|
||||
private let deltaNode: TextNode
|
||||
|
||||
var currentBackgroundColor: UIColor?
|
||||
var pressed: (() -> Void)?
|
||||
|
||||
override init() {
|
||||
self.valueNode = TextNode()
|
||||
self.titleNode = TextNode()
|
||||
self.deltaNode = TextNode()
|
||||
|
||||
super.init()
|
||||
|
||||
self.isUserInteractionEnabled = false
|
||||
|
||||
self.addSubnode(self.valueNode)
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.deltaNode)
|
||||
}
|
||||
|
||||
static func asyncLayout(_ current: ValueItemNode?) -> (_ width: CGFloat, _ presentationData: ItemListPresentationData, _ value: String, _ title: String, _ delta: (String, DeltaColor)?) -> (CGSize, () -> ValueItemNode) {
|
||||
|
||||
let maybeMakeValueLayout = (current?.valueNode).flatMap(TextNode.asyncLayout)
|
||||
let maybeMakeTitleLayout = (current?.titleNode).flatMap(TextNode.asyncLayout)
|
||||
let maybeMakeDeltaLayout = (current?.deltaNode).flatMap(TextNode.asyncLayout)
|
||||
|
||||
return { width, presentationData, value, title, delta in
|
||||
let targetNode: ValueItemNode
|
||||
if let current = current {
|
||||
targetNode = current
|
||||
} else {
|
||||
targetNode = ValueItemNode()
|
||||
}
|
||||
|
||||
let makeValueLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode)
|
||||
if let maybeMakeValueLayout {
|
||||
makeValueLayout = maybeMakeValueLayout
|
||||
} else {
|
||||
makeValueLayout = TextNode.asyncLayout(targetNode.valueNode)
|
||||
}
|
||||
|
||||
let makeTitleLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode)
|
||||
if let maybeMakeTitleLayout {
|
||||
makeTitleLayout = maybeMakeTitleLayout
|
||||
} else {
|
||||
makeTitleLayout = TextNode.asyncLayout(targetNode.titleNode)
|
||||
}
|
||||
|
||||
let makeDeltaLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode)
|
||||
if let maybeMakeDeltaLayout {
|
||||
makeDeltaLayout = maybeMakeDeltaLayout
|
||||
} else {
|
||||
makeDeltaLayout = TextNode.asyncLayout(targetNode.deltaNode)
|
||||
}
|
||||
|
||||
let valueFont = Font.semibold(presentationData.fontSize.itemListBaseFontSize)
|
||||
let titleFont = Font.regular(presentationData.fontSize.itemListBaseHeaderFontSize)
|
||||
let deltaFont = Font.regular(presentationData.fontSize.itemListBaseHeaderFontSize)
|
||||
|
||||
let valueColor = presentationData.theme.list.itemPrimaryTextColor
|
||||
let titleColor = presentationData.theme.list.sectionHeaderTextColor
|
||||
|
||||
let deltaColor: UIColor
|
||||
if let (_, color) = delta {
|
||||
switch color {
|
||||
case .generic:
|
||||
deltaColor = titleColor
|
||||
case .positive:
|
||||
deltaColor = presentationData.theme.list.freeTextSuccessColor
|
||||
case .negative:
|
||||
deltaColor = presentationData.theme.list.freeTextErrorColor
|
||||
}
|
||||
} else {
|
||||
deltaColor = presentationData.theme.list.freeTextErrorColor
|
||||
}
|
||||
|
||||
let constrainedSize = CGSize(width: width, height: CGFloat.greatestFiniteMagnitude)
|
||||
let (valueLayout, valueApply) = makeValueLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: value, font: valueFont, textColor: valueColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: constrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: titleFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: constrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let (deltaLayout, deltaApply) = makeDeltaLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: delta?.0 ?? "", font: deltaFont, textColor: deltaColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: constrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let horizontalSpacing: CGFloat = 4.0
|
||||
let size = CGSize(width: valueLayout.size.width + horizontalSpacing + deltaLayout.size.width, height: valueLayout.size.height + titleLayout.size.height)
|
||||
return (size, {
|
||||
let _ = valueApply()
|
||||
let _ = titleApply()
|
||||
let _ = deltaApply()
|
||||
|
||||
let valueFrame = CGRect(origin: .zero, size: valueLayout.size)
|
||||
let titleFrame = CGRect(origin: CGPoint(x: 0.0, y: valueFrame.maxY), size: titleLayout.size)
|
||||
let deltaFrame = CGRect(origin: CGPoint(x: valueFrame.maxX + horizontalSpacing, y: valueFrame.maxY - deltaLayout.size.height - 2.0), size: deltaLayout.size)
|
||||
|
||||
targetNode.valueNode.frame = valueFrame
|
||||
targetNode.titleNode.frame = titleFrame
|
||||
targetNode.deltaNode.frame = deltaFrame
|
||||
|
||||
return targetNode
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class StatsOverviewItemNode: ListViewItemNode {
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let topStripeNode: ASDisplayNode
|
||||
private let bottomStripeNode: ASDisplayNode
|
||||
private let maskNode: ASImageNode
|
||||
|
||||
private let topLeftValueLabel: ImmediateTextNode
|
||||
private let bottomLeftValueLabel: ImmediateTextNode
|
||||
private let bottomRightValueLabel: ImmediateTextNode
|
||||
private let topRightValueLabel: ImmediateTextNode
|
||||
|
||||
private let topLeftTitleLabel: ImmediateTextNode
|
||||
private let bottomLeftTitleLabel: ImmediateTextNode
|
||||
private let bottomRightTitleLabel: ImmediateTextNode
|
||||
private let topRightTitleLabel: ImmediateTextNode
|
||||
|
||||
private let topLeftDeltaLabel: ImmediateTextNode
|
||||
private let bottomLeftDeltaLabel: ImmediateTextNode
|
||||
private let bottomRightDeltaLabel: ImmediateTextNode
|
||||
private let topRightDeltaLabel: ImmediateTextNode
|
||||
private let topLeftItem: ValueItemNode
|
||||
private let topRightItem: ValueItemNode
|
||||
private let middle1LeftItem: ValueItemNode
|
||||
private let middle1RightItem: ValueItemNode
|
||||
private let middle2LeftItem: ValueItemNode
|
||||
private let middle2RightItem: ValueItemNode
|
||||
private let bottomLeftItem: ValueItemNode
|
||||
private let bottomRightItem: ValueItemNode
|
||||
|
||||
private var item: StatsOverviewItem?
|
||||
|
||||
|
|
@ -109,56 +226,38 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||
|
||||
self.maskNode = ASImageNode()
|
||||
|
||||
self.topLeftValueLabel = ImmediateTextNode()
|
||||
self.bottomLeftValueLabel = ImmediateTextNode()
|
||||
self.bottomRightValueLabel = ImmediateTextNode()
|
||||
self.topRightValueLabel = ImmediateTextNode()
|
||||
|
||||
self.topLeftTitleLabel = ImmediateTextNode()
|
||||
self.bottomLeftTitleLabel = ImmediateTextNode()
|
||||
self.bottomRightTitleLabel = ImmediateTextNode()
|
||||
self.topRightTitleLabel = ImmediateTextNode()
|
||||
|
||||
self.topLeftDeltaLabel = ImmediateTextNode()
|
||||
self.bottomLeftDeltaLabel = ImmediateTextNode()
|
||||
self.bottomRightDeltaLabel = ImmediateTextNode()
|
||||
self.topRightDeltaLabel = ImmediateTextNode()
|
||||
self.topLeftItem = ValueItemNode()
|
||||
self.topRightItem = ValueItemNode()
|
||||
self.middle1LeftItem = ValueItemNode()
|
||||
self.middle1RightItem = ValueItemNode()
|
||||
self.middle2LeftItem = ValueItemNode()
|
||||
self.middle2RightItem = ValueItemNode()
|
||||
self.bottomLeftItem = ValueItemNode()
|
||||
self.bottomRightItem = ValueItemNode()
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
|
||||
self.clipsToBounds = true
|
||||
|
||||
self.addSubnode(self.topLeftValueLabel)
|
||||
self.addSubnode(self.bottomLeftValueLabel)
|
||||
self.addSubnode(self.bottomRightValueLabel)
|
||||
self.addSubnode(self.topRightValueLabel)
|
||||
|
||||
self.addSubnode(self.topLeftTitleLabel)
|
||||
self.addSubnode(self.bottomLeftTitleLabel)
|
||||
self.addSubnode(self.bottomRightTitleLabel)
|
||||
self.addSubnode(self.topRightTitleLabel)
|
||||
|
||||
self.addSubnode(self.topLeftDeltaLabel)
|
||||
self.addSubnode(self.bottomLeftDeltaLabel)
|
||||
self.addSubnode(self.bottomRightDeltaLabel)
|
||||
self.addSubnode(self.topRightDeltaLabel)
|
||||
self.addSubnode(self.topLeftItem)
|
||||
self.addSubnode(self.topRightItem)
|
||||
self.addSubnode(self.middle1LeftItem)
|
||||
self.addSubnode(self.middle1RightItem)
|
||||
self.addSubnode(self.middle2LeftItem)
|
||||
self.addSubnode(self.middle2RightItem)
|
||||
self.addSubnode(self.bottomLeftItem)
|
||||
self.addSubnode(self.bottomRightItem)
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: StatsOverviewItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
let makeTopLeftValueLabelLayout = TextNode.asyncLayout(self.topLeftValueLabel)
|
||||
let makeTopRightValueLabelLayout = TextNode.asyncLayout(self.topRightValueLabel)
|
||||
let makeBottomLeftValueLabelLayout = TextNode.asyncLayout(self.bottomLeftValueLabel)
|
||||
let makeBottomRightValueLabelLayout = TextNode.asyncLayout(self.bottomRightValueLabel)
|
||||
|
||||
let makeTopLeftTitleLabelLayout = TextNode.asyncLayout(self.topLeftTitleLabel)
|
||||
let makeTopRightTitleLabelLayout = TextNode.asyncLayout(self.topRightTitleLabel)
|
||||
let makeBottomLeftTitleLabelLayout = TextNode.asyncLayout(self.bottomLeftTitleLabel)
|
||||
let makeBottomRightTitleLabelLayout = TextNode.asyncLayout(self.bottomRightTitleLabel)
|
||||
|
||||
let makeTopLeftDeltaLabelLayout = TextNode.asyncLayout(self.topLeftDeltaLabel)
|
||||
let makeTopRightDeltaLabelLayout = TextNode.asyncLayout(self.topRightDeltaLabel)
|
||||
let makeBottomLeftDeltaLabelLayout = TextNode.asyncLayout(self.bottomLeftDeltaLabel)
|
||||
let makeBottomRightDeltaLabelLayout = TextNode.asyncLayout(self.bottomRightDeltaLabel)
|
||||
let makeTopLeftItemLayout = ValueItemNode.asyncLayout(self.topLeftItem)
|
||||
let makeTopRightItemLayout = ValueItemNode.asyncLayout(self.topRightItem)
|
||||
let makeMiddle1LeftItemLayout = ValueItemNode.asyncLayout(self.middle1LeftItem)
|
||||
let makeMiddle1RightItemLayout = ValueItemNode.asyncLayout(self.middle1RightItem)
|
||||
let makeMiddle2LeftItemLayout = ValueItemNode.asyncLayout(self.middle2LeftItem)
|
||||
let makeMiddle2RightItemLayout = ValueItemNode.asyncLayout(self.middle2RightItem)
|
||||
let makeBottomLeftItemLayout = ValueItemNode.asyncLayout(self.bottomLeftItem)
|
||||
let makeBottomRightItemLayout = ValueItemNode.asyncLayout(self.bottomRightItem)
|
||||
|
||||
let currentItem = self.item
|
||||
|
||||
|
|
@ -176,7 +275,6 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||
var height: CGFloat = topInset * 2.0
|
||||
|
||||
let leftInset = params.leftInset
|
||||
let rightInset: CGFloat = params.rightInset
|
||||
var updatedTheme: PresentationTheme?
|
||||
|
||||
if currentItem?.presentationData.theme !== item.presentationData.theme {
|
||||
|
|
@ -194,28 +292,18 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||
insets = itemListNeighborsGroupedInsets(neighbors, params)
|
||||
}
|
||||
|
||||
let valueFont = Font.semibold(item.presentationData.fontSize.itemListBaseFontSize)
|
||||
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseHeaderFontSize)
|
||||
let deltaFont = Font.regular(item.presentationData.fontSize.itemListBaseHeaderFontSize)
|
||||
let twoColumnLayout = "".isEmpty
|
||||
|
||||
let topLeftValueLabelLayoutAndApply: ((Display.TextNodeLayout, () -> Display.TextNode))?
|
||||
let topRightValueLabelLayoutAndApply: ((Display.TextNodeLayout, () -> Display.TextNode))?
|
||||
let bottomLeftValueLabelLayoutAndApply: ((Display.TextNodeLayout, () -> Display.TextNode))?
|
||||
let bottomRightValueLabelLayoutAndApply: ((Display.TextNodeLayout, () -> Display.TextNode))?
|
||||
|
||||
let topLeftTitleLabelLayoutAndApply: ((Display.TextNodeLayout, () -> Display.TextNode))?
|
||||
let topRightTitleLabelLayoutAndApply: ((Display.TextNodeLayout, () -> Display.TextNode))?
|
||||
let bottomLeftTitleLabelLayoutAndApply: ((Display.TextNodeLayout, () -> Display.TextNode))?
|
||||
let bottomRightTitleLabelLayoutAndApply: ((Display.TextNodeLayout, () -> Display.TextNode))?
|
||||
var topLeftItemLayoutAndApply: (CGSize, () -> ValueItemNode)?
|
||||
var topRightItemLayoutAndApply: (CGSize, () -> ValueItemNode)?
|
||||
var middle1LeftItemLayoutAndApply: (CGSize, () -> ValueItemNode)?
|
||||
var middle1RightItemLayoutAndApply: (CGSize, () -> ValueItemNode)?
|
||||
var middle2LeftItemLayoutAndApply: (CGSize, () -> ValueItemNode)?
|
||||
var middle2RightItemLayoutAndApply: (CGSize, () -> ValueItemNode)?
|
||||
var bottomLeftItemLayoutAndApply: (CGSize, () -> ValueItemNode)?
|
||||
var bottomRightItemLayoutAndApply: (CGSize, () -> ValueItemNode)?
|
||||
|
||||
let topLeftDeltaLabelLayoutAndApply: ((Display.TextNodeLayout, () -> Display.TextNode))?
|
||||
let topRightDeltaLabelLayoutAndApply: ((Display.TextNodeLayout, () -> Display.TextNode))?
|
||||
let bottomLeftDeltaLabelLayoutAndApply: ((Display.TextNodeLayout, () -> Display.TextNode))?
|
||||
let bottomRightDeltaLabelLayoutAndApply: ((Display.TextNodeLayout, () -> Display.TextNode))?
|
||||
|
||||
var twoColumnLayout = true
|
||||
|
||||
func deltaText(_ value: StatsValue) -> (String, Bool, Bool) {
|
||||
func deltaText(_ value: StatsValue) -> (text: String, positive: Bool, hasValue: Bool) {
|
||||
let deltaValue = value.current - value.previous
|
||||
let deltaCompact = compactNumericCountString(abs(Int(deltaValue)))
|
||||
let delta = deltaValue > 0 ? "+\(deltaCompact)" : "-\(deltaCompact)"
|
||||
|
|
@ -227,12 +315,103 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||
return (abs(deltaPercentage) > 0.0 ? String(format: "%@ (%.02f%%)", delta, deltaPercentage * 100.0) : "", deltaValue > 0.0, abs(deltaValue) > 0.0)
|
||||
}
|
||||
|
||||
if let stats = item.stats as? ChannelBoostStatus {
|
||||
topLeftValueLabelLayoutAndApply = makeTopLeftValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "\(stats.level)", font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
topRightValueLabelLayoutAndApply = makeTopRightValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "≈\(Int(stats.premiumAudience?.value ?? 0))", font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
if let stats = item.stats as? MessageStats {
|
||||
topLeftItemLayoutAndApply = makeTopLeftItemLayout(
|
||||
params.width,
|
||||
item.presentationData,
|
||||
compactNumericCountString(stats.views),
|
||||
item.presentationData.strings.Stats_Message_Views,
|
||||
nil
|
||||
)
|
||||
|
||||
bottomLeftValueLabelLayoutAndApply = makeBottomLeftValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "\(stats.boosts)", font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
topRightItemLayoutAndApply = makeTopRightItemLayout(
|
||||
params.width,
|
||||
item.presentationData,
|
||||
item.publicShares.flatMap { compactNumericCountString(Int($0)) } ?? "–",
|
||||
item.presentationData.strings.Stats_Message_PublicShares,
|
||||
nil
|
||||
)
|
||||
|
||||
middle1LeftItemLayoutAndApply = makeMiddle1LeftItemLayout(
|
||||
params.width,
|
||||
item.presentationData,
|
||||
compactNumericCountString(stats.reactions),
|
||||
item.presentationData.strings.Stats_Message_Reactions,
|
||||
nil
|
||||
)
|
||||
|
||||
middle1RightItemLayoutAndApply = makeMiddle1RightItemLayout(
|
||||
params.width,
|
||||
item.presentationData,
|
||||
item.publicShares.flatMap { "≈\( compactNumericCountString(max(0, stats.forwards - Int($0))))" } ?? "–",
|
||||
item.presentationData.strings.Stats_Message_PrivateShares,
|
||||
nil
|
||||
)
|
||||
|
||||
height += topRightItemLayoutAndApply!.0.height * 2.0 + verticalSpacing
|
||||
} else if let stats = item.stats as? StoryStats {
|
||||
topLeftItemLayoutAndApply = makeTopLeftItemLayout(
|
||||
params.width,
|
||||
item.presentationData,
|
||||
compactNumericCountString(stats.views),
|
||||
item.presentationData.strings.Stats_Message_Views,
|
||||
nil
|
||||
)
|
||||
|
||||
topRightItemLayoutAndApply = makeTopRightItemLayout(
|
||||
params.width,
|
||||
item.presentationData,
|
||||
item.publicShares.flatMap { compactNumericCountString(Int($0)) } ?? "–",
|
||||
item.presentationData.strings.Stats_Message_PublicShares,
|
||||
nil
|
||||
)
|
||||
|
||||
middle1LeftItemLayoutAndApply = makeMiddle1LeftItemLayout(
|
||||
params.width,
|
||||
item.presentationData,
|
||||
compactNumericCountString(stats.reactions),
|
||||
item.presentationData.strings.Stats_Message_Reactions,
|
||||
nil
|
||||
)
|
||||
|
||||
middle1RightItemLayoutAndApply = makeMiddle1RightItemLayout(
|
||||
params.width,
|
||||
item.presentationData,
|
||||
compactNumericCountString(stats.forwards),
|
||||
item.presentationData.strings.Stats_Message_PrivateShares,
|
||||
nil
|
||||
)
|
||||
|
||||
height += topRightItemLayoutAndApply!.0.height * 2.0 + verticalSpacing
|
||||
} else if let stats = item.stats as? ChannelBoostStatus {
|
||||
topLeftItemLayoutAndApply = makeTopLeftItemLayout(
|
||||
params.width,
|
||||
item.presentationData,
|
||||
"\(stats.level)",
|
||||
item.presentationData.strings.Stats_Boosts_Level,
|
||||
nil
|
||||
)
|
||||
|
||||
var premiumSubscribers: Double = 0.0
|
||||
if let premiumAudience = stats.premiumAudience, premiumAudience.total > 0 {
|
||||
premiumSubscribers = premiumAudience.value / premiumAudience.total
|
||||
}
|
||||
|
||||
topRightItemLayoutAndApply = makeTopRightItemLayout(
|
||||
params.width,
|
||||
item.presentationData,
|
||||
"≈\(Int(stats.premiumAudience?.value ?? 0))",
|
||||
item.presentationData.strings.Stats_Boosts_PremiumSubscribers,
|
||||
(String(format: "%.02f%%", premiumSubscribers * 100.0), .generic)
|
||||
)
|
||||
|
||||
middle1LeftItemLayoutAndApply = makeMiddle1LeftItemLayout(
|
||||
params.width,
|
||||
item.presentationData,
|
||||
"\(stats.boosts)",
|
||||
item.presentationData.strings.Stats_Boosts_ExistingBoosts,
|
||||
nil
|
||||
)
|
||||
|
||||
let boostsLeft: Int32
|
||||
if let nextLevelBoosts = stats.nextLevelBoosts {
|
||||
|
|
@ -240,162 +419,202 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||
} else {
|
||||
boostsLeft = 0
|
||||
}
|
||||
middle1RightItemLayoutAndApply = makeMiddle1RightItemLayout(
|
||||
params.width,
|
||||
item.presentationData,
|
||||
"\(boostsLeft)",
|
||||
item.presentationData.strings.Stats_Boosts_BoostsToLevelUp,
|
||||
nil
|
||||
)
|
||||
|
||||
bottomRightValueLabelLayoutAndApply = makeBottomRightValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "\(boostsLeft)", font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
topLeftTitleLabelLayoutAndApply = makeTopLeftTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stats_Boosts_Level, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
topRightTitleLabelLayoutAndApply = makeTopRightTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stats_Boosts_PremiumSubscribers, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
bottomLeftTitleLabelLayoutAndApply = makeBottomLeftTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stats_Boosts_ExistingBoosts, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
bottomRightTitleLabelLayoutAndApply = makeBottomRightTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stats_Boosts_BoostsToLevelUp, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
|
||||
topLeftDeltaLabelLayoutAndApply = nil
|
||||
|
||||
var premiumSubscribers: Double = 0.0
|
||||
if let premiumAudience = stats.premiumAudience, premiumAudience.total > 0 {
|
||||
premiumSubscribers = premiumAudience.value / premiumAudience.total
|
||||
if twoColumnLayout {
|
||||
height += topRightItemLayoutAndApply!.0.height * 2.0 + verticalSpacing
|
||||
} else {
|
||||
height += topLeftItemLayoutAndApply!.0.height * 4.0 + verticalSpacing * 3.0
|
||||
}
|
||||
|
||||
topRightDeltaLabelLayoutAndApply = makeTopRightDeltaLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: String(format: "%.02f%%", premiumSubscribers * 100.0), font: deltaFont, textColor: item.presentationData.theme.list.freeTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
bottomLeftDeltaLabelLayoutAndApply = nil
|
||||
bottomRightDeltaLabelLayoutAndApply = nil
|
||||
|
||||
height += topRightValueLabelLayoutAndApply!.0.size.height + topRightTitleLabelLayoutAndApply!.0.size.height
|
||||
|
||||
height += verticalSpacing
|
||||
height += bottomRightValueLabelLayoutAndApply!.0.size.height + bottomRightTitleLabelLayoutAndApply!.0.size.height
|
||||
} else if let stats = item.stats as? ChannelStats {
|
||||
let viewsPerPostDelta = deltaText(stats.viewsPerPost)
|
||||
let sharesPerPostDelta = deltaText(stats.sharesPerPost)
|
||||
let reactionsPerPostDelta = deltaText(stats.reactionsPerPost)
|
||||
|
||||
let viewsPerStoryDelta = deltaText(stats.viewsPerStory)
|
||||
let sharesPerStoryDelta = deltaText(stats.sharesPerStory)
|
||||
let reactionsPerStoryDelta = deltaText(stats.reactionsPerStory)
|
||||
|
||||
let displayBottomRow = stats.sharesPerPost.current > 0 || viewsPerPostDelta.2 || stats.viewsPerPost.current > 0 || sharesPerPostDelta.2
|
||||
let followersDelta = deltaText(stats.followers)
|
||||
topLeftItemLayoutAndApply = makeTopLeftItemLayout(
|
||||
params.width,
|
||||
item.presentationData,
|
||||
compactNumericCountString(Int(stats.followers.current)),
|
||||
item.presentationData.strings.Stats_Followers,
|
||||
(followersDelta.text, followersDelta.positive ? .positive : .negative)
|
||||
)
|
||||
|
||||
topLeftValueLabelLayoutAndApply = makeTopLeftValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: compactNumericCountString(Int(stats.followers.current)), font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
var enabledNotifications: Double = 0.0
|
||||
if stats.enabledNotifications.total > 0 {
|
||||
enabledNotifications = stats.enabledNotifications.value / stats.enabledNotifications.total
|
||||
}
|
||||
topRightItemLayoutAndApply = makeTopRightItemLayout(
|
||||
params.width,
|
||||
item.presentationData,
|
||||
String(format: "%.02f%%", enabledNotifications * 100.0),
|
||||
item.presentationData.strings.Stats_EnabledNotifications,
|
||||
nil
|
||||
)
|
||||
|
||||
topRightValueLabelLayoutAndApply = makeTopRightValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: String(format: "%.02f%%", enabledNotifications * 100.0), font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
let hasMessages = stats.viewsPerPost.current > 0
|
||||
let hasStories = stats.viewsPerStory.current > 0 || viewsPerStoryDelta.hasValue
|
||||
|
||||
bottomLeftValueLabelLayoutAndApply = makeBottomLeftValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: displayBottomRow ? compactNumericCountString(Int(stats.viewsPerPost.current)) : "", font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
bottomRightValueLabelLayoutAndApply = makeBottomRightValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: displayBottomRow ? compactNumericCountString(Int(stats.sharesPerPost.current)) : "", font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
topLeftTitleLabelLayoutAndApply = makeTopLeftTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stats_Followers, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
topRightTitleLabelLayoutAndApply = makeTopRightTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stats_EnabledNotifications, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
bottomLeftTitleLabelLayoutAndApply = makeBottomLeftTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: displayBottomRow ? item.presentationData.strings.Stats_ViewsPerPost : "", font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
bottomRightTitleLabelLayoutAndApply = makeBottomRightTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: displayBottomRow ? item.presentationData.strings.Stats_SharesPerPost : "", font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let followersDelta = deltaText(stats.followers)
|
||||
topLeftDeltaLabelLayoutAndApply = makeTopLeftDeltaLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: followersDelta.0, font: deltaFont, textColor: followersDelta.1 ? item.presentationData.theme.list.freeTextSuccessColor : item.presentationData.theme.list.freeTextErrorColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
topRightDeltaLabelLayoutAndApply = nil
|
||||
|
||||
bottomLeftDeltaLabelLayoutAndApply = makeBottomLeftDeltaLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: viewsPerPostDelta.0, font: deltaFont, textColor: viewsPerPostDelta.1 ? item.presentationData.theme.list.freeTextSuccessColor : item.presentationData.theme.list.freeTextErrorColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
bottomRightDeltaLabelLayoutAndApply = makeBottomRightDeltaLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: sharesPerPostDelta.0, font: deltaFont, textColor: sharesPerPostDelta.1 ? item.presentationData.theme.list.freeTextSuccessColor : item.presentationData.theme.list.freeTextErrorColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
|
||||
height += topRightValueLabelLayoutAndApply!.0.size.height + topRightTitleLabelLayoutAndApply!.0.size.height
|
||||
|
||||
if max(topLeftValueLabelLayoutAndApply!.0.size.width + topLeftDeltaLabelLayoutAndApply!.0.size.width + horizontalSpacing + topRightValueLabelLayoutAndApply!.0.size.width, bottomLeftValueLabelLayoutAndApply!.0.size.width + bottomLeftDeltaLabelLayoutAndApply!.0.size.width + horizontalSpacing + bottomRightValueLabelLayoutAndApply!.0.size.width + bottomRightDeltaLabelLayoutAndApply!.0.size.width) > params.width - leftInset - rightInset {
|
||||
twoColumnLayout = false
|
||||
var items: [Int: (String, String, (String, ValueItemNode.DeltaColor)?)] = [:]
|
||||
if hasMessages {
|
||||
items[0] = (
|
||||
compactNumericCountString(Int(stats.viewsPerPost.current)),
|
||||
item.presentationData.strings.Stats_ViewsPerPost,
|
||||
(viewsPerPostDelta.text, viewsPerPostDelta.positive ? .positive : .negative)
|
||||
)
|
||||
}
|
||||
if hasMessages {
|
||||
let index = hasStories ? 2 : 1
|
||||
items[index] = (
|
||||
compactNumericCountString(Int(stats.sharesPerPost.current)),
|
||||
item.presentationData.strings.Stats_SharesPerPost,
|
||||
(sharesPerPostDelta.text, sharesPerPostDelta.positive ? .positive : .negative)
|
||||
)
|
||||
}
|
||||
if stats.reactionsPerPost.current > 0 || reactionsPerStoryDelta.hasValue {
|
||||
let index = hasStories ? 4 : 2
|
||||
items[index] = (
|
||||
compactNumericCountString(Int(stats.reactionsPerPost.current)),
|
||||
item.presentationData.strings.Stats_ReactionsPerPost,
|
||||
(reactionsPerPostDelta.text, reactionsPerPostDelta.positive ? .positive : .negative)
|
||||
)
|
||||
}
|
||||
if hasStories {
|
||||
items[1] = (
|
||||
compactNumericCountString(Int(stats.viewsPerStory.current)),
|
||||
item.presentationData.strings.Stats_ViewsPerStory,
|
||||
(viewsPerStoryDelta.text, viewsPerStoryDelta.positive ? .positive : .negative)
|
||||
)
|
||||
items[3] = (
|
||||
compactNumericCountString(Int(stats.sharesPerStory.current)),
|
||||
item.presentationData.strings.Stats_SharesPerStory,
|
||||
(sharesPerStoryDelta.text, sharesPerStoryDelta.positive ? .positive : .negative)
|
||||
)
|
||||
items[5] = (
|
||||
compactNumericCountString(Int(stats.reactionsPerStory.current)),
|
||||
item.presentationData.strings.Stats_ReactionsPerStory,
|
||||
(reactionsPerStoryDelta.text, reactionsPerStoryDelta.positive ? .positive : .negative)
|
||||
)
|
||||
}
|
||||
|
||||
if let (value, title, delta) = items[0] {
|
||||
middle1LeftItemLayoutAndApply = makeMiddle1LeftItemLayout(
|
||||
params.width,
|
||||
item.presentationData,
|
||||
value,
|
||||
title,
|
||||
delta
|
||||
)
|
||||
}
|
||||
if let (value, title, delta) = items[1] {
|
||||
middle1RightItemLayoutAndApply = makeMiddle1RightItemLayout(
|
||||
params.width,
|
||||
item.presentationData,
|
||||
value,
|
||||
title,
|
||||
delta
|
||||
)
|
||||
}
|
||||
if let (value, title, delta) = items[2] {
|
||||
middle2LeftItemLayoutAndApply = makeMiddle2LeftItemLayout(
|
||||
params.width,
|
||||
item.presentationData,
|
||||
value,
|
||||
title,
|
||||
delta
|
||||
)
|
||||
}
|
||||
if let (value, title, delta) = items[3] {
|
||||
middle2RightItemLayoutAndApply = makeMiddle2RightItemLayout(
|
||||
params.width,
|
||||
item.presentationData,
|
||||
value,
|
||||
title,
|
||||
delta
|
||||
)
|
||||
}
|
||||
if let (value, title, delta) = items[4] {
|
||||
bottomLeftItemLayoutAndApply = makeBottomLeftItemLayout(
|
||||
params.width,
|
||||
item.presentationData,
|
||||
value,
|
||||
title,
|
||||
delta
|
||||
)
|
||||
}
|
||||
if let (value, title, delta) = items[5] {
|
||||
bottomRightItemLayoutAndApply = makeBottomRightItemLayout(
|
||||
params.width,
|
||||
item.presentationData,
|
||||
value,
|
||||
title,
|
||||
delta
|
||||
)
|
||||
}
|
||||
|
||||
let valuesCount = CGFloat(2 + items.count)
|
||||
if twoColumnLayout {
|
||||
if displayBottomRow {
|
||||
height += verticalSpacing
|
||||
height += bottomRightValueLabelLayoutAndApply!.0.size.height + bottomRightTitleLabelLayoutAndApply!.0.size.height
|
||||
}
|
||||
let rowsCount = ceil(valuesCount / 2.0)
|
||||
height += topLeftItemLayoutAndApply!.0.height * rowsCount + (verticalSpacing * (rowsCount - 1.0))
|
||||
} else {
|
||||
height += verticalSpacing
|
||||
height += topRightValueLabelLayoutAndApply!.0.size.height + topRightTitleLabelLayoutAndApply!.0.size.height
|
||||
if !stats.viewsPerPost.current.isZero || viewsPerPostDelta.2 {
|
||||
height += verticalSpacing
|
||||
height += bottomLeftValueLabelLayoutAndApply!.0.size.height + bottomLeftTitleLabelLayoutAndApply!.0.size.height
|
||||
}
|
||||
if !stats.sharesPerPost.current.isZero || sharesPerPostDelta.2 {
|
||||
height += verticalSpacing
|
||||
height += bottomRightValueLabelLayoutAndApply!.0.size.height + bottomRightTitleLabelLayoutAndApply!.0.size.height
|
||||
}
|
||||
height += topLeftItemLayoutAndApply!.0.height * valuesCount + (verticalSpacing * (valuesCount - 1.0))
|
||||
}
|
||||
} else if let stats = item.stats as? GroupStats {
|
||||
let viewersDelta = deltaText(stats.viewers)
|
||||
let postersDelta = deltaText(stats.posters)
|
||||
let displayBottomRow = stats.viewers.current > 0 || viewersDelta.2 || stats.posters.current > 0 || postersDelta.2
|
||||
|
||||
topLeftValueLabelLayoutAndApply = makeTopLeftValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: compactNumericCountString(Int(stats.members.current)), font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
topRightValueLabelLayoutAndApply = makeTopRightValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: compactNumericCountString(Int(stats.messages.current)), font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
bottomLeftValueLabelLayoutAndApply = makeBottomLeftValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: displayBottomRow ? compactNumericCountString(Int(stats.viewers.current)) : "", font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
bottomRightValueLabelLayoutAndApply = makeBottomRightValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: displayBottomRow ? compactNumericCountString(Int(stats.posters.current)) : "", font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
topLeftTitleLabelLayoutAndApply = makeTopLeftTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stats_GroupMembers, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
topRightTitleLabelLayoutAndApply = makeTopRightTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stats_GroupMessages, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
bottomLeftTitleLabelLayoutAndApply = makeBottomLeftTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: displayBottomRow ? item.presentationData.strings.Stats_GroupViewers : "", font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
bottomRightTitleLabelLayoutAndApply = makeBottomRightTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: displayBottomRow ? item.presentationData.strings.Stats_GroupPosters : "", font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
|
||||
let membersDelta = deltaText(stats.members)
|
||||
topLeftDeltaLabelLayoutAndApply = makeTopLeftDeltaLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: membersDelta.0, font: deltaFont, textColor: membersDelta.1 ? item.presentationData.theme.list.freeTextSuccessColor : item.presentationData.theme.list.freeTextErrorColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
topLeftItemLayoutAndApply = makeTopLeftItemLayout(
|
||||
params.width,
|
||||
item.presentationData,
|
||||
compactNumericCountString(Int(stats.members.current)),
|
||||
item.presentationData.strings.Stats_GroupMembers,
|
||||
(membersDelta.text, membersDelta.positive ? .positive : .negative)
|
||||
)
|
||||
|
||||
let messagesDelta = deltaText(stats.messages)
|
||||
topRightDeltaLabelLayoutAndApply = makeTopRightDeltaLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: messagesDelta.0, font: deltaFont, textColor: messagesDelta.1 ? item.presentationData.theme.list.freeTextSuccessColor : item.presentationData.theme.list.freeTextErrorColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
topRightItemLayoutAndApply = makeTopRightItemLayout(
|
||||
params.width,
|
||||
item.presentationData,
|
||||
compactNumericCountString(Int(stats.messages.current)),
|
||||
item.presentationData.strings.Stats_GroupMessages,
|
||||
(messagesDelta.text, messagesDelta.positive ? .positive : .negative)
|
||||
)
|
||||
|
||||
bottomLeftDeltaLabelLayoutAndApply = makeBottomLeftDeltaLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: viewersDelta.0, font: deltaFont, textColor: viewersDelta.1 ? item.presentationData.theme.list.freeTextSuccessColor : item.presentationData.theme.list.freeTextErrorColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
bottomRightDeltaLabelLayoutAndApply = makeBottomRightDeltaLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: postersDelta.0, font: deltaFont, textColor: postersDelta.1 ? item.presentationData.theme.list.freeTextSuccessColor : item.presentationData.theme.list.freeTextErrorColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
|
||||
height += topRightValueLabelLayoutAndApply!.0.size.height + topRightTitleLabelLayoutAndApply!.0.size.height
|
||||
|
||||
if max(topLeftValueLabelLayoutAndApply!.0.size.width + topLeftDeltaLabelLayoutAndApply!.0.size.width + horizontalSpacing + topRightValueLabelLayoutAndApply!.0.size.width, bottomLeftValueLabelLayoutAndApply!.0.size.width + bottomLeftDeltaLabelLayoutAndApply!.0.size.width + horizontalSpacing + bottomRightValueLabelLayoutAndApply!.0.size.width + bottomRightDeltaLabelLayoutAndApply!.0.size.width) > params.width - leftInset - rightInset {
|
||||
twoColumnLayout = false
|
||||
if displayBottomRow {
|
||||
middle1LeftItemLayoutAndApply = makeMiddle1LeftItemLayout(
|
||||
params.width,
|
||||
item.presentationData,
|
||||
compactNumericCountString(Int(stats.viewers.current)),
|
||||
item.presentationData.strings.Stats_GroupViewers,
|
||||
(viewersDelta.text, viewersDelta.positive ? .positive : .negative)
|
||||
)
|
||||
|
||||
middle1RightItemLayoutAndApply = makeMiddle1RightItemLayout(
|
||||
params.width,
|
||||
item.presentationData,
|
||||
compactNumericCountString(Int(stats.posters.current)),
|
||||
item.presentationData.strings.Stats_GroupPosters,
|
||||
(postersDelta.text, postersDelta.positive ? .positive : .negative)
|
||||
)
|
||||
}
|
||||
|
||||
if twoColumnLayout {
|
||||
if !stats.viewers.current.isZero || viewersDelta.2 || !stats.posters.current.isZero || postersDelta.2 {
|
||||
height += verticalSpacing
|
||||
height += bottomRightValueLabelLayoutAndApply!.0.size.height + bottomRightTitleLabelLayoutAndApply!.0.size.height
|
||||
}
|
||||
if twoColumnLayout || !displayBottomRow {
|
||||
height += topRightItemLayoutAndApply!.0.height * 2.0 + verticalSpacing
|
||||
} else {
|
||||
height += verticalSpacing
|
||||
height += topRightValueLabelLayoutAndApply!.0.size.height + topRightTitleLabelLayoutAndApply!.0.size.height
|
||||
if !stats.viewers.current.isZero || viewersDelta.2 {
|
||||
height += verticalSpacing
|
||||
height += bottomLeftValueLabelLayoutAndApply!.0.size.height + bottomLeftTitleLabelLayoutAndApply!.0.size.height
|
||||
}
|
||||
if !stats.posters.current.isZero || postersDelta.2 {
|
||||
height += verticalSpacing
|
||||
height += bottomRightValueLabelLayoutAndApply!.0.size.height + bottomRightTitleLabelLayoutAndApply!.0.size.height
|
||||
}
|
||||
height += topLeftItemLayoutAndApply!.0.height * 4.0 + verticalSpacing * 3.0
|
||||
}
|
||||
} else {
|
||||
topLeftValueLabelLayoutAndApply = nil
|
||||
topRightValueLabelLayoutAndApply = nil
|
||||
bottomLeftValueLabelLayoutAndApply = nil
|
||||
bottomRightValueLabelLayoutAndApply = nil
|
||||
topLeftTitleLabelLayoutAndApply = nil
|
||||
topRightTitleLabelLayoutAndApply = nil
|
||||
bottomLeftTitleLabelLayoutAndApply = nil
|
||||
bottomRightTitleLabelLayoutAndApply = nil
|
||||
topLeftDeltaLabelLayoutAndApply = nil
|
||||
topRightDeltaLabelLayoutAndApply = nil
|
||||
bottomLeftDeltaLabelLayoutAndApply = nil
|
||||
bottomRightDeltaLabelLayoutAndApply = nil
|
||||
}
|
||||
|
||||
let contentSize = CGSize(width: params.width, height: height)
|
||||
|
|
@ -405,18 +624,14 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||
if let strongSelf = self {
|
||||
strongSelf.item = item
|
||||
|
||||
let _ = topLeftValueLabelLayoutAndApply?.1()
|
||||
let _ = topRightValueLabelLayoutAndApply?.1()
|
||||
let _ = bottomLeftValueLabelLayoutAndApply?.1()
|
||||
let _ = bottomRightValueLabelLayoutAndApply?.1()
|
||||
let _ = topLeftTitleLabelLayoutAndApply?.1()
|
||||
let _ = topRightTitleLabelLayoutAndApply?.1()
|
||||
let _ = bottomLeftTitleLabelLayoutAndApply?.1()
|
||||
let _ = bottomRightTitleLabelLayoutAndApply?.1()
|
||||
let _ = topLeftDeltaLabelLayoutAndApply?.1()
|
||||
let _ = topRightDeltaLabelLayoutAndApply?.1()
|
||||
let _ = bottomLeftDeltaLabelLayoutAndApply?.1()
|
||||
let _ = bottomRightDeltaLabelLayoutAndApply?.1()
|
||||
let _ = topLeftItemLayoutAndApply?.1()
|
||||
let _ = topRightItemLayoutAndApply?.1()
|
||||
let _ = middle1LeftItemLayoutAndApply?.1()
|
||||
let _ = middle1RightItemLayoutAndApply?.1()
|
||||
let _ = middle2LeftItemLayoutAndApply?.1()
|
||||
let _ = middle2RightItemLayoutAndApply?.1()
|
||||
let _ = bottomLeftItemLayoutAndApply?.1()
|
||||
let _ = bottomRightItemLayoutAndApply?.1()
|
||||
|
||||
if let _ = updatedTheme {
|
||||
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
|
||||
|
|
@ -482,47 +697,63 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight))
|
||||
}
|
||||
|
||||
var secondColumnX = sideInset + leftInset
|
||||
let firstColumnX = sideInset + leftInset
|
||||
var secondColumnX = firstColumnX
|
||||
|
||||
if let topLeftValueLabelLayout = topLeftValueLabelLayoutAndApply?.0, let topLeftTitleLabelLayout = topLeftTitleLabelLayoutAndApply?.0 {
|
||||
strongSelf.topLeftValueLabel.frame = CGRect(origin: CGPoint(x: sideInset + leftInset, y: topInset), size: topLeftValueLabelLayout.size)
|
||||
strongSelf.topLeftTitleLabel.frame = CGRect(origin: CGPoint(x: sideInset + leftInset, y: strongSelf.topLeftValueLabel.frame.maxY), size: topLeftTitleLabelLayout.size)
|
||||
|
||||
if twoColumnLayout {
|
||||
let topWidth = topLeftValueLabelLayout.size.width + (topLeftDeltaLabelLayoutAndApply?.0.size.width ?? 0)
|
||||
let bottomWidth = (bottomLeftValueLabelLayoutAndApply?.0.size.width ?? 0.0) + (bottomLeftDeltaLabelLayoutAndApply?.0.size.width ?? 0.0)
|
||||
secondColumnX = max(layout.size.width / 2.0, sideInset + leftInset + max(topWidth, bottomWidth) + horizontalSpacing)
|
||||
if twoColumnLayout {
|
||||
var maxLeftWidth: CGFloat = 0.0
|
||||
if let topLeftItemLayout = topLeftItemLayoutAndApply?.0 {
|
||||
maxLeftWidth = max(maxLeftWidth, topLeftItemLayout.width)
|
||||
}
|
||||
}
|
||||
if let topLeftDeltaLabelLayout = topLeftDeltaLabelLayoutAndApply?.0 {
|
||||
strongSelf.topLeftDeltaLabel.frame = CGRect(origin: CGPoint(x: strongSelf.topLeftValueLabel.frame.maxX + horizontalSpacing, y: strongSelf.topLeftValueLabel.frame.maxY - topLeftDeltaLabelLayout.size.height - 2.0), size: topLeftDeltaLabelLayout.size)
|
||||
if let middle1LeftItemLayout = middle1LeftItemLayoutAndApply?.0 {
|
||||
maxLeftWidth = max(maxLeftWidth, middle1LeftItemLayout.width)
|
||||
}
|
||||
if let middle2LeftItemLayout = middle2LeftItemLayoutAndApply?.0 {
|
||||
maxLeftWidth = max(maxLeftWidth, middle2LeftItemLayout.width)
|
||||
}
|
||||
if let bottomLeftItemLayout = bottomLeftItemLayoutAndApply?.0 {
|
||||
maxLeftWidth = max(maxLeftWidth, bottomLeftItemLayout.width)
|
||||
}
|
||||
secondColumnX = max(layout.size.width / 2.0, firstColumnX + maxLeftWidth + horizontalSpacing)
|
||||
}
|
||||
|
||||
if let topRightValueLabelLayout = topRightValueLabelLayoutAndApply?.0, let topRightTitleLabelLayout = topRightTitleLabelLayoutAndApply?.0 {
|
||||
let topRightY = twoColumnLayout ? topInset : strongSelf.topLeftTitleLabel.frame.maxY + verticalSpacing
|
||||
strongSelf.topRightValueLabel.frame = CGRect(origin: CGPoint(x: secondColumnX, y: topRightY), size: topRightValueLabelLayout.size)
|
||||
strongSelf.topRightTitleLabel.frame = CGRect(origin: CGPoint(x: secondColumnX, y: strongSelf.topRightValueLabel.frame.maxY), size: topRightTitleLabelLayout.size)
|
||||
}
|
||||
if let topRightDeltaLabelLayout = topRightDeltaLabelLayoutAndApply?.0 {
|
||||
strongSelf.topRightDeltaLabel.frame = CGRect(origin: CGPoint(x: strongSelf.topRightValueLabel.frame.maxX + horizontalSpacing, y: strongSelf.topRightValueLabel.frame.maxY - topRightDeltaLabelLayout.size.height - 2.0), size: topRightDeltaLabelLayout.size)
|
||||
if let topLeftItemLayout = topLeftItemLayoutAndApply?.0 {
|
||||
strongSelf.topLeftItem.frame = CGRect(origin: CGPoint(x: firstColumnX, y: topInset), size: topLeftItemLayout)
|
||||
}
|
||||
|
||||
if let bottomLeftValueLabelLayout = bottomLeftValueLabelLayoutAndApply?.0, let bottomLeftTitleLabelLayout = bottomLeftTitleLabelLayoutAndApply?.0 {
|
||||
let bottomLeftY = twoColumnLayout ? strongSelf.topLeftTitleLabel.frame.maxY + verticalSpacing : strongSelf.topRightTitleLabel.frame.maxY + verticalSpacing
|
||||
strongSelf.bottomLeftValueLabel.frame = CGRect(origin: CGPoint(x: sideInset + leftInset, y: bottomLeftY), size: bottomLeftValueLabelLayout.size)
|
||||
strongSelf.bottomLeftTitleLabel.frame = CGRect(origin: CGPoint(x: sideInset + leftInset, y: strongSelf.bottomLeftValueLabel.frame.maxY), size: bottomLeftTitleLabelLayout.size)
|
||||
}
|
||||
if let bottomLeftDeltaLabelLayout = bottomLeftDeltaLabelLayoutAndApply?.0 {
|
||||
strongSelf.bottomLeftDeltaLabel.frame = CGRect(origin: CGPoint(x: strongSelf.bottomLeftValueLabel.frame.maxX + horizontalSpacing, y: strongSelf.bottomLeftValueLabel.frame.maxY - bottomLeftDeltaLabelLayout.size.height - 2.0), size: bottomLeftDeltaLabelLayout.size)
|
||||
if let topRightItemLayout = topRightItemLayoutAndApply?.0 {
|
||||
let originY = twoColumnLayout ? topInset : strongSelf.topLeftItem.frame.maxY + verticalSpacing
|
||||
strongSelf.topRightItem.frame = CGRect(origin: CGPoint(x: secondColumnX, y: originY), size: topRightItemLayout)
|
||||
}
|
||||
|
||||
if let bottomRightValueLabelLayout = bottomRightValueLabelLayoutAndApply?.0, let bottomRightTitleLabelLayout = bottomRightTitleLabelLayoutAndApply?.0 {
|
||||
let bottomRightY = twoColumnLayout ? strongSelf.topRightTitleLabel.frame.maxY + verticalSpacing : strongSelf.bottomLeftTitleLabel.frame.maxY + verticalSpacing
|
||||
strongSelf.bottomRightValueLabel.frame = CGRect(origin: CGPoint(x: secondColumnX, y: bottomRightY), size: bottomRightValueLabelLayout.size)
|
||||
strongSelf.bottomRightTitleLabel.frame = CGRect(origin: CGPoint(x: secondColumnX, y: strongSelf.bottomRightValueLabel.frame.maxY), size: bottomRightTitleLabelLayout.size)
|
||||
if let middle1LeftItemLayout = middle1LeftItemLayoutAndApply?.0 {
|
||||
let originY = (twoColumnLayout ? strongSelf.topLeftItem.frame.maxY : strongSelf.topRightItem.frame.maxY) + verticalSpacing
|
||||
strongSelf.middle1LeftItem.frame = CGRect(origin: CGPoint(x: firstColumnX, y: originY), size: middle1LeftItemLayout)
|
||||
}
|
||||
if let bottomRightDeltaLabelLayout = bottomRightDeltaLabelLayoutAndApply?.0 {
|
||||
strongSelf.bottomRightDeltaLabel.frame = CGRect(origin: CGPoint(x: strongSelf.bottomRightValueLabel.frame.maxX + horizontalSpacing, y: strongSelf.bottomRightValueLabel.frame.maxY - bottomRightDeltaLabelLayout.size.height - 2.0), size: bottomRightDeltaLabelLayout.size)
|
||||
|
||||
if let middle1RightItemLayout = middle1RightItemLayoutAndApply?.0 {
|
||||
let originY = (twoColumnLayout ? strongSelf.topRightItem.frame.maxY : strongSelf.middle1LeftItem.frame.maxY) + verticalSpacing
|
||||
strongSelf.middle1RightItem.frame = CGRect(origin: CGPoint(x: secondColumnX, y: originY), size: middle1RightItemLayout)
|
||||
}
|
||||
|
||||
if let middle2LeftItemLayout = middle2LeftItemLayoutAndApply?.0 {
|
||||
let originY = (twoColumnLayout ? strongSelf.middle1LeftItem.frame.maxY : strongSelf.middle1RightItem.frame.maxY) + verticalSpacing
|
||||
strongSelf.middle2LeftItem.frame = CGRect(origin: CGPoint(x: firstColumnX, y: originY), size: middle2LeftItemLayout)
|
||||
}
|
||||
|
||||
if let middle2RightItemLayout = middle2RightItemLayoutAndApply?.0 {
|
||||
let originY = (twoColumnLayout ? strongSelf.middle1RightItem.frame.maxY : strongSelf.middle2LeftItem.frame.maxY) + verticalSpacing
|
||||
strongSelf.middle2RightItem.frame = CGRect(origin: CGPoint(x: secondColumnX, y: originY), size: middle2RightItemLayout)
|
||||
}
|
||||
|
||||
if let bottomLeftItemLayout = bottomLeftItemLayoutAndApply?.0 {
|
||||
let originY = (twoColumnLayout ? strongSelf.middle2LeftItem.frame.maxY : strongSelf.middle2RightItem.frame.maxY) + verticalSpacing
|
||||
strongSelf.bottomLeftItem.frame = CGRect(origin: CGPoint(x: firstColumnX, y: originY), size: bottomLeftItemLayout)
|
||||
}
|
||||
|
||||
if let bottomRightItemLayout = bottomRightItemLayoutAndApply?.0 {
|
||||
let originY = (twoColumnLayout ? strongSelf.middle2RightItem.frame.maxY : strongSelf.bottomLeftItem.frame.maxY) + verticalSpacing
|
||||
strongSelf.bottomRightItem.frame = CGRect(origin: CGPoint(x: secondColumnX, y: originY), size: bottomRightItemLayout)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1160,7 +1160,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
|||
dict[946083368] = { return Api.messages.StickerSetInstallResult.parse_stickerSetInstallResultSuccess($0) }
|
||||
dict[816245886] = { return Api.messages.Stickers.parse_stickers($0) }
|
||||
dict[-244016606] = { return Api.messages.Stickers.parse_stickersNotModified($0) }
|
||||
dict[-1821037486] = { return Api.messages.TranscribedAudio.parse_transcribedAudio($0) }
|
||||
dict[-809903785] = { return Api.messages.TranscribedAudio.parse_transcribedAudio($0) }
|
||||
dict[870003448] = { return Api.messages.TranslatedText.parse_translateResult($0) }
|
||||
dict[1218005070] = { return Api.messages.VotesList.parse_votesList($0) }
|
||||
dict[-44166467] = { return Api.messages.WebPage.parse_webPage($0) }
|
||||
|
|
|
|||
|
|
@ -274,25 +274,27 @@ public extension Api.messages {
|
|||
}
|
||||
public extension Api.messages {
|
||||
enum TranscribedAudio: TypeConstructorDescription {
|
||||
case transcribedAudio(flags: Int32, transcriptionId: Int64, text: String)
|
||||
case transcribedAudio(flags: Int32, transcriptionId: Int64, text: String, trialRemainsNum: Int32?, trialRemainsUntilDate: Int32?)
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .transcribedAudio(let flags, let transcriptionId, let text):
|
||||
case .transcribedAudio(let flags, let transcriptionId, let text, let trialRemainsNum, let trialRemainsUntilDate):
|
||||
if boxed {
|
||||
buffer.appendInt32(-1821037486)
|
||||
buffer.appendInt32(-809903785)
|
||||
}
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
serializeInt64(transcriptionId, buffer: buffer, boxed: false)
|
||||
serializeString(text, buffer: buffer, boxed: false)
|
||||
if Int(flags) & Int(1 << 1) != 0 {serializeInt32(trialRemainsNum!, buffer: buffer, boxed: false)}
|
||||
if Int(flags) & Int(1 << 1) != 0 {serializeInt32(trialRemainsUntilDate!, buffer: buffer, boxed: false)}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||
switch self {
|
||||
case .transcribedAudio(let flags, let transcriptionId, let text):
|
||||
return ("transcribedAudio", [("flags", flags as Any), ("transcriptionId", transcriptionId as Any), ("text", text as Any)])
|
||||
case .transcribedAudio(let flags, let transcriptionId, let text, let trialRemainsNum, let trialRemainsUntilDate):
|
||||
return ("transcribedAudio", [("flags", flags as Any), ("transcriptionId", transcriptionId as Any), ("text", text as Any), ("trialRemainsNum", trialRemainsNum as Any), ("trialRemainsUntilDate", trialRemainsUntilDate as Any)])
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -303,11 +305,17 @@ public extension Api.messages {
|
|||
_2 = reader.readInt64()
|
||||
var _3: String?
|
||||
_3 = parseString(reader)
|
||||
var _4: Int32?
|
||||
if Int(_1!) & Int(1 << 1) != 0 {_4 = reader.readInt32() }
|
||||
var _5: Int32?
|
||||
if Int(_1!) & Int(1 << 1) != 0 {_5 = reader.readInt32() }
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = _2 != nil
|
||||
let _c3 = _3 != nil
|
||||
if _c1 && _c2 && _c3 {
|
||||
return Api.messages.TranscribedAudio.transcribedAudio(flags: _1!, transcriptionId: _2!, text: _3!)
|
||||
let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil
|
||||
let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 && _c5 {
|
||||
return Api.messages.TranscribedAudio.transcribedAudio(flags: _1!, transcriptionId: _2!, text: _3!, trialRemainsNum: _4, trialRemainsUntilDate: _5)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ private func uploadPeerMedia(postbox: Postbox, network: Network, stateManager: A
|
|||
|
||||
}
|
||||
} else {
|
||||
return _internal_setChatWallpaper(postbox: postbox, network: network, stateManager: stateManager, peerId: peerId, wallpaper: wallpaper, forBoth: forBoth, applyUpdates: false)
|
||||
return _internal_setChatWallpaper(postbox: postbox, network: network, stateManager: stateManager, peerId: peerId, wallpaper: wallpaper, forBoth: forBoth, applyUpdates: false)
|
||||
|> mapError { error -> PeerMediaUploadingItem.Error in
|
||||
switch error {
|
||||
case .generic:
|
||||
|
|
|
|||
|
|
@ -4652,7 +4652,8 @@ func replayFinalState(
|
|||
isForwardingDisabled: item.isForwardingDisabled,
|
||||
isEdited: item.isEdited,
|
||||
isMy: item.isMy,
|
||||
myReaction: updatedReaction
|
||||
myReaction: updatedReaction,
|
||||
forwardInfo: item.forwardInfo
|
||||
))
|
||||
if let entry = CodableEntry(updatedItem) {
|
||||
updatedPeerEntries[index] = StoryItemsTableEntry(value: entry, id: item.id, expirationTimestamp: item.expirationTimestamp, isCloseFriends: item.isCloseFriends)
|
||||
|
|
@ -4683,7 +4684,8 @@ func replayFinalState(
|
|||
isForwardingDisabled: item.isForwardingDisabled,
|
||||
isEdited: item.isEdited,
|
||||
isMy: item.isMy,
|
||||
myReaction: MessageReaction.Reaction(apiReaction: reaction)
|
||||
myReaction: MessageReaction.Reaction(apiReaction: reaction),
|
||||
forwardInfo: item.forwardInfo
|
||||
))
|
||||
if let entry = CodableEntry(updatedItem) {
|
||||
transaction.setStory(id: StoryId(peerId: peerId, id: id), value: entry)
|
||||
|
|
|
|||
|
|
@ -33,6 +33,12 @@ func updateAppConfigurationOnce(postbox: Postbox, network: Network) -> Signal<Vo
|
|||
configuration.hash = result.hash
|
||||
return configuration
|
||||
})
|
||||
|
||||
if let audioTranscriptionCooldownUntilTimestamp = data["transcribe_audio_trial_cooldown_until"] as? Double {
|
||||
_internal_updateAudioTranscriptionTrialState(transaction: transaction, { $0.withUpdatedCooldownUntilTime(Int32(audioTranscriptionCooldownUntilTimestamp)) })
|
||||
} else {
|
||||
_internal_updateAudioTranscriptionTrialState(transaction: transaction, { $0.withUpdatedCooldownUntilTime(nil) })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,13 +7,15 @@ import MtProtoKit
|
|||
public struct MessageStats: Equatable {
|
||||
public let views: Int
|
||||
public let forwards: Int
|
||||
public let reactions: Int
|
||||
public let interactionsGraph: StatsGraph
|
||||
public let interactionsGraphDelta: Int64
|
||||
public let reactionsGraph: StatsGraph
|
||||
|
||||
init(views: Int, forwards: Int, interactionsGraph: StatsGraph, interactionsGraphDelta: Int64, reactionsGraph: StatsGraph) {
|
||||
init(views: Int, forwards: Int, reactions: Int, interactionsGraph: StatsGraph, interactionsGraphDelta: Int64, reactionsGraph: StatsGraph) {
|
||||
self.views = views
|
||||
self.forwards = forwards
|
||||
self.reactions = reactions
|
||||
self.interactionsGraph = interactionsGraph
|
||||
self.interactionsGraphDelta = interactionsGraphDelta
|
||||
self.reactionsGraph = reactionsGraph
|
||||
|
|
@ -26,6 +28,9 @@ public struct MessageStats: Equatable {
|
|||
if lhs.forwards != rhs.forwards {
|
||||
return false
|
||||
}
|
||||
if lhs.reactions != rhs.reactions {
|
||||
return false
|
||||
}
|
||||
if lhs.interactionsGraph != rhs.interactionsGraph {
|
||||
return false
|
||||
}
|
||||
|
|
@ -39,7 +44,7 @@ public struct MessageStats: Equatable {
|
|||
}
|
||||
|
||||
public func withUpdatedInteractionsGraph(_ interactionsGraph: StatsGraph) -> MessageStats {
|
||||
return MessageStats(views: self.views, forwards: self.forwards, interactionsGraph: interactionsGraph, interactionsGraphDelta: self.interactionsGraphDelta, reactionsGraph: self.reactionsGraph)
|
||||
return MessageStats(views: self.views, forwards: self.forwards, reactions: self.reactions, interactionsGraph: interactionsGraph, interactionsGraphDelta: self.interactionsGraphDelta, reactionsGraph: self.reactionsGraph)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -78,11 +83,16 @@ private func requestMessageStats(postbox: Postbox, network: Network, messageId:
|
|||
|
||||
var views: Int = 0
|
||||
var forwards: Int = 0
|
||||
var reactions: Int = 0
|
||||
for attribute in message.attributes {
|
||||
if let viewsAttribute = attribute as? ViewCountMessageAttribute {
|
||||
views = viewsAttribute.count
|
||||
} else if let forwardsAttribute = attribute as? ForwardCountMessageAttribute {
|
||||
forwards = forwardsAttribute.count
|
||||
} else if let reactionsAttribute = attribute as? ReactionsMessageAttribute {
|
||||
reactions = Int(reactionsAttribute.reactions.reduce(0, { partialResult, reaction in
|
||||
return partialResult + reaction.count
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -110,6 +120,7 @@ private func requestMessageStats(postbox: Postbox, network: Network, messageId:
|
|||
return .single(MessageStats(
|
||||
views: views,
|
||||
forwards: forwards,
|
||||
reactions: reactions,
|
||||
interactionsGraph: interactionsGraph,
|
||||
interactionsGraphDelta: interactionsGraphDelta,
|
||||
reactionsGraph: reactionsGraph
|
||||
|
|
@ -196,9 +207,9 @@ public final class MessageStatsContext {
|
|||
}
|
||||
}
|
||||
|
||||
public init(postbox: Postbox, network: Network, messageId: MessageId) {
|
||||
public init(account: Account, messageId: MessageId) {
|
||||
self.impl = QueueLocalObject(queue: Queue.mainQueue(), generate: {
|
||||
return MessageStatsContextImpl(postbox: postbox, network: network, messageId: messageId)
|
||||
return MessageStatsContextImpl(postbox: account.postbox, network: account.network, messageId: messageId)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,13 +7,15 @@ import MtProtoKit
|
|||
public struct StoryStats: Equatable {
|
||||
public let views: Int
|
||||
public let forwards: Int
|
||||
public let reactions: Int
|
||||
public let interactionsGraph: StatsGraph
|
||||
public let interactionsGraphDelta: Int64
|
||||
public let reactionsGraph: StatsGraph
|
||||
|
||||
init(views: Int, forwards: Int, interactionsGraph: StatsGraph, interactionsGraphDelta: Int64, reactionsGraph: StatsGraph) {
|
||||
init(views: Int, forwards: Int, reactions: Int, interactionsGraph: StatsGraph, interactionsGraphDelta: Int64, reactionsGraph: StatsGraph) {
|
||||
self.views = views
|
||||
self.forwards = forwards
|
||||
self.reactions = reactions
|
||||
self.interactionsGraph = interactionsGraph
|
||||
self.interactionsGraphDelta = interactionsGraphDelta
|
||||
self.reactionsGraph = reactionsGraph
|
||||
|
|
@ -26,6 +28,9 @@ public struct StoryStats: Equatable {
|
|||
if lhs.forwards != rhs.forwards {
|
||||
return false
|
||||
}
|
||||
if lhs.reactions != rhs.reactions {
|
||||
return false
|
||||
}
|
||||
if lhs.interactionsGraph != rhs.interactionsGraph {
|
||||
return false
|
||||
}
|
||||
|
|
@ -39,7 +44,7 @@ public struct StoryStats: Equatable {
|
|||
}
|
||||
|
||||
public func withUpdatedInteractionsGraph(_ interactionsGraph: StatsGraph) -> StoryStats {
|
||||
return StoryStats(views: self.views, forwards: self.forwards, interactionsGraph: interactionsGraph, interactionsGraphDelta: self.interactionsGraphDelta, reactionsGraph: self.reactionsGraph)
|
||||
return StoryStats(views: self.views, forwards: self.forwards, reactions: self.reactions, interactionsGraph: interactionsGraph, interactionsGraphDelta: self.interactionsGraphDelta, reactionsGraph: self.reactionsGraph)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -47,79 +52,90 @@ public struct StoryStatsContextState: Equatable {
|
|||
public var stats: StoryStats?
|
||||
}
|
||||
|
||||
private func requestStoryStats(postbox: Postbox, network: Network, peerId: EnginePeer.Id, storyId: Int32, dark: Bool = false) -> Signal<StoryStats?, NoError> {
|
||||
return postbox.transaction { transaction -> (Int32, Peer, Stories.Item)? in
|
||||
if let peer = transaction.getPeer(peerId), let storedItem = transaction.getStory(id: StoryId(peerId: peerId, id: storyId))?.get(Stories.StoredItem.self), case let .item(story) = storedItem, let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData {
|
||||
return (cachedData.statsDatacenterId, peer, story)
|
||||
private func requestStoryStats(accountPeerId: PeerId, postbox: Postbox, network: Network, peerId: EnginePeer.Id, storyId: Int32, dark: Bool = false) -> Signal<StoryStats?, NoError> {
|
||||
return postbox.transaction { transaction -> (Int32, Peer)? in
|
||||
if let peer = transaction.getPeer(peerId), let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData {
|
||||
return (cachedData.statsDatacenterId, peer)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
} |> mapToSignal { data -> Signal<StoryStats?, NoError> in
|
||||
guard let (statsDatacenterId, peer, story) = data, let inputPeer = apiInputPeer(peer) else {
|
||||
}
|
||||
|> mapToSignal { data -> Signal<StoryStats?, NoError> in
|
||||
guard let (statsDatacenterId, peer) = data, let peerReference = PeerReference(peer) else {
|
||||
return .never()
|
||||
}
|
||||
|
||||
var flags: Int32 = 0
|
||||
if dark {
|
||||
flags |= (1 << 1)
|
||||
}
|
||||
|
||||
let request = Api.functions.stats.getStoryStats(flags: flags, peer: inputPeer, id: storyId)
|
||||
let signal: Signal<Api.stats.StoryStats, MTRpcError>
|
||||
if network.datacenterId != statsDatacenterId {
|
||||
signal = network.download(datacenterId: Int(statsDatacenterId), isMedia: false, tag: nil)
|
||||
|> castError(MTRpcError.self)
|
||||
|> mapToSignal { worker in
|
||||
return worker.request(request)
|
||||
return _internal_getStoriesById(accountPeerId: accountPeerId, postbox: postbox, network: network, peer: peerReference, ids: [storyId])
|
||||
|> mapToSignal { stories -> Signal<StoryStats?, NoError> in
|
||||
guard let storyItem = stories.first, case let .item(story) = storyItem, let inputPeer = apiInputPeer(peer) else {
|
||||
return .never()
|
||||
}
|
||||
} else {
|
||||
signal = network.request(request)
|
||||
}
|
||||
|
||||
var views: Int = 0
|
||||
var forwards: Int = 0
|
||||
if let storyViews = story.views {
|
||||
views = storyViews.seenCount
|
||||
forwards = storyViews.forwardCount
|
||||
}
|
||||
|
||||
return signal
|
||||
|> mapToSignal { result -> Signal<StoryStats?, MTRpcError> in
|
||||
if case let .storyStats(apiInteractionsGraph, apiReactionsGraph) = result {
|
||||
let interactionsGraph = StatsGraph(apiStatsGraph: apiInteractionsGraph)
|
||||
var interactionsGraphDelta: Int64 = 86400
|
||||
if case let .Loaded(_, data) = interactionsGraph {
|
||||
if let start = data.range(of: "[\"x\",") {
|
||||
let substring = data.suffix(from: start.upperBound)
|
||||
if let end = substring.range(of: "],") {
|
||||
let valuesString = substring.prefix(through: substring.index(before: end.lowerBound))
|
||||
let values = valuesString.components(separatedBy: ",").compactMap { Int64($0) }
|
||||
if values.count > 1 {
|
||||
let first = values[0]
|
||||
let second = values[1]
|
||||
let delta = abs(second - first) / 1000
|
||||
interactionsGraphDelta = delta
|
||||
|
||||
var flags: Int32 = 0
|
||||
if dark {
|
||||
flags |= (1 << 1)
|
||||
}
|
||||
|
||||
let request = Api.functions.stats.getStoryStats(flags: flags, peer: inputPeer, id: storyId)
|
||||
let signal: Signal<Api.stats.StoryStats, MTRpcError>
|
||||
if network.datacenterId != statsDatacenterId {
|
||||
signal = network.download(datacenterId: Int(statsDatacenterId), isMedia: false, tag: nil)
|
||||
|> castError(MTRpcError.self)
|
||||
|> mapToSignal { worker in
|
||||
return worker.request(request)
|
||||
}
|
||||
} else {
|
||||
signal = network.request(request)
|
||||
}
|
||||
|
||||
var views: Int = 0
|
||||
var forwards: Int = 0
|
||||
var reactions: Int = 0
|
||||
if let storyViews = story.views {
|
||||
views = storyViews.seenCount
|
||||
forwards = storyViews.forwardCount
|
||||
reactions = storyViews.reactedCount
|
||||
}
|
||||
|
||||
return signal
|
||||
|> mapToSignal { result -> Signal<StoryStats?, MTRpcError> in
|
||||
if case let .storyStats(apiInteractionsGraph, apiReactionsGraph) = result {
|
||||
let interactionsGraph = StatsGraph(apiStatsGraph: apiInteractionsGraph)
|
||||
var interactionsGraphDelta: Int64 = 86400
|
||||
if case let .Loaded(_, data) = interactionsGraph {
|
||||
if let start = data.range(of: "[\"x\",") {
|
||||
let substring = data.suffix(from: start.upperBound)
|
||||
if let end = substring.range(of: "],") {
|
||||
let valuesString = substring.prefix(through: substring.index(before: end.lowerBound))
|
||||
let values = valuesString.components(separatedBy: ",").compactMap { Int64($0) }
|
||||
if values.count > 1 {
|
||||
let first = values[0]
|
||||
let second = values[1]
|
||||
let delta = abs(second - first) / 1000
|
||||
interactionsGraphDelta = delta
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let reactionsGraph = StatsGraph(apiStatsGraph: apiReactionsGraph)
|
||||
return .single(StoryStats(
|
||||
views: views,
|
||||
forwards: forwards,
|
||||
reactions: reactions,
|
||||
interactionsGraph: interactionsGraph,
|
||||
interactionsGraphDelta: interactionsGraphDelta,
|
||||
reactionsGraph: reactionsGraph
|
||||
))
|
||||
} else {
|
||||
return .single(nil)
|
||||
}
|
||||
let reactionsGraph = StatsGraph(apiStatsGraph: apiReactionsGraph)
|
||||
return .single(StoryStats(
|
||||
views: views,
|
||||
forwards: forwards,
|
||||
interactionsGraph: interactionsGraph,
|
||||
interactionsGraphDelta: interactionsGraphDelta,
|
||||
reactionsGraph: reactionsGraph
|
||||
))
|
||||
} else {
|
||||
return .single(nil)
|
||||
}
|
||||
|> retryRequest
|
||||
}
|
||||
|> retryRequest
|
||||
}
|
||||
}
|
||||
|
||||
private final class StoryStatsContextImpl {
|
||||
private let accountPeerId: EnginePeer.Id
|
||||
private let postbox: Postbox
|
||||
private let network: Network
|
||||
private let peerId: EnginePeer.Id
|
||||
|
|
@ -140,9 +156,10 @@ private final class StoryStatsContextImpl {
|
|||
private let disposable = MetaDisposable()
|
||||
private let disposables = DisposableDict<String>()
|
||||
|
||||
init(postbox: Postbox, network: Network, peerId: EnginePeer.Id, storyId: Int32) {
|
||||
init(accountPeerId: EnginePeer.Id, postbox: Postbox, network: Network, peerId: EnginePeer.Id, storyId: Int32) {
|
||||
assert(Queue.mainQueue().isCurrent())
|
||||
|
||||
self.accountPeerId = accountPeerId
|
||||
self.postbox = postbox
|
||||
self.network = network
|
||||
self.peerId = peerId
|
||||
|
|
@ -162,7 +179,7 @@ private final class StoryStatsContextImpl {
|
|||
private func load() {
|
||||
assert(Queue.mainQueue().isCurrent())
|
||||
|
||||
self.disposable.set((requestStoryStats(postbox: self.postbox, network: self.network, peerId: self.peerId, storyId: self.storyId)
|
||||
self.disposable.set((requestStoryStats(accountPeerId: self.accountPeerId, postbox: self.postbox, network: self.network, peerId: self.peerId, storyId: self.storyId)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] stats in
|
||||
if let strongSelf = self {
|
||||
strongSelf._state = StoryStatsContextState(stats: stats)
|
||||
|
|
@ -195,9 +212,9 @@ public final class StoryStatsContext {
|
|||
}
|
||||
}
|
||||
|
||||
public init(postbox: Postbox, network: Network, peerId: EnginePeer.Id, storyId: Int32) {
|
||||
public init(account: Account, peerId: EnginePeer.Id, storyId: Int32) {
|
||||
self.impl = QueueLocalObject(queue: Queue.mainQueue(), generate: {
|
||||
return StoryStatsContextImpl(postbox: postbox, network: network, peerId: peerId, storyId: storyId)
|
||||
return StoryStatsContextImpl(accountPeerId: account.peerId, postbox: account.postbox, network: account.network, peerId: peerId, storyId: storyId)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -269,6 +269,7 @@ private enum PreferencesKeyValues: Int32 {
|
|||
case chatListFilterUpdates = 30
|
||||
case globalPrivacySettings = 31
|
||||
case storiesConfiguration = 32
|
||||
case audioTranscriptionTrialState = 33
|
||||
}
|
||||
|
||||
public func applicationSpecificPreferencesKey(_ value: Int32) -> ValueBoxKey {
|
||||
|
|
@ -433,6 +434,12 @@ public struct PreferencesKeys {
|
|||
key.setInt32(0, value: PreferencesKeyValues.storiesConfiguration.rawValue)
|
||||
return key
|
||||
}()
|
||||
|
||||
public static let audioTranscriptionTrialState: ValueBoxKey = {
|
||||
let key = ValueBoxKey(length: 4)
|
||||
key.setInt32(0, value: PreferencesKeyValues.audioTranscriptionTrialState.rawValue)
|
||||
return key
|
||||
}()
|
||||
}
|
||||
|
||||
private enum SharedDataKeyValues: Int32 {
|
||||
|
|
|
|||
|
|
@ -502,5 +502,26 @@ public extension TelegramEngine.EngineData.Item {
|
|||
return value
|
||||
}
|
||||
}
|
||||
|
||||
public struct AudioTranscriptionTrial: TelegramEngineDataItem, PostboxViewDataItem {
|
||||
public typealias Result = AudioTranscription.TrialState
|
||||
|
||||
public init() {
|
||||
}
|
||||
|
||||
var key: PostboxViewKey {
|
||||
return .preferences(keys: Set([PreferencesKeys.audioTranscriptionTrialState]))
|
||||
}
|
||||
|
||||
func extract(view: PostboxView) -> Result {
|
||||
guard let view = view as? PreferencesView else {
|
||||
preconditionFailure()
|
||||
}
|
||||
guard let value = view.values[PreferencesKeys.audioTranscriptionTrialState]?.get(AudioTranscription.TrialState.self) else {
|
||||
return AudioTranscription.TrialState.defaultValue
|
||||
}
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ public final class EngineStoryViewListContext {
|
|||
}
|
||||
|
||||
public enum SortMode {
|
||||
case repostsFirst
|
||||
case reactionsFirst
|
||||
case recentFirst
|
||||
}
|
||||
|
|
@ -176,6 +177,16 @@ public final class EngineStoryViewListContext {
|
|||
}
|
||||
}
|
||||
switch sortMode {
|
||||
case .repostsFirst:
|
||||
items.sort(by: { lhs, rhs in
|
||||
if (lhs.reaction == nil) != (rhs.reaction == nil) {
|
||||
return lhs.reaction != nil
|
||||
}
|
||||
if lhs.timestamp != rhs.timestamp {
|
||||
return lhs.timestamp > rhs.timestamp
|
||||
}
|
||||
return lhs.peer.id < rhs.peer.id
|
||||
})
|
||||
case .reactionsFirst:
|
||||
items.sort(by: { lhs, rhs in
|
||||
if (lhs.reaction == nil) != (rhs.reaction == nil) {
|
||||
|
|
@ -287,7 +298,7 @@ public final class EngineStoryViewListContext {
|
|||
switch sortMode {
|
||||
case .reactionsFirst:
|
||||
flags |= (1 << 2)
|
||||
case .recentFirst:
|
||||
case .recentFirst, .repostsFirst:
|
||||
break
|
||||
}
|
||||
if searchQuery != nil {
|
||||
|
|
@ -376,7 +387,8 @@ public final class EngineStoryViewListContext {
|
|||
isForwardingDisabled: item.isForwardingDisabled,
|
||||
isEdited: item.isEdited,
|
||||
isMy: item.isMy,
|
||||
myReaction: item.myReaction
|
||||
myReaction: item.myReaction,
|
||||
forwardInfo: item.forwardInfo
|
||||
))
|
||||
if let entry = CodableEntry(updatedItem) {
|
||||
transaction.setStory(id: StoryId(peerId: account.peerId, id: storyId), value: entry)
|
||||
|
|
@ -413,7 +425,8 @@ public final class EngineStoryViewListContext {
|
|||
isForwardingDisabled: item.isForwardingDisabled,
|
||||
isEdited: item.isEdited,
|
||||
isMy: item.isMy,
|
||||
myReaction: item.myReaction
|
||||
myReaction: item.myReaction,
|
||||
forwardInfo: item.forwardInfo
|
||||
))
|
||||
if let entry = CodableEntry(updatedItem) {
|
||||
currentItems[i] = StoryItemsTableEntry(value: entry, id: updatedItem.id, expirationTimestamp: updatedItem.expirationTimestamp, isCloseFriends: updatedItem.isCloseFriends)
|
||||
|
|
|
|||
|
|
@ -39,6 +39,40 @@ public extension Stories {
|
|||
}
|
||||
}
|
||||
|
||||
struct PendingForwardInfo: Codable, Equatable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case peerId = "peerId"
|
||||
case storyId = "storyId"
|
||||
case isForwardingDisabled = "isForwardingDisabled"
|
||||
}
|
||||
|
||||
public let peerId: EnginePeer.Id
|
||||
public let storyId: Int32
|
||||
public let isForwardingDisabled: Bool
|
||||
|
||||
public init(peerId: EnginePeer.Id, storyId: Int32, isForwardingDisabled: Bool) {
|
||||
self.peerId = peerId
|
||||
self.storyId = storyId
|
||||
self.isForwardingDisabled = isForwardingDisabled
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
self.peerId = EnginePeer.Id(try container.decode(Int64.self, forKey: .peerId))
|
||||
self.storyId = try container.decode(Int32.self, forKey: .storyId)
|
||||
self.isForwardingDisabled = try container.decode(Bool.self, forKey: .isForwardingDisabled)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
try container.encode(self.peerId.toInt64(), forKey: .peerId)
|
||||
try container.encode(self.storyId, forKey: .storyId)
|
||||
try container.encode(self.isForwardingDisabled, forKey: .isForwardingDisabled)
|
||||
}
|
||||
}
|
||||
|
||||
final class PendingItem: Equatable, Codable {
|
||||
private enum CodingKeys: CodingKey {
|
||||
case target
|
||||
|
|
@ -54,6 +88,7 @@ public extension Stories {
|
|||
case isForwardingDisabled
|
||||
case period
|
||||
case randomId
|
||||
case forwardInfo
|
||||
}
|
||||
|
||||
public let target: PendingTarget
|
||||
|
|
@ -69,6 +104,7 @@ public extension Stories {
|
|||
public let isForwardingDisabled: Bool
|
||||
public let period: Int32
|
||||
public let randomId: Int64
|
||||
public let forwardInfo: PendingForwardInfo?
|
||||
|
||||
public init(
|
||||
target: PendingTarget,
|
||||
|
|
@ -83,7 +119,8 @@ public extension Stories {
|
|||
privacy: EngineStoryPrivacy,
|
||||
isForwardingDisabled: Bool,
|
||||
period: Int32,
|
||||
randomId: Int64
|
||||
randomId: Int64,
|
||||
forwardInfo: PendingForwardInfo?
|
||||
) {
|
||||
self.target = target
|
||||
self.stableId = stableId
|
||||
|
|
@ -98,6 +135,7 @@ public extension Stories {
|
|||
self.isForwardingDisabled = isForwardingDisabled
|
||||
self.period = period
|
||||
self.randomId = randomId
|
||||
self.forwardInfo = forwardInfo
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
|
|
@ -123,6 +161,8 @@ public extension Stories {
|
|||
self.isForwardingDisabled = try container.decodeIfPresent(Bool.self, forKey: .isForwardingDisabled) ?? false
|
||||
self.period = try container.decode(Int32.self, forKey: .period)
|
||||
self.randomId = try container.decode(Int64.self, forKey: .randomId)
|
||||
|
||||
self.forwardInfo = try container.decodeIfPresent(PendingForwardInfo.self, forKey: .forwardInfo)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
|
|
@ -150,6 +190,7 @@ public extension Stories {
|
|||
try container.encode(self.isForwardingDisabled, forKey: .isForwardingDisabled)
|
||||
try container.encode(self.period, forKey: .period)
|
||||
try container.encode(self.randomId, forKey: .randomId)
|
||||
try container.encodeIfPresent(self.forwardInfo, forKey: .forwardInfo)
|
||||
}
|
||||
|
||||
public static func ==(lhs: PendingItem, rhs: PendingItem) -> Bool {
|
||||
|
|
@ -186,6 +227,9 @@ public extension Stories {
|
|||
if lhs.randomId != rhs.randomId {
|
||||
return false
|
||||
}
|
||||
if lhs.forwardInfo != rhs.forwardInfo {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
@ -359,8 +403,9 @@ final class PendingStoryManager {
|
|||
case let .peer(peerId):
|
||||
toPeerId = peerId
|
||||
}
|
||||
|
||||
let stableId = firstItem.stableId
|
||||
pendingItemContext.disposable = (_internal_uploadStoryImpl(postbox: self.postbox, network: self.network, accountPeerId: self.accountPeerId, stateManager: self.stateManager, messageMediaPreuploadManager: self.messageMediaPreuploadManager, revalidationContext: self.revalidationContext, auxiliaryMethods: self.auxiliaryMethods, toPeerId: toPeerId, stableId: stableId, media: firstItem.media, mediaAreas: firstItem.mediaAreas, text: firstItem.text, entities: firstItem.entities, embeddedStickers: firstItem.embeddedStickers, pin: firstItem.pin, privacy: firstItem.privacy, isForwardingDisabled: firstItem.isForwardingDisabled, period: Int(firstItem.period), randomId: firstItem.randomId)
|
||||
pendingItemContext.disposable = (_internal_uploadStoryImpl(postbox: self.postbox, network: self.network, accountPeerId: self.accountPeerId, stateManager: self.stateManager, messageMediaPreuploadManager: self.messageMediaPreuploadManager, revalidationContext: self.revalidationContext, auxiliaryMethods: self.auxiliaryMethods, toPeerId: toPeerId, stableId: stableId, media: firstItem.media, mediaAreas: firstItem.mediaAreas, text: firstItem.text, entities: firstItem.entities, embeddedStickers: firstItem.embeddedStickers, pin: firstItem.pin, privacy: firstItem.privacy, isForwardingDisabled: firstItem.isForwardingDisabled, period: Int(firstItem.period), randomId: firstItem.randomId, forwardInfo: firstItem.forwardInfo)
|
||||
|> deliverOn(self.queue)).start(next: { [weak self] event in
|
||||
guard let `self` = self else {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -6,11 +6,14 @@ import TelegramApi
|
|||
public enum EngineStoryInputMedia {
|
||||
case image(dimensions: PixelDimensions, data: Data, stickers: [TelegramMediaFile])
|
||||
case video(dimensions: PixelDimensions, duration: Double, resource: TelegramMediaResource, firstFrameFile: TempBoxFile?, stickers: [TelegramMediaFile])
|
||||
case existing(media: Media)
|
||||
|
||||
var embeddedStickers: [TelegramMediaFile] {
|
||||
switch self {
|
||||
case let .image(_, _, stickers), let .video(_, _, _, _, stickers):
|
||||
return stickers
|
||||
case .existing:
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -36,6 +39,34 @@ public extension EngineStoryPrivacy {
|
|||
}
|
||||
}
|
||||
|
||||
public extension EngineStoryItem.ForwardInfo {
|
||||
init?(_ forwardInfo: Stories.Item.ForwardInfo, transaction: Transaction) {
|
||||
switch forwardInfo {
|
||||
case let .known(peerId, storyId):
|
||||
if let peer = transaction.getPeer(peerId) {
|
||||
self = .known(peer: EnginePeer(peer), storyId: storyId)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
case let .unknown(name):
|
||||
self = .unknown(name: name)
|
||||
}
|
||||
}
|
||||
|
||||
init?(_ forwardInfo: Stories.Item.ForwardInfo, peers: [PeerId: Peer]) {
|
||||
switch forwardInfo {
|
||||
case let .known(peerId, storyId):
|
||||
if let peer = peers[peerId] {
|
||||
self = .known(peer: EnginePeer(peer), storyId: storyId)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
case let .unknown(name):
|
||||
self = .unknown(name: name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum Stories {
|
||||
public final class Item: Codable, Equatable {
|
||||
public struct Views: Codable, Equatable {
|
||||
|
|
@ -162,6 +193,49 @@ public enum Stories {
|
|||
}
|
||||
}
|
||||
|
||||
public enum ForwardInfo: Codable, Equatable {
|
||||
public enum DecodingError: Error {
|
||||
case generic
|
||||
}
|
||||
|
||||
private enum CodingKeys: CodingKey {
|
||||
case discriminator
|
||||
case authorPeerId
|
||||
case storyId
|
||||
case authorName
|
||||
}
|
||||
|
||||
case known(peerId: EnginePeer.Id, storyId: Int32)
|
||||
case unknown(name: String)
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
switch try container.decode(Int32.self, forKey: .discriminator) {
|
||||
case 0:
|
||||
self = .known(peerId: EnginePeer.Id(try container.decode(Int64.self, forKey: .authorPeerId)), storyId: try container.decode(Int32.self, forKey: .storyId))
|
||||
case 1:
|
||||
self = .unknown(name: try container.decode(String.self, forKey: .authorName))
|
||||
default:
|
||||
throw DecodingError.generic
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
switch self {
|
||||
case let .known(peerId, storyId):
|
||||
try container.encode(0 as Int32, forKey: .discriminator)
|
||||
try container.encode(peerId.toInt64(), forKey: .authorPeerId)
|
||||
try container.encode(storyId, forKey: .storyId)
|
||||
case let .unknown(name):
|
||||
try container.encode(1 as Int32, forKey: .discriminator)
|
||||
try container.encode(name, forKey: .authorName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case timestamp
|
||||
|
|
@ -182,6 +256,7 @@ public enum Stories {
|
|||
case isEdited
|
||||
case isMy
|
||||
case myReaction
|
||||
case forwardInfo
|
||||
}
|
||||
|
||||
public let id: Int32
|
||||
|
|
@ -203,6 +278,7 @@ public enum Stories {
|
|||
public let isEdited: Bool
|
||||
public let isMy: Bool
|
||||
public let myReaction: MessageReaction.Reaction?
|
||||
public let forwardInfo: ForwardInfo?
|
||||
|
||||
public init(
|
||||
id: Int32,
|
||||
|
|
@ -223,7 +299,8 @@ public enum Stories {
|
|||
isForwardingDisabled: Bool,
|
||||
isEdited: Bool,
|
||||
isMy: Bool,
|
||||
myReaction: MessageReaction.Reaction?
|
||||
myReaction: MessageReaction.Reaction?,
|
||||
forwardInfo: ForwardInfo?
|
||||
) {
|
||||
self.id = id
|
||||
self.timestamp = timestamp
|
||||
|
|
@ -244,6 +321,7 @@ public enum Stories {
|
|||
self.isEdited = isEdited
|
||||
self.isMy = isMy
|
||||
self.myReaction = myReaction
|
||||
self.forwardInfo = forwardInfo
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
|
|
@ -274,6 +352,7 @@ public enum Stories {
|
|||
self.isEdited = try container.decodeIfPresent(Bool.self, forKey: .isEdited) ?? false
|
||||
self.isMy = try container.decodeIfPresent(Bool.self, forKey: .isMy) ?? false
|
||||
self.myReaction = try container.decodeIfPresent(MessageReaction.Reaction.self, forKey: .myReaction)
|
||||
self.forwardInfo = try container.decodeIfPresent(ForwardInfo.self, forKey: .forwardInfo)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
|
|
@ -305,6 +384,7 @@ public enum Stories {
|
|||
try container.encode(self.isEdited, forKey: .isEdited)
|
||||
try container.encode(self.isMy, forKey: .isMy)
|
||||
try container.encodeIfPresent(self.myReaction, forKey: .myReaction)
|
||||
try container.encodeIfPresent(self.forwardInfo, forKey: .forwardInfo)
|
||||
}
|
||||
|
||||
public static func ==(lhs: Item, rhs: Item) -> Bool {
|
||||
|
|
@ -369,7 +449,9 @@ public enum Stories {
|
|||
if lhs.myReaction != rhs.myReaction {
|
||||
return false
|
||||
}
|
||||
|
||||
if lhs.forwardInfo != rhs.forwardInfo {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
@ -752,6 +834,8 @@ private func prepareUploadStoryContent(account: Account, media: EngineStoryInput
|
|||
)
|
||||
|
||||
return fileMedia
|
||||
case let .existing(media):
|
||||
return media
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -845,7 +929,7 @@ private func apiInputPrivacyRules(privacy: EngineStoryPrivacy, transaction: Tran
|
|||
return privacyRules
|
||||
}
|
||||
|
||||
func _internal_uploadStory(account: Account, target: Stories.PendingTarget, media: EngineStoryInputMedia, mediaAreas: [MediaArea], text: String, entities: [MessageTextEntity], pin: Bool, privacy: EngineStoryPrivacy, isForwardingDisabled: Bool, period: Int, randomId: Int64) -> Signal<Int32, NoError> {
|
||||
func _internal_uploadStory(account: Account, target: Stories.PendingTarget, media: EngineStoryInputMedia, mediaAreas: [MediaArea], text: String, entities: [MessageTextEntity], pin: Bool, privacy: EngineStoryPrivacy, isForwardingDisabled: Bool, period: Int, randomId: Int64, forwardInfo: Stories.PendingForwardInfo?) -> Signal<Int32, NoError> {
|
||||
let inputMedia = prepareUploadStoryContent(account: account, media: media)
|
||||
|
||||
return (account.postbox.transaction { transaction in
|
||||
|
|
@ -872,7 +956,8 @@ func _internal_uploadStory(account: Account, target: Stories.PendingTarget, medi
|
|||
privacy: privacy,
|
||||
isForwardingDisabled: isForwardingDisabled,
|
||||
period: Int32(period),
|
||||
randomId: randomId
|
||||
randomId: randomId,
|
||||
forwardInfo: forwardInfo
|
||||
))
|
||||
transaction.setLocalStoryState(state: CodableEntry(currentState))
|
||||
return stableId
|
||||
|
|
@ -917,7 +1002,28 @@ private func _internal_putPendingStoryIdMapping(peerId: PeerId, stableId: Int32,
|
|||
}
|
||||
}
|
||||
|
||||
func _internal_uploadStoryImpl(postbox: Postbox, network: Network, accountPeerId: PeerId, stateManager: AccountStateManager, messageMediaPreuploadManager: MessageMediaPreuploadManager, revalidationContext: MediaReferenceRevalidationContext, auxiliaryMethods: AccountAuxiliaryMethods, toPeerId: PeerId, stableId: Int32, media: Media, mediaAreas: [MediaArea], text: String, entities: [MessageTextEntity], embeddedStickers: [TelegramMediaFile], pin: Bool, privacy: EngineStoryPrivacy, isForwardingDisabled: Bool, period: Int, randomId: Int64) -> Signal<StoryUploadResult, NoError> {
|
||||
func _internal_uploadStoryImpl(
|
||||
postbox: Postbox,
|
||||
network: Network,
|
||||
accountPeerId: PeerId,
|
||||
stateManager: AccountStateManager,
|
||||
messageMediaPreuploadManager: MessageMediaPreuploadManager,
|
||||
revalidationContext: MediaReferenceRevalidationContext,
|
||||
auxiliaryMethods: AccountAuxiliaryMethods,
|
||||
toPeerId: PeerId,
|
||||
stableId: Int32,
|
||||
media: Media,
|
||||
mediaAreas: [MediaArea],
|
||||
text: String,
|
||||
entities: [MessageTextEntity],
|
||||
embeddedStickers: [TelegramMediaFile],
|
||||
pin: Bool,
|
||||
privacy: EngineStoryPrivacy,
|
||||
isForwardingDisabled: Bool,
|
||||
period: Int,
|
||||
randomId: Int64,
|
||||
forwardInfo: Stories.PendingForwardInfo?
|
||||
) -> Signal<StoryUploadResult, NoError> {
|
||||
return postbox.transaction { transaction -> Api.InputPeer? in
|
||||
return transaction.getPeer(toPeerId).flatMap(apiInputPeer)
|
||||
}
|
||||
|
|
@ -975,6 +1081,14 @@ func _internal_uploadStoryImpl(postbox: Postbox, network: Network, accountPeerId
|
|||
flags |= 1 << 5
|
||||
}
|
||||
|
||||
var fwdFromId: Api.InputPeer?
|
||||
var fwdFromStory: Int32?
|
||||
if let forwardInfo = forwardInfo, let inputPeer = transaction.getPeer(forwardInfo.peerId).flatMap({ apiInputPeer($0) }) {
|
||||
flags |= 1 << 6
|
||||
fwdFromId = inputPeer
|
||||
fwdFromStory = forwardInfo.storyId
|
||||
}
|
||||
|
||||
return network.request(Api.functions.stories.sendStory(
|
||||
flags: flags,
|
||||
peer: inputPeer,
|
||||
|
|
@ -985,8 +1099,8 @@ func _internal_uploadStoryImpl(postbox: Postbox, network: Network, accountPeerId
|
|||
privacyRules: privacyRules,
|
||||
randomId: randomId,
|
||||
period: Int32(period),
|
||||
fwdFromId: nil,
|
||||
fwdFromStory: nil
|
||||
fwdFromId: fwdFromId,
|
||||
fwdFromStory: fwdFromStory
|
||||
))
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
|
||||
|
|
@ -1034,7 +1148,8 @@ func _internal_uploadStoryImpl(postbox: Postbox, network: Network, accountPeerId
|
|||
isForwardingDisabled: item.isForwardingDisabled,
|
||||
isEdited: item.isEdited,
|
||||
isMy: item.isMy,
|
||||
myReaction: item.myReaction
|
||||
myReaction: item.myReaction,
|
||||
forwardInfo: item.forwardInfo
|
||||
)
|
||||
if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) {
|
||||
items.append(StoryItemsTableEntry(value: entry, id: item.id, expirationTimestamp: updatedItem.expirationTimestamp, isCloseFriends: updatedItem.isCloseFriends))
|
||||
|
|
@ -1214,7 +1329,8 @@ func _internal_editStoryPrivacy(account: Account, id: Int32, privacy: EngineStor
|
|||
isForwardingDisabled: item.isForwardingDisabled,
|
||||
isEdited: item.isEdited,
|
||||
isMy: item.isMy,
|
||||
myReaction: item.myReaction
|
||||
myReaction: item.myReaction,
|
||||
forwardInfo: item.forwardInfo
|
||||
)
|
||||
if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) {
|
||||
transaction.setStory(id: storyId, value: entry)
|
||||
|
|
@ -1243,7 +1359,8 @@ func _internal_editStoryPrivacy(account: Account, id: Int32, privacy: EngineStor
|
|||
isForwardingDisabled: item.isForwardingDisabled,
|
||||
isEdited: item.isEdited,
|
||||
isMy: item.isMy,
|
||||
myReaction: item.myReaction
|
||||
myReaction: item.myReaction,
|
||||
forwardInfo: item.forwardInfo
|
||||
)
|
||||
if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) {
|
||||
items[index] = StoryItemsTableEntry(value: entry, id: item.id, expirationTimestamp: updatedItem.expirationTimestamp, isCloseFriends: updatedItem.isCloseFriends)
|
||||
|
|
@ -1435,7 +1552,8 @@ func _internal_updateStoriesArePinned(account: Account, peerId: PeerId, ids: [In
|
|||
isForwardingDisabled: item.isForwardingDisabled,
|
||||
isEdited: item.isEdited,
|
||||
isMy: item.isMy,
|
||||
myReaction: item.myReaction
|
||||
myReaction: item.myReaction,
|
||||
forwardInfo: item.forwardInfo
|
||||
)
|
||||
if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) {
|
||||
items[index] = StoryItemsTableEntry(value: entry, id: item.id, expirationTimestamp: updatedItem.expirationTimestamp, isCloseFriends: updatedItem.isCloseFriends)
|
||||
|
|
@ -1463,7 +1581,8 @@ func _internal_updateStoriesArePinned(account: Account, peerId: PeerId, ids: [In
|
|||
isForwardingDisabled: item.isForwardingDisabled,
|
||||
isEdited: item.isEdited,
|
||||
isMy: item.isMy,
|
||||
myReaction: item.myReaction
|
||||
myReaction: item.myReaction,
|
||||
forwardInfo: item.forwardInfo
|
||||
)
|
||||
updatedItems.append(updatedItem)
|
||||
}
|
||||
|
|
@ -1538,11 +1657,26 @@ extension Stories.Item.Views {
|
|||
}
|
||||
}
|
||||
|
||||
extension Stories.Item.ForwardInfo {
|
||||
init?(apiForwardInfo: Api.StoryFwdHeader) {
|
||||
switch apiForwardInfo {
|
||||
case let .storyFwdHeader(_, from, fromName, storyId):
|
||||
if let from = from, let storyId = storyId {
|
||||
self = .known(peerId: from.peerId, storyId: storyId)
|
||||
return
|
||||
} else if let fromName = fromName {
|
||||
self = .unknown(name: fromName)
|
||||
return
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
extension Stories.StoredItem {
|
||||
init?(apiStoryItem: Api.StoryItem, existingItem: Stories.Item? = nil, peerId: PeerId, transaction: Transaction) {
|
||||
switch apiStoryItem {
|
||||
case let .storyItem(flags, id, date, fwdFrom, expireDate, caption, entities, media, mediaAreas, privacy, views, sentReaction):
|
||||
let _ = fwdFrom
|
||||
case let .storyItem(flags, id, date, forwardFrom, expireDate, caption, entities, media, mediaAreas, privacy, views, sentReaction):
|
||||
let (parsedMedia, _, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, peerId)
|
||||
if let parsedMedia = parsedMedia {
|
||||
var parsedPrivacy: Stories.Item.Privacy?
|
||||
|
|
@ -1613,6 +1747,13 @@ extension Stories.StoredItem {
|
|||
mergedIsMy = (flags & (1 << 16)) != 0
|
||||
}
|
||||
|
||||
var mergedForwardInfo: Stories.Item.ForwardInfo?
|
||||
if isMin, let existingItem = existingItem {
|
||||
mergedForwardInfo = existingItem.forwardInfo
|
||||
} else {
|
||||
mergedForwardInfo = forwardFrom.flatMap(Stories.Item.ForwardInfo.init(apiForwardInfo:))
|
||||
}
|
||||
|
||||
let item = Stories.Item(
|
||||
id: id,
|
||||
timestamp: date,
|
||||
|
|
@ -1632,7 +1773,8 @@ extension Stories.StoredItem {
|
|||
isForwardingDisabled: isForwardingDisabled,
|
||||
isEdited: isEdited,
|
||||
isMy: mergedIsMy,
|
||||
myReaction: mergedMyReaction
|
||||
myReaction: mergedMyReaction,
|
||||
forwardInfo: mergedForwardInfo
|
||||
)
|
||||
self = .item(item)
|
||||
} else {
|
||||
|
|
@ -2104,7 +2246,8 @@ func _internal_setStoryReaction(account: Account, peerId: EnginePeer.Id, id: Int
|
|||
isForwardingDisabled: item.isForwardingDisabled,
|
||||
isEdited: item.isEdited,
|
||||
isMy: item.isMy,
|
||||
myReaction: reaction
|
||||
myReaction: reaction,
|
||||
forwardInfo: item.forwardInfo
|
||||
))
|
||||
updatedItemValue = updatedItem
|
||||
if let entry = CodableEntry(updatedItem) {
|
||||
|
|
@ -2135,7 +2278,8 @@ func _internal_setStoryReaction(account: Account, peerId: EnginePeer.Id, id: Int
|
|||
isForwardingDisabled: item.isForwardingDisabled,
|
||||
isEdited: item.isEdited,
|
||||
isMy: item.isMy,
|
||||
myReaction: reaction
|
||||
myReaction: reaction,
|
||||
forwardInfo: item.forwardInfo
|
||||
))
|
||||
updatedItemValue = updatedItem
|
||||
if let entry = CodableEntry(updatedItem) {
|
||||
|
|
|
|||
|
|
@ -51,6 +51,11 @@ public final class EngineStoryItem: Equatable {
|
|||
}
|
||||
}
|
||||
|
||||
public enum ForwardInfo: Equatable {
|
||||
case known(peer: EnginePeer, storyId: Int32)
|
||||
case unknown(name: String)
|
||||
}
|
||||
|
||||
public let id: Int32
|
||||
public let timestamp: Int32
|
||||
public let expirationTimestamp: Int32
|
||||
|
|
@ -71,8 +76,9 @@ public final class EngineStoryItem: Equatable {
|
|||
public let isEdited: Bool
|
||||
public let isMy: Bool
|
||||
public let myReaction: MessageReaction.Reaction?
|
||||
public let forwardInfo: ForwardInfo?
|
||||
|
||||
public init(id: Int32, timestamp: Int32, expirationTimestamp: Int32, media: EngineMedia, mediaAreas: [MediaArea], text: String, entities: [MessageTextEntity], views: Views?, privacy: EngineStoryPrivacy?, isPinned: Bool, isExpired: Bool, isPublic: Bool, isPending: Bool, isCloseFriends: Bool, isContacts: Bool, isSelectedContacts: Bool, isForwardingDisabled: Bool, isEdited: Bool, isMy: Bool, myReaction: MessageReaction.Reaction?) {
|
||||
public init(id: Int32, timestamp: Int32, expirationTimestamp: Int32, media: EngineMedia, mediaAreas: [MediaArea], text: String, entities: [MessageTextEntity], views: Views?, privacy: EngineStoryPrivacy?, isPinned: Bool, isExpired: Bool, isPublic: Bool, isPending: Bool, isCloseFriends: Bool, isContacts: Bool, isSelectedContacts: Bool, isForwardingDisabled: Bool, isEdited: Bool, isMy: Bool, myReaction: MessageReaction.Reaction?, forwardInfo: ForwardInfo?) {
|
||||
self.id = id
|
||||
self.timestamp = timestamp
|
||||
self.expirationTimestamp = expirationTimestamp
|
||||
|
|
@ -93,6 +99,7 @@ public final class EngineStoryItem: Equatable {
|
|||
self.isEdited = isEdited
|
||||
self.isMy = isMy
|
||||
self.myReaction = myReaction
|
||||
self.forwardInfo = forwardInfo
|
||||
}
|
||||
|
||||
public static func ==(lhs: EngineStoryItem, rhs: EngineStoryItem) -> Bool {
|
||||
|
|
@ -156,11 +163,25 @@ public final class EngineStoryItem: Equatable {
|
|||
if lhs.myReaction != rhs.myReaction {
|
||||
return false
|
||||
}
|
||||
if lhs.forwardInfo != rhs.forwardInfo {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
extension EngineStoryItem {
|
||||
extension EngineStoryItem.ForwardInfo {
|
||||
var storedForwardInfo: Stories.Item.ForwardInfo {
|
||||
switch self {
|
||||
case let .known(peer, storyId):
|
||||
return .known(peerId: peer.id, storyId: storyId)
|
||||
case let .unknown(name):
|
||||
return .unknown(name: name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension EngineStoryItem {
|
||||
func asStoryItem() -> Stories.Item {
|
||||
return Stories.Item(
|
||||
id: self.id,
|
||||
|
|
@ -195,7 +216,8 @@ extension EngineStoryItem {
|
|||
isForwardingDisabled: self.isForwardingDisabled,
|
||||
isEdited: self.isEdited,
|
||||
isMy: self.isMy,
|
||||
myReaction: self.myReaction
|
||||
myReaction: self.myReaction,
|
||||
forwardInfo: self.forwardInfo?.storedForwardInfo
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -570,7 +592,8 @@ public final class PeerStoryListContext {
|
|||
isForwardingDisabled: item.isForwardingDisabled,
|
||||
isEdited: item.isEdited,
|
||||
isMy: item.isMy,
|
||||
myReaction: item.myReaction
|
||||
myReaction: item.myReaction,
|
||||
forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, transaction: transaction) }
|
||||
)
|
||||
items.append(mappedItem)
|
||||
|
||||
|
|
@ -713,7 +736,8 @@ public final class PeerStoryListContext {
|
|||
isForwardingDisabled: item.isForwardingDisabled,
|
||||
isEdited: item.isEdited,
|
||||
isMy: item.isMy,
|
||||
myReaction: item.myReaction
|
||||
myReaction: item.myReaction,
|
||||
forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, transaction: transaction) }
|
||||
)
|
||||
storyItems.append(mappedItem)
|
||||
}
|
||||
|
|
@ -802,6 +826,11 @@ public final class PeerStoryListContext {
|
|||
}
|
||||
}
|
||||
}
|
||||
if let forwardInfo = item.forwardInfo, case let .known(peerId, _) = forwardInfo {
|
||||
if let peer = transaction.getPeer(peerId) {
|
||||
peers[peer.id] = peer
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
|
|
@ -868,7 +897,8 @@ public final class PeerStoryListContext {
|
|||
isForwardingDisabled: item.isForwardingDisabled,
|
||||
isEdited: item.isEdited,
|
||||
isMy: item.isMy,
|
||||
myReaction: item.myReaction
|
||||
myReaction: item.myReaction,
|
||||
forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, peers: peers) }
|
||||
)
|
||||
finalUpdatedState = updatedState
|
||||
}
|
||||
|
|
@ -914,7 +944,8 @@ public final class PeerStoryListContext {
|
|||
isForwardingDisabled: item.isForwardingDisabled,
|
||||
isEdited: item.isEdited,
|
||||
isMy: item.isMy,
|
||||
myReaction: item.myReaction
|
||||
myReaction: item.myReaction,
|
||||
forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, peers: peers) }
|
||||
)
|
||||
finalUpdatedState = updatedState
|
||||
} else {
|
||||
|
|
@ -962,7 +993,8 @@ public final class PeerStoryListContext {
|
|||
isForwardingDisabled: item.isForwardingDisabled,
|
||||
isEdited: item.isEdited,
|
||||
isMy: item.isMy,
|
||||
myReaction: item.myReaction
|
||||
myReaction: item.myReaction,
|
||||
forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, peers: peers) }
|
||||
))
|
||||
updatedState.items.sort(by: { lhs, rhs in
|
||||
return lhs.timestamp > rhs.timestamp
|
||||
|
|
@ -1006,7 +1038,8 @@ public final class PeerStoryListContext {
|
|||
isForwardingDisabled: item.isForwardingDisabled,
|
||||
isEdited: item.isEdited,
|
||||
isMy: item.isMy,
|
||||
myReaction: item.myReaction
|
||||
myReaction: item.myReaction,
|
||||
forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, peers: peers) }
|
||||
))
|
||||
updatedState.items.sort(by: { lhs, rhs in
|
||||
return lhs.timestamp > rhs.timestamp
|
||||
|
|
@ -1174,7 +1207,8 @@ public final class PeerExpiringStoryListContext {
|
|||
isForwardingDisabled: item.isForwardingDisabled,
|
||||
isEdited: item.isEdited,
|
||||
isMy: item.isMy,
|
||||
myReaction: item.myReaction
|
||||
myReaction: item.myReaction,
|
||||
forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, transaction: transaction) }
|
||||
)
|
||||
items.append(.item(mappedItem))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -476,7 +476,7 @@ public extension TelegramEngine {
|
|||
public func transcribeAudio(messageId: MessageId) -> Signal<EngineAudioTranscriptionResult, NoError> {
|
||||
return _internal_transcribeAudio(postbox: self.account.postbox, network: self.account.network, messageId: messageId)
|
||||
}
|
||||
|
||||
|
||||
public func storeLocallyTranscribedAudio(messageId: MessageId, text: String, isFinal: Bool, error: AudioTranscriptionMessageAttribute.TranscriptionError?) -> Signal<Never, NoError> {
|
||||
return self.account.postbox.transaction { transaction -> Void in
|
||||
transaction.updateMessage(messageId, update: { currentMessage in
|
||||
|
|
@ -1178,7 +1178,8 @@ public extension TelegramEngine {
|
|||
isForwardingDisabled: item.isForwardingDisabled,
|
||||
isEdited: item.isEdited,
|
||||
isMy: item.isMy,
|
||||
myReaction: item.myReaction
|
||||
myReaction: item.myReaction,
|
||||
forwardInfo: item.forwardInfo
|
||||
))
|
||||
if let entry = CodableEntry(updatedItem) {
|
||||
currentItems[i] = StoryItemsTableEntry(value: entry, id: updatedItem.id, expirationTimestamp: updatedItem.expirationTimestamp, isCloseFriends: updatedItem.isCloseFriends)
|
||||
|
|
@ -1192,8 +1193,8 @@ public extension TelegramEngine {
|
|||
}
|
||||
}
|
||||
|
||||
public func uploadStory(target: Stories.PendingTarget, media: EngineStoryInputMedia, mediaAreas: [MediaArea], text: String, entities: [MessageTextEntity], pin: Bool, privacy: EngineStoryPrivacy, isForwardingDisabled: Bool, period: Int, randomId: Int64) -> Signal<Int32, NoError> {
|
||||
return _internal_uploadStory(account: self.account, target: target, media: media, mediaAreas: mediaAreas, text: text, entities: entities, pin: pin, privacy: privacy, isForwardingDisabled: isForwardingDisabled, period: period, randomId: randomId)
|
||||
public func uploadStory(target: Stories.PendingTarget, media: EngineStoryInputMedia, mediaAreas: [MediaArea], text: String, entities: [MessageTextEntity], pin: Bool, privacy: EngineStoryPrivacy, isForwardingDisabled: Bool, period: Int, randomId: Int64, forwardInfo: Stories.PendingForwardInfo?) -> Signal<Int32, NoError> {
|
||||
return _internal_uploadStory(account: self.account, target: target, media: media, mediaAreas: mediaAreas, text: text, entities: entities, pin: pin, privacy: privacy, isForwardingDisabled: isForwardingDisabled, period: period, randomId: randomId, forwardInfo: forwardInfo)
|
||||
}
|
||||
|
||||
public func allStoriesUploadEvents() -> Signal<(Int32, Int32), NoError> {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,153 @@
|
|||
import Foundation
|
||||
import Postbox
|
||||
import SwiftSignalKit
|
||||
import TelegramApi
|
||||
import MtProtoKit
|
||||
|
||||
public enum EngineAudioTranscriptionResult {
|
||||
case success
|
||||
case error
|
||||
}
|
||||
|
||||
func _internal_transcribeAudio(postbox: Postbox, network: Network, messageId: MessageId) -> Signal<EngineAudioTranscriptionResult, NoError> {
|
||||
return postbox.transaction { transaction -> Api.InputPeer? in
|
||||
return transaction.getPeer(messageId.peerId).flatMap(apiInputPeer)
|
||||
}
|
||||
|> mapToSignal { inputPeer -> Signal<EngineAudioTranscriptionResult, NoError> in
|
||||
guard let inputPeer = inputPeer else {
|
||||
return .single(.error)
|
||||
}
|
||||
return network.request(Api.functions.messages.transcribeAudio(peer: inputPeer, msgId: messageId.id))
|
||||
|> map { result -> Result<Api.messages.TranscribedAudio, AudioTranscriptionMessageAttribute.TranscriptionError> in
|
||||
return .success(result)
|
||||
}
|
||||
|> `catch` { error -> Signal<Result<Api.messages.TranscribedAudio, AudioTranscriptionMessageAttribute.TranscriptionError>, NoError> in
|
||||
let mappedError: AudioTranscriptionMessageAttribute.TranscriptionError
|
||||
if error.errorDescription == "MSG_VOICE_TOO_LONG" {
|
||||
mappedError = .tooLong
|
||||
} else {
|
||||
mappedError = .generic
|
||||
}
|
||||
return .single(.failure(mappedError))
|
||||
}
|
||||
|> mapToSignal { result -> Signal<EngineAudioTranscriptionResult, NoError> in
|
||||
return postbox.transaction { transaction -> EngineAudioTranscriptionResult in
|
||||
let updatedAttribute: AudioTranscriptionMessageAttribute
|
||||
switch result {
|
||||
case let .success(transcribedAudio):
|
||||
switch transcribedAudio {
|
||||
case let .transcribedAudio(flags, transcriptionId, text, trialRemainingCount, trialUntilDate):
|
||||
let isPending = (flags & (1 << 0)) != 0
|
||||
|
||||
_internal_updateAudioTranscriptionTrialState(transaction: transaction) { current in
|
||||
var updated = current
|
||||
if let trialRemainingCount = trialRemainingCount, trialRemainingCount > 0 {
|
||||
updated = updated.withUpdatedRemainingCount(trialRemainingCount)
|
||||
} else if let trialUntilDate = trialUntilDate {
|
||||
updated = updated.withUpdatedCooldownUntilTime(trialUntilDate)
|
||||
} else {
|
||||
updated = updated.withUpdatedCooldownUntilTime(nil)
|
||||
}
|
||||
return updated
|
||||
}
|
||||
updatedAttribute = AudioTranscriptionMessageAttribute(id: transcriptionId, text: text, isPending: isPending, didRate: false, error: nil)
|
||||
}
|
||||
case let .failure(error):
|
||||
updatedAttribute = AudioTranscriptionMessageAttribute(id: 0, text: "", isPending: false, didRate: false, error: error)
|
||||
}
|
||||
|
||||
transaction.updateMessage(messageId, update: { currentMessage in
|
||||
let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init)
|
||||
var attributes = currentMessage.attributes.filter { !($0 is AudioTranscriptionMessageAttribute) }
|
||||
|
||||
attributes.append(updatedAttribute)
|
||||
|
||||
return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
|
||||
})
|
||||
|
||||
if updatedAttribute.error == nil {
|
||||
return .success
|
||||
} else {
|
||||
return .error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func _internal_rateAudioTranscription(postbox: Postbox, network: Network, messageId: MessageId, id: Int64, isGood: Bool) -> Signal<Never, NoError> {
|
||||
return postbox.transaction { transaction -> Api.InputPeer? in
|
||||
transaction.updateMessage(messageId, update: { currentMessage in
|
||||
var storeForwardInfo: StoreMessageForwardInfo?
|
||||
if let forwardInfo = currentMessage.forwardInfo {
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
|
||||
}
|
||||
var attributes = currentMessage.attributes
|
||||
for i in 0 ..< attributes.count {
|
||||
if let attribute = attributes[i] as? AudioTranscriptionMessageAttribute {
|
||||
attributes[i] = attribute.withDidRate()
|
||||
}
|
||||
}
|
||||
return .update(StoreMessage(
|
||||
id: currentMessage.id,
|
||||
globallyUniqueId: currentMessage.globallyUniqueId,
|
||||
groupingKey: currentMessage.groupingKey,
|
||||
threadId: currentMessage.threadId,
|
||||
timestamp: currentMessage.timestamp,
|
||||
flags: StoreMessageFlags(currentMessage.flags),
|
||||
tags: currentMessage.tags,
|
||||
globalTags: currentMessage.globalTags,
|
||||
localTags: currentMessage.localTags,
|
||||
forwardInfo: storeForwardInfo,
|
||||
authorId: currentMessage.author?.id,
|
||||
text: currentMessage.text,
|
||||
attributes: attributes,
|
||||
media: currentMessage.media
|
||||
))
|
||||
})
|
||||
|
||||
return transaction.getPeer(messageId.peerId).flatMap(apiInputPeer)
|
||||
}
|
||||
|> mapToSignal { inputPeer -> Signal<Never, NoError> in
|
||||
guard let inputPeer = inputPeer else {
|
||||
return .complete()
|
||||
}
|
||||
return network.request(Api.functions.messages.rateTranscribedAudio(peer: inputPeer, msgId: messageId.id, transcriptionId: id, good: isGood ? .boolTrue : .boolFalse))
|
||||
|> `catch` { _ -> Signal<Api.Bool, NoError> in
|
||||
return .single(.boolFalse)
|
||||
}
|
||||
|> ignoreValues
|
||||
}
|
||||
}
|
||||
|
||||
public enum AudioTranscription {
|
||||
public struct TrialState: Equatable, Codable {
|
||||
public let cooldownUntilTime: Int32?
|
||||
public let remainingCount: Int32
|
||||
|
||||
func withUpdatedCooldownUntilTime(_ time: Int32?) -> AudioTranscription.TrialState {
|
||||
return AudioTranscription.TrialState(cooldownUntilTime: time, remainingCount: time != nil ? 0 : max(1, self.remainingCount))
|
||||
}
|
||||
|
||||
func withUpdatedRemainingCount(_ remainingCount: Int32) -> AudioTranscription.TrialState {
|
||||
return AudioTranscription.TrialState(remainingCount: remainingCount)
|
||||
}
|
||||
|
||||
public init(cooldownUntilTime: Int32? = nil, remainingCount: Int32) {
|
||||
self.cooldownUntilTime = cooldownUntilTime
|
||||
self.remainingCount = remainingCount
|
||||
}
|
||||
|
||||
public static var defaultValue: AudioTranscription.TrialState {
|
||||
return AudioTranscription.TrialState(
|
||||
cooldownUntilTime: nil,
|
||||
remainingCount: 1
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func _internal_updateAudioTranscriptionTrialState(transaction: Transaction, _ f: (AudioTranscription.TrialState) -> AudioTranscription.TrialState) {
|
||||
let current = transaction.getPreferencesEntry(key: PreferencesKeys.audioTranscriptionTrialState)?.get(AudioTranscription.TrialState.self) ?? .defaultValue
|
||||
transaction.setPreferencesEntry(key: PreferencesKeys.audioTranscriptionTrialState, value: PreferencesEntry(f(current)))
|
||||
}
|
||||
|
|
@ -155,108 +155,3 @@ func _internal_togglePeerMessagesTranslationHidden(account: Account, peerId: Eng
|
|||
|> ignoreValues
|
||||
}
|
||||
}
|
||||
|
||||
public enum EngineAudioTranscriptionResult {
|
||||
case success
|
||||
case error
|
||||
}
|
||||
|
||||
func _internal_transcribeAudio(postbox: Postbox, network: Network, messageId: MessageId) -> Signal<EngineAudioTranscriptionResult, NoError> {
|
||||
return postbox.transaction { transaction -> Api.InputPeer? in
|
||||
return transaction.getPeer(messageId.peerId).flatMap(apiInputPeer)
|
||||
}
|
||||
|> mapToSignal { inputPeer -> Signal<EngineAudioTranscriptionResult, NoError> in
|
||||
guard let inputPeer = inputPeer else {
|
||||
return .single(.error)
|
||||
}
|
||||
return network.request(Api.functions.messages.transcribeAudio(peer: inputPeer, msgId: messageId.id))
|
||||
|> map { result -> Result<Api.messages.TranscribedAudio, AudioTranscriptionMessageAttribute.TranscriptionError> in
|
||||
return .success(result)
|
||||
}
|
||||
|> `catch` { error -> Signal<Result<Api.messages.TranscribedAudio, AudioTranscriptionMessageAttribute.TranscriptionError>, NoError> in
|
||||
let mappedError: AudioTranscriptionMessageAttribute.TranscriptionError
|
||||
if error.errorDescription == "MSG_VOICE_TOO_LONG" {
|
||||
mappedError = .tooLong
|
||||
} else {
|
||||
mappedError = .generic
|
||||
}
|
||||
return .single(.failure(mappedError))
|
||||
}
|
||||
|> mapToSignal { result -> Signal<EngineAudioTranscriptionResult, NoError> in
|
||||
return postbox.transaction { transaction -> EngineAudioTranscriptionResult in
|
||||
let updatedAttribute: AudioTranscriptionMessageAttribute
|
||||
switch result {
|
||||
case let .success(transcribedAudio):
|
||||
switch transcribedAudio {
|
||||
case let .transcribedAudio(flags, transcriptionId, text):
|
||||
let isPending = (flags & (1 << 0)) != 0
|
||||
|
||||
updatedAttribute = AudioTranscriptionMessageAttribute(id: transcriptionId, text: text, isPending: isPending, didRate: false, error: nil)
|
||||
}
|
||||
case let .failure(error):
|
||||
updatedAttribute = AudioTranscriptionMessageAttribute(id: 0, text: "", isPending: false, didRate: false, error: error)
|
||||
}
|
||||
|
||||
transaction.updateMessage(messageId, update: { currentMessage in
|
||||
let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init)
|
||||
var attributes = currentMessage.attributes.filter { !($0 is AudioTranscriptionMessageAttribute) }
|
||||
|
||||
attributes.append(updatedAttribute)
|
||||
|
||||
return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
|
||||
})
|
||||
|
||||
if updatedAttribute.error == nil {
|
||||
return .success
|
||||
} else {
|
||||
return .error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func _internal_rateAudioTranscription(postbox: Postbox, network: Network, messageId: MessageId, id: Int64, isGood: Bool) -> Signal<Never, NoError> {
|
||||
return postbox.transaction { transaction -> Api.InputPeer? in
|
||||
transaction.updateMessage(messageId, update: { currentMessage in
|
||||
var storeForwardInfo: StoreMessageForwardInfo?
|
||||
if let forwardInfo = currentMessage.forwardInfo {
|
||||
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
|
||||
}
|
||||
var attributes = currentMessage.attributes
|
||||
for i in 0 ..< attributes.count {
|
||||
if let attribute = attributes[i] as? AudioTranscriptionMessageAttribute {
|
||||
attributes[i] = attribute.withDidRate()
|
||||
}
|
||||
}
|
||||
return .update(StoreMessage(
|
||||
id: currentMessage.id,
|
||||
globallyUniqueId: currentMessage.globallyUniqueId,
|
||||
groupingKey: currentMessage.groupingKey,
|
||||
threadId: currentMessage.threadId,
|
||||
timestamp: currentMessage.timestamp,
|
||||
flags: StoreMessageFlags(currentMessage.flags),
|
||||
tags: currentMessage.tags,
|
||||
globalTags: currentMessage.globalTags,
|
||||
localTags: currentMessage.localTags,
|
||||
forwardInfo: storeForwardInfo,
|
||||
authorId: currentMessage.author?.id,
|
||||
text: currentMessage.text,
|
||||
attributes: attributes,
|
||||
media: currentMessage.media
|
||||
))
|
||||
})
|
||||
|
||||
return transaction.getPeer(messageId.peerId).flatMap(apiInputPeer)
|
||||
}
|
||||
|> mapToSignal { inputPeer -> Signal<Never, NoError> in
|
||||
guard let inputPeer = inputPeer else {
|
||||
return .complete()
|
||||
}
|
||||
return network.request(Api.functions.messages.rateTranscribedAudio(peer: inputPeer, msgId: messageId.id, transcriptionId: id, good: isGood ? .boolTrue : .boolFalse))
|
||||
|> `catch` { _ -> Signal<Api.Bool, NoError> in
|
||||
return .single(.boolFalse)
|
||||
}
|
||||
|> ignoreValues
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -104,17 +104,23 @@ func _internal_recommendedChannels(account: Account, peerId: EnginePeer.Id) -> S
|
|||
let key = PostboxViewKey.cachedItem(entryId(peerId: peerId))
|
||||
return account.postbox.combinedView(keys: [key])
|
||||
|> mapToSignal { views -> Signal<RecommendedChannels?, NoError> in
|
||||
guard let cachedChannels = (views.views[key] as? CachedItemView)?.value?.get(CachedRecommendedChannels.self) else {
|
||||
guard let cachedChannels = (views.views[key] as? CachedItemView)?.value?.get(CachedRecommendedChannels.self), !cachedChannels.peerIds.isEmpty else {
|
||||
return .single(nil)
|
||||
}
|
||||
return account.postbox.transaction { transaction -> RecommendedChannels? in
|
||||
var channels: [RecommendedChannels.Channel] = []
|
||||
for peerId in cachedChannels.peerIds {
|
||||
if let peer = transaction.getPeer(peerId), let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData {
|
||||
channels.append(RecommendedChannels.Channel(peer: EnginePeer(peer), subscribers: cachedData.participantsSummary.memberCount ?? 0))
|
||||
return account.postbox.multiplePeersView(cachedChannels.peerIds)
|
||||
|> mapToSignal { view in
|
||||
return account.postbox.transaction { transaction -> RecommendedChannels? in
|
||||
var channels: [RecommendedChannels.Channel] = []
|
||||
for peerId in cachedChannels.peerIds {
|
||||
if let peer = view.peers[peerId] as? TelegramChannel, let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData {
|
||||
if case .member = peer.participationStatus {
|
||||
} else {
|
||||
channels.append(RecommendedChannels.Channel(peer: EnginePeer(peer), subscribers: cachedData.participantsSummary.memberCount ?? 0))
|
||||
}
|
||||
}
|
||||
}
|
||||
return RecommendedChannels(channels: channels, isHidden: cachedChannels.isHidden)
|
||||
}
|
||||
return RecommendedChannels(channels: channels, isHidden: cachedChannels.isHidden)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -186,7 +186,7 @@ func _internal_revertChatWallpaper(account: Account, peerId: EnginePeer.Id) -> S
|
|||
return account.network.request(Api.functions.messages.setChatWallPaper(flags: flags, peer: inputPeer, wallpaper: nil, settings: nil, id: nil), automaticFloodWait: false)
|
||||
|> map(Optional.init)
|
||||
|> `catch` { error -> Signal<Api.Updates?, RevertChatWallpaperError> in
|
||||
if error.description == "WALLPAPER_NOT_FOUND" {
|
||||
if error.errorDescription == "WALLPAPER_NOT_FOUND" {
|
||||
return .single(nil)
|
||||
}
|
||||
return .fail(.generic)
|
||||
|
|
@ -215,7 +215,7 @@ public enum SetExistingChatWallpaperError {
|
|||
case generic
|
||||
}
|
||||
|
||||
func _internal_setExistingChatWallpaper(account: Account, messageId: MessageId, settings: WallpaperSettings?) -> Signal<Void, SetExistingChatWallpaperError> {
|
||||
func _internal_setExistingChatWallpaper(account: Account, messageId: MessageId, settings: WallpaperSettings?, forBoth: Bool) -> Signal<Void, SetExistingChatWallpaperError> {
|
||||
return account.postbox.transaction { transaction -> Peer? in
|
||||
if let peer = transaction.getPeer(messageId.peerId), let message = transaction.getMessage(messageId) {
|
||||
if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .setChatWallpaper(wallpaper, _) = action.action {
|
||||
|
|
@ -248,6 +248,9 @@ func _internal_setExistingChatWallpaper(account: Account, messageId: MessageId,
|
|||
flags |= 1 << 2
|
||||
inputSettings = apiWallpaperSettings(settings)
|
||||
}
|
||||
if forBoth {
|
||||
flags |= 1 << 3
|
||||
}
|
||||
return account.network.request(Api.functions.messages.setChatWallPaper(flags: flags, peer: inputPeer, wallpaper: nil, settings: inputSettings, id: messageId.id), automaticFloodWait: false)
|
||||
|> `catch` { _ -> Signal<Api.Updates, SetExistingChatWallpaperError> in
|
||||
return .fail(.generic)
|
||||
|
|
|
|||
|
|
@ -22,8 +22,8 @@ public extension TelegramEngine {
|
|||
|> ignoreValues
|
||||
}
|
||||
|
||||
public func setExistingChatWallpaper(messageId: MessageId, settings: WallpaperSettings?) -> Signal<Void, SetExistingChatWallpaperError> {
|
||||
return _internal_setExistingChatWallpaper(account: self.account, messageId: messageId, settings: settings)
|
||||
public func setExistingChatWallpaper(messageId: MessageId, settings: WallpaperSettings?, forBoth: Bool) -> Signal<Void, SetExistingChatWallpaperError> {
|
||||
return _internal_setExistingChatWallpaper(account: self.account, messageId: messageId, settings: settings, forBoth: forBoth)
|
||||
}
|
||||
|
||||
public func revertChatWallpaper(peerId: EnginePeer.Id) -> Signal<Void, RevertChatWallpaperError> {
|
||||
|
|
|
|||
|
|
@ -431,13 +431,13 @@ public func stringForRelativeActivityTimestamp(strings: PresentationStrings, dat
|
|||
}
|
||||
}
|
||||
|
||||
public func stringForStoryActivityTimestamp(strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, preciseTime: Bool = false, relativeTimestamp: Int32, relativeTo timestamp: Int32) -> String {
|
||||
public func stringForStoryActivityTimestamp(strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, preciseTime: Bool = false, relativeTimestamp: Int32, relativeTo timestamp: Int32, short: Bool = false) -> String {
|
||||
let difference = timestamp - relativeTimestamp
|
||||
if difference < 60 {
|
||||
return strings.Time_JustNow
|
||||
return short ? strings.ShortTime_JustNow : strings.Time_JustNow
|
||||
} else if difference < 60 * 60 {
|
||||
let minutes = difference / 60
|
||||
return strings.Time_MinutesAgo(minutes)
|
||||
return short ? strings.ShortTime_MinutesAgo(minutes) : strings.Time_MinutesAgo(minutes)
|
||||
} else {
|
||||
var t: time_t = time_t(relativeTimestamp)
|
||||
var timeinfo: tm = tm()
|
||||
|
|
@ -456,7 +456,7 @@ public func stringForStoryActivityTimestamp(strings: PresentationStrings, dateTi
|
|||
let day: RelativeTimestampFormatDay
|
||||
if dayDifference == 0 {
|
||||
let minutes = difference / (60 * 60)
|
||||
return strings.Time_HoursAgo(minutes)
|
||||
return short ? strings.ShortTime_HoursAgo(minutes) : strings.Time_HoursAgo(minutes)
|
||||
} else {
|
||||
day = .yesterday
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ swift_library(
|
|||
"//submodules/Display:Display",
|
||||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||
"//submodules/Components/LottieAnimationComponent:LottieAnimationComponent",
|
||||
"//submodules/Components/BundleIconComponent:BundleIconComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import AppBundle
|
|||
import Display
|
||||
import TelegramPresentationData
|
||||
import LottieAnimationComponent
|
||||
import BundleIconComponent
|
||||
|
||||
public final class AudioTranscriptionButtonComponent: Component {
|
||||
public enum Theme: Equatable {
|
||||
|
|
@ -33,6 +34,7 @@ public final class AudioTranscriptionButtonComponent: Component {
|
|||
case inProgress
|
||||
case expanded
|
||||
case collapsed
|
||||
case locked
|
||||
}
|
||||
|
||||
public let theme: AudioTranscriptionButtonComponent.Theme
|
||||
|
|
@ -64,25 +66,21 @@ public final class AudioTranscriptionButtonComponent: Component {
|
|||
|
||||
private let blurredBackgroundNode: NavigationBackgroundNode
|
||||
private let backgroundLayer: SimpleLayer
|
||||
private let animationView: ComponentHostView<Empty>
|
||||
private var iconView: ComponentView<Empty>?
|
||||
private var animationView: ComponentView<Empty>?
|
||||
|
||||
private var progressAnimationView: ComponentHostView<Empty>?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.blurredBackgroundNode = NavigationBackgroundNode(color: .clear)
|
||||
self.backgroundLayer = SimpleLayer()
|
||||
self.animationView = ComponentHostView<Empty>()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.backgroundLayer.masksToBounds = true
|
||||
self.backgroundLayer.cornerRadius = 10.0
|
||||
self.layer.addSublayer(self.backgroundLayer)
|
||||
|
||||
self.animationView.isUserInteractionEnabled = false
|
||||
|
||||
self.addSubview(self.animationView)
|
||||
|
||||
|
||||
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
|
||||
}
|
||||
|
||||
|
|
@ -115,58 +113,120 @@ public final class AudioTranscriptionButtonComponent: Component {
|
|||
}
|
||||
|
||||
if self.component?.transcriptionState != component.transcriptionState {
|
||||
switch component.transcriptionState {
|
||||
case .inProgress:
|
||||
if self.progressAnimationView == nil {
|
||||
let progressAnimationView = ComponentHostView<Empty>()
|
||||
self.progressAnimationView = progressAnimationView
|
||||
self.addSubview(progressAnimationView)
|
||||
}
|
||||
default:
|
||||
if let progressAnimationView = self.progressAnimationView {
|
||||
self.progressAnimationView = nil
|
||||
if case .none = transition.animation {
|
||||
progressAnimationView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak progressAnimationView] _ in
|
||||
progressAnimationView?.removeFromSuperview()
|
||||
if case .locked = component.transcriptionState {
|
||||
if let animationView = self.animationView {
|
||||
self.animationView = nil
|
||||
if let view = animationView.view {
|
||||
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
|
||||
view.removeFromSuperview()
|
||||
})
|
||||
} else {
|
||||
progressAnimationView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
let iconView: ComponentView<Empty>
|
||||
if let current = self.iconView {
|
||||
iconView = current
|
||||
} else {
|
||||
iconView = ComponentView<Empty>()
|
||||
self.iconView = iconView
|
||||
}
|
||||
|
||||
let iconSize = iconView.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(BundleIconComponent(
|
||||
name: "Chat/Message/TranscriptionLocked",
|
||||
tintColor: foregroundColor
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 30.0, height: 30.0)
|
||||
)
|
||||
|
||||
if let view = iconView.view {
|
||||
if view.superview == nil {
|
||||
view.isUserInteractionEnabled = false
|
||||
self.addSubview(view)
|
||||
}
|
||||
view.frame = CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) / 2.0), y: floor((size.width - iconSize.height) / 2.0)), size: iconSize)
|
||||
}
|
||||
} else {
|
||||
if let iconView = self.iconView {
|
||||
self.iconView = nil
|
||||
if let view = iconView.view {
|
||||
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
|
||||
view.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let animationView: ComponentView<Empty>
|
||||
if let current = self.animationView {
|
||||
animationView = current
|
||||
} else {
|
||||
animationView = ComponentView<Empty>()
|
||||
self.animationView = animationView
|
||||
}
|
||||
|
||||
switch component.transcriptionState {
|
||||
case .inProgress:
|
||||
if self.progressAnimationView == nil {
|
||||
let progressAnimationView = ComponentHostView<Empty>()
|
||||
self.progressAnimationView = progressAnimationView
|
||||
self.addSubview(progressAnimationView)
|
||||
}
|
||||
default:
|
||||
if let progressAnimationView = self.progressAnimationView {
|
||||
self.progressAnimationView = nil
|
||||
if case .none = transition.animation {
|
||||
progressAnimationView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak progressAnimationView] _ in
|
||||
progressAnimationView?.removeFromSuperview()
|
||||
})
|
||||
} else {
|
||||
progressAnimationView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let animationName: String
|
||||
switch component.transcriptionState {
|
||||
case .inProgress:
|
||||
animationName = "voiceToText"
|
||||
case .collapsed:
|
||||
animationName = "voiceToText"
|
||||
case .expanded:
|
||||
animationName = "textToVoice"
|
||||
case .locked:
|
||||
animationName = "voiceToText"
|
||||
}
|
||||
let animationSize = animationView.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(LottieAnimationComponent(
|
||||
animation: LottieAnimationComponent.AnimationItem(
|
||||
name: animationName,
|
||||
mode: .animateTransitionFromPrevious
|
||||
),
|
||||
colors: [
|
||||
"icon.Group 3.Stroke 1": foregroundColor,
|
||||
"icon.Group 1.Stroke 1": foregroundColor,
|
||||
"icon.Group 4.Stroke 1": foregroundColor,
|
||||
"icon.Group 2.Stroke 1": foregroundColor,
|
||||
"Artboard Copy 2 Outlines.Group 5.Stroke 1": foregroundColor,
|
||||
"Artboard Copy 2 Outlines.Group 1.Stroke 1": foregroundColor,
|
||||
"Artboard Copy 2 Outlines.Group 4.Stroke 1": foregroundColor,
|
||||
"Artboard Copy Outlines.Group 1.Stroke 1": foregroundColor,
|
||||
],
|
||||
size: CGSize(width: 30.0, height: 30.0)
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 30.0, height: 30.0)
|
||||
)
|
||||
if let view = animationView.view {
|
||||
if view.superview == nil {
|
||||
view.isUserInteractionEnabled = false
|
||||
self.addSubview(view)
|
||||
}
|
||||
view.frame = CGRect(origin: CGPoint(x: floor((size.width - animationSize.width) / 2.0), y: floor((size.width - animationSize.height) / 2.0)), size: animationSize)
|
||||
}
|
||||
}
|
||||
|
||||
let animationName: String
|
||||
switch component.transcriptionState {
|
||||
case .inProgress:
|
||||
animationName = "voiceToText"
|
||||
case .collapsed:
|
||||
animationName = "voiceToText"
|
||||
case .expanded:
|
||||
animationName = "textToVoice"
|
||||
}
|
||||
let animationSize = self.animationView.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(LottieAnimationComponent(
|
||||
animation: LottieAnimationComponent.AnimationItem(
|
||||
name: animationName,
|
||||
mode: .animateTransitionFromPrevious
|
||||
),
|
||||
colors: [
|
||||
"icon.Group 3.Stroke 1": foregroundColor,
|
||||
"icon.Group 1.Stroke 1": foregroundColor,
|
||||
"icon.Group 4.Stroke 1": foregroundColor,
|
||||
"icon.Group 2.Stroke 1": foregroundColor,
|
||||
"Artboard Copy 2 Outlines.Group 5.Stroke 1": foregroundColor,
|
||||
"Artboard Copy 2 Outlines.Group 1.Stroke 1": foregroundColor,
|
||||
"Artboard Copy 2 Outlines.Group 4.Stroke 1": foregroundColor,
|
||||
"Artboard Copy Outlines.Group 1.Stroke 1": foregroundColor,
|
||||
],
|
||||
size: CGSize(width: 30.0, height: 30.0)
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 30.0, height: 30.0)
|
||||
)
|
||||
self.animationView.frame = CGRect(origin: CGPoint(x: floor((size.width - animationSize.width) / 2.0), y: floor((size.width - animationSize.height) / 2.0)), size: animationSize)
|
||||
}
|
||||
|
||||
self.backgroundLayer.backgroundColor = backgroundColor.cgColor
|
||||
|
|
|
|||
|
|
@ -674,7 +674,19 @@ private final class CameraScreenComponent: CombinedComponent {
|
|||
self.resultDisposable.set((camera.stopRecording()
|
||||
|> deliverOnMainQueue).start(next: { [weak self] result in
|
||||
if let self, case let .finished(mainResult, additionalResult, duration, positionChangeTimestamps, _) = result {
|
||||
self.completion.invoke(.single(.video(CameraScreen.Result.Video(videoPath: mainResult.0, coverImage: mainResult.1, mirror: mainResult.2, additionalVideoPath: additionalResult?.0, additionalCoverImage: additionalResult?.1, dimensions: PixelDimensions(mainResult.3), duration: duration, positionChangeTimestamps: positionChangeTimestamps, additionalVideoPosition: .topRight))))
|
||||
self.completion.invoke(.single(
|
||||
.video(CameraScreen.Result.Video(
|
||||
videoPath: mainResult.path,
|
||||
coverImage: mainResult.thumbnail,
|
||||
mirror: mainResult.isMirrored,
|
||||
additionalVideoPath: additionalResult?.path,
|
||||
additionalCoverImage: additionalResult?.thumbnail,
|
||||
dimensions: PixelDimensions(mainResult.dimensions),
|
||||
duration: duration,
|
||||
positionChangeTimestamps: positionChangeTimestamps,
|
||||
additionalVideoPosition: .topRight
|
||||
))
|
||||
))
|
||||
}
|
||||
}))
|
||||
self.isTransitioning = true
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import ChatControllerInteraction
|
|||
import ChatMessageDateAndStatusNode
|
||||
import ChatHistoryEntry
|
||||
import ChatMessageItemCommon
|
||||
import TelegramStringFormatting
|
||||
|
||||
private struct FetchControls {
|
||||
let fetch: (Bool) -> Void
|
||||
|
|
@ -334,34 +335,48 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||
}
|
||||
|
||||
private func transcribe() {
|
||||
guard let arguments = self.arguments, let context = self.context, let message = self.message, let presentationData = self.presentationData else {
|
||||
guard let arguments = self.arguments, let context = self.context, let message = self.message else {
|
||||
return
|
||||
}
|
||||
|
||||
guard arguments.associatedData.isPremium else {
|
||||
if self.hapticFeedback == nil {
|
||||
self.hapticFeedback = HapticFeedback()
|
||||
}
|
||||
self.hapticFeedback?.impact(.medium)
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let tipController = UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_voiceToText", scale: 0.065, colors: [:], title: nil, text: presentationData.strings.Message_AudioTranscription_SubscribeToPremium, customUndoText: presentationData.strings.Message_AudioTranscription_SubscribeToPremiumAction, timeout: nil), elevatedLayout: false, position: .top, animateInAsReplacement: false, action: { action in
|
||||
if case .undo = action {
|
||||
var replaceImpl: ((ViewController) -> Void)?
|
||||
let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .voiceToText, action: {
|
||||
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .settings, forceDark: false, dismissed: nil)
|
||||
replaceImpl?(controller)
|
||||
})
|
||||
replaceImpl = { [weak controller] c in
|
||||
controller?.replace(with: c)
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: arguments.context.currentAppConfiguration.with { $0 })
|
||||
|
||||
let transcriptionText = self.forcedAudioTranscriptionText ?? transcribedText(message: message)
|
||||
if transcriptionText == nil {
|
||||
if premiumConfiguration.audioTransciptionTrialCount > 0 {
|
||||
if !arguments.associatedData.isPremium {
|
||||
if self.presentAudioTranscriptionTooltip(finished: false) {
|
||||
return
|
||||
}
|
||||
arguments.controllerInteraction.navigationController()?.pushViewController(controller, animated: true)
|
||||
|
||||
let _ = ApplicationSpecificNotice.incrementAudioTranscriptionSuggestion(accountManager: context.sharedContext.accountManager).startStandalone()
|
||||
}
|
||||
return false })
|
||||
arguments.controllerInteraction.presentControllerInCurrent(tipController, nil)
|
||||
return
|
||||
} else {
|
||||
guard arguments.associatedData.isPremium else {
|
||||
if self.hapticFeedback == nil {
|
||||
self.hapticFeedback = HapticFeedback()
|
||||
}
|
||||
self.hapticFeedback?.impact(.medium)
|
||||
|
||||
let tipController = UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_voiceToText", scale: 0.065, colors: [:], title: nil, text: presentationData.strings.Message_AudioTranscription_SubscribeToPremium, customUndoText: presentationData.strings.Message_AudioTranscription_SubscribeToPremiumAction, timeout: nil), elevatedLayout: false, position: .top, animateInAsReplacement: false, action: { action in
|
||||
if case .undo = action {
|
||||
var replaceImpl: ((ViewController) -> Void)?
|
||||
let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .voiceToText, action: {
|
||||
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .settings, forceDark: false, dismissed: nil)
|
||||
replaceImpl?(controller)
|
||||
})
|
||||
replaceImpl = { [weak controller] c in
|
||||
controller?.replace(with: c)
|
||||
}
|
||||
arguments.controllerInteraction.navigationController()?.pushViewController(controller, animated: true)
|
||||
|
||||
let _ = ApplicationSpecificNotice.incrementAudioTranscriptionSuggestion(accountManager: context.sharedContext.accountManager).startStandalone()
|
||||
}
|
||||
return false })
|
||||
arguments.controllerInteraction.presentControllerInCurrent(tipController, nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var shouldBeginTranscription = false
|
||||
|
|
@ -450,6 +465,12 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||
}
|
||||
strongSelf.transcribeDisposable?.dispose()
|
||||
strongSelf.transcribeDisposable = nil
|
||||
|
||||
if let arguments = strongSelf.arguments, !arguments.associatedData.isPremium {
|
||||
Queue.mainQueue().after(0.1, {
|
||||
let _ = strongSelf.presentAudioTranscriptionTooltip(finished: true)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -471,6 +492,58 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||
}
|
||||
}
|
||||
|
||||
private func presentAudioTranscriptionTooltip(finished: Bool) -> Bool {
|
||||
guard let arguments = self.arguments, !arguments.associatedData.isPremium else {
|
||||
return false
|
||||
}
|
||||
|
||||
let presentationData = arguments.context.sharedContext.currentPresentationData.with { $0 }
|
||||
var text: String?
|
||||
var timeout: Double = 5.0
|
||||
|
||||
let currentTime = Int32(Date().timeIntervalSince1970)
|
||||
if let cooldownUntilTime = arguments.associatedData.audioTranscriptionTrial.cooldownUntilTime, cooldownUntilTime > currentTime {
|
||||
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: arguments.context.currentAppConfiguration.with { $0 })
|
||||
|
||||
let time = stringForMediumDate(timestamp: cooldownUntilTime, strings: arguments.presentationData.strings, dateTimeFormat: arguments.presentationData.dateTimeFormat)
|
||||
let usedString = arguments.presentationData.strings.Conversation_FreeTranscriptionCooldownTooltip(premiumConfiguration.audioTransciptionTrialCount)
|
||||
let waitString = arguments.presentationData.strings.Conversation_FreeTranscriptionWaitOrSubscribe(time).string
|
||||
let fullString = "\(usedString) \(waitString)"
|
||||
text = fullString
|
||||
|
||||
if self.hapticFeedback == nil {
|
||||
self.hapticFeedback = HapticFeedback()
|
||||
}
|
||||
self.hapticFeedback?.impact(.medium)
|
||||
timeout = 7.0
|
||||
} else if finished {
|
||||
let remainingCount = arguments.associatedData.audioTranscriptionTrial.remainingCount
|
||||
text = arguments.presentationData.strings.Conversation_FreeTranscriptionLimitTooltip(remainingCount)
|
||||
}
|
||||
|
||||
guard let text else {
|
||||
return false
|
||||
}
|
||||
let context = arguments.context
|
||||
let tipController = UndoOverlayController(presentationData: presentationData, content: .universal(animation: "Transcribe", scale: 0.06, colors: [:], title: nil, text: text, customUndoText: nil, timeout: timeout), elevatedLayout: false, position: .top, animateInAsReplacement: false, action: { action in
|
||||
if case .info = action {
|
||||
var replaceImpl: ((ViewController) -> Void)?
|
||||
let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .voiceToText, action: {
|
||||
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .settings, forceDark: false, dismissed: nil)
|
||||
replaceImpl?(controller)
|
||||
})
|
||||
replaceImpl = { [weak controller] c in
|
||||
controller?.replace(with: c)
|
||||
}
|
||||
arguments.controllerInteraction.navigationController()?.pushViewController(controller, animated: true)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
arguments.controllerInteraction.presentControllerInCurrent(tipController, nil)
|
||||
return true
|
||||
}
|
||||
|
||||
public func asyncLayout() -> (Arguments) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool, ListViewItemUpdateAnimation, ListViewItemApply?) -> Void))) {
|
||||
let currentFile = self.file
|
||||
|
||||
|
|
@ -670,23 +743,24 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||
var textString: NSAttributedString?
|
||||
var updatedAudioTranscriptionState: AudioTranscriptionButtonComponent.TranscriptionState?
|
||||
|
||||
let displayTranscribe: Bool
|
||||
var displayTranscribe = false
|
||||
if arguments.message.id.peerId.namespace != Namespaces.Peer.SecretChat {
|
||||
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: arguments.context.currentAppConfiguration.with { $0 })
|
||||
if arguments.associatedData.isPremium {
|
||||
displayTranscribe = true
|
||||
} else if premiumConfiguration.audioTransciptionTrialCount > 0 {
|
||||
if arguments.incoming {
|
||||
if audioDuration < premiumConfiguration.audioTransciptionTrialMaxDuration {
|
||||
displayTranscribe = true
|
||||
}
|
||||
}
|
||||
} else if arguments.associatedData.alwaysDisplayTranscribeButton.canBeDisplayed {
|
||||
if audioDuration >= 60 {
|
||||
displayTranscribe = true
|
||||
} else if arguments.incoming && isConsumed == false && arguments.associatedData.alwaysDisplayTranscribeButton.displayForNotConsumed {
|
||||
displayTranscribe = true
|
||||
} else {
|
||||
displayTranscribe = false
|
||||
}
|
||||
} else {
|
||||
displayTranscribe = false
|
||||
}
|
||||
} else {
|
||||
displayTranscribe = false
|
||||
}
|
||||
|
||||
let transcribedText = forcedAudioTranscriptionText ?? transcribedText(message: arguments.message)
|
||||
|
|
@ -700,6 +774,11 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||
break
|
||||
}
|
||||
|
||||
let currentTime = Int32(Date().timeIntervalSince1970)
|
||||
if transcribedText == nil, let cooldownUntilTime = arguments.associatedData.audioTranscriptionTrial.cooldownUntilTime, cooldownUntilTime > currentTime {
|
||||
updatedAudioTranscriptionState = .locked
|
||||
}
|
||||
|
||||
let effectiveAudioTranscriptionState = updatedAudioTranscriptionState ?? audioTranscriptionState
|
||||
|
||||
var displayTrailingAnimatedDots = false
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import InstantVideoRadialStatusNode
|
|||
import ChatInstantVideoMessageDurationNode
|
||||
import ChatControllerInteraction
|
||||
import WallpaperBackgroundNode
|
||||
import TelegramStringFormatting
|
||||
|
||||
public struct ChatMessageInstantVideoItemLayoutResult {
|
||||
public let contentSize: CGSize
|
||||
|
|
@ -590,6 +591,11 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
|||
updatedTranscriptionText = transcribedText
|
||||
}
|
||||
|
||||
let currentTime = Int32(Date().timeIntervalSince1970)
|
||||
if transcribedText == nil, let cooldownUntilTime = item.associatedData.audioTranscriptionTrial.cooldownUntilTime, cooldownUntilTime > currentTime {
|
||||
updatedAudioTranscriptionState = .locked
|
||||
}
|
||||
|
||||
let effectiveAudioTranscriptionState = updatedAudioTranscriptionState ?? audioTranscriptionState
|
||||
|
||||
return (result, { [weak self] layoutData, animation in
|
||||
|
|
@ -775,21 +781,22 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
|||
}))
|
||||
}
|
||||
|
||||
var displayTranscribe: Bool
|
||||
var displayTranscribe = false
|
||||
if item.message.id.peerId.namespace != Namespaces.Peer.SecretChat && statusDisplayType == .free {
|
||||
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: item.context.currentAppConfiguration.with { $0 })
|
||||
if item.associatedData.isPremium {
|
||||
displayTranscribe = true
|
||||
} else if premiumConfiguration.audioTransciptionTrialCount > 0 {
|
||||
if incoming {
|
||||
displayTranscribe = true
|
||||
}
|
||||
} else if item.associatedData.alwaysDisplayTranscribeButton.canBeDisplayed {
|
||||
if incoming && notConsumed && item.associatedData.alwaysDisplayTranscribeButton.displayForNotConsumed {
|
||||
displayTranscribe = true
|
||||
} else {
|
||||
displayTranscribe = false
|
||||
}
|
||||
} else {
|
||||
displayTranscribe = false
|
||||
}
|
||||
} else {
|
||||
displayTranscribe = false
|
||||
}
|
||||
|
||||
if displayTranscribe, let durationBlurColor = durationBlurColor {
|
||||
|
|
@ -1614,32 +1621,47 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
|||
guard let item = self.item, item.message.id.namespace == Namespaces.Message.Cloud else {
|
||||
return
|
||||
}
|
||||
|
||||
let presentationData = item.context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
guard item.associatedData.isPremium else {
|
||||
if self.hapticFeedback == nil {
|
||||
self.hapticFeedback = HapticFeedback()
|
||||
}
|
||||
self.hapticFeedback?.impact(.medium)
|
||||
|
||||
let presentationData = item.context.sharedContext.currentPresentationData.with { $0 }
|
||||
let tipController = UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_voiceToText", scale: 0.065, colors: [:], title: nil, text: presentationData.strings.Message_AudioTranscription_SubscribeToPremium, customUndoText: presentationData.strings.Message_AudioTranscription_SubscribeToPremiumAction, timeout: nil), elevatedLayout: false, position: .top, animateInAsReplacement: false, action: { action in
|
||||
if case .undo = action {
|
||||
let context = item.context
|
||||
var replaceImpl: ((ViewController) -> Void)?
|
||||
let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .voiceToText, action: {
|
||||
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .settings, forceDark: false, dismissed: nil)
|
||||
replaceImpl?(controller)
|
||||
})
|
||||
replaceImpl = { [weak controller] c in
|
||||
controller?.replace(with: c)
|
||||
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: item.context.currentAppConfiguration.with { $0 })
|
||||
|
||||
let transcriptionText = transcribedText(message: item.message)
|
||||
if transcriptionText == nil {
|
||||
if premiumConfiguration.audioTransciptionTrialCount > 0 {
|
||||
if !item.associatedData.isPremium {
|
||||
if self.presentAudioTranscriptionTooltip(finished: false) {
|
||||
return
|
||||
}
|
||||
item.controllerInteraction.navigationController()?.pushViewController(controller, animated: true)
|
||||
|
||||
let _ = ApplicationSpecificNotice.incrementAudioTranscriptionSuggestion(accountManager: item.context.sharedContext.accountManager).startStandalone()
|
||||
}
|
||||
return false })
|
||||
item.controllerInteraction.presentControllerInCurrent(tipController, nil)
|
||||
return
|
||||
} else {
|
||||
guard item.associatedData.isPremium else {
|
||||
if self.hapticFeedback == nil {
|
||||
self.hapticFeedback = HapticFeedback()
|
||||
}
|
||||
self.hapticFeedback?.impact(.medium)
|
||||
|
||||
|
||||
let tipController = UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_voiceToText", scale: 0.065, colors: [:], title: nil, text: presentationData.strings.Message_AudioTranscription_SubscribeToPremium, customUndoText: presentationData.strings.Message_AudioTranscription_SubscribeToPremiumAction, timeout: nil), elevatedLayout: false, position: .top, animateInAsReplacement: false, action: { action in
|
||||
if case .undo = action {
|
||||
let context = item.context
|
||||
var replaceImpl: ((ViewController) -> Void)?
|
||||
let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .voiceToText, action: {
|
||||
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .settings, forceDark: false, dismissed: nil)
|
||||
replaceImpl?(controller)
|
||||
})
|
||||
replaceImpl = { [weak controller] c in
|
||||
controller?.replace(with: c)
|
||||
}
|
||||
item.controllerInteraction.navigationController()?.pushViewController(controller, animated: true)
|
||||
|
||||
let _ = ApplicationSpecificNotice.incrementAudioTranscriptionSuggestion(accountManager: item.context.sharedContext.accountManager).startStandalone()
|
||||
}
|
||||
return false })
|
||||
item.controllerInteraction.presentControllerInCurrent(tipController, nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var shouldBeginTranscription = false
|
||||
|
|
@ -1673,6 +1695,12 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
|||
}
|
||||
strongSelf.transcribeDisposable?.dispose()
|
||||
strongSelf.transcribeDisposable = nil
|
||||
|
||||
if let item = strongSelf.item, !item.associatedData.isPremium {
|
||||
Queue.mainQueue().after(0.1, {
|
||||
let _ = strongSelf.presentAudioTranscriptionTooltip(finished: true)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1694,6 +1722,58 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
|||
self.updateTranscriptionExpanded?(self.audioTranscriptionState)
|
||||
}
|
||||
|
||||
private func presentAudioTranscriptionTooltip(finished: Bool) -> Bool {
|
||||
guard let item = self.item, !item.associatedData.isPremium else {
|
||||
return false
|
||||
}
|
||||
|
||||
let presentationData = item.context.sharedContext.currentPresentationData.with { $0 }
|
||||
var text: String?
|
||||
var timeout: Double = 5.0
|
||||
|
||||
let currentTime = Int32(Date().timeIntervalSince1970)
|
||||
if let cooldownUntilTime = item.associatedData.audioTranscriptionTrial.cooldownUntilTime, cooldownUntilTime > currentTime {
|
||||
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: item.context.currentAppConfiguration.with { $0 })
|
||||
|
||||
let time = stringForMediumDate(timestamp: cooldownUntilTime, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat)
|
||||
let usedString = presentationData.strings.Conversation_FreeTranscriptionCooldownTooltip(premiumConfiguration.audioTransciptionTrialCount)
|
||||
let waitString = presentationData.strings.Conversation_FreeTranscriptionWaitOrSubscribe(time).string
|
||||
let fullString = "\(usedString) \(waitString)"
|
||||
text = fullString
|
||||
|
||||
if self.hapticFeedback == nil {
|
||||
self.hapticFeedback = HapticFeedback()
|
||||
}
|
||||
self.hapticFeedback?.impact(.medium)
|
||||
timeout = 7.0
|
||||
} else if finished {
|
||||
let remainingCount = item.associatedData.audioTranscriptionTrial.remainingCount
|
||||
text = presentationData.strings.Conversation_FreeTranscriptionLimitTooltip(remainingCount)
|
||||
}
|
||||
|
||||
guard let text else {
|
||||
return false
|
||||
}
|
||||
let context = item.context
|
||||
let tipController = UndoOverlayController(presentationData: presentationData, content: .universal(animation: "Transcribe", scale: 0.06, colors: [:], title: nil, text: text, customUndoText: nil, timeout: timeout), elevatedLayout: false, position: .top, animateInAsReplacement: false, action: { action in
|
||||
if case .info = action {
|
||||
var replaceImpl: ((ViewController) -> Void)?
|
||||
let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .voiceToText, action: {
|
||||
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .settings, forceDark: false, dismissed: nil)
|
||||
replaceImpl?(controller)
|
||||
})
|
||||
replaceImpl = { [weak controller] c in
|
||||
controller?.replace(with: c)
|
||||
}
|
||||
item.controllerInteraction.navigationController()?.pushViewController(controller, animated: true)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
item.controllerInteraction.presentControllerInCurrent(tipController, nil)
|
||||
return true
|
||||
}
|
||||
|
||||
public final class AnimateFileNodeDescription {
|
||||
public let node: ASDisplayNode
|
||||
public let textClippingNode: ASDisplayNode
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ swift_library(
|
|||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/Components/BundleIconComponent",
|
||||
"//submodules/ChatMessageBackground",
|
||||
"//submodules/ContextUI",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import AvatarNode
|
|||
import MultilineTextComponent
|
||||
import BundleIconComponent
|
||||
import ChatMessageBackground
|
||||
import ContextUI
|
||||
|
||||
private func attributedServiceMessageString(theme: ChatPresentationThemeData, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: EngineMessage, accountPeerId: EnginePeer.Id) -> NSAttributedString? {
|
||||
return universalServiceMessageString(presentationData: (theme.theme, theme.wallpaper), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: false, forForumOverview: false)
|
||||
|
|
@ -332,6 +333,9 @@ public class ChatMessageJoinedChannelBubbleContentNode: ChatMessageBubbleContent
|
|||
addAppLogEvent(postbox: item.context.account.postbox, type: "channels.open_recommended_channel", data: json)
|
||||
}
|
||||
item.controllerInteraction.openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: nil), nil, .default)
|
||||
},
|
||||
contextAction: { peer, sourceView, gesture in
|
||||
item.controllerInteraction.openRecommendedChannelContextMenu(peer, sourceView, gesture)
|
||||
}
|
||||
)
|
||||
),
|
||||
|
|
@ -509,19 +513,22 @@ private final class ChannelItemComponent: Component {
|
|||
let peer: EnginePeer
|
||||
let subtitle: String
|
||||
let action: (EnginePeer) -> Void
|
||||
let contextAction: (EnginePeer, UIView, ContextGesture?) -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
theme: PresentationTheme,
|
||||
peer: EnginePeer,
|
||||
subtitle: String,
|
||||
action: @escaping (EnginePeer) -> Void
|
||||
action: @escaping (EnginePeer) -> Void,
|
||||
contextAction: @escaping (EnginePeer, UIView, ContextGesture?) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.peer = peer
|
||||
self.subtitle = subtitle
|
||||
self.action = action
|
||||
self.contextAction = contextAction
|
||||
}
|
||||
|
||||
static func ==(lhs: ChannelItemComponent, rhs: ChannelItemComponent) -> Bool {
|
||||
|
|
@ -541,6 +548,7 @@ private final class ChannelItemComponent: Component {
|
|||
}
|
||||
|
||||
final class View: UIView {
|
||||
private let contextContainer: ContextControllerSourceView
|
||||
private let containerButton: HighlightTrackingButton
|
||||
|
||||
private let title = ComponentView<Empty>()
|
||||
|
|
@ -553,6 +561,8 @@ private final class ChannelItemComponent: Component {
|
|||
private weak var state: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.contextContainer = ContextControllerSourceView()
|
||||
|
||||
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0))
|
||||
self.avatarNode.isUserInteractionEnabled = false
|
||||
|
||||
|
|
@ -562,13 +572,21 @@ private final class ChannelItemComponent: Component {
|
|||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.containerButton)
|
||||
self.addSubnode(self.avatarNode)
|
||||
self.addSubview(self.contextContainer)
|
||||
|
||||
self.contextContainer.addSubview(self.containerButton)
|
||||
self.contextContainer.addSubnode(self.avatarNode)
|
||||
self.avatarNode.view.addSubview(self.avatarBadge)
|
||||
|
||||
self.avatarNode.badgeView = self.avatarBadge
|
||||
|
||||
self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
|
||||
|
||||
self.contextContainer.activated = { [weak self] gesture, point in
|
||||
if let self, let component = self.component {
|
||||
component.contextAction(component.peer, self.contextContainer, gesture)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
|
|
@ -628,21 +646,21 @@ private final class ChannelItemComponent: Component {
|
|||
if let titleView = self.title.view {
|
||||
if titleView.superview == nil {
|
||||
titleView.isUserInteractionEnabled = false
|
||||
self.addSubview(titleView)
|
||||
self.contextContainer.addSubview(titleView)
|
||||
}
|
||||
titleView.frame = titleFrame
|
||||
}
|
||||
if let subtitleView = self.subtitle.view {
|
||||
if subtitleView.superview == nil {
|
||||
subtitleView.isUserInteractionEnabled = false
|
||||
self.addSubview(subtitleView)
|
||||
self.contextContainer.addSubview(subtitleView)
|
||||
}
|
||||
subtitleView.frame = subtitleFrame
|
||||
}
|
||||
if let subtitleIconView = self.subtitleIcon.view {
|
||||
if subtitleIconView.superview == nil {
|
||||
subtitleIconView.isUserInteractionEnabled = false
|
||||
self.addSubview(subtitleIconView)
|
||||
self.contextContainer.addSubview(subtitleIconView)
|
||||
}
|
||||
subtitleIconView.frame = subtitleIconFrame
|
||||
}
|
||||
|
|
@ -654,7 +672,9 @@ private final class ChannelItemComponent: Component {
|
|||
let avatarBadgeFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((avatarFrame.width - avatarBadgeSize.width) / 2.0), y: avatarFrame.height - avatarBadgeSize.height + 2.0), size: avatarBadgeSize).insetBy(dx: -strokeWidth, dy: -strokeWidth)
|
||||
self.avatarBadge.frame = avatarBadgeFrame
|
||||
|
||||
self.containerButton.frame = CGRect(origin: .zero, size: itemSize)
|
||||
let bounds = CGRect(origin: .zero, size: itemSize)
|
||||
self.contextContainer.frame = bounds
|
||||
self.containerButton.frame = bounds
|
||||
|
||||
return itemSize
|
||||
}
|
||||
|
|
@ -676,17 +696,20 @@ final class ChannelListPanelComponent: Component {
|
|||
let theme: PresentationTheme
|
||||
let peers: RecommendedChannels
|
||||
let action: (EnginePeer) -> Void
|
||||
let contextAction: (EnginePeer, UIView, ContextGesture?) -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
theme: PresentationTheme,
|
||||
peers: RecommendedChannels,
|
||||
action: @escaping (EnginePeer) -> Void
|
||||
action: @escaping (EnginePeer) -> Void,
|
||||
contextAction: @escaping (EnginePeer, UIView, ContextGesture?) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.peers = peers
|
||||
self.action = action
|
||||
self.contextAction = contextAction
|
||||
}
|
||||
|
||||
static func ==(lhs: ChannelListPanelComponent, rhs: ChannelListPanelComponent) -> Bool {
|
||||
|
|
@ -832,7 +855,8 @@ final class ChannelListPanelComponent: Component {
|
|||
theme: component.theme,
|
||||
peer: item.peer,
|
||||
subtitle: subtitle,
|
||||
action: component.action
|
||||
action: component.action,
|
||||
contextAction: component.contextAction
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: itemLayout.itemWidth, height: itemLayout.containerHeight)
|
||||
|
|
|
|||
|
|
@ -1850,7 +1850,7 @@ private class QrContentNode: ASDisplayNode, ContentNode {
|
|||
func update(theme: PresentationTheme, wallpaper: TelegramWallpaper, isDarkAppearance: Bool, selectedEmoticon: String?) {
|
||||
self.currentParams = (theme, wallpaper, isDarkAppearance, selectedEmoticon)
|
||||
|
||||
self.wallpaperBackgroundNode.update(wallpaper: wallpaper)
|
||||
self.wallpaperBackgroundNode.update(wallpaper: wallpaper, animated: false)
|
||||
|
||||
self.codeForegroundDimNode.alpha = isDarkAppearance ? 0.5 : 0.3
|
||||
|
||||
|
|
@ -2185,7 +2185,7 @@ private class MessageContentNode: ASDisplayNode, ContentNode {
|
|||
func update(theme: PresentationTheme, wallpaper: TelegramWallpaper, isDarkAppearance: Bool, selectedEmoticon: String?) {
|
||||
self.currentParams = (theme, wallpaper, isDarkAppearance, selectedEmoticon)
|
||||
|
||||
self.wallpaperBackgroundNode.update(wallpaper: wallpaper)
|
||||
self.wallpaperBackgroundNode.update(wallpaper: wallpaper, animated: false)
|
||||
|
||||
self.linkBackgroundDimNode.alpha = isDarkAppearance ? 0.6 : 0.2
|
||||
|
||||
|
|
|
|||
|
|
@ -570,6 +570,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
|||
}, openNoAdsDemo: {
|
||||
}, displayGiveawayParticipationStatus: { _ in
|
||||
}, openPremiumStatusInfo: { _, _, _, _ in
|
||||
}, openRecommendedChannelContextMenu: { _, _, _ in
|
||||
}, requestMessageUpdate: { _, _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
}, dismissTextInput: {
|
||||
|
|
@ -663,7 +664,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
|||
self.chatPresentationData = ChatPresentationData(theme: ChatPresentationThemeData(theme: presentationData.theme, wallpaper: presentationData.chatWallpaper), fontSize: presentationData.chatFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true, largeEmoji: presentationData.largeEmoji, chatBubbleCorners: presentationData.chatBubbleCorners)
|
||||
self.chatPresentationDataPromise.set(.single(self.chatPresentationData))
|
||||
|
||||
self.backgroundNode.update(wallpaper: presentationData.chatWallpaper)
|
||||
self.backgroundNode.update(wallpaper: presentationData.chatWallpaper, animated: false)
|
||||
self.backgroundNode.updateBubbleTheme(bubbleTheme: presentationData.theme, bubbleCorners: presentationData.chatBubbleCorners)
|
||||
|
||||
self.panelBackgroundNode.updateColor(color: presentationData.theme.chat.inputPanel.panelBackgroundColor, transition: .immediate)
|
||||
|
|
|
|||
|
|
@ -230,6 +230,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
|
|||
public let openNoAdsDemo: () -> Void
|
||||
public let displayGiveawayParticipationStatus: (EngineMessage.Id) -> Void
|
||||
public let openPremiumStatusInfo: (EnginePeer.Id, UIView, Int64?, PeerNameColor) -> Void
|
||||
public let openRecommendedChannelContextMenu: (EnginePeer, UIView, ContextGesture?) -> Void
|
||||
|
||||
public let requestMessageUpdate: (MessageId, Bool) -> Void
|
||||
public let cancelInteractiveKeyboardGestures: () -> Void
|
||||
|
|
@ -349,6 +350,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
|
|||
openNoAdsDemo: @escaping () -> Void,
|
||||
displayGiveawayParticipationStatus: @escaping (EngineMessage.Id) -> Void,
|
||||
openPremiumStatusInfo: @escaping (EnginePeer.Id, UIView, Int64?, PeerNameColor) -> Void,
|
||||
openRecommendedChannelContextMenu: @escaping (EnginePeer, UIView, ContextGesture?) -> Void,
|
||||
requestMessageUpdate: @escaping (MessageId, Bool) -> Void,
|
||||
cancelInteractiveKeyboardGestures: @escaping () -> Void,
|
||||
dismissTextInput: @escaping () -> Void,
|
||||
|
|
@ -450,6 +452,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
|
|||
self.openNoAdsDemo = openNoAdsDemo
|
||||
self.displayGiveawayParticipationStatus = displayGiveawayParticipationStatus
|
||||
self.openPremiumStatusInfo = openPremiumStatusInfo
|
||||
self.openRecommendedChannelContextMenu = openRecommendedChannelContextMenu
|
||||
self.requestMessageUpdate = requestMessageUpdate
|
||||
self.cancelInteractiveKeyboardGestures = cancelInteractiveKeyboardGestures
|
||||
self.dismissTextInput = dismissTextInput
|
||||
|
|
|
|||
|
|
@ -249,6 +249,7 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView {
|
|||
audioRecorder: nil,
|
||||
videoRecordingStatus: nil,
|
||||
isRecordingLocked: false,
|
||||
hasRecordedVideo: false,
|
||||
recordedAudioPreview: nil,
|
||||
hasRecordedVideoPreview: false,
|
||||
wasRecordingDismissed: false,
|
||||
|
|
@ -261,6 +262,7 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView {
|
|||
customInputView: self.inputView,
|
||||
forceIsEditing: false,
|
||||
disabledPlaceholder: nil,
|
||||
header: nil,
|
||||
isChannel: false,
|
||||
storyItem: nil,
|
||||
chatLocation: self.chatLocation
|
||||
|
|
|
|||
|
|
@ -5,42 +5,8 @@ import MetalKit
|
|||
import Display
|
||||
import Accelerate
|
||||
|
||||
func loadTexture(image: UIImage, device: MTLDevice) -> MTLTexture? {
|
||||
func dataForImage(_ image: UIImage) -> UnsafeMutablePointer<UInt8> {
|
||||
let imageRef = image.cgImage
|
||||
let width = Int(image.size.width)
|
||||
let height = Int(image.size.height)
|
||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
|
||||
let rawData = UnsafeMutablePointer<UInt8>.allocate(capacity: width * height * 4)
|
||||
let bytePerPixel = 4
|
||||
let bytesPerRow = bytePerPixel * Int(width)
|
||||
let bitsPerComponent = 8
|
||||
let bitmapInfo = CGBitmapInfo.byteOrder32Little.rawValue + CGImageAlphaInfo.premultipliedFirst.rawValue
|
||||
let context = CGContext.init(data: rawData, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo)
|
||||
context?.draw(imageRef!, in: CGRect(x: 0, y: 0, width: width, height: height))
|
||||
|
||||
return rawData
|
||||
}
|
||||
|
||||
let width = Int(image.size.width * image.scale)
|
||||
let height = Int(image.size.height * image.scale)
|
||||
let bytePerPixel = 4
|
||||
let bytesPerRow = bytePerPixel * width
|
||||
|
||||
var texture : MTLTexture?
|
||||
let region = MTLRegionMake2D(0, 0, Int(width), Int(height))
|
||||
let textureDescriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .bgra8Unorm, width: width, height: height, mipmapped: false)
|
||||
texture = device.makeTexture(descriptor: textureDescriptor)
|
||||
|
||||
let data = dataForImage(image)
|
||||
texture?.replace(region: region, mipmapLevel: 0, withBytes: data, bytesPerRow: bytesPerRow)
|
||||
|
||||
return texture
|
||||
}
|
||||
|
||||
final class ImageTextureSource: TextureSource {
|
||||
weak var output: TextureConsumer?
|
||||
weak var output: MediaEditorRenderer?
|
||||
|
||||
var texture: MTLTexture?
|
||||
|
||||
|
|
@ -50,10 +16,10 @@ final class ImageTextureSource: TextureSource {
|
|||
}
|
||||
}
|
||||
|
||||
func connect(to consumer: TextureConsumer) {
|
||||
func connect(to consumer: MediaEditorRenderer) {
|
||||
self.output = consumer
|
||||
if let texture = self.texture {
|
||||
self.output?.consumeTexture(texture, render: false)
|
||||
self.output?.consume(main: .texture(texture, .zero), additional: nil, render: false)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -61,36 +27,3 @@ final class ImageTextureSource: TextureSource {
|
|||
self.texture = nil
|
||||
}
|
||||
}
|
||||
|
||||
func pixelBufferToMTLTexture(pixelBuffer: CVPixelBuffer, textureCache: CVMetalTextureCache) -> MTLTexture? {
|
||||
let width = CVPixelBufferGetWidth(pixelBuffer)
|
||||
let height = CVPixelBufferGetHeight(pixelBuffer)
|
||||
|
||||
let format: MTLPixelFormat = .r8Unorm
|
||||
var textureRef : CVMetalTexture?
|
||||
let status = CVMetalTextureCacheCreateTextureFromImage(nil, textureCache, pixelBuffer, nil, format, width, height, 0, &textureRef)
|
||||
if status == kCVReturnSuccess {
|
||||
return CVMetalTextureGetTexture(textureRef!)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getTextureImage(device: MTLDevice, texture: MTLTexture, mirror: Bool = false) -> UIImage? {
|
||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
let context = CIContext(mtlDevice: device, options: [:])
|
||||
guard var ciImage = CIImage(mtlTexture: texture, options: [.colorSpace: colorSpace]) else {
|
||||
return nil
|
||||
}
|
||||
let transform: CGAffineTransform
|
||||
if mirror {
|
||||
transform = CGAffineTransform(-1.0, 0.0, 0.0, -1.0, ciImage.extent.width, ciImage.extent.height)
|
||||
} else {
|
||||
transform = CGAffineTransform(1.0, 0.0, 0.0, -1.0, 0.0, ciImage.extent.height)
|
||||
}
|
||||
ciImage = ciImage.transformed(by: transform)
|
||||
guard let cgImage = context.createCGImage(ciImage, from: CGRect(origin: .zero, size: CGSize(width: ciImage.extent.width, height: ciImage.extent.height))) else {
|
||||
return nil
|
||||
}
|
||||
return UIImage(cgImage: cgImage)
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -26,14 +26,17 @@ public func mediaEditorGenerateGradientImage(size: CGSize, colors: [UIColor]) ->
|
|||
return image
|
||||
}
|
||||
|
||||
public func mediaEditorGetGradientColors(from image: UIImage) -> (UIColor, UIColor) {
|
||||
public func mediaEditorGetGradientColors(from image: UIImage) -> MediaEditor.GradientColors {
|
||||
let context = DrawingContext(size: CGSize(width: 5.0, height: 5.0), scale: 1.0, clear: false)!
|
||||
context.withFlippedContext({ context in
|
||||
if let cgImage = image.cgImage {
|
||||
context.draw(cgImage, in: CGRect(x: 0.0, y: 0.0, width: 5.0, height: 5.0))
|
||||
}
|
||||
})
|
||||
return (context.colorAt(CGPoint(x: 2.0, y: 0.0)), context.colorAt(CGPoint(x: 2.0, y: 4.0)))
|
||||
return MediaEditor.GradientColors(
|
||||
top: context.colorAt(CGPoint(x: 2.0, y: 0.0)),
|
||||
bottom: context.colorAt(CGPoint(x: 2.0, y: 4.0))
|
||||
)
|
||||
}
|
||||
|
||||
final class MediaEditorComposer {
|
||||
|
|
@ -110,9 +113,9 @@ final class MediaEditorComposer {
|
|||
if let additionalSampleBuffer, let additionalImageBuffer = CMSampleBufferGetImageBuffer(additionalSampleBuffer) {
|
||||
additionalPixelBuffer = VideoPixelBuffer(pixelBuffer: additionalImageBuffer, rotation: additionalTextureRotation, timestamp: time)
|
||||
}
|
||||
self.renderer.consumeVideoPixelBuffer(pixelBuffer: mainPixelBuffer, additionalPixelBuffer: additionalPixelBuffer, render: true)
|
||||
self.renderer.consume(main: .videoBuffer(mainPixelBuffer), additional: additionalPixelBuffer.flatMap { .videoBuffer($0) }, render: true)
|
||||
|
||||
if let finalTexture = self.renderer.finalTexture, var ciImage = CIImage(mtlTexture: finalTexture, options: [.colorSpace: self.colorSpace]) {
|
||||
if let finalTexture = self.renderer.resultTexture, var ciImage = CIImage(mtlTexture: finalTexture, options: [.colorSpace: self.colorSpace]) {
|
||||
ciImage = ciImage.transformed(by: CGAffineTransformMakeScale(1.0, -1.0).translatedBy(x: 0.0, y: -ciImage.extent.height))
|
||||
|
||||
var pixelBuffer: CVPixelBuffer?
|
||||
|
|
@ -144,9 +147,9 @@ final class MediaEditorComposer {
|
|||
}
|
||||
if self.filteredImage == nil, let device = self.device {
|
||||
if let texture = loadTexture(image: inputImage, device: device) {
|
||||
self.renderer.consumeTexture(texture, render: true)
|
||||
self.renderer.consume(main: .texture(texture, .zero), additional: nil, render: true)
|
||||
|
||||
if let finalTexture = self.renderer.finalTexture, var ciImage = CIImage(mtlTexture: finalTexture, options: [.colorSpace: self.colorSpace]) {
|
||||
if let finalTexture = self.renderer.resultTexture, var ciImage = CIImage(mtlTexture: finalTexture, options: [.colorSpace: self.colorSpace]) {
|
||||
ciImage = ciImage.transformed(by: CGAffineTransformMakeScale(1.0, -1.0).translatedBy(x: 0.0, y: -ciImage.extent.height))
|
||||
self.filteredImage = ciImage
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,7 +65,6 @@ func composerEntitiesForDrawingEntity(postbox: Postbox, textScale: CGFloat, enti
|
|||
if let entity = entity as? DrawingStickerEntity {
|
||||
if case let .file(_, type) = entity.content, case .reaction = type {
|
||||
return []
|
||||
// return [MediaEditorComposerStaticEntity(image: image, position: entity.position, scale: entity.scale, rotation: entity.rotation, baseSize: entity.baseSize, mirrored: false)]
|
||||
} else {
|
||||
let content: MediaEditorComposerStickerEntity.Content
|
||||
switch entity.content {
|
||||
|
|
|
|||
|
|
@ -21,11 +21,6 @@ final class VideoPixelBuffer {
|
|||
}
|
||||
}
|
||||
|
||||
protocol TextureConsumer: AnyObject {
|
||||
func consumeTexture(_ texture: MTLTexture, render: Bool)
|
||||
func consumeVideoPixelBuffer(pixelBuffer: VideoPixelBuffer, additionalPixelBuffer: VideoPixelBuffer?, render: Bool)
|
||||
}
|
||||
|
||||
final class RenderingContext {
|
||||
let device: MTLDevice
|
||||
let commandBuffer: MTLCommandBuffer
|
||||
|
|
@ -45,7 +40,7 @@ protocol RenderPass: AnyObject {
|
|||
}
|
||||
|
||||
protocol TextureSource {
|
||||
func connect(to: TextureConsumer)
|
||||
func connect(to renderer: MediaEditorRenderer)
|
||||
func invalidate()
|
||||
}
|
||||
|
||||
|
|
@ -60,7 +55,7 @@ protocol RenderTarget: AnyObject {
|
|||
func redraw()
|
||||
}
|
||||
|
||||
final class MediaEditorRenderer: TextureConsumer {
|
||||
final class MediaEditorRenderer {
|
||||
var textureSource: TextureSource? {
|
||||
didSet {
|
||||
self.textureSource?.connect(to: self)
|
||||
|
|
@ -70,9 +65,9 @@ final class MediaEditorRenderer: TextureConsumer {
|
|||
private var semaphore = DispatchSemaphore(value: 3)
|
||||
private var renderPasses: [RenderPass] = []
|
||||
|
||||
private let videoInputPass = VideoInputPass()
|
||||
private let mainVideoInputPass = VideoInputPass()
|
||||
private let additionalVideoInputPass = VideoInputPass()
|
||||
let videoFinishPass = VideoInputScalePass()
|
||||
let videoFinishPass = VideoFinishPass()
|
||||
|
||||
private let outputRenderPass = OutputRenderPass()
|
||||
private weak var renderTarget: RenderTarget? {
|
||||
|
|
@ -86,16 +81,33 @@ final class MediaEditorRenderer: TextureConsumer {
|
|||
private var commandQueue: MTLCommandQueue?
|
||||
private var textureCache: CVMetalTextureCache?
|
||||
|
||||
private var currentTexture: MTLTexture?
|
||||
private var currentAdditionalTexture: MTLTexture?
|
||||
private var currentTime: CMTime = .zero
|
||||
enum Input {
|
||||
case texture(MTLTexture, CMTime)
|
||||
case videoBuffer(VideoPixelBuffer)
|
||||
|
||||
var timestamp: CMTime {
|
||||
switch self {
|
||||
case let .texture(_, timestamp):
|
||||
return timestamp
|
||||
case let .videoBuffer(videoBuffer):
|
||||
return videoBuffer.timestamp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var currentPixelBuffer: VideoPixelBuffer?
|
||||
private var currentAdditionalPixelBuffer: VideoPixelBuffer?
|
||||
private var currentMainInput: Input?
|
||||
private var currentAdditionalInput: Input?
|
||||
|
||||
// private var currentTexture: MTLTexture?
|
||||
// private var currentAdditionalTexture: MTLTexture?
|
||||
// private var currentTime: CMTime = .zero
|
||||
//
|
||||
// private var currentPixelBuffer: VideoPixelBuffer?
|
||||
// private var currentAdditionalPixelBuffer: VideoPixelBuffer?
|
||||
|
||||
public var onNextRender: (() -> Void)?
|
||||
|
||||
var finalTexture: MTLTexture?
|
||||
var resultTexture: MTLTexture?
|
||||
|
||||
public init() {
|
||||
|
||||
|
|
@ -142,7 +154,7 @@ final class MediaEditorRenderer: TextureConsumer {
|
|||
|
||||
self.commandQueue = device.makeCommandQueue()
|
||||
self.commandQueue?.label = "Media Editor Command Queue"
|
||||
self.videoInputPass.setup(device: device, library: library)
|
||||
self.mainVideoInputPass.setup(device: device, library: library)
|
||||
self.additionalVideoInputPass.setup(device: device, library: library)
|
||||
self.videoFinishPass.setup(device: device, library: library)
|
||||
self.renderPasses.forEach { $0.setup(device: device, library: library) }
|
||||
|
|
@ -171,15 +183,48 @@ final class MediaEditorRenderer: TextureConsumer {
|
|||
|
||||
self.commandQueue = device.makeCommandQueue()
|
||||
self.commandQueue?.label = "Media Editor Command Queue"
|
||||
self.videoInputPass.setup(device: device, library: library)
|
||||
self.mainVideoInputPass.setup(device: device, library: library)
|
||||
self.additionalVideoInputPass.setup(device: device, library: library)
|
||||
self.videoFinishPass.setup(device: device, library: library)
|
||||
self.renderPasses.forEach { $0.setup(device: device, library: library) }
|
||||
}
|
||||
|
||||
public var displayEnabled = true
|
||||
var renderPassedEnabled = true
|
||||
var skipEditingPasses = true
|
||||
var needsDisplay = false
|
||||
|
||||
private func combinedTextureFromCurrentInputs(device: MTLDevice, commandBuffer: MTLCommandBuffer, textureCache: CVMetalTextureCache) -> MTLTexture? {
|
||||
var mainTexture: MTLTexture?
|
||||
var additionalTexture: MTLTexture?
|
||||
|
||||
func textureFromInput(_ input: MediaEditorRenderer.Input, videoInputPass: VideoInputPass) -> MTLTexture? {
|
||||
switch input {
|
||||
case let .texture(texture, _):
|
||||
return texture
|
||||
case let .videoBuffer(videoBuffer):
|
||||
return videoInputPass.processPixelBuffer(videoBuffer, textureCache: textureCache, device: device, commandBuffer: commandBuffer)
|
||||
}
|
||||
}
|
||||
|
||||
guard let mainInput = self.currentMainInput else {
|
||||
return nil
|
||||
}
|
||||
|
||||
mainTexture = textureFromInput(mainInput, videoInputPass: self.mainVideoInputPass)
|
||||
if let additionalInput = self.currentAdditionalInput {
|
||||
additionalTexture = textureFromInput(additionalInput, videoInputPass: self.additionalVideoInputPass)
|
||||
}
|
||||
|
||||
if let mainTexture, let additionalTexture {
|
||||
if let result = self.videoFinishPass.process(input: mainTexture, secondInput: additionalTexture, timestamp: mainInput.timestamp, device: device, commandBuffer: commandBuffer) {
|
||||
return result
|
||||
} else {
|
||||
return mainTexture
|
||||
}
|
||||
} else {
|
||||
return mainTexture
|
||||
}
|
||||
}
|
||||
|
||||
func renderFrame() {
|
||||
let device: MTLDevice?
|
||||
|
|
@ -192,52 +237,22 @@ final class MediaEditorRenderer: TextureConsumer {
|
|||
}
|
||||
guard let device = device,
|
||||
let commandQueue = self.commandQueue,
|
||||
let textureCache = self.textureCache else {
|
||||
let textureCache = self.textureCache,
|
||||
let commandBuffer = commandQueue.makeCommandBuffer(),
|
||||
var texture = self.combinedTextureFromCurrentInputs(device: device, commandBuffer: commandBuffer, textureCache: textureCache)
|
||||
else {
|
||||
self.didRenderFrame()
|
||||
return
|
||||
}
|
||||
|
||||
guard let commandBuffer = commandQueue.makeCommandBuffer() else {
|
||||
self.didRenderFrame()
|
||||
return
|
||||
}
|
||||
|
||||
var texture: MTLTexture
|
||||
if let currentAdditionalTexture = self.currentAdditionalTexture, let currentTexture = self.currentTexture {
|
||||
self.videoFinishPass.mainTextureRotation = .rotate0Degrees
|
||||
self.videoFinishPass.additionalTextureRotation = .rotate0DegreesMirrored
|
||||
if let result = self.videoFinishPass.process(input: currentTexture, secondInput: currentAdditionalTexture, timestamp: self.currentTime, device: device, commandBuffer: commandBuffer) {
|
||||
texture = result
|
||||
} else {
|
||||
texture = currentTexture
|
||||
}
|
||||
} else if let currentTexture = self.currentTexture {
|
||||
texture = currentTexture
|
||||
} else if let currentPixelBuffer = self.currentPixelBuffer, let currentAdditionalPixelBuffer = self.currentAdditionalPixelBuffer, let videoTexture = self.videoInputPass.processPixelBuffer(currentPixelBuffer, textureCache: textureCache, device: device, commandBuffer: commandBuffer), let additionalVideoTexture = self.additionalVideoInputPass.processPixelBuffer(currentAdditionalPixelBuffer, textureCache: textureCache, device: device, commandBuffer: commandBuffer) {
|
||||
if let result = self.videoFinishPass.process(input: videoTexture, secondInput: additionalVideoTexture, timestamp: currentPixelBuffer.timestamp, device: device, commandBuffer: commandBuffer) {
|
||||
texture = result
|
||||
} else {
|
||||
texture = videoTexture
|
||||
}
|
||||
} else if let currentPixelBuffer = self.currentPixelBuffer, let videoTexture = self.videoInputPass.processPixelBuffer(currentPixelBuffer, textureCache: textureCache, device: device, commandBuffer: commandBuffer) {
|
||||
if let result = self.videoFinishPass.process(input: videoTexture, secondInput: nil, timestamp: currentPixelBuffer.timestamp, device: device, commandBuffer: commandBuffer) {
|
||||
texture = result
|
||||
} else {
|
||||
texture = videoTexture
|
||||
}
|
||||
} else {
|
||||
self.didRenderFrame()
|
||||
return
|
||||
}
|
||||
|
||||
if self.renderPassedEnabled {
|
||||
if !self.skipEditingPasses {
|
||||
for renderPass in self.renderPasses {
|
||||
if let nextTexture = renderPass.process(input: texture, device: device, commandBuffer: commandBuffer) {
|
||||
texture = nextTexture
|
||||
}
|
||||
}
|
||||
}
|
||||
self.finalTexture = texture
|
||||
self.resultTexture = texture
|
||||
|
||||
if self.renderTarget == nil {
|
||||
commandBuffer.addCompletedHandler { [weak self] _ in
|
||||
|
|
@ -265,7 +280,7 @@ final class MediaEditorRenderer: TextureConsumer {
|
|||
let device = renderTarget.mtlDevice,
|
||||
let commandQueue = self.commandQueue,
|
||||
let commandBuffer = commandQueue.makeCommandBuffer(),
|
||||
let texture = self.finalTexture
|
||||
let texture = self.resultTexture
|
||||
else {
|
||||
self.needsDisplay = false
|
||||
self.didRenderFrame()
|
||||
|
|
@ -299,50 +314,70 @@ final class MediaEditorRenderer: TextureConsumer {
|
|||
self.semaphore.signal()
|
||||
}
|
||||
|
||||
func consumeTexture(_ texture: MTLTexture, render: Bool) {
|
||||
if render {
|
||||
self.willRenderFrame()
|
||||
}
|
||||
|
||||
self.currentTexture = texture
|
||||
if render {
|
||||
self.renderFrame()
|
||||
}
|
||||
}
|
||||
|
||||
func consumeTexture(_ texture: MTLTexture, additionalTexture: MTLTexture?, time: CMTime, render: Bool) {
|
||||
self.displayEnabled = false
|
||||
func consume(
|
||||
main: MediaEditorRenderer.Input,
|
||||
additional: MediaEditorRenderer.Input?,
|
||||
render: Bool,
|
||||
displayEnabled: Bool = true
|
||||
) {
|
||||
self.displayEnabled = displayEnabled
|
||||
|
||||
if render {
|
||||
self.willRenderFrame()
|
||||
}
|
||||
|
||||
self.currentTexture = texture
|
||||
self.currentAdditionalTexture = additionalTexture
|
||||
self.currentTime = time
|
||||
self.currentMainInput = main
|
||||
self.currentAdditionalInput = additional
|
||||
|
||||
if render {
|
||||
self.renderFrame()
|
||||
}
|
||||
}
|
||||
|
||||
var previousPresentationTimestamp: CMTime?
|
||||
func consumeVideoPixelBuffer(pixelBuffer: VideoPixelBuffer, additionalPixelBuffer: VideoPixelBuffer?, render: Bool) {
|
||||
self.willRenderFrame()
|
||||
|
||||
self.currentPixelBuffer = pixelBuffer
|
||||
if additionalPixelBuffer == nil && self.currentAdditionalPixelBuffer != nil {
|
||||
} else {
|
||||
self.currentAdditionalPixelBuffer = additionalPixelBuffer
|
||||
}
|
||||
if render {
|
||||
if self.previousPresentationTimestamp == pixelBuffer.timestamp {
|
||||
self.didRenderFrame()
|
||||
} else {
|
||||
self.renderFrame()
|
||||
}
|
||||
}
|
||||
self.previousPresentationTimestamp = pixelBuffer.timestamp
|
||||
}
|
||||
// func consumeTexture(_ texture: MTLTexture, render: Bool) {
|
||||
// if render {
|
||||
// self.willRenderFrame()
|
||||
// }
|
||||
//
|
||||
// self.currentTexture = texture
|
||||
// if render {
|
||||
// self.renderFrame()
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// func consumeTexture(_ texture: MTLTexture, additionalTexture: MTLTexture?, time: CMTime, render: Bool) {
|
||||
// self.displayEnabled = false
|
||||
//
|
||||
// if render {
|
||||
// self.willRenderFrame()
|
||||
// }
|
||||
//
|
||||
// self.currentTexture = texture
|
||||
// self.currentAdditionalTexture = additionalTexture
|
||||
// self.currentTime = time
|
||||
// if render {
|
||||
// self.renderFrame()
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// var previousPresentationTimestamp: CMTime?
|
||||
// func consumeVideoPixelBuffer(pixelBuffer: VideoPixelBuffer, additionalPixelBuffer: VideoPixelBuffer?, render: Bool) {
|
||||
// self.willRenderFrame()
|
||||
//
|
||||
// self.currentPixelBuffer = pixelBuffer
|
||||
// if additionalPixelBuffer == nil && self.currentAdditionalPixelBuffer != nil {
|
||||
// } else {
|
||||
// self.currentAdditionalPixelBuffer = additionalPixelBuffer
|
||||
// }
|
||||
// if render {
|
||||
// if self.previousPresentationTimestamp == pixelBuffer.timestamp {
|
||||
// self.didRenderFrame()
|
||||
// } else {
|
||||
// self.renderFrame()
|
||||
// }
|
||||
// }
|
||||
// self.previousPresentationTimestamp = pixelBuffer.timestamp
|
||||
// }
|
||||
|
||||
func renderTargetDidChange(_ target: RenderTarget?) {
|
||||
self.renderTarget = target
|
||||
|
|
@ -354,7 +389,7 @@ final class MediaEditorRenderer: TextureConsumer {
|
|||
}
|
||||
|
||||
func finalRenderedImage(mirror: Bool = false) -> UIImage? {
|
||||
if let finalTexture = self.finalTexture, let device = self.renderTarget?.mtlDevice {
|
||||
if let finalTexture = self.resultTexture, let device = self.renderTarget?.mtlDevice {
|
||||
return getTextureImage(device: device, texture: finalTexture, mirror: mirror)
|
||||
} else {
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import AVFoundation
|
||||
import SwiftSignalKit
|
||||
|
||||
|
|
@ -39,3 +40,92 @@ extension AVPlayer {
|
|||
return timer
|
||||
}
|
||||
}
|
||||
|
||||
func textureRotatonForAVAsset(_ asset: AVAsset, mirror: Bool = false) -> TextureRotation {
|
||||
for track in asset.tracks {
|
||||
if track.mediaType == .video {
|
||||
let t = track.preferredTransform
|
||||
if t.a == -1.0 && t.d == -1.0 {
|
||||
return .rotate180Degrees
|
||||
} else if t.a == 1.0 && t.d == 1.0 {
|
||||
return .rotate0Degrees
|
||||
} else if t.b == -1.0 && t.c == 1.0 {
|
||||
return .rotate270Degrees
|
||||
} else if t.a == -1.0 && t.d == 1.0 {
|
||||
return .rotate270Degrees
|
||||
} else if t.a == 1.0 && t.d == -1.0 {
|
||||
return .rotate180Degrees
|
||||
} else {
|
||||
return mirror ? .rotate90DegreesMirrored : .rotate90Degrees
|
||||
}
|
||||
}
|
||||
}
|
||||
return .rotate0Degrees
|
||||
}
|
||||
|
||||
func loadTexture(image: UIImage, device: MTLDevice) -> MTLTexture? {
|
||||
func dataForImage(_ image: UIImage) -> UnsafeMutablePointer<UInt8> {
|
||||
let imageRef = image.cgImage
|
||||
let width = Int(image.size.width)
|
||||
let height = Int(image.size.height)
|
||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
|
||||
let rawData = UnsafeMutablePointer<UInt8>.allocate(capacity: width * height * 4)
|
||||
let bytePerPixel = 4
|
||||
let bytesPerRow = bytePerPixel * Int(width)
|
||||
let bitsPerComponent = 8
|
||||
let bitmapInfo = CGBitmapInfo.byteOrder32Little.rawValue + CGImageAlphaInfo.premultipliedFirst.rawValue
|
||||
let context = CGContext.init(data: rawData, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo)
|
||||
context?.draw(imageRef!, in: CGRect(x: 0, y: 0, width: width, height: height))
|
||||
|
||||
return rawData
|
||||
}
|
||||
|
||||
let width = Int(image.size.width * image.scale)
|
||||
let height = Int(image.size.height * image.scale)
|
||||
let bytePerPixel = 4
|
||||
let bytesPerRow = bytePerPixel * width
|
||||
|
||||
var texture : MTLTexture?
|
||||
let region = MTLRegionMake2D(0, 0, Int(width), Int(height))
|
||||
let textureDescriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .bgra8Unorm, width: width, height: height, mipmapped: false)
|
||||
texture = device.makeTexture(descriptor: textureDescriptor)
|
||||
|
||||
let data = dataForImage(image)
|
||||
texture?.replace(region: region, mipmapLevel: 0, withBytes: data, bytesPerRow: bytesPerRow)
|
||||
|
||||
return texture
|
||||
}
|
||||
|
||||
func pixelBufferToMTLTexture(pixelBuffer: CVPixelBuffer, textureCache: CVMetalTextureCache) -> MTLTexture? {
|
||||
let width = CVPixelBufferGetWidth(pixelBuffer)
|
||||
let height = CVPixelBufferGetHeight(pixelBuffer)
|
||||
|
||||
let format: MTLPixelFormat = .r8Unorm
|
||||
var textureRef : CVMetalTexture?
|
||||
let status = CVMetalTextureCacheCreateTextureFromImage(nil, textureCache, pixelBuffer, nil, format, width, height, 0, &textureRef)
|
||||
if status == kCVReturnSuccess {
|
||||
return CVMetalTextureGetTexture(textureRef!)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getTextureImage(device: MTLDevice, texture: MTLTexture, mirror: Bool = false) -> UIImage? {
|
||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
let context = CIContext(mtlDevice: device, options: [:])
|
||||
guard var ciImage = CIImage(mtlTexture: texture, options: [.colorSpace: colorSpace]) else {
|
||||
return nil
|
||||
}
|
||||
let transform: CGAffineTransform
|
||||
if mirror {
|
||||
transform = CGAffineTransform(-1.0, 0.0, 0.0, -1.0, ciImage.extent.width, ciImage.extent.height)
|
||||
} else {
|
||||
transform = CGAffineTransform(1.0, 0.0, 0.0, -1.0, 0.0, ciImage.extent.height)
|
||||
}
|
||||
ciImage = ciImage.transformed(by: transform)
|
||||
guard let cgImage = context.createCGImage(ciImage, from: CGRect(origin: .zero, size: CGSize(width: ciImage.extent.width, height: ciImage.extent.height))) else {
|
||||
return nil
|
||||
}
|
||||
return UIImage(cgImage: cgImage)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -279,6 +279,15 @@ public final class MediaEditorValues: Codable, Equatable {
|
|||
if lhs.additionalVideoPositionChanges != rhs.additionalVideoPositionChanges {
|
||||
return false
|
||||
}
|
||||
if lhs.additionalVideoTrimRange != rhs.additionalVideoTrimRange {
|
||||
return false
|
||||
}
|
||||
if lhs.additionalVideoOffset != rhs.additionalVideoOffset {
|
||||
return false
|
||||
}
|
||||
if lhs.additionalVideoVolume != rhs.additionalVideoVolume {
|
||||
return false
|
||||
}
|
||||
if lhs.drawing !== rhs.drawing {
|
||||
return false
|
||||
}
|
||||
|
|
@ -352,6 +361,10 @@ public final class MediaEditorValues: Codable, Equatable {
|
|||
case additionalVideoScale
|
||||
case additionalVideoRotation
|
||||
case additionalVideoPositionChanges
|
||||
case additionalVideoTrimRange
|
||||
case additionalVideoOffset
|
||||
case additionalVideoVolume
|
||||
|
||||
case drawing
|
||||
case entities
|
||||
case toolValues
|
||||
|
|
@ -385,6 +398,10 @@ public final class MediaEditorValues: Codable, Equatable {
|
|||
public let additionalVideoRotation: CGFloat?
|
||||
public let additionalVideoPositionChanges: [VideoPositionChange]
|
||||
|
||||
public let additionalVideoTrimRange: Range<Double>?
|
||||
public let additionalVideoOffset: Double?
|
||||
public let additionalVideoVolume: CGFloat?
|
||||
|
||||
public let drawing: UIImage?
|
||||
public let entities: [CodableDrawingEntity]
|
||||
public let toolValues: [EditorToolKey: Any]
|
||||
|
|
@ -420,6 +437,9 @@ public final class MediaEditorValues: Codable, Equatable {
|
|||
additionalVideoScale: CGFloat?,
|
||||
additionalVideoRotation: CGFloat?,
|
||||
additionalVideoPositionChanges: [VideoPositionChange],
|
||||
additionalVideoTrimRange: Range<Double>?,
|
||||
additionalVideoOffset: Double?,
|
||||
additionalVideoVolume: CGFloat?,
|
||||
drawing: UIImage?,
|
||||
entities: [CodableDrawingEntity],
|
||||
toolValues: [EditorToolKey: Any],
|
||||
|
|
@ -448,6 +468,9 @@ public final class MediaEditorValues: Codable, Equatable {
|
|||
self.additionalVideoScale = additionalVideoScale
|
||||
self.additionalVideoRotation = additionalVideoRotation
|
||||
self.additionalVideoPositionChanges = additionalVideoPositionChanges
|
||||
self.additionalVideoTrimRange = additionalVideoTrimRange
|
||||
self.additionalVideoOffset = additionalVideoOffset
|
||||
self.additionalVideoVolume = additionalVideoVolume
|
||||
self.drawing = drawing
|
||||
self.entities = entities
|
||||
self.toolValues = toolValues
|
||||
|
|
@ -491,6 +514,9 @@ public final class MediaEditorValues: Codable, Equatable {
|
|||
self.additionalVideoScale = try container.decodeIfPresent(CGFloat.self, forKey: .additionalVideoScale)
|
||||
self.additionalVideoRotation = try container.decodeIfPresent(CGFloat.self, forKey: .additionalVideoRotation)
|
||||
self.additionalVideoPositionChanges = try container.decodeIfPresent([VideoPositionChange].self, forKey: .additionalVideoPositionChanges) ?? []
|
||||
self.additionalVideoTrimRange = try container.decodeIfPresent(Range<Double>.self, forKey: .additionalVideoTrimRange)
|
||||
self.additionalVideoOffset = try container.decodeIfPresent(Double.self, forKey: .additionalVideoOffset)
|
||||
self.additionalVideoVolume = try container.decodeIfPresent(CGFloat.self, forKey: .additionalVideoVolume)
|
||||
|
||||
if let drawingData = try container.decodeIfPresent(Data.self, forKey: .drawing), let image = UIImage(data: drawingData) {
|
||||
self.drawing = image
|
||||
|
|
@ -571,68 +597,81 @@ public final class MediaEditorValues: Codable, Equatable {
|
|||
}
|
||||
|
||||
public func makeCopy() -> MediaEditorValues {
|
||||
return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset)
|
||||
return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset)
|
||||
}
|
||||
|
||||
func withUpdatedCrop(offset: CGPoint, scale: CGFloat, rotation: CGFloat, mirroring: Bool) -> MediaEditorValues {
|
||||
return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: offset, cropRect: self.cropRect, cropScale: scale, cropRotation: rotation, cropMirroring: mirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset)
|
||||
return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: offset, cropRect: self.cropRect, cropScale: scale, cropRotation: rotation, cropMirroring: mirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset)
|
||||
}
|
||||
|
||||
func withUpdatedGradientColors(gradientColors: [UIColor]) -> MediaEditorValues {
|
||||
return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset)
|
||||
return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset)
|
||||
}
|
||||
|
||||
func withUpdatedVideoIsMuted(_ videoIsMuted: Bool) -> MediaEditorValues {
|
||||
return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset)
|
||||
return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset)
|
||||
}
|
||||
|
||||
func withUpdatedVideoIsFullHd(_ videoIsFullHd: Bool) -> MediaEditorValues {
|
||||
return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset)
|
||||
return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset)
|
||||
}
|
||||
|
||||
|
||||
func withUpdatedVideoIsMirrored(_ videoIsMirrored: Bool) -> MediaEditorValues {
|
||||
return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset)
|
||||
return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset)
|
||||
}
|
||||
|
||||
func withUpdatedAdditionalVideo(path: String, positionChanges: [VideoPositionChange]) -> MediaEditorValues {
|
||||
return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: path, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: positionChanges, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset)
|
||||
func withUpdatedAdditionalVideo(path: String?, positionChanges: [VideoPositionChange]) -> MediaEditorValues {
|
||||
return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: path, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: positionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset)
|
||||
}
|
||||
|
||||
func withUpdatedAdditionalVideo(position: CGPoint, scale: CGFloat, rotation: CGFloat) -> MediaEditorValues {
|
||||
return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: position, additionalVideoScale: scale, additionalVideoRotation: rotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset)
|
||||
return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: position, additionalVideoScale: scale, additionalVideoRotation: rotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset)
|
||||
}
|
||||
|
||||
func withUpdatedAdditionalVideoTrimRange(_ additionalVideoTrimRange: Range<Double>) -> MediaEditorValues {
|
||||
return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset)
|
||||
}
|
||||
|
||||
|
||||
func withUpdatedAdditionalVideoOffset(_ additionalVideoOffset: Double?) -> MediaEditorValues {
|
||||
return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset)
|
||||
}
|
||||
|
||||
func withUpdatedAdditionalVideoVolume(_ additionalVideoVolume: CGFloat?) -> MediaEditorValues {
|
||||
return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: additionalVideoVolume, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset)
|
||||
}
|
||||
|
||||
func withUpdatedVideoTrimRange(_ videoTrimRange: Range<Double>) -> MediaEditorValues {
|
||||
return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset)
|
||||
return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset)
|
||||
}
|
||||
|
||||
func withUpdatedDrawingAndEntities(drawing: UIImage?, entities: [CodableDrawingEntity]) -> MediaEditorValues {
|
||||
return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, drawing: drawing, entities: entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset)
|
||||
return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, drawing: drawing, entities: entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset)
|
||||
}
|
||||
|
||||
func withUpdatedToolValues(_ toolValues: [EditorToolKey: Any]) -> MediaEditorValues {
|
||||
return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, drawing: self.drawing, entities: self.entities, toolValues: toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset)
|
||||
return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, drawing: self.drawing, entities: self.entities, toolValues: toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset)
|
||||
}
|
||||
|
||||
func withUpdatedAudioTrack(_ audioTrack: MediaAudioTrack?) -> MediaEditorValues {
|
||||
return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset)
|
||||
return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset)
|
||||
}
|
||||
|
||||
func withUpdatedAudioTrackTrimRange(_ audioTrackTrimRange: Range<Double>?) -> MediaEditorValues {
|
||||
return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset)
|
||||
return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset)
|
||||
}
|
||||
|
||||
func withUpdatedAudioTrackOffset(_ audioTrackOffset: Double?) -> MediaEditorValues {
|
||||
return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset)
|
||||
return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset)
|
||||
}
|
||||
|
||||
func withUpdatedAudioTrackVolume(_ audioTrackVolume: CGFloat?) -> MediaEditorValues {
|
||||
return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset)
|
||||
return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset)
|
||||
}
|
||||
|
||||
func withUpdatedAudioTrackSamples(_ audioTrackSamples: MediaAudioTrackSamples?) -> MediaEditorValues {
|
||||
return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: audioTrackSamples, qualityPreset: self.qualityPreset)
|
||||
return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, additionalVideoPath: self.additionalVideoPath, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: audioTrackSamples, qualityPreset: self.qualityPreset)
|
||||
}
|
||||
|
||||
public var resultDimensions: PixelDimensions {
|
||||
|
|
|
|||
|
|
@ -232,6 +232,23 @@ public final class MediaEditorVideoExport {
|
|||
}
|
||||
}
|
||||
|
||||
var additionalVideoTimeRange: CMTimeRange? {
|
||||
if let videoTrimRange = self.values.additionalVideoTrimRange {
|
||||
return CMTimeRange(start: CMTime(seconds: videoTrimRange.lowerBound, preferredTimescale: CMTimeScale(NSEC_PER_SEC)), end: CMTime(seconds: videoTrimRange.upperBound, preferredTimescale: CMTimeScale(NSEC_PER_SEC)))
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var additionalVideoStartTime: CMTime {
|
||||
if let range = self.values.additionalVideoTrimRange {
|
||||
let offset = -min(0.0, self.values.additionalVideoOffset ?? 0.0)
|
||||
return CMTime(seconds: offset + range.lowerBound, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
|
||||
} else {
|
||||
return .zero
|
||||
}
|
||||
}
|
||||
|
||||
var audioTimeRange: CMTimeRange? {
|
||||
if let audioTrack = self.values.audioTrack {
|
||||
let offset = max(0.0, self.values.audioTrackOffset ?? 0.0)
|
||||
|
|
@ -475,11 +492,19 @@ public final class MediaEditorVideoExport {
|
|||
}
|
||||
if let timeRange = self.configuration.timeRange {
|
||||
reader.timeRange = timeRange
|
||||
self.additionalReader?.timeRange = timeRange
|
||||
if let additionalTimeRange = self.configuration.additionalVideoTimeRange {
|
||||
self.additionalReader?.timeRange = additionalTimeRange
|
||||
} else {
|
||||
self.additionalReader?.timeRange = timeRange
|
||||
}
|
||||
} else if asset.duration.seconds > 60.0 && isStory {
|
||||
let trimmedRange = CMTimeRange(start: CMTime(seconds: 0.0, preferredTimescale: CMTimeScale(NSEC_PER_SEC)), end: CMTime(seconds: 60.0, preferredTimescale: CMTimeScale(NSEC_PER_SEC)))
|
||||
reader.timeRange = trimmedRange
|
||||
self.additionalReader?.timeRange = trimmedRange
|
||||
if let additionalTimeRange = self.configuration.additionalVideoTimeRange {
|
||||
self.additionalReader?.timeRange = additionalTimeRange
|
||||
} else {
|
||||
self.additionalReader?.timeRange = trimmedRange
|
||||
}
|
||||
}
|
||||
|
||||
self.writer = MediaEditorVideoAVAssetWriter()
|
||||
|
|
@ -802,7 +827,13 @@ public final class MediaEditorVideoExport {
|
|||
self.statusValue = .progress(Float(progress))
|
||||
}
|
||||
|
||||
let additionalSampleBuffer = self.additionalVideoOutput?.copyNextSampleBuffer()
|
||||
var additionalSampleBuffer: CMSampleBuffer?
|
||||
if let additionalVideoOutput = self.additionalVideoOutput {
|
||||
if timestamp < self.configuration.additionalVideoStartTime {
|
||||
} else {
|
||||
additionalSampleBuffer = additionalVideoOutput.copyNextSampleBuffer()
|
||||
}
|
||||
}
|
||||
|
||||
if let composer = self.composer {
|
||||
composer.processSampleBuffer(sampleBuffer: sampleBuffer, textureRotation: self.textureRotation, additionalSampleBuffer: additionalSampleBuffer, additionalTextureRotation: self.additionalTextureRotation, pool: writer.pixelBufferPool, completion: { pixelBuffer in
|
||||
|
|
|
|||
|
|
@ -0,0 +1,235 @@
|
|||
import Foundation
|
||||
import AVFoundation
|
||||
import Metal
|
||||
import MetalKit
|
||||
|
||||
final class UniversalTextureSource: TextureSource {
|
||||
enum Input {
|
||||
case image(UIImage)
|
||||
case video(AVPlayerItem)
|
||||
|
||||
fileprivate func createContext(renderTarget: RenderTarget, queue: DispatchQueue, additional: Bool) -> InputContext {
|
||||
switch self {
|
||||
case .image:
|
||||
return ImageInputContext(input: self, renderTarget: renderTarget, queue: queue)
|
||||
case .video:
|
||||
return VideoInputContext(input: self, renderTarget: renderTarget, queue: queue, additional: additional)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private weak var renderTarget: RenderTarget?
|
||||
private var displayLink: CADisplayLink?
|
||||
private let queue: DispatchQueue
|
||||
|
||||
private var mainInputContext: InputContext?
|
||||
private var additionalInputContext: InputContext?
|
||||
|
||||
weak var output: MediaEditorRenderer?
|
||||
|
||||
init(renderTarget: RenderTarget) {
|
||||
self.renderTarget = renderTarget
|
||||
|
||||
self.queue = DispatchQueue(
|
||||
label: "UniversalTextureSource Queue",
|
||||
qos: .userInteractive,
|
||||
attributes: [],
|
||||
autoreleaseFrequency: .workItem,
|
||||
target: nil
|
||||
)
|
||||
}
|
||||
|
||||
func setMainInput(_ input: Input) {
|
||||
guard let renderTarget = self.renderTarget else {
|
||||
return
|
||||
}
|
||||
self.mainInputContext = input.createContext(renderTarget: renderTarget, queue: self.queue, additional: false)
|
||||
self.update(forced: true)
|
||||
}
|
||||
|
||||
func setAdditionalInput(_ input: Input?) {
|
||||
guard let renderTarget = self.renderTarget else {
|
||||
return
|
||||
}
|
||||
if let input {
|
||||
self.additionalInputContext = input.createContext(renderTarget: renderTarget, queue: self.queue, additional: true)
|
||||
} else {
|
||||
self.additionalInputContext = nil
|
||||
}
|
||||
self.update(forced: true)
|
||||
}
|
||||
|
||||
private var previousAdditionalOutput: MediaEditorRenderer.Input?
|
||||
private func update(forced: Bool) {
|
||||
let time = CACurrentMediaTime()
|
||||
|
||||
let needsDisplayLink = (self.mainInputContext?.needsDisplayLink ?? false) || (self.additionalInputContext?.needsDisplayLink ?? false)
|
||||
if needsDisplayLink {
|
||||
if self.displayLink == nil {
|
||||
let displayLink = CADisplayLink(target: DisplayLinkTarget({ [weak self] in
|
||||
self?.update(forced: false)
|
||||
}), selector: #selector(DisplayLinkTarget.handleDisplayLinkUpdate(sender:)))
|
||||
displayLink.preferredFramesPerSecond = 60
|
||||
displayLink.add(to: .main, forMode: .common)
|
||||
self.displayLink = displayLink
|
||||
}
|
||||
} else {
|
||||
if let displayLink = self.displayLink {
|
||||
self.displayLink = nil
|
||||
displayLink.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
let main = self.mainInputContext?.output(time: time)
|
||||
var additional = self.additionalInputContext?.output(time: time)
|
||||
if let additional {
|
||||
self.previousAdditionalOutput = additional
|
||||
} else if self.additionalInputContext != nil {
|
||||
additional = self.previousAdditionalOutput
|
||||
}
|
||||
|
||||
guard let main else {
|
||||
return
|
||||
}
|
||||
|
||||
self.output?.consume(main: main, additional: additional, render: true)
|
||||
}
|
||||
|
||||
func connect(to consumer: MediaEditorRenderer) {
|
||||
self.output = consumer
|
||||
self.update(forced: true)
|
||||
}
|
||||
|
||||
func invalidate() {
|
||||
self.mainInputContext?.invalidate()
|
||||
self.additionalInputContext?.invalidate()
|
||||
}
|
||||
|
||||
private class DisplayLinkTarget {
|
||||
private let update: () -> Void
|
||||
init(_ update: @escaping () -> Void) {
|
||||
self.update = update
|
||||
}
|
||||
@objc func handleDisplayLinkUpdate(sender: CADisplayLink) {
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
//
|
||||
// private func setupDisplayLink(frameRate: Int) {
|
||||
// self.displayLink?.invalidate()
|
||||
// self.displayLink = nil
|
||||
//
|
||||
// if self.playerItemOutput != nil {
|
||||
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
private protocol InputContext {
|
||||
typealias Input = UniversalTextureSource.Input
|
||||
typealias Output = MediaEditorRenderer.Input
|
||||
|
||||
var input: Input { get }
|
||||
func output(time: Double) -> Output?
|
||||
|
||||
var needsDisplayLink: Bool { get }
|
||||
|
||||
func invalidate()
|
||||
}
|
||||
|
||||
private class ImageInputContext: InputContext {
|
||||
fileprivate var input: Input
|
||||
private var texture: MTLTexture?
|
||||
|
||||
init(input: Input, renderTarget: RenderTarget, queue: DispatchQueue) {
|
||||
guard case let .image(image) = input else {
|
||||
fatalError()
|
||||
}
|
||||
self.input = input
|
||||
if let device = renderTarget.mtlDevice {
|
||||
self.texture = loadTexture(image: image, device: device)
|
||||
}
|
||||
}
|
||||
|
||||
func output(time: Double) -> Output? {
|
||||
return self.texture.flatMap { .texture($0, .zero) }
|
||||
}
|
||||
|
||||
func invalidate() {
|
||||
self.texture = nil
|
||||
}
|
||||
|
||||
var needsDisplayLink: Bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private class VideoInputContext: NSObject, InputContext, AVPlayerItemOutputPullDelegate {
|
||||
fileprivate var input: Input
|
||||
private var videoOutput: AVPlayerItemVideoOutput?
|
||||
private var textureRotation: TextureRotation = .rotate0Degrees
|
||||
|
||||
var playerItem: AVPlayerItem {
|
||||
guard case let .video(playerItem) = self.input else {
|
||||
fatalError()
|
||||
}
|
||||
return playerItem
|
||||
}
|
||||
|
||||
init(input: Input, renderTarget: RenderTarget, queue: DispatchQueue, additional: Bool) {
|
||||
guard case .video = input else {
|
||||
fatalError()
|
||||
}
|
||||
self.input = input
|
||||
super.init()
|
||||
|
||||
//TODO: mirror if self.additionalPlayer == nil && self.mirror
|
||||
self.textureRotation = textureRotatonForAVAsset(self.playerItem.asset, mirror: additional)
|
||||
|
||||
let colorProperties: [String: Any] = [
|
||||
AVVideoColorPrimariesKey: AVVideoColorPrimaries_ITU_R_709_2,
|
||||
AVVideoTransferFunctionKey: AVVideoTransferFunction_ITU_R_709_2,
|
||||
AVVideoYCbCrMatrixKey: AVVideoYCbCrMatrix_ITU_R_709_2
|
||||
]
|
||||
|
||||
let outputSettings: [String: Any] = [
|
||||
kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
|
||||
kCVPixelBufferMetalCompatibilityKey as String: true,
|
||||
AVVideoColorPropertiesKey: colorProperties
|
||||
]
|
||||
|
||||
let videoOutput = AVPlayerItemVideoOutput(outputSettings: outputSettings)
|
||||
videoOutput.suppressesPlayerRendering = true
|
||||
videoOutput.setDelegate(self, queue: queue)
|
||||
self.playerItem.add(videoOutput)
|
||||
self.videoOutput = videoOutput
|
||||
}
|
||||
|
||||
func output(time: Double) -> Output? {
|
||||
guard let videoOutput = self.videoOutput else {
|
||||
return nil
|
||||
}
|
||||
let requestTime = videoOutput.itemTime(forHostTime: time)
|
||||
if requestTime < .zero {
|
||||
return nil
|
||||
}
|
||||
var presentationTime: CMTime = .zero
|
||||
var videoPixelBuffer: VideoPixelBuffer?
|
||||
if let pixelBuffer = videoOutput.copyPixelBuffer(forItemTime: requestTime, itemTimeForDisplay: &presentationTime) {
|
||||
videoPixelBuffer = VideoPixelBuffer(pixelBuffer: pixelBuffer, rotation: self.textureRotation, timestamp: presentationTime)
|
||||
}
|
||||
return videoPixelBuffer.flatMap { .videoBuffer($0) }
|
||||
}
|
||||
|
||||
func invalidate() {
|
||||
if let videoOutput = self.videoOutput {
|
||||
self.videoOutput = nil
|
||||
self.playerItem.remove(videoOutput)
|
||||
videoOutput.setDelegate(nil, queue: nil)
|
||||
}
|
||||
}
|
||||
|
||||
var needsDisplayLink: Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,502 @@
|
|||
import Foundation
|
||||
import AVFoundation
|
||||
import Metal
|
||||
import MetalKit
|
||||
|
||||
private func verticesData(
|
||||
textureRotation: TextureRotation,
|
||||
containerSize: CGSize,
|
||||
position: CGPoint,
|
||||
size: CGSize,
|
||||
rotation: CGFloat,
|
||||
z: Float = 0.0
|
||||
) -> [VertexData] {
|
||||
let topLeft: simd_float2
|
||||
let topRight: simd_float2
|
||||
let bottomLeft: simd_float2
|
||||
let bottomRight: simd_float2
|
||||
|
||||
switch textureRotation {
|
||||
case .rotate0Degrees:
|
||||
topLeft = simd_float2(0.0, 1.0)
|
||||
topRight = simd_float2(1.0, 1.0)
|
||||
bottomLeft = simd_float2(0.0, 0.0)
|
||||
bottomRight = simd_float2(1.0, 0.0)
|
||||
case .rotate0DegreesMirrored:
|
||||
topLeft = simd_float2(1.0, 1.0)
|
||||
topRight = simd_float2(0.0, 1.0)
|
||||
bottomLeft = simd_float2(1.0, 0.0)
|
||||
bottomRight = simd_float2(0.0, 0.0)
|
||||
case .rotate180Degrees:
|
||||
topLeft = simd_float2(1.0, 0.0)
|
||||
topRight = simd_float2(0.0, 0.0)
|
||||
bottomLeft = simd_float2(1.0, 1.0)
|
||||
bottomRight = simd_float2(0.0, 1.0)
|
||||
case .rotate90Degrees:
|
||||
topLeft = simd_float2(1.0, 1.0)
|
||||
topRight = simd_float2(1.0, 0.0)
|
||||
bottomLeft = simd_float2(0.0, 1.0)
|
||||
bottomRight = simd_float2(0.0, 0.0)
|
||||
case .rotate90DegreesMirrored:
|
||||
topLeft = simd_float2(1.0, 0.0)
|
||||
topRight = simd_float2(1.0, 1.0)
|
||||
bottomLeft = simd_float2(0.0, 0.0)
|
||||
bottomRight = simd_float2(0.0, 1.0)
|
||||
case .rotate270Degrees:
|
||||
topLeft = simd_float2(0.0, 0.0)
|
||||
topRight = simd_float2(0.0, 1.0)
|
||||
bottomLeft = simd_float2(1.0, 0.0)
|
||||
bottomRight = simd_float2(1.0, 1.0)
|
||||
}
|
||||
|
||||
let angle = Float(.pi - rotation)
|
||||
let cosAngle = cos(angle)
|
||||
let sinAngle = sin(angle)
|
||||
|
||||
let centerX = Float(position.x)
|
||||
let centerY = Float(position.y)
|
||||
|
||||
let halfWidth = Float(size.width / 2.0)
|
||||
let halfHeight = Float(size.height / 2.0)
|
||||
|
||||
return [
|
||||
VertexData(
|
||||
pos: simd_float4(
|
||||
x: (centerX + (halfWidth * cosAngle) - (halfHeight * sinAngle)) / Float(containerSize.width) * 2.0,
|
||||
y: (centerY + (halfWidth * sinAngle) + (halfHeight * cosAngle)) / Float(containerSize.height) * 2.0,
|
||||
z: z,
|
||||
w: 1
|
||||
),
|
||||
texCoord: topLeft,
|
||||
localPos: simd_float2(0.0, 0.0)
|
||||
),
|
||||
VertexData(
|
||||
pos: simd_float4(
|
||||
x: (centerX - (halfWidth * cosAngle) - (halfHeight * sinAngle)) / Float(containerSize.width) * 2.0,
|
||||
y: (centerY - (halfWidth * sinAngle) + (halfHeight * cosAngle)) / Float(containerSize.height) * 2.0,
|
||||
z: z,
|
||||
w: 1
|
||||
),
|
||||
texCoord: topRight,
|
||||
localPos: simd_float2(1.0, 0.0)
|
||||
),
|
||||
VertexData(
|
||||
pos: simd_float4(
|
||||
x: (centerX + (halfWidth * cosAngle) + (halfHeight * sinAngle)) / Float(containerSize.width) * 2.0,
|
||||
y: (centerY + (halfWidth * sinAngle) - (halfHeight * cosAngle)) / Float(containerSize.height) * 2.0,
|
||||
z: z,
|
||||
w: 1
|
||||
),
|
||||
texCoord: bottomLeft,
|
||||
localPos: simd_float2(0.0, 1.0)
|
||||
),
|
||||
VertexData(
|
||||
pos: simd_float4(
|
||||
x: (centerX - (halfWidth * cosAngle) + (halfHeight * sinAngle)) / Float(containerSize.width) * 2.0,
|
||||
y: (centerY - (halfWidth * sinAngle) - (halfHeight * cosAngle)) / Float(containerSize.height) * 2.0,
|
||||
z: z,
|
||||
w: 1
|
||||
),
|
||||
texCoord: bottomRight,
|
||||
localPos: simd_float2(1.0, 1.0)
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
private func lookupSpringValue(_ t: CGFloat) -> CGFloat {
|
||||
let table: [(CGFloat, CGFloat)] = [
|
||||
(0.0, 0.0),
|
||||
(0.0625, 0.1123005598783493),
|
||||
(0.125, 0.31598418951034546),
|
||||
(0.1875, 0.5103585720062256),
|
||||
(0.25, 0.6650152802467346),
|
||||
(0.3125, 0.777747631072998),
|
||||
(0.375, 0.8557760119438171),
|
||||
(0.4375, 0.9079672694206238),
|
||||
(0.5, 0.942038357257843),
|
||||
(0.5625, 0.9638798832893372),
|
||||
(0.625, 0.9776856303215027),
|
||||
(0.6875, 0.9863143563270569),
|
||||
(0.75, 0.991658091545105),
|
||||
(0.8125, 0.9949421286582947),
|
||||
(0.875, 0.9969474077224731),
|
||||
(0.9375, 0.9981651306152344),
|
||||
(1.0, 1.0)
|
||||
]
|
||||
|
||||
for i in 0 ..< table.count - 2 {
|
||||
let lhs = table[i]
|
||||
let rhs = table[i + 1]
|
||||
|
||||
if t >= lhs.0 && t <= rhs.0 {
|
||||
let fraction = (t - lhs.0) / (rhs.0 - lhs.0)
|
||||
let value = lhs.1 + fraction * (rhs.1 - lhs.1)
|
||||
return value
|
||||
}
|
||||
}
|
||||
return 1.0
|
||||
}
|
||||
|
||||
final class VideoFinishPass: RenderPass {
|
||||
private var cachedTexture: MTLTexture?
|
||||
|
||||
var mainPipelineState: MTLRenderPipelineState?
|
||||
var mainVerticesBuffer: MTLBuffer?
|
||||
var mainTextureRotation: TextureRotation = .rotate0Degrees
|
||||
|
||||
var additionalVerticesBuffer: MTLBuffer?
|
||||
var additionalTextureRotation: TextureRotation = .rotate0Degrees
|
||||
|
||||
var pixelFormat: MTLPixelFormat {
|
||||
return .bgra8Unorm
|
||||
}
|
||||
|
||||
func setup(device: MTLDevice, library: MTLLibrary) {
|
||||
let descriptor = MTLRenderPipelineDescriptor()
|
||||
descriptor.vertexFunction = library.makeFunction(name: "defaultVertexShader")
|
||||
descriptor.fragmentFunction = library.makeFunction(name: "dualFragmentShader")
|
||||
descriptor.colorAttachments[0].pixelFormat = self.pixelFormat
|
||||
descriptor.colorAttachments[0].isBlendingEnabled = true
|
||||
descriptor.colorAttachments[0].rgbBlendOperation = .add
|
||||
descriptor.colorAttachments[0].alphaBlendOperation = .add
|
||||
descriptor.colorAttachments[0].sourceRGBBlendFactor = .sourceAlpha
|
||||
descriptor.colorAttachments[0].sourceAlphaBlendFactor = .sourceAlpha
|
||||
descriptor.colorAttachments[0].destinationRGBBlendFactor = .oneMinusSourceAlpha
|
||||
descriptor.colorAttachments[0].destinationAlphaBlendFactor = .oneMinusSourceAlpha
|
||||
|
||||
do {
|
||||
self.mainPipelineState = try device.makeRenderPipelineState(descriptor: descriptor)
|
||||
} catch {
|
||||
print(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
func encodeVideo(
|
||||
using encoder: MTLRenderCommandEncoder,
|
||||
containerSize: CGSize,
|
||||
texture: MTLTexture,
|
||||
textureRotation: TextureRotation,
|
||||
position: VideoPosition,
|
||||
roundness: Float,
|
||||
alpha: Float,
|
||||
zPosition: Float,
|
||||
device: MTLDevice
|
||||
) {
|
||||
encoder.setFragmentTexture(texture, index: 0)
|
||||
|
||||
let center = CGPoint(
|
||||
x: position.position.x - containerSize.width / 2.0,
|
||||
y: containerSize.height - position.position.y - containerSize.height / 2.0
|
||||
)
|
||||
|
||||
let size = CGSize(
|
||||
width: position.size.width * position.scale,
|
||||
height: position.size.height * position.scale
|
||||
)
|
||||
|
||||
let vertices = verticesData(textureRotation: textureRotation, containerSize: containerSize, position: center, size: size, rotation: position.rotation, z: zPosition)
|
||||
let buffer = device.makeBuffer(
|
||||
bytes: vertices,
|
||||
length: MemoryLayout<VertexData>.stride * vertices.count,
|
||||
options: [])
|
||||
encoder.setVertexBuffer(buffer, offset: 0, index: 0)
|
||||
|
||||
var resolution = simd_uint2(UInt32(size.width), UInt32(size.height))
|
||||
encoder.setFragmentBytes(&resolution, length: MemoryLayout<simd_uint2>.size * 2, index: 0)
|
||||
|
||||
var roundness = roundness
|
||||
encoder.setFragmentBytes(&roundness, length: MemoryLayout<simd_float1>.size, index: 1)
|
||||
|
||||
var alpha = alpha
|
||||
encoder.setFragmentBytes(&alpha, length: MemoryLayout<simd_float1>.size, index: 2)
|
||||
|
||||
encoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4)
|
||||
}
|
||||
|
||||
func update(values: MediaEditorValues) {
|
||||
if let position = values.additionalVideoPosition, let scale = values.additionalVideoScale, let rotation = values.additionalVideoRotation {
|
||||
self.additionalPosition = VideoFinishPass.VideoPosition(position: position, size: CGSize(width: 1080.0 / 4.0, height: 1440.0 / 4.0), scale: scale, rotation: rotation)
|
||||
}
|
||||
if !values.additionalVideoPositionChanges.isEmpty {
|
||||
self.videoPositionChanges = values.additionalVideoPositionChanges
|
||||
}
|
||||
if let additionalVideoTrimRange = values.additionalVideoTrimRange {
|
||||
self.additionalVideoRange = additionalVideoTrimRange
|
||||
}
|
||||
if let additionalVideoOffset = values.additionalVideoOffset {
|
||||
self.additionalVideoOffset = additionalVideoOffset
|
||||
}
|
||||
}
|
||||
|
||||
private var mainPosition = VideoPosition(
|
||||
position: CGPoint(x: 1080 / 2.0, y: 1920.0 / 2.0),
|
||||
size: CGSize(width: 1080.0, height: 1920.0),
|
||||
scale: 1.0,
|
||||
rotation: 0.0
|
||||
)
|
||||
|
||||
private var additionalPosition = VideoPosition(
|
||||
position: CGPoint(x: 1080 / 2.0, y: 1920.0 / 2.0),
|
||||
size: CGSize(width: 1440.0, height: 1920.0),
|
||||
scale: 0.5,
|
||||
rotation: 0.0
|
||||
)
|
||||
|
||||
private var transitionDuration = 0.5
|
||||
private var apperanceDuration = 0.2
|
||||
private var videoPositionChanges: [VideoPositionChange] = []
|
||||
private var additionalVideoRange: Range<Double>?
|
||||
private var additionalVideoOffset: Double?
|
||||
|
||||
enum VideoType {
|
||||
case main
|
||||
case additional
|
||||
case transition
|
||||
}
|
||||
|
||||
struct VideoPosition {
|
||||
let position: CGPoint
|
||||
let size: CGSize
|
||||
let scale: CGFloat
|
||||
let rotation: CGFloat
|
||||
|
||||
func mixed(with other: VideoPosition, fraction: CGFloat) -> VideoPosition {
|
||||
let position = CGPoint(
|
||||
x: self.position.x + (other.position.x - self.position.x) * fraction,
|
||||
y: self.position.y + (other.position.y - self.position.y) * fraction
|
||||
)
|
||||
let size = CGSize(
|
||||
width: self.size.width + (other.size.width - self.size.width) * fraction,
|
||||
height: self.size.height + (other.size.height - self.size.height) * fraction
|
||||
)
|
||||
let scale = self.scale + (other.scale - self.scale) * fraction
|
||||
let rotation = self.rotation + (other.rotation - self.rotation) * fraction
|
||||
|
||||
return VideoPosition(
|
||||
position: position,
|
||||
size: size,
|
||||
scale: scale,
|
||||
rotation: rotation
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct VideoState {
|
||||
let texture: MTLTexture
|
||||
let textureRotation: TextureRotation
|
||||
let position: VideoPosition
|
||||
let roundness: Float
|
||||
let alpha: Float
|
||||
}
|
||||
|
||||
func transitionState(for time: CMTime, mainInput: MTLTexture, additionalInput: MTLTexture?) -> (VideoState, VideoState?, VideoState?) {
|
||||
let timestamp = time.seconds
|
||||
|
||||
var backgroundTexture = mainInput
|
||||
var backgroundTextureRotation = self.mainTextureRotation
|
||||
|
||||
var foregroundTexture = additionalInput
|
||||
var foregroundTextureRotation = self.additionalTextureRotation
|
||||
|
||||
var mainPosition = self.mainPosition
|
||||
var additionalPosition = self.additionalPosition
|
||||
var disappearingPosition = self.mainPosition
|
||||
|
||||
var transitionFraction = 1.0
|
||||
if let additionalInput {
|
||||
var previousChange: VideoPositionChange?
|
||||
for change in self.videoPositionChanges {
|
||||
if timestamp >= change.timestamp {
|
||||
previousChange = change
|
||||
}
|
||||
if timestamp < change.timestamp {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if let previousChange {
|
||||
if previousChange.additional {
|
||||
backgroundTexture = additionalInput
|
||||
backgroundTextureRotation = self.additionalTextureRotation
|
||||
|
||||
mainPosition = VideoPosition(position: mainPosition.position, size: CGSize(width: 1440.0, height: 1920.0), scale: mainPosition.scale, rotation: mainPosition.rotation)
|
||||
additionalPosition = VideoPosition(position: additionalPosition.position, size: CGSize(width: 1080.0 / 4.0, height: 1920.0 / 4.0), scale: additionalPosition.scale, rotation: additionalPosition.rotation)
|
||||
|
||||
foregroundTexture = mainInput
|
||||
foregroundTextureRotation = self.mainTextureRotation
|
||||
} else {
|
||||
disappearingPosition = VideoPosition(position: mainPosition.position, size: CGSize(width: 1440.0, height: 1920.0), scale: mainPosition.scale, rotation: mainPosition.rotation)
|
||||
}
|
||||
if previousChange.timestamp > 0.0 && timestamp < previousChange.timestamp + transitionDuration {
|
||||
transitionFraction = (timestamp - previousChange.timestamp) / transitionDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var backgroundVideoState = VideoState(texture: backgroundTexture, textureRotation: backgroundTextureRotation, position: mainPosition, roundness: 0.0, alpha: 1.0)
|
||||
var foregroundVideoState: VideoState?
|
||||
var disappearingVideoState: VideoState?
|
||||
|
||||
if let foregroundTexture {
|
||||
var foregroundPosition = additionalPosition
|
||||
var foregroundAlpha: Float = 1.0
|
||||
if transitionFraction < 1.0 {
|
||||
let springFraction = lookupSpringValue(transitionFraction)
|
||||
|
||||
let appearingPosition = VideoPosition(position: additionalPosition.position, size: additionalPosition.size, scale: 0.01, rotation: self.additionalPosition.rotation)
|
||||
let backgroundInitialPosition = VideoPosition(position: additionalPosition.position, size: CGSize(width: mainPosition.size.width / 4.0, height: mainPosition.size.height / 4.0), scale: additionalPosition.scale, rotation: additionalPosition.rotation)
|
||||
|
||||
foregroundPosition = appearingPosition.mixed(with: additionalPosition, fraction: springFraction)
|
||||
|
||||
disappearingVideoState = VideoState(texture: foregroundTexture, textureRotation: foregroundTextureRotation, position: disappearingPosition, roundness: 0.0, alpha: 1.0)
|
||||
backgroundVideoState = VideoState(texture: backgroundTexture, textureRotation: backgroundTextureRotation, position: backgroundInitialPosition.mixed(with: mainPosition, fraction: springFraction), roundness: Float(1.0 - springFraction), alpha: 1.0)
|
||||
|
||||
foregroundAlpha = min(1.0, max(0.0, Float(transitionFraction) * 2.5))
|
||||
}
|
||||
|
||||
var isVisible = true
|
||||
|
||||
var trimRangeLowerBound: Double?
|
||||
var trimRangeUpperBound: Double?
|
||||
if let additionalVideoRange = self.additionalVideoRange {
|
||||
if let additionalVideoOffset = self.additionalVideoOffset {
|
||||
trimRangeLowerBound = additionalVideoRange.lowerBound - additionalVideoOffset
|
||||
trimRangeUpperBound = additionalVideoRange.upperBound - additionalVideoOffset
|
||||
} else {
|
||||
trimRangeLowerBound = additionalVideoRange.lowerBound
|
||||
trimRangeUpperBound = additionalVideoRange.upperBound
|
||||
}
|
||||
} else if let additionalVideoOffset = self.additionalVideoOffset {
|
||||
trimRangeLowerBound = -additionalVideoOffset
|
||||
}
|
||||
|
||||
if trimRangeLowerBound != nil || trimRangeUpperBound != nil {
|
||||
let disappearingPosition = VideoPosition(position: foregroundPosition.position, size: foregroundPosition.size, scale: 0.01, rotation: foregroundPosition.rotation)
|
||||
if let trimRangeLowerBound, trimRangeLowerBound > 0.0, timestamp < trimRangeLowerBound + apperanceDuration {
|
||||
let visibilityFraction = max(0.0, min(1.0, (timestamp - trimRangeLowerBound) / apperanceDuration))
|
||||
if visibilityFraction.isZero {
|
||||
isVisible = false
|
||||
}
|
||||
foregroundAlpha = Float(visibilityFraction)
|
||||
foregroundPosition = disappearingPosition.mixed(with: foregroundPosition, fraction: visibilityFraction)
|
||||
} else if let trimRangeUpperBound, timestamp > trimRangeUpperBound - apperanceDuration {
|
||||
let visibilityFraction = 1.0 - max(0.0, min(1.0, (timestamp - trimRangeUpperBound) / apperanceDuration))
|
||||
if visibilityFraction.isZero {
|
||||
isVisible = false
|
||||
}
|
||||
foregroundAlpha = Float(visibilityFraction)
|
||||
foregroundPosition = disappearingPosition.mixed(with: foregroundPosition, fraction: visibilityFraction)
|
||||
}
|
||||
}
|
||||
|
||||
if isVisible {
|
||||
foregroundVideoState = VideoState(texture: foregroundTexture, textureRotation: foregroundTextureRotation, position: foregroundPosition, roundness: 1.0, alpha: foregroundAlpha)
|
||||
}
|
||||
}
|
||||
|
||||
return (backgroundVideoState, foregroundVideoState, disappearingVideoState)
|
||||
}
|
||||
|
||||
func process(input: MTLTexture, secondInput: MTLTexture?, timestamp: CMTime, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
guard max(input.width, input.height) > 1920 || secondInput != nil else {
|
||||
return input
|
||||
}
|
||||
|
||||
let scaledSize = CGSize(width: input.width, height: input.height).fitted(CGSize(width: 1920.0, height: 1920.0))
|
||||
let width: Int
|
||||
let height: Int
|
||||
|
||||
if secondInput != nil {
|
||||
width = 1080
|
||||
height = 1920
|
||||
} else {
|
||||
width = Int(scaledSize.width)
|
||||
height = Int(scaledSize.height)
|
||||
}
|
||||
self.mainPosition = VideoPosition(position: CGPoint(x: width / 2, y: height / 2), size: CGSize(width: width, height: height), scale: 1.0, rotation: 0.0)
|
||||
|
||||
let containerSize = CGSize(width: width, height: height)
|
||||
|
||||
if self.cachedTexture == nil || self.cachedTexture?.width != width || self.cachedTexture?.height != height {
|
||||
let textureDescriptor = MTLTextureDescriptor()
|
||||
textureDescriptor.textureType = .type2D
|
||||
textureDescriptor.width = width
|
||||
textureDescriptor.height = height
|
||||
textureDescriptor.pixelFormat = input.pixelFormat
|
||||
textureDescriptor.storageMode = .private
|
||||
textureDescriptor.usage = [.shaderRead, .shaderWrite, .renderTarget]
|
||||
guard let texture = device.makeTexture(descriptor: textureDescriptor) else {
|
||||
return input
|
||||
}
|
||||
self.cachedTexture = texture
|
||||
texture.label = "scaledVideoTexture"
|
||||
}
|
||||
|
||||
let renderPassDescriptor = MTLRenderPassDescriptor()
|
||||
renderPassDescriptor.colorAttachments[0].texture = self.cachedTexture!
|
||||
renderPassDescriptor.colorAttachments[0].loadAction = .dontCare
|
||||
renderPassDescriptor.colorAttachments[0].storeAction = .store
|
||||
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 1.0)
|
||||
guard let renderCommandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else {
|
||||
return input
|
||||
}
|
||||
|
||||
renderCommandEncoder.setViewport(MTLViewport(
|
||||
originX: 0, originY: 0,
|
||||
width: Double(width), height: Double(height),
|
||||
znear: -1.0, zfar: 1.0)
|
||||
)
|
||||
|
||||
renderCommandEncoder.setRenderPipelineState(self.mainPipelineState!)
|
||||
|
||||
let (mainVideoState, additionalVideoState, transitionVideoState) = self.transitionState(for: timestamp, mainInput: input, additionalInput: secondInput)
|
||||
|
||||
if let transitionVideoState {
|
||||
self.encodeVideo(
|
||||
using: renderCommandEncoder,
|
||||
containerSize: containerSize,
|
||||
texture: transitionVideoState.texture,
|
||||
textureRotation: transitionVideoState.textureRotation,
|
||||
position: transitionVideoState.position,
|
||||
roundness: transitionVideoState.roundness,
|
||||
alpha: transitionVideoState.alpha,
|
||||
zPosition: 0.75,
|
||||
device: device
|
||||
)
|
||||
}
|
||||
|
||||
self.encodeVideo(
|
||||
using: renderCommandEncoder,
|
||||
containerSize: containerSize,
|
||||
texture: mainVideoState.texture,
|
||||
textureRotation: mainVideoState.textureRotation,
|
||||
position: mainVideoState.position,
|
||||
roundness: mainVideoState.roundness,
|
||||
alpha: mainVideoState.alpha,
|
||||
zPosition: 0.0,
|
||||
device: device
|
||||
)
|
||||
|
||||
if let additionalVideoState {
|
||||
self.encodeVideo(
|
||||
using: renderCommandEncoder,
|
||||
containerSize: containerSize,
|
||||
texture: additionalVideoState.texture,
|
||||
textureRotation: additionalVideoState.textureRotation,
|
||||
position: additionalVideoState.position,
|
||||
roundness: additionalVideoState.roundness,
|
||||
alpha: additionalVideoState.alpha,
|
||||
zPosition: 0.5,
|
||||
device: device
|
||||
)
|
||||
}
|
||||
|
||||
renderCommandEncoder.endEncoding()
|
||||
|
||||
return self.cachedTexture!
|
||||
}
|
||||
|
||||
func process(input: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
import Foundation
|
||||
import AVFoundation
|
||||
import Metal
|
||||
import MetalKit
|
||||
|
||||
final class VideoInputPass: DefaultRenderPass {
|
||||
private var cachedTexture: MTLTexture?
|
||||
|
||||
override var fragmentShaderFunctionName: String {
|
||||
return "bt709ToRGBFragmentShader"
|
||||
}
|
||||
|
||||
override func setup(device: MTLDevice, library: MTLLibrary) {
|
||||
super.setup(device: device, library: library)
|
||||
}
|
||||
|
||||
func processPixelBuffer(_ pixelBuffer: VideoPixelBuffer, textureCache: CVMetalTextureCache, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
func textureFromPixelBuffer(_ pixelBuffer: CVPixelBuffer, pixelFormat: MTLPixelFormat, width: Int, height: Int, plane: Int) -> MTLTexture? {
|
||||
var textureRef : CVMetalTexture?
|
||||
let status = CVMetalTextureCacheCreateTextureFromImage(nil, textureCache, pixelBuffer, nil, pixelFormat, width, height, plane, &textureRef)
|
||||
if status == kCVReturnSuccess, let textureRef {
|
||||
return CVMetalTextureGetTexture(textureRef)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
let width = CVPixelBufferGetWidth(pixelBuffer.pixelBuffer)
|
||||
let height = CVPixelBufferGetHeight(pixelBuffer.pixelBuffer)
|
||||
guard let inputYTexture = textureFromPixelBuffer(pixelBuffer.pixelBuffer, pixelFormat: .r8Unorm, width: width, height: height, plane: 0),
|
||||
let inputCbCrTexture = textureFromPixelBuffer(pixelBuffer.pixelBuffer, pixelFormat: .rg8Unorm, width: width >> 1, height: height >> 1, plane: 1) else {
|
||||
return nil
|
||||
}
|
||||
return self.process(yTexture: inputYTexture, cbcrTexture: inputCbCrTexture, width: width, height: height, rotation: pixelBuffer.rotation, device: device, commandBuffer: commandBuffer)
|
||||
}
|
||||
|
||||
func process(yTexture: MTLTexture, cbcrTexture: MTLTexture, width: Int, height: Int, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
self.setupVerticesBuffer(device: device, rotation: rotation)
|
||||
|
||||
func textureDimensionsForRotation(width: Int, height: Int, rotation: TextureRotation) -> (width: Int, height: Int) {
|
||||
switch rotation {
|
||||
case .rotate90Degrees, .rotate270Degrees, .rotate90DegreesMirrored:
|
||||
return (height, width)
|
||||
default:
|
||||
return (width, height)
|
||||
}
|
||||
}
|
||||
|
||||
let (outputWidth, outputHeight) = textureDimensionsForRotation(width: width, height: height, rotation: rotation)
|
||||
if self.cachedTexture == nil {
|
||||
let textureDescriptor = MTLTextureDescriptor()
|
||||
textureDescriptor.textureType = .type2D
|
||||
textureDescriptor.width = outputWidth
|
||||
textureDescriptor.height = outputHeight
|
||||
textureDescriptor.pixelFormat = self.pixelFormat
|
||||
textureDescriptor.storageMode = .private
|
||||
textureDescriptor.usage = [.shaderRead, .shaderWrite, .renderTarget]
|
||||
if let texture = device.makeTexture(descriptor: textureDescriptor) {
|
||||
self.cachedTexture = texture
|
||||
}
|
||||
}
|
||||
|
||||
let renderPassDescriptor = MTLRenderPassDescriptor()
|
||||
renderPassDescriptor.colorAttachments[0].texture = self.cachedTexture!
|
||||
renderPassDescriptor.colorAttachments[0].loadAction = .dontCare
|
||||
renderPassDescriptor.colorAttachments[0].storeAction = .store
|
||||
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0)
|
||||
guard let renderCommandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
renderCommandEncoder.setViewport(MTLViewport(
|
||||
originX: 0, originY: 0,
|
||||
width: Double(outputWidth), height: Double(outputHeight),
|
||||
znear: -1.0, zfar: 1.0)
|
||||
)
|
||||
|
||||
renderCommandEncoder.setFragmentTexture(yTexture, index: 0)
|
||||
renderCommandEncoder.setFragmentTexture(cbcrTexture, index: 1)
|
||||
|
||||
self.encodeDefaultCommands(using: renderCommandEncoder)
|
||||
|
||||
renderCommandEncoder.endEncoding()
|
||||
|
||||
return self.cachedTexture
|
||||
}
|
||||
}
|
||||
|
|
@ -3,50 +3,27 @@ import AVFoundation
|
|||
import Metal
|
||||
import MetalKit
|
||||
|
||||
func textureRotatonForAVAsset(_ asset: AVAsset, mirror: Bool = false) -> TextureRotation {
|
||||
for track in asset.tracks {
|
||||
if track.mediaType == .video {
|
||||
let t = track.preferredTransform
|
||||
if t.a == -1.0 && t.d == -1.0 {
|
||||
return .rotate180Degrees
|
||||
} else if t.a == 1.0 && t.d == 1.0 {
|
||||
return .rotate0Degrees
|
||||
} else if t.b == -1.0 && t.c == 1.0 {
|
||||
return .rotate270Degrees
|
||||
} else if t.a == -1.0 && t.d == 1.0 {
|
||||
return .rotate270Degrees
|
||||
} else if t.a == 1.0 && t.d == -1.0 {
|
||||
return .rotate180Degrees
|
||||
} else {
|
||||
return mirror ? .rotate90DegreesMirrored : .rotate90Degrees
|
||||
}
|
||||
}
|
||||
}
|
||||
return .rotate0Degrees
|
||||
}
|
||||
|
||||
final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullDelegate {
|
||||
private weak var player: AVPlayer?
|
||||
private weak var additionalPlayer: AVPlayer?
|
||||
private weak var playerItem: AVPlayerItem?
|
||||
private weak var additionalPlayerItem: AVPlayerItem?
|
||||
private let device: MTLDevice?
|
||||
private var displayLink: CADisplayLink?
|
||||
|
||||
private let mirror: Bool
|
||||
|
||||
private weak var player: AVPlayer?
|
||||
private weak var playerItem: AVPlayerItem?
|
||||
private var playerItemOutput: AVPlayerItemVideoOutput?
|
||||
private var additionalPlayerItemOutput: AVPlayerItemVideoOutput?
|
||||
|
||||
private var displayLink: CADisplayLink?
|
||||
|
||||
private let device: MTLDevice?
|
||||
private var textureRotation: TextureRotation = .rotate0Degrees
|
||||
private var additionalTextureRotation: TextureRotation = .rotate0Degrees
|
||||
|
||||
private var forceUpdate: Bool = false
|
||||
|
||||
weak var output: TextureConsumer?
|
||||
private weak var additionalPlayer: AVPlayer?
|
||||
private weak var additionalPlayerItem: AVPlayerItem?
|
||||
private var additionalPlayerItemOutput: AVPlayerItemVideoOutput?
|
||||
private var additionalTextureRotation: TextureRotation = .rotate0Degrees
|
||||
|
||||
weak var output: MediaEditorRenderer?
|
||||
var queue: DispatchQueue!
|
||||
var started: Bool = false
|
||||
|
||||
private var forceUpdate: Bool = false
|
||||
|
||||
init(player: AVPlayer, additionalPlayer: AVPlayer?, mirror: Bool, renderTarget: RenderTarget) {
|
||||
self.player = player
|
||||
|
|
@ -59,23 +36,14 @@ final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullD
|
|||
qos: .userInteractive,
|
||||
attributes: [],
|
||||
autoreleaseFrequency: .workItem,
|
||||
target: nil)
|
||||
target: nil
|
||||
)
|
||||
|
||||
super.init()
|
||||
|
||||
self.playerItem = player.currentItem
|
||||
self.additionalPlayerItem = additionalPlayer?.currentItem
|
||||
self.handleReadyToPlay()
|
||||
}
|
||||
|
||||
func invalidate() {
|
||||
self.playerItemOutput?.setDelegate(nil, queue: nil)
|
||||
self.playerItemOutput = nil
|
||||
self.displayLink?.invalidate()
|
||||
self.displayLink = nil
|
||||
}
|
||||
|
||||
private func handleReadyToPlay() {
|
||||
guard let playerItem = self.playerItem else {
|
||||
return
|
||||
}
|
||||
|
|
@ -91,11 +59,49 @@ final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullD
|
|||
break
|
||||
}
|
||||
}
|
||||
self.textureRotation = textureRotatonForAVAsset(playerItem.asset, mirror: additionalPlayer == nil && mirror)
|
||||
|
||||
if !hasVideoTrack {
|
||||
return
|
||||
}
|
||||
|
||||
self.textureRotation = textureRotatonForAVAsset(playerItem.asset, mirror: self.additionalPlayer == nil && self.mirror)
|
||||
self.playerItemOutput = self.setupPlayerVideoOutput(playerItem: playerItem)
|
||||
if let additionalPlayerItem = self.additionalPlayerItem {
|
||||
self.additionalTextureRotation = textureRotatonForAVAsset(additionalPlayerItem.asset, mirror: true)
|
||||
self.additionalPlayerItemOutput = self.setupPlayerVideoOutput(playerItem: additionalPlayerItem)
|
||||
}
|
||||
|
||||
self.setupDisplayLink(frameRate: min(60, frameRate))
|
||||
}
|
||||
|
||||
func invalidate() {
|
||||
self.playerItemOutput?.setDelegate(nil, queue: nil)
|
||||
self.playerItemOutput = nil
|
||||
self.additionalPlayerItemOutput?.setDelegate(nil, queue: nil)
|
||||
self.additionalPlayerItemOutput = nil
|
||||
self.displayLink?.invalidate()
|
||||
self.displayLink = nil
|
||||
}
|
||||
|
||||
func setAdditionalPlayer(_ additionalPlayer: AVPlayer?) {
|
||||
self.additionalPlayer = additionalPlayer
|
||||
self.additionalPlayerItem = additionalPlayer?.currentItem
|
||||
|
||||
if let additionalPlayerItem = self.additionalPlayerItem {
|
||||
self.additionalTextureRotation = textureRotatonForAVAsset(additionalPlayerItem.asset, mirror: true)
|
||||
self.additionalPlayerItemOutput = self.setupPlayerVideoOutput(playerItem: additionalPlayerItem)
|
||||
} else if let additionalPlayerItemOutput = self.additionalPlayerItemOutput {
|
||||
self.additionalPlayerItemOutput = nil
|
||||
additionalPlayerItemOutput.setDelegate(nil, queue: nil)
|
||||
|
||||
if let additionalPlayerItem = self.additionalPlayerItem {
|
||||
self.additionalPlayerItem = nil
|
||||
additionalPlayerItem.remove(additionalPlayerItemOutput)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func setupPlayerVideoOutput(playerItem: AVPlayerItem) -> AVPlayerItemVideoOutput {
|
||||
let colorProperties: [String: Any] = [
|
||||
AVVideoColorPrimariesKey: AVVideoColorPrimaries_ITU_R_709_2,
|
||||
AVVideoTransferFunctionKey: AVVideoTransferFunction_ITU_R_709_2,
|
||||
|
|
@ -112,19 +118,7 @@ final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullD
|
|||
output.suppressesPlayerRendering = true
|
||||
output.setDelegate(self, queue: self.queue)
|
||||
playerItem.add(output)
|
||||
self.playerItemOutput = output
|
||||
|
||||
if let additionalPlayerItem = self.additionalPlayerItem {
|
||||
self.additionalTextureRotation = textureRotatonForAVAsset(additionalPlayerItem.asset, mirror: true)
|
||||
|
||||
let output = AVPlayerItemVideoOutput(outputSettings: outputSettings)
|
||||
output.suppressesPlayerRendering = true
|
||||
output.setDelegate(self, queue: self.queue)
|
||||
additionalPlayerItem.add(output)
|
||||
self.additionalPlayerItemOutput = output
|
||||
}
|
||||
|
||||
self.setupDisplayLink(frameRate: min(60, frameRate))
|
||||
return output
|
||||
}
|
||||
|
||||
private class DisplayLinkTarget {
|
||||
|
|
@ -193,7 +187,7 @@ final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullD
|
|||
}
|
||||
|
||||
if let mainPixelBuffer {
|
||||
self.output?.consumeVideoPixelBuffer(pixelBuffer: mainPixelBuffer, additionalPixelBuffer: additionalPixelBuffer, render: true)
|
||||
self.output?.consume(main: .videoBuffer(mainPixelBuffer), additional: additionalPixelBuffer.flatMap { .videoBuffer($0) }, render: true)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -209,7 +203,7 @@ final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullD
|
|||
}
|
||||
}
|
||||
|
||||
func connect(to consumer: TextureConsumer) {
|
||||
func connect(to consumer: MediaEditorRenderer) {
|
||||
self.output = consumer
|
||||
}
|
||||
|
||||
|
|
@ -217,536 +211,3 @@ final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullD
|
|||
self.displayLink?.isPaused = false
|
||||
}
|
||||
}
|
||||
|
||||
final class VideoInputPass: DefaultRenderPass {
|
||||
private var cachedTexture: MTLTexture?
|
||||
|
||||
override var fragmentShaderFunctionName: String {
|
||||
return "bt709ToRGBFragmentShader"
|
||||
}
|
||||
|
||||
override func setup(device: MTLDevice, library: MTLLibrary) {
|
||||
super.setup(device: device, library: library)
|
||||
}
|
||||
|
||||
func processPixelBuffer(_ pixelBuffer: VideoPixelBuffer, textureCache: CVMetalTextureCache, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
func textureFromPixelBuffer(_ pixelBuffer: CVPixelBuffer, pixelFormat: MTLPixelFormat, width: Int, height: Int, plane: Int) -> MTLTexture? {
|
||||
var textureRef : CVMetalTexture?
|
||||
let status = CVMetalTextureCacheCreateTextureFromImage(nil, textureCache, pixelBuffer, nil, pixelFormat, width, height, plane, &textureRef)
|
||||
if status == kCVReturnSuccess, let textureRef {
|
||||
return CVMetalTextureGetTexture(textureRef)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
let width = CVPixelBufferGetWidth(pixelBuffer.pixelBuffer)
|
||||
let height = CVPixelBufferGetHeight(pixelBuffer.pixelBuffer)
|
||||
guard let inputYTexture = textureFromPixelBuffer(pixelBuffer.pixelBuffer, pixelFormat: .r8Unorm, width: width, height: height, plane: 0),
|
||||
let inputCbCrTexture = textureFromPixelBuffer(pixelBuffer.pixelBuffer, pixelFormat: .rg8Unorm, width: width >> 1, height: height >> 1, plane: 1) else {
|
||||
return nil
|
||||
}
|
||||
return self.process(yTexture: inputYTexture, cbcrTexture: inputCbCrTexture, width: width, height: height, rotation: pixelBuffer.rotation, device: device, commandBuffer: commandBuffer)
|
||||
}
|
||||
|
||||
func process(yTexture: MTLTexture, cbcrTexture: MTLTexture, width: Int, height: Int, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
self.setupVerticesBuffer(device: device, rotation: rotation)
|
||||
|
||||
func textureDimensionsForRotation(width: Int, height: Int, rotation: TextureRotation) -> (width: Int, height: Int) {
|
||||
switch rotation {
|
||||
case .rotate90Degrees, .rotate270Degrees, .rotate90DegreesMirrored:
|
||||
return (height, width)
|
||||
default:
|
||||
return (width, height)
|
||||
}
|
||||
}
|
||||
|
||||
let (outputWidth, outputHeight) = textureDimensionsForRotation(width: width, height: height, rotation: rotation)
|
||||
if self.cachedTexture == nil {
|
||||
let textureDescriptor = MTLTextureDescriptor()
|
||||
textureDescriptor.textureType = .type2D
|
||||
textureDescriptor.width = outputWidth
|
||||
textureDescriptor.height = outputHeight
|
||||
textureDescriptor.pixelFormat = self.pixelFormat
|
||||
textureDescriptor.storageMode = .private
|
||||
textureDescriptor.usage = [.shaderRead, .shaderWrite, .renderTarget]
|
||||
if let texture = device.makeTexture(descriptor: textureDescriptor) {
|
||||
self.cachedTexture = texture
|
||||
}
|
||||
}
|
||||
|
||||
let renderPassDescriptor = MTLRenderPassDescriptor()
|
||||
renderPassDescriptor.colorAttachments[0].texture = self.cachedTexture!
|
||||
renderPassDescriptor.colorAttachments[0].loadAction = .dontCare
|
||||
renderPassDescriptor.colorAttachments[0].storeAction = .store
|
||||
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0)
|
||||
guard let renderCommandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
renderCommandEncoder.setViewport(MTLViewport(
|
||||
originX: 0, originY: 0,
|
||||
width: Double(outputWidth), height: Double(outputHeight),
|
||||
znear: -1.0, zfar: 1.0)
|
||||
)
|
||||
|
||||
renderCommandEncoder.setFragmentTexture(yTexture, index: 0)
|
||||
renderCommandEncoder.setFragmentTexture(cbcrTexture, index: 1)
|
||||
|
||||
self.encodeDefaultCommands(using: renderCommandEncoder)
|
||||
|
||||
renderCommandEncoder.endEncoding()
|
||||
|
||||
return self.cachedTexture
|
||||
}
|
||||
}
|
||||
|
||||
private func verticesData(
|
||||
textureRotation: TextureRotation,
|
||||
containerSize: CGSize,
|
||||
position: CGPoint,
|
||||
size: CGSize,
|
||||
rotation: CGFloat,
|
||||
z: Float = 0.0
|
||||
) -> [VertexData] {
|
||||
let topLeft: simd_float2
|
||||
let topRight: simd_float2
|
||||
let bottomLeft: simd_float2
|
||||
let bottomRight: simd_float2
|
||||
|
||||
switch textureRotation {
|
||||
case .rotate0Degrees:
|
||||
topLeft = simd_float2(0.0, 1.0)
|
||||
topRight = simd_float2(1.0, 1.0)
|
||||
bottomLeft = simd_float2(0.0, 0.0)
|
||||
bottomRight = simd_float2(1.0, 0.0)
|
||||
case .rotate0DegreesMirrored:
|
||||
topLeft = simd_float2(1.0, 1.0)
|
||||
topRight = simd_float2(0.0, 1.0)
|
||||
bottomLeft = simd_float2(1.0, 0.0)
|
||||
bottomRight = simd_float2(0.0, 0.0)
|
||||
case .rotate180Degrees:
|
||||
topLeft = simd_float2(1.0, 0.0)
|
||||
topRight = simd_float2(0.0, 0.0)
|
||||
bottomLeft = simd_float2(1.0, 1.0)
|
||||
bottomRight = simd_float2(0.0, 1.0)
|
||||
case .rotate90Degrees:
|
||||
topLeft = simd_float2(1.0, 1.0)
|
||||
topRight = simd_float2(1.0, 0.0)
|
||||
bottomLeft = simd_float2(0.0, 1.0)
|
||||
bottomRight = simd_float2(0.0, 0.0)
|
||||
case .rotate90DegreesMirrored:
|
||||
topLeft = simd_float2(1.0, 0.0)
|
||||
topRight = simd_float2(1.0, 1.0)
|
||||
bottomLeft = simd_float2(0.0, 0.0)
|
||||
bottomRight = simd_float2(0.0, 1.0)
|
||||
case .rotate270Degrees:
|
||||
topLeft = simd_float2(0.0, 0.0)
|
||||
topRight = simd_float2(0.0, 1.0)
|
||||
bottomLeft = simd_float2(1.0, 0.0)
|
||||
bottomRight = simd_float2(1.0, 1.0)
|
||||
}
|
||||
|
||||
let angle = Float(.pi - rotation)
|
||||
let cosAngle = cos(angle)
|
||||
let sinAngle = sin(angle)
|
||||
|
||||
let centerX = Float(position.x)
|
||||
let centerY = Float(position.y)
|
||||
|
||||
let halfWidth = Float(size.width / 2.0)
|
||||
let halfHeight = Float(size.height / 2.0)
|
||||
|
||||
return [
|
||||
VertexData(
|
||||
pos: simd_float4(
|
||||
x: (centerX + (halfWidth * cosAngle) - (halfHeight * sinAngle)) / Float(containerSize.width) * 2.0,
|
||||
y: (centerY + (halfWidth * sinAngle) + (halfHeight * cosAngle)) / Float(containerSize.height) * 2.0,
|
||||
z: z,
|
||||
w: 1
|
||||
),
|
||||
texCoord: topLeft,
|
||||
localPos: simd_float2(0.0, 0.0)
|
||||
),
|
||||
VertexData(
|
||||
pos: simd_float4(
|
||||
x: (centerX - (halfWidth * cosAngle) - (halfHeight * sinAngle)) / Float(containerSize.width) * 2.0,
|
||||
y: (centerY - (halfWidth * sinAngle) + (halfHeight * cosAngle)) / Float(containerSize.height) * 2.0,
|
||||
z: z,
|
||||
w: 1
|
||||
),
|
||||
texCoord: topRight,
|
||||
localPos: simd_float2(1.0, 0.0)
|
||||
),
|
||||
VertexData(
|
||||
pos: simd_float4(
|
||||
x: (centerX + (halfWidth * cosAngle) + (halfHeight * sinAngle)) / Float(containerSize.width) * 2.0,
|
||||
y: (centerY + (halfWidth * sinAngle) - (halfHeight * cosAngle)) / Float(containerSize.height) * 2.0,
|
||||
z: z,
|
||||
w: 1
|
||||
),
|
||||
texCoord: bottomLeft,
|
||||
localPos: simd_float2(0.0, 1.0)
|
||||
),
|
||||
VertexData(
|
||||
pos: simd_float4(
|
||||
x: (centerX - (halfWidth * cosAngle) + (halfHeight * sinAngle)) / Float(containerSize.width) * 2.0,
|
||||
y: (centerY - (halfWidth * sinAngle) - (halfHeight * cosAngle)) / Float(containerSize.height) * 2.0,
|
||||
z: z,
|
||||
w: 1
|
||||
),
|
||||
texCoord: bottomRight,
|
||||
localPos: simd_float2(1.0, 1.0)
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
private func lookupSpringValue(_ t: CGFloat) -> CGFloat {
|
||||
let table: [(CGFloat, CGFloat)] = [
|
||||
(0.0, 0.0),
|
||||
(0.0625, 0.1123005598783493),
|
||||
(0.125, 0.31598418951034546),
|
||||
(0.1875, 0.5103585720062256),
|
||||
(0.25, 0.6650152802467346),
|
||||
(0.3125, 0.777747631072998),
|
||||
(0.375, 0.8557760119438171),
|
||||
(0.4375, 0.9079672694206238),
|
||||
(0.5, 0.942038357257843),
|
||||
(0.5625, 0.9638798832893372),
|
||||
(0.625, 0.9776856303215027),
|
||||
(0.6875, 0.9863143563270569),
|
||||
(0.75, 0.991658091545105),
|
||||
(0.8125, 0.9949421286582947),
|
||||
(0.875, 0.9969474077224731),
|
||||
(0.9375, 0.9981651306152344),
|
||||
(1.0, 1.0)
|
||||
]
|
||||
|
||||
for i in 0 ..< table.count - 2 {
|
||||
let lhs = table[i]
|
||||
let rhs = table[i + 1]
|
||||
|
||||
if t >= lhs.0 && t <= rhs.0 {
|
||||
let fraction = (t - lhs.0) / (rhs.0 - lhs.0)
|
||||
let value = lhs.1 + fraction * (rhs.1 - lhs.1)
|
||||
return value
|
||||
}
|
||||
}
|
||||
return 1.0
|
||||
}
|
||||
|
||||
final class VideoInputScalePass: RenderPass {
|
||||
private var cachedTexture: MTLTexture?
|
||||
|
||||
var mainPipelineState: MTLRenderPipelineState?
|
||||
var mainVerticesBuffer: MTLBuffer?
|
||||
var mainTextureRotation: TextureRotation = .rotate0Degrees
|
||||
|
||||
var additionalVerticesBuffer: MTLBuffer?
|
||||
var additionalTextureRotation: TextureRotation = .rotate0Degrees
|
||||
|
||||
var pixelFormat: MTLPixelFormat {
|
||||
return .bgra8Unorm
|
||||
}
|
||||
|
||||
func setup(device: MTLDevice, library: MTLLibrary) {
|
||||
let descriptor = MTLRenderPipelineDescriptor()
|
||||
descriptor.vertexFunction = library.makeFunction(name: "defaultVertexShader")
|
||||
descriptor.fragmentFunction = library.makeFunction(name: "dualFragmentShader")
|
||||
descriptor.colorAttachments[0].pixelFormat = self.pixelFormat
|
||||
descriptor.colorAttachments[0].isBlendingEnabled = true
|
||||
descriptor.colorAttachments[0].rgbBlendOperation = .add
|
||||
descriptor.colorAttachments[0].alphaBlendOperation = .add
|
||||
descriptor.colorAttachments[0].sourceRGBBlendFactor = .sourceAlpha
|
||||
descriptor.colorAttachments[0].sourceAlphaBlendFactor = .sourceAlpha
|
||||
descriptor.colorAttachments[0].destinationRGBBlendFactor = .oneMinusSourceAlpha
|
||||
descriptor.colorAttachments[0].destinationAlphaBlendFactor = .oneMinusSourceAlpha
|
||||
|
||||
do {
|
||||
self.mainPipelineState = try device.makeRenderPipelineState(descriptor: descriptor)
|
||||
} catch {
|
||||
print(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
func encodeVideo(
|
||||
using encoder: MTLRenderCommandEncoder,
|
||||
containerSize: CGSize,
|
||||
texture: MTLTexture,
|
||||
textureRotation: TextureRotation,
|
||||
position: VideoPosition,
|
||||
roundness: Float,
|
||||
alpha: Float,
|
||||
zPosition: Float,
|
||||
device: MTLDevice
|
||||
) {
|
||||
encoder.setFragmentTexture(texture, index: 0)
|
||||
|
||||
let center = CGPoint(
|
||||
x: position.position.x - containerSize.width / 2.0,
|
||||
y: containerSize.height - position.position.y - containerSize.height / 2.0
|
||||
)
|
||||
|
||||
let size = CGSize(
|
||||
width: position.size.width * position.scale,
|
||||
height: position.size.height * position.scale
|
||||
)
|
||||
|
||||
let vertices = verticesData(textureRotation: textureRotation, containerSize: containerSize, position: center, size: size, rotation: position.rotation, z: zPosition)
|
||||
let buffer = device.makeBuffer(
|
||||
bytes: vertices,
|
||||
length: MemoryLayout<VertexData>.stride * vertices.count,
|
||||
options: [])
|
||||
encoder.setVertexBuffer(buffer, offset: 0, index: 0)
|
||||
|
||||
var resolution = simd_uint2(UInt32(size.width), UInt32(size.height))
|
||||
encoder.setFragmentBytes(&resolution, length: MemoryLayout<simd_uint2>.size * 2, index: 0)
|
||||
|
||||
var roundness = roundness
|
||||
encoder.setFragmentBytes(&roundness, length: MemoryLayout<simd_float1>.size, index: 1)
|
||||
|
||||
var alpha = alpha
|
||||
encoder.setFragmentBytes(&alpha, length: MemoryLayout<simd_float1>.size, index: 2)
|
||||
|
||||
encoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4)
|
||||
}
|
||||
|
||||
func update(values: MediaEditorValues) {
|
||||
if let position = values.additionalVideoPosition, let scale = values.additionalVideoScale, let rotation = values.additionalVideoRotation {
|
||||
self.additionalPosition = VideoInputScalePass.VideoPosition(position: position, size: CGSize(width: 1080.0 / 4.0, height: 1440.0 / 4.0), scale: scale, rotation: rotation)
|
||||
}
|
||||
if !values.additionalVideoPositionChanges.isEmpty {
|
||||
self.videoPositionChanges = values.additionalVideoPositionChanges
|
||||
}
|
||||
}
|
||||
|
||||
private var mainPosition = VideoPosition(
|
||||
position: CGPoint(x: 1080 / 2.0, y: 1920.0 / 2.0),
|
||||
size: CGSize(width: 1080.0, height: 1920.0),
|
||||
scale: 1.0,
|
||||
rotation: 0.0
|
||||
)
|
||||
|
||||
private var additionalPosition = VideoPosition(
|
||||
position: CGPoint(x: 1080 / 2.0, y: 1920.0 / 2.0),
|
||||
size: CGSize(width: 1440.0, height: 1920.0),
|
||||
scale: 0.5,
|
||||
rotation: 0.0
|
||||
)
|
||||
|
||||
private var transitionDuration = 0.5
|
||||
private var videoPositionChanges: [VideoPositionChange] = []
|
||||
|
||||
enum VideoType {
|
||||
case main
|
||||
case additional
|
||||
case transition
|
||||
}
|
||||
|
||||
struct VideoPosition {
|
||||
let position: CGPoint
|
||||
let size: CGSize
|
||||
let scale: CGFloat
|
||||
let rotation: CGFloat
|
||||
|
||||
func mixed(with other: VideoPosition, fraction: CGFloat) -> VideoPosition {
|
||||
let position = CGPoint(
|
||||
x: self.position.x + (other.position.x - self.position.x) * fraction,
|
||||
y: self.position.y + (other.position.y - self.position.y) * fraction
|
||||
)
|
||||
let size = CGSize(
|
||||
width: self.size.width + (other.size.width - self.size.width) * fraction,
|
||||
height: self.size.height + (other.size.height - self.size.height) * fraction
|
||||
)
|
||||
let scale = self.scale + (other.scale - self.scale) * fraction
|
||||
let rotation = self.rotation + (other.rotation - self.rotation) * fraction
|
||||
|
||||
return VideoPosition(
|
||||
position: position,
|
||||
size: size,
|
||||
scale: scale,
|
||||
rotation: rotation
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct VideoState {
|
||||
let texture: MTLTexture
|
||||
let textureRotation: TextureRotation
|
||||
let position: VideoPosition
|
||||
let roundness: Float
|
||||
let alpha: Float
|
||||
}
|
||||
|
||||
func transitionState(for time: CMTime, mainInput: MTLTexture, additionalInput: MTLTexture?) -> (VideoState, VideoState?, VideoState?) {
|
||||
let timestamp = time.seconds
|
||||
|
||||
var backgroundTexture = mainInput
|
||||
var backgroundTextureRotation = self.mainTextureRotation
|
||||
|
||||
var foregroundTexture = additionalInput
|
||||
var foregroundTextureRotation = self.additionalTextureRotation
|
||||
|
||||
var mainPosition = self.mainPosition
|
||||
var additionalPosition = self.additionalPosition
|
||||
var disappearingPosition = self.mainPosition
|
||||
|
||||
var transitionFraction = 1.0
|
||||
if let additionalInput {
|
||||
var previousChange: VideoPositionChange?
|
||||
for change in self.videoPositionChanges {
|
||||
if timestamp >= change.timestamp {
|
||||
previousChange = change
|
||||
}
|
||||
if timestamp < change.timestamp {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if let previousChange {
|
||||
if previousChange.additional {
|
||||
backgroundTexture = additionalInput
|
||||
backgroundTextureRotation = self.additionalTextureRotation
|
||||
|
||||
mainPosition = VideoPosition(position: mainPosition.position, size: CGSize(width: 1440.0, height: 1920.0), scale: mainPosition.scale, rotation: mainPosition.rotation)
|
||||
additionalPosition = VideoPosition(position: additionalPosition.position, size: CGSize(width: 1080.0 / 4.0, height: 1920.0 / 4.0), scale: additionalPosition.scale, rotation: additionalPosition.rotation)
|
||||
|
||||
foregroundTexture = mainInput
|
||||
foregroundTextureRotation = self.mainTextureRotation
|
||||
} else {
|
||||
disappearingPosition = VideoPosition(position: mainPosition.position, size: CGSize(width: 1440.0, height: 1920.0), scale: mainPosition.scale, rotation: mainPosition.rotation)
|
||||
}
|
||||
if previousChange.timestamp > 0.0 && timestamp < previousChange.timestamp + transitionDuration {
|
||||
transitionFraction = (timestamp - previousChange.timestamp) / transitionDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var backgroundVideoState = VideoState(texture: backgroundTexture, textureRotation: backgroundTextureRotation, position: mainPosition, roundness: 0.0, alpha: 1.0)
|
||||
var foregroundVideoState: VideoState?
|
||||
var disappearingVideoState: VideoState?
|
||||
|
||||
if let foregroundTexture {
|
||||
var foregroundPosition = additionalPosition
|
||||
var foregroundAlpha: Float = 1.0
|
||||
if transitionFraction < 1.0 {
|
||||
let springFraction = lookupSpringValue(transitionFraction)
|
||||
|
||||
let appearingPosition = VideoPosition(position: additionalPosition.position, size: additionalPosition.size, scale: 0.01, rotation: self.additionalPosition.rotation)
|
||||
let backgroundInitialPosition = VideoPosition(position: additionalPosition.position, size: CGSize(width: mainPosition.size.width / 4.0, height: mainPosition.size.height / 4.0), scale: additionalPosition.scale, rotation: additionalPosition.rotation)
|
||||
|
||||
foregroundPosition = appearingPosition.mixed(with: additionalPosition, fraction: springFraction)
|
||||
|
||||
disappearingVideoState = VideoState(texture: foregroundTexture, textureRotation: foregroundTextureRotation, position: disappearingPosition, roundness: 0.0, alpha: 1.0)
|
||||
backgroundVideoState = VideoState(texture: backgroundTexture, textureRotation: backgroundTextureRotation, position: backgroundInitialPosition.mixed(with: mainPosition, fraction: springFraction), roundness: Float(1.0 - springFraction), alpha: 1.0)
|
||||
|
||||
foregroundAlpha = min(1.0, max(0.0, Float(transitionFraction) * 2.5))
|
||||
}
|
||||
foregroundVideoState = VideoState(texture: foregroundTexture, textureRotation: foregroundTextureRotation, position: foregroundPosition, roundness: 1.0, alpha: foregroundAlpha)
|
||||
}
|
||||
|
||||
return (backgroundVideoState, foregroundVideoState, disappearingVideoState)
|
||||
}
|
||||
|
||||
func process(input: MTLTexture, secondInput: MTLTexture?, timestamp: CMTime, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
guard max(input.width, input.height) > 1920 || secondInput != nil else {
|
||||
return input
|
||||
}
|
||||
|
||||
let scaledSize = CGSize(width: input.width, height: input.height).fitted(CGSize(width: 1920.0, height: 1920.0))
|
||||
let width: Int
|
||||
let height: Int
|
||||
|
||||
if secondInput != nil {
|
||||
width = 1080
|
||||
height = 1920
|
||||
} else {
|
||||
width = Int(scaledSize.width)
|
||||
height = Int(scaledSize.height)
|
||||
}
|
||||
self.mainPosition = VideoPosition(position: CGPoint(x: width / 2, y: height / 2), size: CGSize(width: width, height: height), scale: 1.0, rotation: 0.0)
|
||||
|
||||
let containerSize = CGSize(width: width, height: height)
|
||||
|
||||
if self.cachedTexture == nil || self.cachedTexture?.width != width || self.cachedTexture?.height != height {
|
||||
let textureDescriptor = MTLTextureDescriptor()
|
||||
textureDescriptor.textureType = .type2D
|
||||
textureDescriptor.width = width
|
||||
textureDescriptor.height = height
|
||||
textureDescriptor.pixelFormat = input.pixelFormat
|
||||
textureDescriptor.storageMode = .private
|
||||
textureDescriptor.usage = [.shaderRead, .shaderWrite, .renderTarget]
|
||||
guard let texture = device.makeTexture(descriptor: textureDescriptor) else {
|
||||
return input
|
||||
}
|
||||
self.cachedTexture = texture
|
||||
texture.label = "scaledVideoTexture"
|
||||
}
|
||||
|
||||
let renderPassDescriptor = MTLRenderPassDescriptor()
|
||||
renderPassDescriptor.colorAttachments[0].texture = self.cachedTexture!
|
||||
renderPassDescriptor.colorAttachments[0].loadAction = .dontCare
|
||||
renderPassDescriptor.colorAttachments[0].storeAction = .store
|
||||
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 1.0)
|
||||
guard let renderCommandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else {
|
||||
return input
|
||||
}
|
||||
|
||||
renderCommandEncoder.setViewport(MTLViewport(
|
||||
originX: 0, originY: 0,
|
||||
width: Double(width), height: Double(height),
|
||||
znear: -1.0, zfar: 1.0)
|
||||
)
|
||||
|
||||
renderCommandEncoder.setRenderPipelineState(self.mainPipelineState!)
|
||||
|
||||
let (mainVideoState, additionalVideoState, transitionVideoState) = self.transitionState(for: timestamp, mainInput: input, additionalInput: secondInput)
|
||||
|
||||
if let transitionVideoState {
|
||||
self.encodeVideo(
|
||||
using: renderCommandEncoder,
|
||||
containerSize: containerSize,
|
||||
texture: transitionVideoState.texture,
|
||||
textureRotation: transitionVideoState.textureRotation,
|
||||
position: transitionVideoState.position,
|
||||
roundness: transitionVideoState.roundness,
|
||||
alpha: transitionVideoState.alpha,
|
||||
zPosition: 0.75,
|
||||
device: device
|
||||
)
|
||||
}
|
||||
|
||||
self.encodeVideo(
|
||||
using: renderCommandEncoder,
|
||||
containerSize: containerSize,
|
||||
texture: mainVideoState.texture,
|
||||
textureRotation: mainVideoState.textureRotation,
|
||||
position: mainVideoState.position,
|
||||
roundness: mainVideoState.roundness,
|
||||
alpha: mainVideoState.alpha,
|
||||
zPosition: 0.0,
|
||||
device: device
|
||||
)
|
||||
|
||||
if let additionalVideoState {
|
||||
self.encodeVideo(
|
||||
using: renderCommandEncoder,
|
||||
containerSize: containerSize,
|
||||
texture: additionalVideoState.texture,
|
||||
textureRotation: additionalVideoState.textureRotation,
|
||||
position: additionalVideoState.position,
|
||||
roundness: additionalVideoState.roundness,
|
||||
alpha: additionalVideoState.alpha,
|
||||
zPosition: 0.5,
|
||||
device: device
|
||||
)
|
||||
}
|
||||
|
||||
renderCommandEncoder.endEncoding()
|
||||
|
||||
return self.cachedTexture!
|
||||
}
|
||||
|
||||
func process(input: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ swift_library(
|
|||
"//submodules/TelegramUI/Components/AudioWaveformComponent",
|
||||
"//submodules/ReactionSelectionNode",
|
||||
"//submodules/TelegramUI/Components/VolumeSliderContextItem",
|
||||
"//submodules/TelegramUI/Components/Stories/ForwardInfoPanelComponent"
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
|
||||
final class FlipButtonContentComponent: Component {
|
||||
init() {
|
||||
|
||||
}
|
||||
|
||||
static func ==(lhs: FlipButtonContentComponent, rhs: FlipButtonContentComponent) -> Bool {
|
||||
return lhs === rhs
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private var component: FlipButtonContentComponent?
|
||||
|
||||
private let backgroundView: BlurredBackgroundView
|
||||
private let icon = SimpleLayer()
|
||||
|
||||
init() {
|
||||
self.backgroundView = BlurredBackgroundView(color: UIColor(white: 0.0, alpha: 0.5), enableBlur: true)
|
||||
|
||||
super.init(frame: CGRect())
|
||||
|
||||
self.addSubview(self.backgroundView)
|
||||
self.layer.addSublayer(self.icon)
|
||||
|
||||
self.icon.contents = UIImage(bundleImageName: "Camera/FlipIcon")?.withRenderingMode(.alwaysTemplate).cgImage
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func playAnimation() {
|
||||
let animation = CASpringAnimation(keyPath: "transform.rotation.z")
|
||||
animation.fromValue = 0.0 as NSNumber
|
||||
animation.toValue = CGFloat.pi as NSNumber
|
||||
animation.mass = 5.0
|
||||
animation.stiffness = 900.0
|
||||
animation.damping = 90.0
|
||||
animation.duration = animation.settlingDuration
|
||||
if #available(iOS 15.0, *) {
|
||||
let maxFps = Float(UIScreen.main.maximumFramesPerSecond)
|
||||
animation.preferredFrameRateRange = CAFrameRateRange(minimum: 30.0, maximum: maxFps, preferred: maxFps)
|
||||
}
|
||||
self.icon.add(animation, forKey: "transform.rotation.z")
|
||||
}
|
||||
|
||||
func update(component: FlipButtonContentComponent, availableSize: CGSize, transition: Transition) -> CGSize {
|
||||
self.component = component
|
||||
|
||||
let size = CGSize(width: 48.0, height: 48.0)
|
||||
let backgroundFrame = CGRect(x: 4.0, y: 4.0, width: 40.0, height: 40.0)
|
||||
|
||||
self.icon.layerTintColor = UIColor.white.cgColor
|
||||
self.icon.position = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
|
||||
self.icon.bounds = CGRect(origin: .zero, size: size)
|
||||
|
||||
self.backgroundView.frame = backgroundFrame
|
||||
self.backgroundView.update(size: backgroundFrame.size, cornerRadius: backgroundFrame.width / 2.0, transition: .immediate)
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View()
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, transition: transition)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import CoreLocation
|
||||
import Photos
|
||||
import TelegramCore
|
||||
import AccountContext
|
||||
import MediaEditor
|
||||
import DrawingUI
|
||||
|
||||
extension MediaEditorScreen {
|
||||
func isEligibleForDraft() -> Bool {
|
||||
if self.isEditingStory {
|
||||
return false
|
||||
}
|
||||
guard let mediaEditor = self.node.mediaEditor else {
|
||||
return false
|
||||
}
|
||||
let entities = self.node.entitiesView.entities.filter { !($0 is DrawingMediaEntity) }
|
||||
let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.node.entitiesView)
|
||||
mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities)
|
||||
|
||||
let caption = self.getCaption()
|
||||
|
||||
if let subject = self.node.subject, case .asset = subject, self.node.mediaEditor?.values.hasChanges == false && caption.string.isEmpty {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func saveDraft(id: Int64?) {
|
||||
guard let subject = self.node.subject, let mediaEditor = self.node.mediaEditor else {
|
||||
return
|
||||
}
|
||||
try? FileManager.default.createDirectory(atPath: draftPath(engine: self.context.engine), withIntermediateDirectories: true)
|
||||
|
||||
let values = mediaEditor.values
|
||||
let privacy = self.state.privacy
|
||||
let caption = self.getCaption()
|
||||
let duration = mediaEditor.duration ?? 0.0
|
||||
|
||||
let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
|
||||
var timestamp: Int32
|
||||
var location: CLLocationCoordinate2D?
|
||||
let expiresOn: Int32
|
||||
if case let .draft(draft, _) = subject {
|
||||
timestamp = draft.timestamp
|
||||
location = draft.location
|
||||
if let _ = id {
|
||||
expiresOn = draft.expiresOn ?? currentTimestamp + 3600 * 24 * 7
|
||||
} else {
|
||||
expiresOn = currentTimestamp + 3600 * 24 * 7
|
||||
}
|
||||
} else {
|
||||
timestamp = currentTimestamp
|
||||
if case let .asset(asset) = subject {
|
||||
location = asset.location?.coordinate
|
||||
}
|
||||
if let _ = id {
|
||||
expiresOn = currentTimestamp + Int32(self.state.privacy.timeout)
|
||||
} else {
|
||||
expiresOn = currentTimestamp + 3600 * 24 * 7
|
||||
}
|
||||
}
|
||||
|
||||
if let resultImage = mediaEditor.resultImage {
|
||||
mediaEditor.seek(0.0, andPlay: false)
|
||||
makeEditorImageComposition(context: self.node.ciContext, postbox: self.context.account.postbox, inputImage: resultImage, dimensions: storyDimensions, values: values, time: .zero, textScale: 2.0, completion: { resultImage in
|
||||
guard let resultImage else {
|
||||
return
|
||||
}
|
||||
let fittedSize = resultImage.size.aspectFitted(CGSize(width: 128.0, height: 128.0))
|
||||
|
||||
let context = self.context
|
||||
let saveImageDraft: (UIImage, PixelDimensions) -> Void = { image, dimensions in
|
||||
if let thumbnailImage = generateScaledImage(image: resultImage, size: fittedSize) {
|
||||
let path = "\(Int64.random(in: .min ... .max)).jpg"
|
||||
if let data = image.jpegData(compressionQuality: 0.87) {
|
||||
let draft = MediaEditorDraft(path: path, isVideo: false, thumbnail: thumbnailImage, dimensions: dimensions, duration: nil, values: values, caption: caption, privacy: privacy, timestamp: timestamp, location: location, expiresOn: expiresOn)
|
||||
try? data.write(to: URL(fileURLWithPath: draft.fullPath(engine: context.engine)))
|
||||
if let id {
|
||||
saveStorySource(engine: context.engine, item: draft, peerId: context.account.peerId, id: id)
|
||||
} else {
|
||||
addStoryDraft(engine: context.engine, item: draft)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let saveVideoDraft: (String, PixelDimensions, Double) -> Void = { videoPath, dimensions, duration in
|
||||
if let thumbnailImage = generateScaledImage(image: resultImage, size: fittedSize) {
|
||||
let path = "\(Int64.random(in: .min ... .max)).mp4"
|
||||
let draft = MediaEditorDraft(path: path, isVideo: true, thumbnail: thumbnailImage, dimensions: dimensions, duration: duration, values: values, caption: caption, privacy: privacy, timestamp: timestamp, location: location, expiresOn: expiresOn)
|
||||
try? FileManager.default.copyItem(atPath: videoPath, toPath: draft.fullPath(engine: context.engine))
|
||||
if let id {
|
||||
saveStorySource(engine: context.engine, item: draft, peerId: context.account.peerId, id: id)
|
||||
} else {
|
||||
addStoryDraft(engine: context.engine, item: draft)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch subject {
|
||||
case let .image(image, dimensions, _, _):
|
||||
saveImageDraft(image, dimensions)
|
||||
case let .video(path, _, _, _, _, dimensions, _, _, _):
|
||||
saveVideoDraft(path, dimensions, duration)
|
||||
case let .asset(asset):
|
||||
if asset.mediaType == .video {
|
||||
PHImageManager.default().requestAVAsset(forVideo: asset, options: nil) { avAsset, _, _ in
|
||||
if let urlAsset = avAsset as? AVURLAsset {
|
||||
saveVideoDraft(urlAsset.url.relativePath, PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight)), duration)
|
||||
}
|
||||
}
|
||||
} 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 {
|
||||
saveVideoDraft(draft.fullPath(engine: context.engine), draft.dimensions, draft.duration ?? 0.0)
|
||||
} else if let image = UIImage(contentsOfFile: draft.fullPath(engine: context.engine)) {
|
||||
saveImageDraft(image, draft.dimensions)
|
||||
}
|
||||
removeStoryDraft(engine: self.context.engine, path: draft.path, delete: false)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import MediaEditor
|
||||
import DrawingUI
|
||||
import ChatPresentationInterfaceState
|
||||
import PresentationDataUtils
|
||||
import TelegramPresentationData
|
||||
|
||||
extension MediaEditorScreen {
|
||||
final class Recording {
|
||||
private weak var controller: MediaEditorScreen?
|
||||
|
||||
private var recorder: EntityVideoRecorder?
|
||||
|
||||
init(controller: MediaEditorScreen) {
|
||||
self.controller = controller
|
||||
}
|
||||
|
||||
func setMediaRecordingActive(_ isActive: Bool, finished: Bool) {
|
||||
guard let controller, let mediaEditor = controller.node.mediaEditor else {
|
||||
return
|
||||
}
|
||||
let entitiesView = controller.node.entitiesView
|
||||
if mediaEditor.values.additionalVideoPath != nil {
|
||||
let presentationData = controller.context.sharedContext.currentPresentationData.with { $0 }
|
||||
let alertController = textAlertController(
|
||||
context: controller.context,
|
||||
forceTheme: defaultDarkColorPresentationTheme,
|
||||
title: nil,
|
||||
text: "Are you sure you want to delete video message?",
|
||||
actions: [
|
||||
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
|
||||
}),
|
||||
TextAlertAction(type: .destructiveAction, title: presentationData.strings.Common_Delete, action: { [weak mediaEditor, weak entitiesView] in
|
||||
mediaEditor?.setAdditionalVideo(nil, positionChanges: [])
|
||||
if let entityView = entitiesView?.getView(where: { entityView in
|
||||
if let entity = entityView.entity as? DrawingStickerEntity, entity.content == .dualVideoReference {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}) {
|
||||
entitiesView?.remove(uuid: entityView.entity.uuid, animated: false)
|
||||
}
|
||||
})
|
||||
]
|
||||
)
|
||||
controller.present(alertController, in: .window(.root))
|
||||
return
|
||||
}
|
||||
|
||||
if isActive {
|
||||
guard self.recorder == nil else {
|
||||
return
|
||||
}
|
||||
let recorder = EntityVideoRecorder(mediaEditor: mediaEditor, entitiesView: controller.node.entitiesView)
|
||||
recorder.setup(
|
||||
referenceDrawingSize: storyDimensions,
|
||||
scale: 1.625,
|
||||
position: PIPPosition.topRight.getPosition(storyDimensions)
|
||||
)
|
||||
recorder.onAutomaticStop = { [weak self] in
|
||||
if let self {
|
||||
self.recorder = nil
|
||||
self.controller?.node.requestLayout(forceUpdate: true, transition: .easeInOut(duration: 0.2))
|
||||
}
|
||||
}
|
||||
self.recorder = recorder
|
||||
controller.node.requestLayout(forceUpdate: true, transition: .easeInOut(duration: 0.2))
|
||||
} else if let recorder = self.recorder {
|
||||
recorder.stopRecording(save: finished, completion: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.recorder = nil
|
||||
self.controller?.node.requestLayout(forceUpdate: true, transition: .easeInOut(duration: 0.2))
|
||||
})
|
||||
|
||||
controller.node.requestLayout(forceUpdate: true, transition: .easeInOut(duration: 0.2))
|
||||
}
|
||||
}
|
||||
|
||||
func togglePosition() {
|
||||
if let recorder = self.recorder {
|
||||
recorder.togglePosition()
|
||||
}
|
||||
}
|
||||
|
||||
var status: InstantVideoControllerRecordingStatus? {
|
||||
if let recorder = self.recorder {
|
||||
return InstantVideoControllerRecordingStatus(
|
||||
micLevel: recorder.micLevel,
|
||||
duration: recorder.duration
|
||||
)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -946,8 +946,6 @@ private final class MediaToolsScreenComponent: Component {
|
|||
}
|
||||
}
|
||||
|
||||
private let storyDimensions = CGSize(width: 1080.0, height: 1920.0)
|
||||
|
||||
public final class MediaToolsScreen: ViewController {
|
||||
fileprivate final class Node: ViewControllerTracingNode, UIGestureRecognizerDelegate {
|
||||
private weak var controller: MediaToolsScreen?
|
||||
|
|
|
|||
|
|
@ -281,6 +281,7 @@ final class StoryPreviewComponent: Component {
|
|||
audioRecorder: nil,
|
||||
videoRecordingStatus: nil,
|
||||
isRecordingLocked: false,
|
||||
hasRecordedVideo: false,
|
||||
recordedAudioPreview: nil,
|
||||
hasRecordedVideoPreview: false,
|
||||
wasRecordingDismissed: false,
|
||||
|
|
@ -293,6 +294,7 @@ final class StoryPreviewComponent: Component {
|
|||
customInputView: nil,
|
||||
forceIsEditing: false,
|
||||
disabledPlaceholder: nil,
|
||||
header: nil,
|
||||
isChannel: false,
|
||||
storyItem: nil,
|
||||
chatLocation: nil
|
||||
|
|
|
|||
|
|
@ -23,6 +23,10 @@ private extension MessageInputActionButtonComponent.Mode {
|
|||
return "Chat/Input/Text/IconForwardSend"
|
||||
case .like:
|
||||
return "Stories/InputLikeOff"
|
||||
case .removeVideoInput:
|
||||
return "Media Editor/RemoveRecordedVideo"
|
||||
case .repost:
|
||||
return "Stories/InputRepost"
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
|
@ -36,12 +40,14 @@ public final class MessageInputActionButtonComponent: Component {
|
|||
case apply
|
||||
case voiceInput
|
||||
case videoInput
|
||||
case removeVideoInput
|
||||
case unavailableVoiceInput
|
||||
case delete
|
||||
case attach
|
||||
case forward
|
||||
case more
|
||||
case like(reaction: MessageReaction.Reaction?, file: TelegramMediaFile?, animationFileId: Int64?)
|
||||
case repost
|
||||
}
|
||||
|
||||
public enum Action {
|
||||
|
|
@ -333,7 +339,7 @@ public final class MessageInputActionButtonComponent: Component {
|
|||
switch component.mode {
|
||||
case .none:
|
||||
break
|
||||
case .send, .apply, .attach, .delete, .forward:
|
||||
case .send, .apply, .attach, .delete, .forward, .removeVideoInput, .repost:
|
||||
sendAlpha = 1.0
|
||||
case let .like(reaction, _, _):
|
||||
if reaction != nil {
|
||||
|
|
@ -548,9 +554,9 @@ public final class MessageInputActionButtonComponent: Component {
|
|||
|
||||
if previousComponent?.mode != component.mode {
|
||||
switch component.mode {
|
||||
case .none, .send, .apply, .voiceInput, .attach, .delete, .forward, .unavailableVoiceInput, .more, .like:
|
||||
case .none, .send, .apply, .voiceInput, .attach, .delete, .forward, .unavailableVoiceInput, .more, .like, .repost:
|
||||
micButton.updateMode(mode: .audio, animated: !transition.animation.isImmediate)
|
||||
case .videoInput:
|
||||
case .videoInput, .removeVideoInput:
|
||||
micButton.updateMode(mode: .video, animated: !transition.animation.isImmediate)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ swift_library(
|
|||
"//submodules/SearchPeerMembers",
|
||||
"//submodules/ContextUI",
|
||||
"//submodules/TelegramUI/Components/ContextReferenceButtonComponent",
|
||||
"//submodules/TelegramUI/Components/Stories/ForwardInfoPanelComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import AnimatedTextComponent
|
|||
import AnimatedCountLabelNode
|
||||
import MessageInputActionButtonComponent
|
||||
import ContextReferenceButtonComponent
|
||||
import ForwardInfoPanelComponent
|
||||
|
||||
private var sharedIsReduceTransparencyEnabled = UIAccessibility.isReduceTransparencyEnabled
|
||||
|
||||
|
|
@ -135,6 +136,7 @@ public final class MessageInputPanelComponent: Component {
|
|||
public let audioRecorder: ManagedAudioRecorder?
|
||||
public let videoRecordingStatus: InstantVideoControllerRecordingStatus?
|
||||
public let isRecordingLocked: Bool
|
||||
public let hasRecordedVideo: Bool
|
||||
public let recordedAudioPreview: ChatRecordedMediaPreview?
|
||||
public let hasRecordedVideoPreview: Bool
|
||||
public let wasRecordingDismissed: Bool
|
||||
|
|
@ -147,6 +149,7 @@ public final class MessageInputPanelComponent: Component {
|
|||
public let customInputView: UIView?
|
||||
public let forceIsEditing: Bool
|
||||
public let disabledPlaceholder: String?
|
||||
public let header: AnyComponent<Empty>?
|
||||
public let isChannel: Bool
|
||||
public let storyItem: EngineStoryItem?
|
||||
public let chatLocation: ChatLocation?
|
||||
|
|
@ -188,6 +191,7 @@ public final class MessageInputPanelComponent: Component {
|
|||
audioRecorder: ManagedAudioRecorder?,
|
||||
videoRecordingStatus: InstantVideoControllerRecordingStatus?,
|
||||
isRecordingLocked: Bool,
|
||||
hasRecordedVideo: Bool,
|
||||
recordedAudioPreview: ChatRecordedMediaPreview?,
|
||||
hasRecordedVideoPreview: Bool,
|
||||
wasRecordingDismissed: Bool,
|
||||
|
|
@ -200,6 +204,7 @@ public final class MessageInputPanelComponent: Component {
|
|||
customInputView: UIView?,
|
||||
forceIsEditing: Bool,
|
||||
disabledPlaceholder: String?,
|
||||
header: AnyComponent<Empty>?,
|
||||
isChannel: Bool,
|
||||
storyItem: EngineStoryItem?,
|
||||
chatLocation: ChatLocation?
|
||||
|
|
@ -240,6 +245,7 @@ public final class MessageInputPanelComponent: Component {
|
|||
self.audioRecorder = audioRecorder
|
||||
self.videoRecordingStatus = videoRecordingStatus
|
||||
self.isRecordingLocked = isRecordingLocked
|
||||
self.hasRecordedVideo = hasRecordedVideo
|
||||
self.wasRecordingDismissed = wasRecordingDismissed
|
||||
self.recordedAudioPreview = recordedAudioPreview
|
||||
self.hasRecordedVideoPreview = hasRecordedVideoPreview
|
||||
|
|
@ -252,6 +258,7 @@ public final class MessageInputPanelComponent: Component {
|
|||
self.customInputView = customInputView
|
||||
self.forceIsEditing = forceIsEditing
|
||||
self.disabledPlaceholder = disabledPlaceholder
|
||||
self.header = header
|
||||
self.isChannel = isChannel
|
||||
self.storyItem = storyItem
|
||||
self.chatLocation = chatLocation
|
||||
|
|
@ -300,6 +307,9 @@ public final class MessageInputPanelComponent: Component {
|
|||
if lhs.isRecordingLocked != rhs.isRecordingLocked {
|
||||
return false
|
||||
}
|
||||
if lhs.hasRecordedVideo != rhs.hasRecordedVideo {
|
||||
return false
|
||||
}
|
||||
if lhs.wasRecordingDismissed != rhs.wasRecordingDismissed {
|
||||
return false
|
||||
}
|
||||
|
|
@ -342,6 +352,9 @@ public final class MessageInputPanelComponent: Component {
|
|||
if lhs.disabledPlaceholder != rhs.disabledPlaceholder {
|
||||
return false
|
||||
}
|
||||
if lhs.header != rhs.header {
|
||||
return false
|
||||
}
|
||||
if (lhs.attachmentAction == nil) != (rhs.attachmentAction == nil) {
|
||||
return false
|
||||
}
|
||||
|
|
@ -380,6 +393,7 @@ public final class MessageInputPanelComponent: Component {
|
|||
private let vibrancyPlaceholder = ComponentView<Empty>()
|
||||
|
||||
private let counter = ComponentView<Empty>()
|
||||
private var header: ComponentView<Empty>?
|
||||
|
||||
private var disabledPlaceholder: ComponentView<Empty>?
|
||||
private var textClippingView = UIView()
|
||||
|
|
@ -436,12 +450,7 @@ public final class MessageInputPanelComponent: Component {
|
|||
override init(frame: CGRect) {
|
||||
self.fieldBackgroundView = BlurredBackgroundView(color: UIColor(white: 0.0, alpha: 0.5), enableBlur: true)
|
||||
|
||||
let style: UIBlurEffect.Style = .dark
|
||||
let blurEffect = UIBlurEffect(style: style)
|
||||
let vibrancyEffect = UIVibrancyEffect(blurEffect: blurEffect)
|
||||
let vibrancyEffectView = UIVisualEffectView(effect: vibrancyEffect)
|
||||
vibrancyEffectView.alpha = 0.0
|
||||
self.vibrancyEffectView = vibrancyEffectView
|
||||
self.vibrancyEffectView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: UIBlurEffect(style: .dark)))
|
||||
|
||||
self.mediaRecordingVibrancyContainer = UIView()
|
||||
self.vibrancyEffectView.contentView.addSubview(self.mediaRecordingVibrancyContainer)
|
||||
|
|
@ -786,11 +795,67 @@ public final class MessageInputPanelComponent: Component {
|
|||
insets.right = insets.left
|
||||
}
|
||||
|
||||
let fieldFrame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: CGSize(width: availableSize.width - insets.left - insets.right, height: textFieldSize.height))
|
||||
var headerHeight: CGFloat = 0.0
|
||||
if let headerComponent = component.header, !hasMediaRecording && !hasMediaEditing {
|
||||
let headerInset: CGFloat = 10.0
|
||||
let header: ComponentView<Empty>
|
||||
var headerTransition = transition
|
||||
if let current = self.header {
|
||||
header = current
|
||||
} else {
|
||||
headerTransition = .immediate
|
||||
header = ComponentView()
|
||||
self.header = header
|
||||
}
|
||||
let headerSize = header.update(
|
||||
transition: .immediate,
|
||||
component: headerComponent,
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - insets.left - insets.right - headerInset * 2.0, height: 100.0)
|
||||
)
|
||||
let headerFrame = CGRect(origin: CGPoint(x: insets.left + headerInset, y: insets.top + headerInset), size: headerSize)
|
||||
if let headerView = header.view {
|
||||
if let headerView = headerView as? ForwardInfoPanelComponent.View {
|
||||
if headerView.superview == nil {
|
||||
self.addSubview(headerView)
|
||||
self.vibrancyEffectView.contentView.addSubview(headerView.backgroundView)
|
||||
|
||||
headerView.backgroundView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
}
|
||||
headerView.backgroundView.frame = headerFrame.offsetBy(dx: -9.0, dy: -14.0)
|
||||
} else {
|
||||
if headerView.superview == nil {
|
||||
self.addSubview(headerView)
|
||||
}
|
||||
}
|
||||
headerTransition.setPosition(view: headerView, position: headerFrame.center)
|
||||
headerView.bounds = CGRect(origin: CGPoint(), size: headerFrame.size)
|
||||
}
|
||||
headerHeight = headerFrame.height + headerInset
|
||||
} else {
|
||||
if let header = self.header {
|
||||
self.header = nil
|
||||
if let headerView = header.view as? ForwardInfoPanelComponent.View {
|
||||
headerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
|
||||
headerView.removeFromSuperview()
|
||||
})
|
||||
headerView.backgroundView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
|
||||
headerView.backgroundView.removeFromSuperview()
|
||||
})
|
||||
} else {
|
||||
header.view?.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var fieldFrame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: CGSize(width: availableSize.width - insets.left - insets.right, height: textFieldSize.height))
|
||||
if hasMediaRecording || hasMediaEditing {
|
||||
fieldFrame.size.height = baseFieldHeight
|
||||
}
|
||||
|
||||
var fieldBackgroundFrame: CGRect
|
||||
if hasMediaRecording {
|
||||
fieldBackgroundFrame = CGRect(origin: CGPoint(x: mediaInsets.left, y: insets.top), size: CGSize(width: availableSize.width - mediaInsets.left - mediaInsets.right, height: textFieldSize.height))
|
||||
fieldBackgroundFrame = CGRect(origin: CGPoint(x: mediaInsets.left, y: insets.top), size: CGSize(width: availableSize.width - mediaInsets.left - mediaInsets.right, height: fieldFrame.height))
|
||||
} else if isEditing || component.style == .editor || component.style == .media {
|
||||
fieldBackgroundFrame = fieldFrame
|
||||
} else {
|
||||
|
|
@ -802,17 +867,17 @@ public final class MessageInputPanelComponent: Component {
|
|||
fieldBackgroundFrame = CGRect(origin: CGPoint(x: mediaInsets.left, y: insets.top), size: CGSize(width: availableSize.width - mediaInsets.left - 50.0, height: textFieldSize.height))
|
||||
}
|
||||
}
|
||||
|
||||
let rawFieldBackgroundFrame = fieldBackgroundFrame
|
||||
fieldBackgroundFrame.size.height += headerHeight
|
||||
|
||||
transition.setFrame(view: self.vibrancyEffectView, frame: CGRect(origin: CGPoint(), size: fieldBackgroundFrame.size))
|
||||
self.vibrancyEffectView.isHidden = component.style == .media
|
||||
if isEditing {
|
||||
self.vibrancyEffectView.alpha = 1.0
|
||||
}
|
||||
|
||||
transition.setFrame(view: self.fieldBackgroundView, frame: fieldBackgroundFrame)
|
||||
self.fieldBackgroundView.update(size: fieldBackgroundFrame.size, cornerRadius: baseFieldHeight * 0.5, transition: transition.containedViewLayoutTransition)
|
||||
self.fieldBackgroundView.update(size: fieldBackgroundFrame.size, cornerRadius: headerHeight > 0.0 ? 18.0 : baseFieldHeight * 0.5, transition: transition.containedViewLayoutTransition)
|
||||
|
||||
var textClippingFrame = fieldBackgroundFrame
|
||||
var textClippingFrame = rawFieldBackgroundFrame.offsetBy(dx: 0.0, dy: headerHeight)
|
||||
if component.style == .media, !isEditing {
|
||||
textClippingFrame.size.height -= 10.0
|
||||
}
|
||||
|
|
@ -828,13 +893,15 @@ public final class MessageInputPanelComponent: Component {
|
|||
if isEditing || component.style == .story {
|
||||
placeholderOriginX = 16.0
|
||||
} else {
|
||||
placeholderOriginX = floorToScreenPixels((availableSize.width - placeholderSize.width) / 2.0)
|
||||
placeholderOriginX = floorToScreenPixels(fieldBackgroundFrame.minX + (fieldBackgroundFrame.width - placeholderSize.width) / 2.0)
|
||||
}
|
||||
let placeholderFrame = CGRect(origin: CGPoint(x: placeholderOriginX, y: floor((fieldBackgroundFrame.height - placeholderSize.height) * 0.5)), size: placeholderSize)
|
||||
let placeholderFrame = CGRect(origin: CGPoint(x: placeholderOriginX, y: headerHeight + floor((rawFieldBackgroundFrame.height - placeholderSize.height) * 0.5)), size: placeholderSize)
|
||||
if let placeholderView = self.placeholder.view, let vibrancyPlaceholderView = self.vibrancyPlaceholder.view {
|
||||
if vibrancyPlaceholderView.superview == nil {
|
||||
vibrancyPlaceholderView.layer.anchorPoint = CGPoint()
|
||||
self.vibrancyEffectView.contentView.addSubview(vibrancyPlaceholderView)
|
||||
|
||||
vibrancyPlaceholderView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
}
|
||||
transition.setPosition(view: vibrancyPlaceholderView, position: placeholderFrame.origin)
|
||||
vibrancyPlaceholderView.bounds = CGRect(origin: CGPoint(), size: placeholderFrame.size)
|
||||
|
|
@ -854,7 +921,10 @@ public final class MessageInputPanelComponent: Component {
|
|||
let fieldAlpha = sharedIsReduceTransparencyEnabled ? 0.09 : 1.0
|
||||
transition.setAlpha(view: self.fieldBackgroundView, alpha: (component.disabledPlaceholder != nil || component.isChannel) ? 0.0 : fieldAlpha)
|
||||
|
||||
let size = CGSize(width: availableSize.width, height: textFieldSize.height + insets.top + insets.bottom)
|
||||
var size = CGSize(width: availableSize.width, height: textFieldSize.height + insets.top + insets.bottom + headerHeight)
|
||||
if hasMediaRecording || hasMediaEditing {
|
||||
size.height = baseFieldHeight + insets.top + insets.bottom
|
||||
}
|
||||
|
||||
var rightButtonsOffsetX: CGFloat = 0.0
|
||||
if component.isChannel, let storyItem = component.storyItem {
|
||||
|
|
@ -1157,7 +1227,11 @@ public final class MessageInputPanelComponent: Component {
|
|||
|
||||
let inputActionButtonMode: MessageInputActionButtonComponent.Mode
|
||||
if case .editor = component.style {
|
||||
inputActionButtonMode = isEditing ? .apply : .none
|
||||
if isEditing {
|
||||
inputActionButtonMode = .apply
|
||||
} else {
|
||||
inputActionButtonMode = component.hasRecordedVideo ? .removeVideoInput : .videoInput
|
||||
}
|
||||
} else if case .media = component.style {
|
||||
inputActionButtonMode = isEditing ? .apply : .none
|
||||
} else {
|
||||
|
|
@ -1217,6 +1291,10 @@ public final class MessageInputPanelComponent: Component {
|
|||
}
|
||||
case .voiceInput, .videoInput:
|
||||
component.setMediaRecordingActive?(action == .down, mode == .videoInput, sendAction)
|
||||
case .removeVideoInput:
|
||||
if case .up = action {
|
||||
component.setMediaRecordingActive?(true, true, false)
|
||||
}
|
||||
case .forward:
|
||||
if case .up = action {
|
||||
component.forwardAction?()
|
||||
|
|
@ -1463,7 +1541,7 @@ public final class MessageInputPanelComponent: Component {
|
|||
if stickerButtonView.superview == nil {
|
||||
self.addSubview(stickerButtonView)
|
||||
}
|
||||
let stickerIconFrame = CGRect(origin: CGPoint(x: fieldIconNextX - stickerButtonSize.width, y: fieldFrame.maxY - 4.0 - stickerButtonSize.height), size: stickerButtonSize)
|
||||
let stickerIconFrame = CGRect(origin: CGPoint(x: fieldIconNextX - stickerButtonSize.width, y: fieldBackgroundFrame.maxY - 4.0 - stickerButtonSize.height), size: stickerButtonSize)
|
||||
transition.setPosition(view: stickerButtonView, position: stickerIconFrame.center)
|
||||
transition.setBounds(view: stickerButtonView, bounds: CGRect(origin: CGPoint(), size: stickerIconFrame.size))
|
||||
|
||||
|
|
@ -1508,12 +1586,13 @@ public final class MessageInputPanelComponent: Component {
|
|||
self.addSubview(timeoutButtonView)
|
||||
}
|
||||
let originX = fieldBackgroundFrame.maxX - 4.0
|
||||
let timeoutIconFrame = CGRect(origin: CGPoint(x: originX - timeoutButtonSize.width, y: fieldFrame.maxY - 4.0 - timeoutButtonSize.height), size: timeoutButtonSize)
|
||||
let timeoutIconFrame = CGRect(origin: CGPoint(x: originX - timeoutButtonSize.width, y: fieldBackgroundFrame.maxY - 4.0 - timeoutButtonSize.height), size: timeoutButtonSize)
|
||||
transition.setPosition(view: timeoutButtonView, position: timeoutIconFrame.center)
|
||||
transition.setBounds(view: timeoutButtonView, bounds: CGRect(origin: CGPoint(), size: timeoutIconFrame.size))
|
||||
|
||||
transition.setAlpha(view: timeoutButtonView, alpha: isEditing ? 0.0 : 1.0)
|
||||
transition.setScale(view: timeoutButtonView, scale: isEditing ? 0.1 : 1.0)
|
||||
let visible = !hasMediaRecording && !hasMediaEditing && !isEditing
|
||||
transition.setAlpha(view: timeoutButtonView, alpha: visible ? 1.0 : 0.0)
|
||||
transition.setScale(view: timeoutButtonView, scale: visible ? 1.0 : 0.1)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ final class PeerInfoGroupsInCommonPaneNode: ASDisplayNode, PeerInfoPaneNode {
|
|||
private let context: AccountContext
|
||||
private let peerId: PeerId
|
||||
private let chatControllerInteraction: ChatControllerInteraction
|
||||
private let openPeerContextAction: (Peer, ASDisplayNode, ContextGesture?) -> Void
|
||||
private let openPeerContextAction: (Bool, Peer, ASDisplayNode, ContextGesture?) -> Void
|
||||
private let groupsInCommonContext: GroupsInCommonContext
|
||||
|
||||
weak var parentController: ViewController?
|
||||
|
|
@ -99,7 +99,7 @@ final class PeerInfoGroupsInCommonPaneNode: ASDisplayNode, PeerInfoPaneNode {
|
|||
|
||||
private var disposable: Disposable?
|
||||
|
||||
init(context: AccountContext, peerId: PeerId, chatControllerInteraction: ChatControllerInteraction, openPeerContextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void, groupsInCommonContext: GroupsInCommonContext) {
|
||||
init(context: AccountContext, peerId: PeerId, chatControllerInteraction: ChatControllerInteraction, openPeerContextAction: @escaping (Bool, Peer, ASDisplayNode, ContextGesture?) -> Void, groupsInCommonContext: GroupsInCommonContext) {
|
||||
self.context = context
|
||||
self.peerId = peerId
|
||||
self.chatControllerInteraction = chatControllerInteraction
|
||||
|
|
@ -190,7 +190,7 @@ final class PeerInfoGroupsInCommonPaneNode: ASDisplayNode, PeerInfoPaneNode {
|
|||
let transaction = preparedTransition(from: self.currentEntries, to: entries, context: self.context, presentationData: presentationData, openPeer: { [weak self] peer in
|
||||
self?.chatControllerInteraction.openPeer(EnginePeer(peer), .default, nil, .default)
|
||||
}, openPeerContextAction: { [weak self] peer, node, gesture in
|
||||
self?.openPeerContextAction(peer, node, gesture)
|
||||
self?.openPeerContextAction(false, peer, node, gesture)
|
||||
})
|
||||
self.currentEntries = entries
|
||||
self.enqueuedTransactions.append(transaction)
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ private enum RecommendedChannelsListEntry: Comparable, Identifiable {
|
|||
}
|
||||
}
|
||||
|
||||
func item(context: AccountContext, presentationData: PresentationData, action: @escaping (EnginePeer) -> Void) -> ListViewItem {
|
||||
func item(context: AccountContext, presentationData: PresentationData, action: @escaping (EnginePeer) -> Void, openPeerContextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void) -> ListViewItem {
|
||||
switch self {
|
||||
case let .peer(_, _, peer, subscribers):
|
||||
let subtitle = presentationData.strings.Conversation_StatusSubscribers(subscribers)
|
||||
|
|
@ -66,17 +66,19 @@ private enum RecommendedChannelsListEntry: Comparable, Identifiable {
|
|||
action(peer)
|
||||
}, setPeerIdWithRevealedOptions: { _, _ in
|
||||
}, removePeer: { _ in
|
||||
}, contextAction: nil, hasTopStripe: false, noInsets: true, noCorners: true, disableInteractiveTransitionIfNecessary: true)
|
||||
}, contextAction: { node, gesture in
|
||||
openPeerContextAction(peer._asPeer(), node, gesture)
|
||||
}, hasTopStripe: false, noInsets: true, noCorners: true, disableInteractiveTransitionIfNecessary: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func preparedTransition(from fromEntries: [RecommendedChannelsListEntry], to toEntries: [RecommendedChannelsListEntry], context: AccountContext, presentationData: PresentationData, action: @escaping (EnginePeer) -> Void) -> RecommendedChannelsListTransaction {
|
||||
private func preparedTransition(from fromEntries: [RecommendedChannelsListEntry], to toEntries: [RecommendedChannelsListEntry], context: AccountContext, presentationData: PresentationData, action: @escaping (EnginePeer) -> Void, openPeerContextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void) -> RecommendedChannelsListTransaction {
|
||||
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
|
||||
|
||||
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
|
||||
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, action: action), directionHint: nil) }
|
||||
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, action: action), directionHint: nil) }
|
||||
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, action: action, openPeerContextAction: openPeerContextAction), directionHint: nil) }
|
||||
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, action: action, openPeerContextAction: openPeerContextAction), directionHint: nil) }
|
||||
|
||||
return RecommendedChannelsListTransaction(deletions: deletions, insertions: insertions, updates: updates, animated: toEntries.count < fromEntries.count)
|
||||
}
|
||||
|
|
@ -84,6 +86,7 @@ private func preparedTransition(from fromEntries: [RecommendedChannelsListEntry]
|
|||
final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode {
|
||||
private let context: AccountContext
|
||||
private let chatControllerInteraction: ChatControllerInteraction
|
||||
private let openPeerContextAction: (Bool, Peer, ASDisplayNode, ContextGesture?) -> Void
|
||||
|
||||
weak var parentController: ViewController?
|
||||
|
||||
|
|
@ -113,9 +116,10 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode
|
|||
|
||||
private var disposable: Disposable?
|
||||
|
||||
init(context: AccountContext, peerId: PeerId, chatControllerInteraction: ChatControllerInteraction) {
|
||||
init(context: AccountContext, peerId: PeerId, chatControllerInteraction: ChatControllerInteraction, openPeerContextAction: @escaping (Bool, Peer, ASDisplayNode, ContextGesture?) -> Void) {
|
||||
self.context = context
|
||||
self.chatControllerInteraction = chatControllerInteraction
|
||||
self.openPeerContextAction = openPeerContextAction
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
self.listNode = ListView()
|
||||
|
|
@ -194,6 +198,8 @@ final class PeerInfoRecommendedChannelsPaneNode: ASDisplayNode, PeerInfoPaneNode
|
|||
|
||||
let transaction = preparedTransition(from: self.currentEntries, to: entries, context: self.context, presentationData: presentationData, action: { [weak self] peer in
|
||||
self?.chatControllerInteraction.openPeer(peer, .default, nil, .default)
|
||||
}, openPeerContextAction: { [weak self] peer, node, gesture in
|
||||
self?.openPeerContextAction(true, peer, node, gesture)
|
||||
})
|
||||
self.currentEntries = entries
|
||||
self.enqueuedTransactions.append(transaction)
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue