2023-05-19 18:10:12 +04:00

1723 lines
80 KiB
Swift

import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ComponentFlow
import SwiftSignalKit
import ViewControllerComponent
import ComponentDisplayAdapters
import TelegramPresentationData
import AccountContext
import TelegramCore
import MultilineTextComponent
import DrawingUI
import MediaEditor
import Photos
import LottieAnimationComponent
import MessageInputPanelComponent
import EntityKeyboard
import TooltipUI
import BlurredBackgroundComponent
import AvatarNode
import ShareWithPeersScreen
enum DrawingScreenType {
case drawing
case text
case sticker
}
private let privacyButtonTag = GenericComponentViewTag()
private let muteButtonTag = GenericComponentViewTag()
private let saveButtonTag = GenericComponentViewTag()
final class MediaEditorScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let mediaEditor: MediaEditor?
let privacy: EngineStoryPrivacy
let openDrawing: (DrawingScreenType) -> Void
let openTools: () -> Void
init(
context: AccountContext,
mediaEditor: MediaEditor?,
privacy: EngineStoryPrivacy,
openDrawing: @escaping (DrawingScreenType) -> Void,
openTools: @escaping () -> Void
) {
self.context = context
self.mediaEditor = mediaEditor
self.privacy = privacy
self.openDrawing = openDrawing
self.openTools = openTools
}
static func ==(lhs: MediaEditorScreenComponent, rhs: MediaEditorScreenComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.privacy != rhs.privacy {
return false
}
return true
}
final class State: ComponentState {
enum ImageKey: Hashable {
case draw
case text
case sticker
case tools
case done
}
private var cachedImages: [ImageKey: UIImage] = [:]
func image(_ key: ImageKey) -> UIImage {
if let image = self.cachedImages[key] {
return image
} else {
var image: UIImage
switch key {
case .draw:
image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Pencil"), color: .white)!
case .text:
image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/AddText"), color: .white)!
case .sticker:
image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/AddSticker"), color: .white)!
case .tools:
image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Tools"), color: .white)!
case .done:
let accentColor = self.context.sharedContext.currentPresentationData.with { $0 }.theme.chat.inputPanel.panelControlAccentColor
image = generateImage(CGSize(width: 33.0, height: 33.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(accentColor.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
context.setBlendMode(.copy)
context.setStrokeColor(UIColor.white.cgColor)
context.setLineWidth(2.0)
context.setLineCap(.round)
context.setLineJoin(.round)
context.translateBy(x: 5.45, y: 4.0)
context.saveGState()
context.translateBy(x: 4.0, y: 4.0)
let _ = try? drawSvgPath(context, path: "M1,7 L7,1 L13,7 S ")
context.restoreGState()
context.saveGState()
context.translateBy(x: 10.0, y: 4.0)
let _ = try? drawSvgPath(context, path: "M1,16 V1 S ")
context.restoreGState()
})!
}
cachedImages[key] = image
return image
}
}
let context: AccountContext
init(context: AccountContext) {
self.context = context
super.init()
}
deinit {
}
}
func makeState() -> State {
return State(
context: self.context
)
}
public final class View: UIView {
private let cancelButton = ComponentView<Empty>()
private let drawButton = ComponentView<Empty>()
private let textButton = ComponentView<Empty>()
private let stickerButton = ComponentView<Empty>()
private let toolsButton = ComponentView<Empty>()
private let doneButton = ComponentView<Empty>()
private let inputPanel = ComponentView<Empty>()
private let inputPanelExternalState = MessageInputPanelComponent.ExternalState()
private let scrubber = ComponentView<Empty>()
private let privacyButton = ComponentView<Empty>()
private let muteButton = ComponentView<Empty>()
private let saveButton = ComponentView<Empty>()
private var component: MediaEditorScreenComponent?
private weak var state: State?
private var environment: ViewControllerComponentContainer.Environment?
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = .clear
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func animateInFromCamera() {
if let view = self.cancelButton.view {
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
}
let buttons = [
self.drawButton,
self.textButton,
self.stickerButton,
self.toolsButton
]
var delay: Double = 0.0
for button in buttons {
if let view = button.view {
view.layer.animatePosition(from: CGPoint(x: 0.0, y: 64.0), to: .zero, duration: 0.3, delay: delay, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: delay)
view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2, delay: delay)
delay += 0.05
}
}
if let view = self.doneButton.view {
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
}
if let view = self.inputPanel.view {
view.layer.animatePosition(from: CGPoint(x: 0.0, y: 44.0), to: .zero, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
}
if let view = self.saveButton.view {
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
}
if let view = self.muteButton.view {
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
}
if let view = self.privacyButton.view {
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
}
}
func animateOutToCamera() {
let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut))
if let view = self.cancelButton.view {
transition.setAlpha(view: view, alpha: 0.0)
transition.setScale(view: view, scale: 0.1)
}
let buttons = [
self.drawButton,
self.textButton,
self.stickerButton,
self.toolsButton
]
for button in buttons {
if let view = button.view {
view.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: 64.0), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true)
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2)
}
}
if let view = self.doneButton.view {
transition.setAlpha(view: view, alpha: 0.0)
transition.setScale(view: view, scale: 0.1)
}
if let view = self.inputPanel.view {
view.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: 44.0), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true)
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2)
}
if let view = self.saveButton.view {
transition.setAlpha(view: view, alpha: 0.0)
transition.setScale(view: view, scale: 0.1)
}
if let view = self.muteButton.view {
transition.setAlpha(view: view, alpha: 0.0)
transition.setScale(view: view, scale: 0.1)
}
if let view = self.privacyButton.view {
transition.setAlpha(view: view, alpha: 0.0)
transition.setScale(view: view, scale: 0.1)
}
}
func animateOutToTool() {
let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut))
if let view = self.cancelButton.view {
view.alpha = 0.0
}
let buttons = [
self.drawButton,
self.textButton,
self.stickerButton,
self.toolsButton
]
for button in buttons {
if let view = button.view {
view.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: -44.0), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
transition.setAlpha(view: view, alpha: 0.0)
view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2)
}
}
if let view = self.doneButton.view {
transition.setAlpha(view: view, alpha: 0.0)
transition.setScale(view: view, scale: 0.1)
}
if let view = self.inputPanel.view {
transition.setAlpha(view: view, alpha: 0.0)
view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2)
}
if let view = self.saveButton.view {
transition.setAlpha(view: view, alpha: 0.0)
transition.setScale(view: view, scale: 0.1)
}
if let view = self.muteButton.view {
transition.setAlpha(view: view, alpha: 0.0)
transition.setScale(view: view, scale: 0.1)
}
if let view = self.privacyButton.view {
transition.setAlpha(view: view, alpha: 0.0)
transition.setScale(view: view, scale: 0.1)
}
}
func animateInFromTool() {
let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut))
if let view = self.cancelButton.view {
view.alpha = 1.0
}
let buttons = [
self.drawButton,
self.textButton,
self.stickerButton,
self.toolsButton
]
for button in buttons {
if let view = button.view {
view.layer.animatePosition(from: CGPoint(x: 0.0, y: -44.0), to: .zero, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
transition.setAlpha(view: view, alpha: 1.0)
view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
}
}
if let view = self.doneButton.view {
transition.setAlpha(view: view, alpha: 1.0)
transition.setScale(view: view, scale: 1.0)
}
if let view = self.inputPanel.view {
transition.setAlpha(view: view, alpha: 1.0)
view.layer.animateScale(from: 0.0, to: 1.0, duration: 0.2)
}
if let view = self.saveButton.view {
transition.setAlpha(view: view, alpha: 1.0)
transition.setScale(view: view, scale: 1.0)
}
if let view = self.muteButton.view {
transition.setAlpha(view: view, alpha: 1.0)
transition.setScale(view: view, scale: 1.0)
}
if let view = self.privacyButton.view {
transition.setAlpha(view: view, alpha: 1.0)
transition.setScale(view: view, scale: 1.0)
}
}
func update(component: MediaEditorScreenComponent, availableSize: CGSize, state: State, environment: Environment<ViewControllerComponentContainer.Environment>, transition: Transition) -> CGSize {
let environment = environment[ViewControllerComponentContainer.Environment.self].value
self.environment = environment
self.component = component
self.state = state
let openDrawing = component.openDrawing
let openTools = component.openTools
let buttonSideInset: CGFloat = 10.0
let buttonBottomInset: CGFloat = 8.0
let cancelButtonSize = self.cancelButton.update(
transition: transition,
component: AnyComponent(Button(
content: AnyComponent(
LottieAnimationComponent(
animation: LottieAnimationComponent.AnimationItem(
name: "media_backToCancel",
mode: .still(position: .begin),
range: nil
),
colors: ["__allcolors__": .white],
size: CGSize(width: 33.0, height: 33.0)
)
),
action: {
guard let controller = environment.controller() as? MediaEditorScreen else {
return
}
controller.requestDismiss(animated: true)
}
)),
environment: {},
containerSize: CGSize(width: 44.0, height: 44.0)
)
let cancelButtonFrame = CGRect(
origin: CGPoint(x: buttonSideInset, y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset),
size: cancelButtonSize
)
if let cancelButtonView = self.cancelButton.view {
if cancelButtonView.superview == nil {
self.addSubview(cancelButtonView)
}
transition.setPosition(view: cancelButtonView, position: cancelButtonFrame.center)
transition.setBounds(view: cancelButtonView, bounds: CGRect(origin: .zero, size: cancelButtonFrame.size))
}
let doneButtonSize = self.doneButton.update(
transition: transition,
component: AnyComponent(Button(
content: AnyComponent(Image(
image: state.image(.done),
size: CGSize(width: 33.0, height: 33.0)
)),
action: { [weak self] in
guard let self, let controller = environment.controller() as? MediaEditorScreen else {
return
}
guard let inputPanelView = self.inputPanel.view as? MessageInputPanelComponent.View else {
return
}
var inputText = NSAttributedString(string: "")
switch inputPanelView.getSendMessageInput() {
case let .text(text):
inputText = NSAttributedString(string: text)
}
controller.requestCompletion(caption: inputText, animated: true)
}
)),
environment: {},
containerSize: CGSize(width: 44.0, height: 44.0)
)
let doneButtonFrame = CGRect(
origin: CGPoint(x: availableSize.width - buttonSideInset - doneButtonSize.width, y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset),
size: doneButtonSize
)
if let doneButtonView = self.doneButton.view {
if doneButtonView.superview == nil {
self.addSubview(doneButtonView)
}
transition.setPosition(view: doneButtonView, position: doneButtonFrame.center)
transition.setBounds(view: doneButtonView, bounds: CGRect(origin: .zero, size: doneButtonFrame.size))
}
let drawButtonSize = self.drawButton.update(
transition: transition,
component: AnyComponent(Button(
content: AnyComponent(Image(
image: state.image(.draw),
size: CGSize(width: 30.0, height: 30.0)
)),
action: {
openDrawing(.drawing)
}
)),
environment: {},
containerSize: CGSize(width: 40.0, height: 40.0)
)
let drawButtonFrame = CGRect(
origin: CGPoint(x: floorToScreenPixels(availableSize.width / 4.0 - 3.0 - drawButtonSize.width / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset),
size: drawButtonSize
)
if let drawButtonView = self.drawButton.view {
if drawButtonView.superview == nil {
self.addSubview(drawButtonView)
}
transition.setFrame(view: drawButtonView, frame: drawButtonFrame)
}
let textButtonSize = self.textButton.update(
transition: transition,
component: AnyComponent(Button(
content: AnyComponent(Image(
image: state.image(.text),
size: CGSize(width: 30.0, height: 30.0)
)),
action: {
openDrawing(.text)
}
)),
environment: {},
containerSize: CGSize(width: 40.0, height: 40.0)
)
let textButtonFrame = CGRect(
origin: CGPoint(x: floorToScreenPixels(availableSize.width / 2.5 + 5.0 - textButtonSize.width / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset),
size: textButtonSize
)
if let textButtonView = self.textButton.view {
if textButtonView.superview == nil {
self.addSubview(textButtonView)
}
transition.setFrame(view: textButtonView, frame: textButtonFrame)
}
let stickerButtonSize = self.stickerButton.update(
transition: transition,
component: AnyComponent(Button(
content: AnyComponent(Image(
image: state.image(.sticker),
size: CGSize(width: 30.0, height: 30.0)
)),
action: {
openDrawing(.sticker)
}
)),
environment: {},
containerSize: CGSize(width: 40.0, height: 40.0)
)
let stickerButtonFrame = CGRect(
origin: CGPoint(x: floorToScreenPixels(availableSize.width - availableSize.width / 2.5 - 5.0 - stickerButtonSize.width / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset),
size: stickerButtonSize
)
if let stickerButtonView = self.stickerButton.view {
if stickerButtonView.superview == nil {
self.addSubview(stickerButtonView)
}
transition.setFrame(view: stickerButtonView, frame: stickerButtonFrame)
}
let toolsButtonSize = self.toolsButton.update(
transition: transition,
component: AnyComponent(Button(
content: AnyComponent(Image(
image: state.image(.tools),
size: CGSize(width: 30.0, height: 30.0)
)),
action: {
openTools()
}
)),
environment: {},
containerSize: CGSize(width: 40.0, height: 40.0)
)
let toolsButtonFrame = CGRect(
origin: CGPoint(x: floorToScreenPixels(availableSize.width / 4.0 * 3.0 + 3.0 - toolsButtonSize.width / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset),
size: toolsButtonSize
)
if let toolsButtonView = self.toolsButton.view {
if toolsButtonView.superview == nil {
self.addSubview(toolsButtonView)
}
transition.setFrame(view: toolsButtonView, frame: toolsButtonFrame)
}
var scrubberBottomInset: CGFloat = 0.0
if !"".isEmpty {
let scrubberInset: CGFloat = 9.0
let scrubberSize = self.scrubber.update(
transition: transition,
component: AnyComponent(VideoScrubberComponent(
context: component.context,
duration: 1.0,
startPosition: 0.0,
endPosition: 1.0
)),
environment: {},
containerSize: CGSize(width: availableSize.width - scrubberInset * 2.0, height: availableSize.height)
)
let scrubberFrame = CGRect(origin: CGPoint(x: scrubberInset, y: availableSize.height - environment.safeInsets.bottom - scrubberSize.height - 8.0), size: scrubberSize)
if let scrubberView = self.scrubber.view {
if scrubberView.superview == nil {
self.addSubview(scrubberView)
}
transition.setFrame(view: scrubberView, frame: scrubberFrame)
}
scrubberBottomInset = scrubberSize.height + 10.0
} else {
}
self.inputPanel.parentState = state
let inputPanelSize = self.inputPanel.update(
transition: transition,
component: AnyComponent(MessageInputPanelComponent(
externalState: self.inputPanelExternalState,
context: component.context,
theme: environment.theme,
strings: environment.strings,
style: .editor,
placeholder: "Add a caption...",
presentController: { [weak self] c in
guard let self, let _ = self.component else {
return
}
//component.presentController(c)
},
sendMessageAction: { [weak self] in
guard let _ = self else {
return
}
//self.performSendMessageAction()
},
setMediaRecordingActive: nil,
lockMediaRecording: nil,
stopAndPreviewMediaRecording: nil,
discardMediaRecordingPreview: nil,
attachmentAction: nil,
reactionAction: nil,
audioRecorder: nil,
videoRecordingStatus: nil,
isRecordingLocked: false,
recordedAudioPreview: nil,
wasRecordingDismissed: false,
displayGradient: false,//component.inputHeight != 0.0,
bottomInset: 0.0 //component.inputHeight != 0.0 ? 0.0 : bottomContentInset
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: 200.0)
)
var inputPanelOffset: CGFloat = 0.0
var inputPanelBottomInset: CGFloat = scrubberBottomInset
if environment.inputHeight > 0.0 {
inputPanelBottomInset = environment.inputHeight - environment.safeInsets.bottom
inputPanelOffset = inputPanelBottomInset
}
let inputPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - environment.safeInsets.bottom - inputPanelBottomInset - inputPanelSize.height - 3.0), size: inputPanelSize)
if let inputPanelView = self.inputPanel.view {
if inputPanelView.superview == nil {
self.addSubview(inputPanelView)
}
transition.setFrame(view: inputPanelView, frame: inputPanelFrame)
}
let privacyText: String
switch component.privacy.base {
case .everyone:
privacyText = "Everyone"
case .closeFriends:
privacyText = "Close Friends"
case .contacts:
privacyText = "Contacts"
case .nobody:
privacyText = "Selected Contacts"
}
let privacyButtonSize = self.privacyButton.update(
transition: transition,
component: AnyComponent(Button(
content: AnyComponent(
PrivacyButtonComponent(
icon: UIImage(bundleImageName: "Media Editor/Recipient")!,
text: privacyText
)
),
action: {
if let controller = environment.controller() as? MediaEditorScreen {
controller.presentPrivacySettings()
}
}
).tagged(privacyButtonTag)),
environment: {},
containerSize: CGSize(width: 44.0, height: 44.0)
)
let privacyButtonFrame = CGRect(
origin: CGPoint(x: 16.0, y: environment.safeInsets.top + 20.0 - inputPanelOffset),
size: privacyButtonSize
)
if let privacyButtonView = self.privacyButton.view {
if privacyButtonView.superview == nil {
self.addSubview(privacyButtonView)
}
transition.setPosition(view: privacyButtonView, position: privacyButtonFrame.center)
transition.setBounds(view: privacyButtonView, bounds: CGRect(origin: .zero, size: privacyButtonFrame.size))
transition.setScale(view: privacyButtonView, scale: self.inputPanelExternalState.isEditing ? 0.01 : 1.0)
transition.setAlpha(view: privacyButtonView, alpha: self.inputPanelExternalState.isEditing ? 0.0 : 1.0)
}
let saveButtonSize = self.saveButton.update(
transition: transition,
component: AnyComponent(Button(
content: AnyComponent(
LottieAnimationComponent(
animation: LottieAnimationComponent.AnimationItem(
name: "anim_storysave",
mode: .still(position: .begin),
range: nil
),
colors: ["__allcolors__": .white],
size: CGSize(width: 33.0, height: 33.0)
).tagged(saveButtonTag)
),
action: { [weak self] in
if let view = self?.saveButton.findTaggedView(tag: saveButtonTag) as? LottieAnimationComponent.View {
view.playOnce()
}
if let controller = environment.controller() as? MediaEditorScreen {
controller.requestSave()
}
}
)),
environment: {},
containerSize: CGSize(width: 44.0, height: 44.0)
)
let saveButtonFrame = CGRect(
origin: CGPoint(x: availableSize.width - 20.0 - saveButtonSize.width, y: environment.safeInsets.top + 20.0 - inputPanelOffset),
size: saveButtonSize
)
if let saveButtonView = self.saveButton.view {
if saveButtonView.superview == nil {
saveButtonView.layer.shadowOffset = CGSize(width: 0.0, height: 0.0)
saveButtonView.layer.shadowRadius = 4.0
saveButtonView.layer.shadowColor = UIColor.black.cgColor
saveButtonView.layer.shadowOpacity = 0.2
self.addSubview(saveButtonView)
}
transition.setPosition(view: saveButtonView, position: saveButtonFrame.center)
transition.setBounds(view: saveButtonView, bounds: CGRect(origin: .zero, size: saveButtonFrame.size))
transition.setScale(view: saveButtonView, scale: self.inputPanelExternalState.isEditing ? 0.01 : 1.0)
transition.setAlpha(view: saveButtonView, alpha: self.inputPanelExternalState.isEditing ? 0.0 : 1.0)
}
let isVideoMuted = component.mediaEditor?.values.videoIsMuted ?? false
let muteButtonSize = self.muteButton.update(
transition: transition,
component: AnyComponent(Button(
content: AnyComponent(
LottieAnimationComponent(
animation: LottieAnimationComponent.AnimationItem(
name: "anim_storymute",
mode: .animating(loop: false),
range: isVideoMuted ? (0.0, 0.5) : (0.5, 1.0)
),
colors: ["__allcolors__": .white],
size: CGSize(width: 33.0, height: 33.0)
).tagged(muteButtonTag)
),
action: { [weak self, weak state] in
if let self, let mediaEditor = self.component?.mediaEditor {
mediaEditor.setVideoIsMuted(!mediaEditor.values.videoIsMuted)
state?.updated()
}
}
)),
environment: {},
containerSize: CGSize(width: 44.0, height: 44.0)
)
let muteButtonFrame = CGRect(
origin: CGPoint(x: availableSize.width - 20.0 - muteButtonSize.width - 50.0, y: environment.safeInsets.top + 20.0 - inputPanelOffset),
size: muteButtonSize
)
if let muteButtonView = self.muteButton.view {
if muteButtonView.superview == nil {
muteButtonView.layer.shadowOffset = CGSize(width: 0.0, height: 0.0)
muteButtonView.layer.shadowRadius = 4.0
muteButtonView.layer.shadowColor = UIColor.black.cgColor
muteButtonView.layer.shadowOpacity = 0.2
//self.addSubview(muteButtonView)
}
transition.setPosition(view: muteButtonView, position: muteButtonFrame.center)
transition.setBounds(view: muteButtonView, bounds: CGRect(origin: .zero, size: muteButtonFrame.size))
transition.setScale(view: muteButtonView, scale: self.inputPanelExternalState.isEditing ? 0.01 : 1.0)
transition.setAlpha(view: muteButtonView, alpha: self.inputPanelExternalState.isEditing ? 0.0 : 1.0)
}
return availableSize
}
}
func makeView() -> View {
return View()
}
public func update(view: View, availableSize: CGSize, state: State, environment: Environment<ViewControllerComponentContainer.Environment>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
private let storyDimensions = CGSize(width: 1080.0, height: 1920.0)
public final class MediaEditorScreen: ViewController {
public final class TransitionIn {
public weak var sourceView: UIView?
public let sourceRect: CGRect
public let sourceCornerRadius: CGFloat
public init(
sourceView: UIView,
sourceRect: CGRect,
sourceCornerRadius: CGFloat
) {
self.sourceView = sourceView
self.sourceRect = sourceRect
self.sourceCornerRadius = sourceCornerRadius
}
}
public final class TransitionOut {
public weak var destinationView: UIView?
public let destinationRect: CGRect
public let destinationCornerRadius: CGFloat
public init(
destinationView: UIView,
destinationRect: CGRect,
destinationCornerRadius: CGFloat
) {
self.destinationView = destinationView
self.destinationRect = destinationRect
self.destinationCornerRadius = destinationCornerRadius
}
}
fileprivate final class Node: ViewControllerTracingNode, UIGestureRecognizerDelegate {
private weak var controller: MediaEditorScreen?
private let context: AccountContext
private let initializationTimestamp = CACurrentMediaTime()
fileprivate var subject: MediaEditorScreen.Subject?
private var subjectDisposable: Disposable?
fileprivate var storyPrivacy: EngineStoryPrivacy = EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: [])
private let backgroundDimView: UIView
fileprivate let componentHost: ComponentView<ViewControllerComponentContainer.Environment>
private let previewContainerView: UIView
private let gradientView: UIImageView
private var gradientColorsDisposable: Disposable?
fileprivate let entitiesContainerView: UIView
fileprivate let entitiesView: DrawingEntitiesView
fileprivate let drawingView: DrawingView
fileprivate let previewView: MediaEditorPreviewView
fileprivate var mediaEditor: MediaEditor?
private let stickerPickerInputData = Promise<StickerPickerInputData>()
private var presentationData: PresentationData
private var validLayout: ContainerViewLayout?
init(controller: MediaEditorScreen) {
self.controller = controller
self.context = controller.context
self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
self.backgroundDimView = UIView()
self.backgroundDimView.alpha = 0.0
self.backgroundDimView.backgroundColor = .black
self.componentHost = ComponentView<ViewControllerComponentContainer.Environment>()
self.previewContainerView = UIView()
self.previewContainerView.alpha = 0.0
self.previewContainerView.clipsToBounds = true
self.previewContainerView.layer.cornerRadius = 12.0
if #available(iOS 13.0, *) {
self.previewContainerView.layer.cornerCurve = .continuous
}
self.gradientView = UIImageView()
self.entitiesContainerView = UIView(frame: CGRect(origin: .zero, size: storyDimensions))
self.entitiesView = DrawingEntitiesView(context: controller.context, size: storyDimensions)
self.entitiesView.getEntityCenterPosition = {
return CGPoint(x: storyDimensions.width / 2.0, y: storyDimensions.height / 2.0)
}
self.previewView = MediaEditorPreviewView(frame: .zero)
self.drawingView = DrawingView(size: storyDimensions)
self.drawingView.isUserInteractionEnabled = false
super.init()
self.backgroundColor = .clear
//self.view.addSubview(self.backgroundDimView)
self.view.addSubview(self.previewContainerView)
self.previewContainerView.addSubview(self.gradientView)
self.previewContainerView.addSubview(self.entitiesContainerView)
self.entitiesContainerView.addSubview(self.entitiesView)
self.previewContainerView.addSubview(self.drawingView)
self.subjectDisposable = (
controller.subject
|> filter {
$0 != nil
}
|> take(1)
|> deliverOnMainQueue
).start(next: { [weak self] subject in
if let self, let subject {
self.setup(with: subject)
}
})
let stickerPickerInputData = self.stickerPickerInputData
Queue.concurrentDefaultQueue().after(0.5, {
let emojiItems = EmojiPagerContentComponent.emojiInputData(
context: controller.context,
animationCache: controller.context.animationCache,
animationRenderer: controller.context.animationRenderer,
isStandalone: false,
isStatusSelection: false,
isReactionSelection: false,
isEmojiSelection: true,
hasTrending: false,
topReactionItems: [],
areUnicodeEmojiEnabled: true,
areCustomEmojiEnabled: true,
chatPeerId: controller.context.account.peerId,
hasSearch: false,
forceHasPremium: true
)
let stickerItems = EmojiPagerContentComponent.stickerInputData(
context: controller.context,
animationCache: controller.context.animationCache,
animationRenderer: controller.context.animationRenderer,
stickerNamespaces: [Namespaces.ItemCollection.CloudStickerPacks],
stickerOrderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudAllPremiumStickers],
chatPeerId: controller.context.account.peerId,
hasSearch: false,
hasTrending: true,
forceHasPremium: true
)
let maskItems = EmojiPagerContentComponent.stickerInputData(
context: controller.context,
animationCache: controller.context.animationCache,
animationRenderer: controller.context.animationRenderer,
stickerNamespaces: [Namespaces.ItemCollection.CloudMaskPacks],
stickerOrderedItemListCollectionIds: [],
chatPeerId: controller.context.account.peerId,
hasSearch: false,
hasTrending: false,
forceHasPremium: true
)
let signal = combineLatest(queue: .mainQueue(),
emojiItems,
stickerItems,
maskItems
) |> map { emoji, stickers, masks -> StickerPickerInputData in
return StickerPickerInputData(emoji: emoji, stickers: stickers, masks: masks)
}
stickerPickerInputData.set(signal)
})
}
deinit {
self.subjectDisposable?.dispose()
self.gradientColorsDisposable?.dispose()
}
private func setup(with subject: MediaEditorScreen.Subject) {
self.subject = subject
guard let _ = self.controller else {
return
}
let mediaDimensions = subject.dimensions
let maxSide: CGFloat = 1920.0 / UIScreen.main.scale
let fittedSize = mediaDimensions.cgSize.fitted(CGSize(width: maxSide, height: maxSide))
let mediaEntity = DrawingMediaEntity(content: subject.mediaContent, size: fittedSize)
mediaEntity.position = CGPoint(x: storyDimensions.width / 2.0, y: storyDimensions.height / 2.0)
if fittedSize.height > fittedSize.width {
mediaEntity.scale = storyDimensions.height / fittedSize.height
} else {
mediaEntity.scale = storyDimensions.width / fittedSize.width
}
self.entitiesView.add(mediaEntity, announce: false)
let initialPosition = mediaEntity.position
let initialScale = mediaEntity.scale
let initialRotation = mediaEntity.rotation
if let entityView = self.entitiesView.getView(for: mediaEntity.uuid) as? DrawingMediaEntityView {
entityView.previewView = self.previewView
entityView.updated = { [weak self, weak mediaEntity] in
if let self, let mediaEntity {
let rotationDelta = mediaEntity.rotation - initialRotation
let positionDelta = CGPoint(x: mediaEntity.position.x - initialPosition.x, y: mediaEntity.position.y - initialPosition.y)
let scaleDelta = mediaEntity.scale / initialScale
self.mediaEditor?.setCrop(offset: positionDelta, scale: scaleDelta, rotation: rotationDelta, mirroring: false)
}
}
}
let mediaEditor = MediaEditor(subject: subject.editorSubject, hasHistogram: true)
mediaEditor.attachPreviewView(self.previewView)
self.gradientColorsDisposable = mediaEditor.gradientColors.start(next: { [weak self] colors in
if let self, let colors {
let (topColor, bottomColor) = colors
let gradientImage = generateGradientImage(size: CGSize(width: 5.0, height: 640.0), colors: [topColor, bottomColor], locations: [0.0, 1.0])
Queue.mainQueue().async {
self.gradientView.image = gradientImage
self.previewContainerView.alpha = 1.0
if CACurrentMediaTime() - self.initializationTimestamp > 0.2 {
self.previewContainerView.layer.allowsGroupOpacity = true
self.previewContainerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, completion: { _ in
self.previewContainerView.layer.allowsGroupOpacity = false
})
}
}
}
})
self.mediaEditor = mediaEditor
}
override func didLoad() {
super.didLoad()
self.view.disablesInteractiveModalDismiss = true
self.view.disablesInteractiveKeyboardGestureRecognizer = true
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:)))
panGestureRecognizer.delegate = self
panGestureRecognizer.minimumNumberOfTouches = 2
panGestureRecognizer.maximumNumberOfTouches = 2
self.previewContainerView.addGestureRecognizer(panGestureRecognizer)
let pinchGestureRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(self.handlePinch(_:)))
pinchGestureRecognizer.delegate = self
self.previewContainerView.addGestureRecognizer(pinchGestureRecognizer)
let rotateGestureRecognizer = UIRotationGestureRecognizer(target: self, action: #selector(self.handleRotate(_:)))
rotateGestureRecognizer.delegate = self
self.previewContainerView.addGestureRecognizer(rotateGestureRecognizer)
}
@objc func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
@objc func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
self.entitiesView.handlePan(gestureRecognizer)
}
@objc func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer) {
self.entitiesView.handlePinch(gestureRecognizer)
}
@objc func handleRotate(_ gestureRecognizer: UIRotationGestureRecognizer) {
self.entitiesView.handleRotate(gestureRecognizer)
}
func animateIn() {
if let sourceHint = self.controller?.sourceHint {
switch sourceHint {
case .camera:
if let view = self.componentHost.view as? MediaEditorScreenComponent.View {
view.animateInFromCamera()
}
}
}
Queue.mainQueue().after(0.5) {
self.presentPrivacyTooltip()
}
}
func animateOut(finished: Bool, completion: @escaping () -> Void) {
guard let controller = self.controller else {
return
}
if let transitionOut = controller.transitionOut(finished), let destinationView = transitionOut.destinationView {
let destinationLocalFrame = destinationView.convert(transitionOut.destinationRect, to: self.view)
let targetScale = destinationLocalFrame.width / self.previewContainerView.frame.width
self.previewContainerView.layer.animatePosition(from: self.previewContainerView.center, to: destinationLocalFrame.center, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
completion()
})
self.previewContainerView.layer.animateScale(from: 1.0, to: targetScale, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
self.previewContainerView.layer.animateBounds(from: self.previewContainerView.bounds, to: CGRect(origin: CGPoint(x: 0.0, y: (self.previewContainerView.bounds.height - self.previewContainerView.bounds.width) / 2.0), size: CGSize(width: self.previewContainerView.bounds.width, height: self.previewContainerView.bounds.width)), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
self.previewContainerView.layer.animate(
from: self.previewContainerView.layer.cornerRadius as NSNumber,
to: self.previewContainerView.bounds.width / 2.0 as NSNumber,
keyPath: "cornerRadius",
timingFunction: kCAMediaTimingFunctionSpring,
duration: 0.4,
removeOnCompletion: false
)
if let componentView = self.componentHost.view {
componentView.clipsToBounds = true
componentView.layer.animatePosition(from: componentView.center, to: destinationLocalFrame.center, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
componentView.layer.animateScale(from: 1.0, to: targetScale, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
componentView.layer.animateBounds(from: componentView.bounds, to: CGRect(origin: CGPoint(x: 0.0, y: (componentView.bounds.height - componentView.bounds.width) / 2.0), size: CGSize(width: componentView.bounds.width, height: componentView.bounds.width)), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
componentView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
componentView.layer.animate(
from: componentView.layer.cornerRadius as NSNumber,
to: componentView.bounds.width / 2.0 as NSNumber,
keyPath: "cornerRadius",
timingFunction: kCAMediaTimingFunctionSpring,
duration: 0.4,
removeOnCompletion: false
)
}
} else if let sourceHint = controller.sourceHint {
switch sourceHint {
case .camera:
if let view = self.componentHost.view as? MediaEditorScreenComponent.View {
view.animateOutToCamera()
}
let transition = Transition(animation: .curve(duration: 0.25, curve: .easeInOut))
transition.setAlpha(view: self.previewContainerView, alpha: 0.0, completion: { _ in
completion()
})
}
} else {
completion()
}
}
func animateOutToTool() {
if let view = self.componentHost.view as? MediaEditorScreenComponent.View {
view.animateOutToTool()
}
}
func animateInFromTool() {
if let view = self.componentHost.view as? MediaEditorScreenComponent.View {
view.animateInFromTool()
}
}
func presentPrivacyTooltip() {
guard let sourceView = self.componentHost.findTaggedView(tag: privacyButtonTag) else {
return
}
let parentFrame = self.view.convert(self.bounds, to: nil)
let absoluteFrame = sourceView.convert(sourceView.bounds, to: nil).offsetBy(dx: -parentFrame.minX, dy: 0.0)
let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.maxY + 3.0), size: CGSize())
let controller = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: "You can set who can view this story", location: .point(location, .top), displayDuration: .manual, inset: 16.0, shouldDismissOnTouch: { _ in
return .ignore
})
self.controller?.present(controller, in: .current)
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = super.hitTest(point, with: event)
if result == self.componentHost.view {
self.controller?.view.endEditing(true)
let point = self.view.convert(point, to: self.previewContainerView)
return self.previewContainerView.hitTest(point, with: event)
}
return result
}
func requestUpdate() {
if let layout = self.validLayout {
self.containerLayoutUpdated(layout: layout, transition: .immediate)
}
}
private var drawingScreen: DrawingScreen?
func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, animateOut: Bool = false, transition: Transition) {
guard let _ = self.controller else {
return
}
let isFirstTime = self.validLayout == nil
self.validLayout = layout
let previewSize = CGSize(width: layout.size.width, height: floorToScreenPixels(layout.size.width * 1.77778))
let topInset: CGFloat = floor(layout.size.height - previewSize.height) / 2.0
let environment = ViewControllerComponentContainer.Environment(
statusBarHeight: layout.statusBarHeight ?? 0.0,
navigationHeight: 0.0,
safeInsets: UIEdgeInsets(
top: topInset,
left: layout.safeInsets.left,
bottom: topInset,
right: layout.safeInsets.right
),
inputHeight: layout.inputHeight ?? 0.0,
metrics: layout.metrics,
deviceMetrics: layout.deviceMetrics,
orientation: nil,
isVisible: true,
theme: self.presentationData.theme,
strings: self.presentationData.strings,
dateTimeFormat: self.presentationData.dateTimeFormat,
controller: { [weak self] in
return self?.controller
}
)
let componentSize = self.componentHost.update(
transition: transition,
component: AnyComponent(
MediaEditorScreenComponent(
context: self.context,
mediaEditor: self.mediaEditor,
privacy: self.storyPrivacy,
openDrawing: { [weak self] mode in
if let self {
let controller = DrawingScreen(context: self.context, sourceHint: .storyEditor, size: self.previewContainerView.frame.size, originalSize: storyDimensions, isVideo: false, isAvatar: false, drawingView: self.drawingView, entitiesView: self.entitiesView, existingStickerPickerInputData: self.stickerPickerInputData)
self.drawingScreen = controller
self.drawingView.isUserInteractionEnabled = true
let selectionContainerView = controller.selectionContainerView
selectionContainerView.frame = self.previewContainerView.bounds
self.previewContainerView.addSubview(selectionContainerView)
controller.requestDismiss = { [weak controller, weak self, weak selectionContainerView] in
self?.drawingScreen = nil
controller?.animateOut({
controller?.dismiss()
})
self?.drawingView.isUserInteractionEnabled = false
self?.animateInFromTool()
selectionContainerView?.removeFromSuperview()
}
controller.requestApply = { [weak controller, weak self, weak selectionContainerView] in
self?.drawingScreen = nil
controller?.animateOut({
controller?.dismiss()
})
self?.drawingView.isUserInteractionEnabled = false
self?.animateInFromTool()
if let result = controller?.generateDrawingResultData() {
self?.mediaEditor?.setDrawingAndEntities(data: result.data, image: result.drawingImage, entities: result.entities)
} else {
self?.mediaEditor?.setDrawingAndEntities(data: nil, image: nil, entities: [])
}
selectionContainerView?.removeFromSuperview()
}
self.controller?.present(controller, in: .current)
switch mode {
case .sticker:
controller.presentStickerSelection()
case .text:
Queue.mainQueue().after(0.05, {
controller.addTextEntity()
})
default:
break
}
self.animateOutToTool()
}
},
openTools: { [weak self] in
if let self, let mediaEditor = self.mediaEditor {
let controller = MediaToolsScreen(context: self.context, mediaEditor: mediaEditor)
controller.dismissed = { [weak self] in
if let self {
self.animateInFromTool()
}
}
self.controller?.present(controller, in: .current)
self.animateOutToTool()
}
}
)
),
environment: {
environment
},
forceUpdate: forceUpdate || animateOut,
containerSize: layout.size
)
if let componentView = self.componentHost.view {
if componentView.superview == nil {
self.view.insertSubview(componentView, at: 3)
componentView.clipsToBounds = true
}
let componentFrame = CGRect(origin: .zero, size: componentSize)
transition.setFrame(view: componentView, frame: CGRect(origin: componentFrame.origin, size: CGSize(width: componentFrame.width, height: componentFrame.height)))
}
var bottomInputOffset: CGFloat = 0.0
if let inputHeight = layout.inputHeight, inputHeight > 0.0 {
bottomInputOffset = inputHeight - topInset
}
transition.setFrame(view: self.backgroundDimView, frame: CGRect(origin: .zero, size: layout.size))
var previewFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset - bottomInputOffset), size: previewSize)
if let inputHeight = layout.inputHeight, inputHeight > 0.0, self.drawingScreen != nil {
previewFrame = previewFrame.offsetBy(dx: 0.0, dy: inputHeight / 2.0)
}
transition.setFrame(view: self.previewContainerView, frame: previewFrame)
let entitiesViewScale = previewSize.width / storyDimensions.width
self.entitiesContainerView.transform = CGAffineTransformMakeScale(entitiesViewScale, entitiesViewScale)
transition.setFrame(view: self.entitiesContainerView, frame: CGRect(origin: .zero, size: previewFrame.size))
transition.setFrame(view: self.gradientView, frame: CGRect(origin: .zero, size: previewFrame.size))
transition.setFrame(view: self.drawingView, frame: CGRect(origin: .zero, size: previewFrame.size))
if isFirstTime {
self.animateIn()
}
}
}
fileprivate var node: Node {
return self.displayNode as! Node
}
public enum Subject {
case image(UIImage, PixelDimensions)
case video(String, PixelDimensions)
case asset(PHAsset)
var dimensions: PixelDimensions {
switch self {
case let .image(_, dimensions), let .video(_, dimensions):
return dimensions
case let .asset(asset):
return PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight))
}
}
var editorSubject: MediaEditor.Subject {
switch self {
case let .image(image, dimensions):
return .image(image, dimensions)
case let .video(videoPath, dimensions):
return .video(videoPath, dimensions)
case let .asset(asset):
return .asset(asset)
}
}
var mediaContent: DrawingMediaEntity.Content {
switch self {
case let .image(image, dimensions):
return .image(image, dimensions)
case let .video(videoPath, dimensions):
return .video(videoPath, dimensions)
case let .asset(asset):
return .asset(asset)
}
}
}
public enum Result {
public enum VideoResult {
case imageFile(path: String)
case videoFile(path: String)
case asset(localIdentifier: String)
}
case image(image: UIImage, dimensions: PixelDimensions, caption: NSAttributedString?)
case video(video: VideoResult, coverImage: UIImage?, values: MediaEditorValues, duration: Double, dimensions: PixelDimensions, caption: NSAttributedString?)
}
fileprivate let context: AccountContext
fileprivate let subject: Signal<Subject?, NoError>
fileprivate let transitionIn: TransitionIn?
fileprivate let transitionOut: (Bool) -> TransitionOut?
public enum SourceHint {
case camera
}
public var sourceHint: SourceHint?
public var cancelled: () -> Void = {}
public var completion: (MediaEditorScreen.Result, @escaping () -> Void, EngineStoryPrivacy) -> Void = { _, _, _ in }
public init(
context: AccountContext,
subject: Signal<Subject?, NoError>,
transitionIn: TransitionIn?,
transitionOut: @escaping (Bool) -> TransitionOut?,
completion: @escaping (MediaEditorScreen.Result, @escaping () -> Void, EngineStoryPrivacy) -> Void
) {
self.context = context
self.subject = subject
self.transitionIn = transitionIn
self.transitionOut = transitionOut
self.completion = completion
super.init(navigationBarPresentationData: nil)
self.navigationPresentation = .flatModal
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
self.statusBar.statusBarStyle = .White
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func loadDisplayNode() {
self.displayNode = Node(controller: self)
super.displayNodeDidLoad()
}
func presentPrivacySettings() {
let stateContext = ShareWithPeersScreen.StateContext(context: self.context)
let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in
guard let self else {
return
}
self.push(ShareWithPeersScreen(context: self.context, initialPrivacy: self.node.storyPrivacy, stateContext: stateContext, completion: { [weak self] privacy in
guard let self else {
return
}
self.node.storyPrivacy = privacy
self.node.requestUpdate()
}))
})
/*enum AdditionalCategoryId: Int {
case everyone
case contacts
case closeFriends
}
let presentationData = self.context.sharedContext.currentPresentationData.with({ $0 })
let additionalCategories: [ChatListNodeAdditionalCategory] = [
ChatListNodeAdditionalCategory(
id: AdditionalCategoryId.everyone.rawValue,
icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Channel"), color: .white), cornerRadius: nil, color: .blue),
smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Channel"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .blue),
title: "Everyone",
appearance: .option(sectionTitle: "WHO CAN VIEW FOR 24 HOURS")
),
ChatListNodeAdditionalCategory(
id: AdditionalCategoryId.contacts.rawValue,
icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Tabs/IconContacts"), color: .white), iconScale: 1.0 * 0.8, cornerRadius: nil, color: .yellow),
smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Tabs/IconContacts"), color: .white), iconScale: 0.6 * 0.8, cornerRadius: 6.0, circleCorners: true, color: .yellow),
title: presentationData.strings.ChatListFolder_CategoryContacts,
appearance: .option(sectionTitle: "WHO CAN VIEW FOR 24 HOURS")
),
ChatListNodeAdditionalCategory(
id: AdditionalCategoryId.closeFriends.rawValue,
icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Call/StarHighlighted"), color: .white), iconScale: 1.0 * 0.6, cornerRadius: nil, color: .green),
smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Call/StarHighlighted"), color: .white), iconScale: 0.6 * 0.6, cornerRadius: 6.0, circleCorners: true, color: .green),
title: "Close Friends",
appearance: .option(sectionTitle: "WHO CAN VIEW FOR 24 HOURS")
)
]
let updatedPresentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)
let selectionController = self.context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: self.context, updatedPresentationData: (initial: updatedPresentationData, signal: .single(updatedPresentationData)), mode: .chatSelection(ContactMultiselectionControllerMode.ChatSelection(
title: "Share Story",
searchPlaceholder: "Search contacts",
selectedChats: Set(),
additionalCategories: ContactMultiselectionControllerAdditionalCategories(categories: additionalCategories, selectedCategories: Set([AdditionalCategoryId.everyone.rawValue])),
chatListFilters: nil,
displayPresence: true
)), options: [], filters: [.excludeSelf], alwaysEnabled: true, limit: 1000, reachedLimit: { _ in
}))
selectionController.navigationPresentation = .modal
self.push(selectionController)
let _ = (selectionController.result
|> take(1)
|> deliverOnMainQueue).start(next: { [weak selectionController, weak self] result in
selectionController?.dismiss()
guard case let .result(peerIds, additionalCategoryIds) = result else {
return
}
var privacy = EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: [])
if additionalCategoryIds.contains(AdditionalCategoryId.everyone.rawValue) {
privacy.base = .everyone
} else if additionalCategoryIds.contains(AdditionalCategoryId.contacts.rawValue) {
privacy.base = .contacts
} else if additionalCategoryIds.contains(AdditionalCategoryId.closeFriends.rawValue) {
privacy.base = .closeFriends
}
privacy.additionallyIncludePeers = peerIds.compactMap { id -> EnginePeer.Id? in
switch id {
case let .peer(peerId):
return peerId
default:
return nil
}
}
self?.node.storyPrivacy = privacy
self?.node.requestUpdate()
})*/
}
func requestDismiss(animated: Bool) {
self.cancelled()
self.node.animateOut(finished: false, completion: { [weak self] in
self?.dismiss()
})
}
func requestCompletion(caption: NSAttributedString, animated: Bool) {
guard let mediaEditor = self.node.mediaEditor, let subject = self.node.subject else {
return
}
if mediaEditor.resultIsVideo {
let videoResult: Result.VideoResult
let duration: Double
switch subject {
case let .image(image, _):
let tempImagePath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).jpg"
if let data = image.jpegData(compressionQuality: 0.85) {
try? data.write(to: URL(fileURLWithPath: tempImagePath))
}
videoResult = .imageFile(path: tempImagePath)
duration = 5.0
case let .video(path, _):
videoResult = .videoFile(path: path)
if let videoTrimRange = mediaEditor.values.videoTrimRange {
duration = videoTrimRange.upperBound - videoTrimRange.lowerBound
} else {
duration = 5.0
}
case let .asset(asset):
videoResult = .asset(localIdentifier: asset.localIdentifier)
if asset.mediaType == .video {
if let videoTrimRange = mediaEditor.values.videoTrimRange {
duration = videoTrimRange.upperBound - videoTrimRange.lowerBound
} else {
duration = asset.duration
}
} else {
duration = 5.0
}
}
self.completion(.video(video: videoResult, coverImage: nil, values: mediaEditor.values, duration: duration, dimensions: PixelDimensions(width: 1080, height: 1920), caption: caption), { [weak self] in
self?.node.animateOut(finished: true, completion: { [weak self] in
self?.dismiss()
})
}, self.node.storyPrivacy)
} else {
if let image = mediaEditor.resultImage {
makeEditorImageComposition(account: self.context.account, inputImage: image, dimensions: storyDimensions, values: mediaEditor.values, time: .zero, completion: { resultImage in
if let resultImage {
self.completion(.image(image: resultImage, dimensions: PixelDimensions(resultImage.size), caption: caption), { [weak self] in
self?.node.animateOut(finished: true, completion: { [weak self] in
self?.dismiss()
})
}, self.node.storyPrivacy)
}
})
}
}
}
private var videoExport: MediaEditorVideoExport?
private var exportDisposable: Disposable?
func requestSave() {
guard let mediaEditor = self.node.mediaEditor, let subject = self.node.subject else {
return
}
let tempVideoPath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).mp4"
let saveToPhotos: (String, Bool) -> Void = { path, isVideo in
PHPhotoLibrary.shared().performChanges({
if isVideo {
if let _ = try? FileManager.default.copyItem(atPath: path, toPath: tempVideoPath) {
PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: URL(fileURLWithPath: path))
}
} else {
if let fileData = try? Data(contentsOf: URL(fileURLWithPath: path)) {
PHAssetCreationRequest.forAsset().addResource(with: .photo, data: fileData, options: nil)
}
}
}, completionHandler: { _, error in
if let error = error {
print("\(error)")
}
let _ = try? FileManager.default.removeItem(atPath: tempVideoPath)
})
}
if mediaEditor.resultIsVideo {
let exportSubject: Signal<MediaEditorVideoExport.Subject, NoError>
switch subject {
case let .video(path, _):
let asset = AVURLAsset(url: NSURL(fileURLWithPath: path) as URL)
exportSubject = .single(.video(asset))
case let .image(image, _):
exportSubject = .single(.image(image))
case let .asset(asset):
exportSubject = Signal { subscriber in
if asset.mediaType == .video {
PHImageManager.default().requestAVAsset(forVideo: asset, options: nil) { avAsset, _, _ in
if let avAsset {
subscriber.putNext(.video(avAsset))
subscriber.putCompletion()
}
}
} else {
let options = PHImageRequestOptions()
options.deliveryMode = .highQualityFormat
PHImageManager.default().requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .default, options: options) { image, _ in
if let image {
subscriber.putNext(.image(image))
subscriber.putCompletion()
}
}
}
return EmptyDisposable
}
}
let _ = exportSubject.start(next: { [weak self] exportSubject in
guard let self else {
return
}
let configuration = recommendedVideoExportConfiguration(values: mediaEditor.values)
let outputPath = NSTemporaryDirectory() + "\(Int64.random(in: 0 ..< .max)).mp4"
let videoExport = MediaEditorVideoExport(account: self.context.account, subject: exportSubject, configuration: configuration, outputPath: outputPath)
self.videoExport = videoExport
videoExport.startExport()
self.exportDisposable = (videoExport.status
|> deliverOnMainQueue).start(next: { [weak self] status in
if let self {
if case .completed = status {
self.videoExport = nil
saveToPhotos(outputPath, true)
}
}
})
})
} else {
if let image = mediaEditor.resultImage {
makeEditorImageComposition(account: self.context.account, inputImage: image, dimensions: storyDimensions, values: mediaEditor.values, time: .zero, completion: { resultImage in
if let data = resultImage?.jpegData(compressionQuality: 0.8) {
let outputPath = NSTemporaryDirectory() + "\(Int64.random(in: 0 ..< .max)).jpg"
try? data.write(to: URL(fileURLWithPath: outputPath))
saveToPhotos(outputPath, false)
}
})
}
}
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
(self.displayNode as! Node).containerLayoutUpdated(layout: layout, transition: Transition(transition))
}
}
final class PrivacyButtonComponent: CombinedComponent {
let icon: UIImage
let text: String
init(
icon: UIImage,
text: String
) {
self.icon = icon
self.text = text
}
static func ==(lhs: PrivacyButtonComponent, rhs: PrivacyButtonComponent) -> Bool {
if lhs.text != rhs.text {
return false
}
return true
}
static var body: Body {
let background = Child(BlurredBackgroundComponent.self)
let icon = Child(Image.self)
let text = Child(Text.self)
return { context in
let icon = icon.update(
component: Image(image: context.component.icon, size: CGSize(width: 9.0, height: 11.0)),
availableSize: CGSize(width: 180.0, height: 100.0),
transition: .immediate
)
let text = text.update(
component: Text(
text: "\(context.component.text)",
font: Font.medium(14.0),
color: .white
),
availableSize: CGSize(width: 180.0, height: 100.0),
transition: .immediate
)
let backgroundSize = CGSize(width: text.size.width + 38.0, height: 30.0)
let background = background.update(
component: BlurredBackgroundComponent(color: UIColor(white: 0.0, alpha: 0.5)),
availableSize: backgroundSize,
transition: .immediate
)
context.add(background
.position(CGPoint(x: backgroundSize.width / 2.0, y: backgroundSize.height / 2.0))
.cornerRadius(min(backgroundSize.width, backgroundSize.height) / 2.0)
.clipsToBounds(true)
)
context.add(icon
.position(CGPoint(x: 16.0, y: backgroundSize.height / 2.0))
)
context.add(text
.position(CGPoint(x: backgroundSize.width / 2.0 + 7.0, y: backgroundSize.height / 2.0))
)
return backgroundSize
}
}
}