Camera and editor improvements

This commit is contained in:
Ilya Laktyushin 2023-05-16 19:35:14 +04:00
parent 97e871fa22
commit d38e81a324
16 changed files with 614 additions and 46 deletions

View File

@ -2454,9 +2454,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
initialFocusedId = AnyHashable(peer.id) initialFocusedId = AnyHashable(peer.id)
} }
if !initialContent.contains(where: { slice in if initialFocusedId == AnyHashable(self.context.account.peerId), let firstItem = initialContent.first, firstItem.id == initialFocusedId && firstItem.items.isEmpty {
return !slice.items.isEmpty
}) {
if let rootController = self.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface { if let rootController = self.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface {
rootController.openStoryCamera(transitionIn: cameraTransitionIn, transitionOut: { [weak self] _ in rootController.openStoryCamera(transitionIn: cameraTransitionIn, transitionOut: { [weak self] _ in
guard let self else { guard let self else {

View File

@ -69,6 +69,7 @@ swift_library(
"//submodules/Camera", "//submodules/Camera",
"//submodules/Components/MultilineTextComponent", "//submodules/Components/MultilineTextComponent",
"//submodules/Components/BlurredBackgroundComponent", "//submodules/Components/BlurredBackgroundComponent",
"//submodules/Components/LottieAnimationComponent:LottieAnimationComponent",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -14,6 +14,7 @@ import Camera
import MultilineTextComponent import MultilineTextComponent
import BlurredBackgroundComponent import BlurredBackgroundComponent
import Photos import Photos
import LottieAnimationComponent
let videoRedColor = UIColor(rgb: 0xff3b30) let videoRedColor = UIColor(rgb: 0xff3b30)
@ -284,17 +285,39 @@ private final class CameraScreenComponent: CombinedComponent {
.cornerRadius(20.0) .cornerRadius(20.0)
) )
let flashIconName: String
switch state.cameraState.flashMode {
case .off:
flashIconName = "flash_off"
case .on:
flashIconName = "flash_on"
case .auto:
flashIconName = "flash_auto"
@unknown default:
flashIconName = "flash_off"
}
let flashButton = flashButton.update( let flashButton = flashButton.update(
component: CameraButton( component: CameraButton(
content: AnyComponent(Image( content: AnyComponent(
image: state.image(.flash) LottieAnimationComponent(
)), animation: LottieAnimationComponent.AnimationItem(
name: flashIconName,
mode: .animating(loop: false),
range: nil
),
colors: [:],
size: CGSize(width: 40.0, height: 40.0)
)
),
action: { [weak state] in action: { [weak state] in
guard let state else { guard let state else {
return return
} }
if state.cameraState.flashMode == .off { if state.cameraState.flashMode == .off {
state.camera.setFlashMode(.on) state.camera.setFlashMode(.on)
} else if state.cameraState.flashMode == .on {
state.camera.setFlashMode(.auto)
} else { } else {
state.camera.setFlashMode(.off) state.camera.setFlashMode(.off)
} }
@ -885,6 +908,13 @@ public class CameraScreen: ViewController {
view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
} }
} }
if let view = self.componentHost.findTaggedView(tag: flashButtonTag) {
view.layer.shadowOffset = CGSize(width: 0.0, height: 0.0)
view.layer.shadowRadius = 4.0
view.layer.shadowColor = UIColor.black.cgColor
view.layer.shadowOpacity = 0.2
}
} }
func animateOut(completion: @escaping () -> Void) { func animateOut(completion: @escaping () -> Void) {
@ -928,11 +958,11 @@ public class CameraScreen: ViewController {
func animateOutToEditor() { func animateOutToEditor() {
let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut))
if let view = self.componentHost.findTaggedView(tag: cancelButtonTag) { if let view = self.componentHost.findTaggedView(tag: cancelButtonTag) {
transition.setScale(view: view, scale: 0.1) view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2)
transition.setAlpha(view: view, alpha: 0.0) transition.setAlpha(view: view, alpha: 0.0)
} }
if let view = self.componentHost.findTaggedView(tag: flashButtonTag) { if let view = self.componentHost.findTaggedView(tag: flashButtonTag) {
transition.setScale(view: view, scale: 0.1) view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2)
transition.setAlpha(view: view, alpha: 0.0) transition.setAlpha(view: view, alpha: 0.0)
} }
if let view = self.componentHost.findTaggedView(tag: zoomControlTag) { if let view = self.componentHost.findTaggedView(tag: zoomControlTag) {
@ -973,15 +1003,15 @@ public class CameraScreen: ViewController {
let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut))
if let view = self.componentHost.findTaggedView(tag: cancelButtonTag) { if let view = self.componentHost.findTaggedView(tag: cancelButtonTag) {
transition.setScale(view: view, scale: 1.0) view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
transition.setAlpha(view: view, alpha: 1.0) transition.setAlpha(view: view, alpha: 1.0)
} }
if let view = self.componentHost.findTaggedView(tag: flashButtonTag) { if let view = self.componentHost.findTaggedView(tag: flashButtonTag) {
transition.setScale(view: view, scale: 1.0) view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
transition.setAlpha(view: view, alpha: 1.0) transition.setAlpha(view: view, alpha: 1.0)
} }
if let view = self.componentHost.findTaggedView(tag: zoomControlTag) { if let view = self.componentHost.findTaggedView(tag: zoomControlTag) {
transition.setScale(view: view, scale: 1.0) view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
transition.setAlpha(view: view, alpha: 1.0) transition.setAlpha(view: view, alpha: 1.0)
} }
if let view = self.componentHost.findTaggedView(tag: captureControlsTag) as? CaptureControlsComponent.View { if let view = self.componentHost.findTaggedView(tag: captureControlsTag) as? CaptureControlsComponent.View {
@ -1143,6 +1173,7 @@ public class CameraScreen: ViewController {
let controller = self.context.sharedContext.makeMediaPickerScreen(context: self.context, completion: { [weak self] asset in let controller = self.context.sharedContext.makeMediaPickerScreen(context: self.context, completion: { [weak self] asset in
dismissGalleryControllerImpl?() dismissGalleryControllerImpl?()
if let self { if let self {
self.node.animateOutToEditor()
self.completion(.single(.asset(asset))) self.completion(.single(.asset(asset)))
} }
}) })

View File

@ -16,7 +16,7 @@ final class ImageTextureSource: TextureSource {
let textureLoader = MTKTextureLoader(device: device) let textureLoader = MTKTextureLoader(device: device)
self.textureLoader = textureLoader self.textureLoader = textureLoader
self.texture = try? textureLoader.newTexture(cgImage: cgImage, options: nil) self.texture = try? textureLoader.newTexture(cgImage: cgImage, options: [.SRGB : false])
} }
func start() { func start() {

View File

@ -113,7 +113,7 @@ final class MediaEditorComposer {
} }
if self.filteredImage == nil, let device = self.device, let cgImage = inputImage.cgImage { if self.filteredImage == nil, let device = self.device, let cgImage = inputImage.cgImage {
let textureLoader = MTKTextureLoader(device: device) let textureLoader = MTKTextureLoader(device: device)
if let texture = try? textureLoader.newTexture(cgImage: cgImage) { if let texture = try? textureLoader.newTexture(cgImage: cgImage, options: [.SRGB : false]) {
self.renderer.consumeTexture(texture, rotation: .rotate0Degrees) self.renderer.consumeTexture(texture, rotation: .rotate0Degrees)
self.renderer.renderFrame() self.renderer.renderFrame()

View File

@ -44,7 +44,6 @@ public final class MediaEditorVideoAVAssetWriter: MediaEditorVideoExportWriter {
private var writer: AVAssetWriter? private var writer: AVAssetWriter?
private var videoInput: AVAssetWriterInput? private var videoInput: AVAssetWriterInput?
private var audioInput: AVAssetWriterInput? private var audioInput: AVAssetWriterInput?
private var adaptor: AVAssetWriterInputPixelBufferAdaptor! private var adaptor: AVAssetWriterInputPixelBufferAdaptor!
func setup(configuration: MediaEditorVideoExport.Configuration, outputPath: String) { func setup(configuration: MediaEditorVideoExport.Configuration, outputPath: String) {
@ -83,8 +82,6 @@ public final class MediaEditorVideoAVAssetWriter: MediaEditorVideoExportWriter {
if writer.canAdd(videoInput) { if writer.canAdd(videoInput) {
writer.add(videoInput) writer.add(videoInput)
} else {
//throw Error.cannotAddVideoInput
} }
self.videoInput = videoInput self.videoInput = videoInput
} }

View File

@ -31,6 +31,9 @@ swift_library(
"//submodules/Components/LottieAnimationComponent:LottieAnimationComponent", "//submodules/Components/LottieAnimationComponent:LottieAnimationComponent",
"//submodules/TelegramUI/Components/MessageInputPanelComponent", "//submodules/TelegramUI/Components/MessageInputPanelComponent",
"//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode", "//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode",
"//submodules/TooltipUI",
"//submodules/Components/BlurredBackgroundComponent",
"//submodules/AvatarNode",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -16,6 +16,9 @@ import Photos
import LottieAnimationComponent import LottieAnimationComponent
import MessageInputPanelComponent import MessageInputPanelComponent
import EntityKeyboard import EntityKeyboard
import TooltipUI
import BlurredBackgroundComponent
import AvatarNode
enum DrawingScreenType { enum DrawingScreenType {
case drawing case drawing
@ -23,6 +26,7 @@ enum DrawingScreenType {
case sticker case sticker
} }
private let privacyButtonTag = GenericComponentViewTag()
private let muteButtonTag = GenericComponentViewTag() private let muteButtonTag = GenericComponentViewTag()
private let saveButtonTag = GenericComponentViewTag() private let saveButtonTag = GenericComponentViewTag()
@ -31,17 +35,20 @@ final class MediaEditorScreenComponent: Component {
let context: AccountContext let context: AccountContext
let mediaEditor: MediaEditor? let mediaEditor: MediaEditor?
let privacy: EngineStoryPrivacy
let openDrawing: (DrawingScreenType) -> Void let openDrawing: (DrawingScreenType) -> Void
let openTools: () -> Void let openTools: () -> Void
init( init(
context: AccountContext, context: AccountContext,
mediaEditor: MediaEditor?, mediaEditor: MediaEditor?,
privacy: EngineStoryPrivacy,
openDrawing: @escaping (DrawingScreenType) -> Void, openDrawing: @escaping (DrawingScreenType) -> Void,
openTools: @escaping () -> Void openTools: @escaping () -> Void
) { ) {
self.context = context self.context = context
self.mediaEditor = mediaEditor self.mediaEditor = mediaEditor
self.privacy = privacy
self.openDrawing = openDrawing self.openDrawing = openDrawing
self.openTools = openTools self.openTools = openTools
} }
@ -50,6 +57,9 @@ final class MediaEditorScreenComponent: Component {
if lhs.context !== rhs.context { if lhs.context !== rhs.context {
return false return false
} }
if lhs.privacy != rhs.privacy {
return false
}
return true return true
} }
@ -139,8 +149,9 @@ final class MediaEditorScreenComponent: Component {
private let scrubber = ComponentView<Empty>() private let scrubber = ComponentView<Empty>()
private let saveButton = ComponentView<Empty>() private let privacyButton = ComponentView<Empty>()
private let muteButton = ComponentView<Empty>() private let muteButton = ComponentView<Empty>()
private let saveButton = ComponentView<Empty>()
private var component: MediaEditorScreenComponent? private var component: MediaEditorScreenComponent?
private weak var state: State? private weak var state: State?
@ -199,6 +210,11 @@ final class MediaEditorScreenComponent: Component {
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
view.layer.animateScale(from: 0.1, 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() { func animateOutToCamera() {
@ -243,6 +259,11 @@ final class MediaEditorScreenComponent: Component {
transition.setAlpha(view: view, alpha: 0.0) transition.setAlpha(view: view, alpha: 0.0)
transition.setScale(view: view, scale: 0.1) 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() { func animateOutToTool() {
@ -285,6 +306,11 @@ final class MediaEditorScreenComponent: Component {
transition.setAlpha(view: view, alpha: 0.0) transition.setAlpha(view: view, alpha: 0.0)
transition.setScale(view: view, scale: 0.1) 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() { func animateInFromTool() {
@ -327,6 +353,11 @@ final class MediaEditorScreenComponent: Component {
transition.setAlpha(view: view, alpha: 1.0) transition.setAlpha(view: view, alpha: 1.0)
transition.setScale(view: view, scale: 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 { func update(component: MediaEditorScreenComponent, availableSize: CGSize, state: State, environment: Environment<ViewControllerComponentContainer.Environment>, transition: Transition) -> CGSize {
@ -592,6 +623,49 @@ final class MediaEditorScreenComponent: Component {
transition.setFrame(view: inputPanelView, frame: inputPanelFrame) 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"
}
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( let saveButtonSize = self.saveButton.update(
transition: transition, transition: transition,
component: AnyComponent(Button( component: AnyComponent(Button(
@ -625,9 +699,9 @@ final class MediaEditorScreenComponent: Component {
if let saveButtonView = self.saveButton.view { if let saveButtonView = self.saveButton.view {
if saveButtonView.superview == nil { if saveButtonView.superview == nil {
saveButtonView.layer.shadowOffset = CGSize(width: 0.0, height: 0.0) saveButtonView.layer.shadowOffset = CGSize(width: 0.0, height: 0.0)
saveButtonView.layer.shadowRadius = 2.0 saveButtonView.layer.shadowRadius = 4.0
saveButtonView.layer.shadowColor = UIColor.black.cgColor saveButtonView.layer.shadowColor = UIColor.black.cgColor
saveButtonView.layer.shadowOpacity = 0.25 saveButtonView.layer.shadowOpacity = 0.2
self.addSubview(saveButtonView) self.addSubview(saveButtonView)
} }
transition.setPosition(view: saveButtonView, position: saveButtonFrame.center) transition.setPosition(view: saveButtonView, position: saveButtonFrame.center)
@ -669,9 +743,9 @@ final class MediaEditorScreenComponent: Component {
if let muteButtonView = self.muteButton.view { if let muteButtonView = self.muteButton.view {
if muteButtonView.superview == nil { if muteButtonView.superview == nil {
muteButtonView.layer.shadowOffset = CGSize(width: 0.0, height: 0.0) muteButtonView.layer.shadowOffset = CGSize(width: 0.0, height: 0.0)
muteButtonView.layer.shadowRadius = 2.0 muteButtonView.layer.shadowRadius = 4.0
muteButtonView.layer.shadowColor = UIColor.black.cgColor muteButtonView.layer.shadowColor = UIColor.black.cgColor
muteButtonView.layer.shadowOpacity = 0.25 muteButtonView.layer.shadowOpacity = 0.2
//self.addSubview(muteButtonView) //self.addSubview(muteButtonView)
} }
transition.setPosition(view: muteButtonView, position: muteButtonFrame.center) transition.setPosition(view: muteButtonView, position: muteButtonFrame.center)
@ -735,6 +809,7 @@ public final class MediaEditorScreen: ViewController {
fileprivate var subject: MediaEditorScreen.Subject? fileprivate var subject: MediaEditorScreen.Subject?
private var subjectDisposable: Disposable? private var subjectDisposable: Disposable?
fileprivate var storyPrivacy: EngineStoryPrivacy = EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: [])
private let backgroundDimView: UIView private let backgroundDimView: UIView
fileprivate let componentHost: ComponentView<ViewControllerComponentContainer.Environment> fileprivate let componentHost: ComponentView<ViewControllerComponentContainer.Environment>
@ -974,6 +1049,10 @@ public final class MediaEditorScreen: ViewController {
} }
} }
} }
Queue.mainQueue().after(0.5) {
self.presentPrivacyTooltip()
}
} }
func animateOut(finished: Bool, completion: @escaping () -> Void) { func animateOut(finished: Bool, completion: @escaping () -> Void) {
@ -1041,6 +1120,21 @@ public final class MediaEditorScreen: ViewController {
} }
} }
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? { override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = super.hitTest(point, with: event) let result = super.hitTest(point, with: event)
if result == self.componentHost.view { if result == self.componentHost.view {
@ -1051,6 +1145,12 @@ public final class MediaEditorScreen: ViewController {
return result return result
} }
func requestUpdate() {
if let layout = self.validLayout {
self.containerLayoutUpdated(layout: layout, transition: .immediate)
}
}
private var drawingScreen: DrawingScreen? private var drawingScreen: DrawingScreen?
func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, animateOut: Bool = false, transition: Transition) { func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, animateOut: Bool = false, transition: Transition) {
guard let _ = self.controller else { guard let _ = self.controller else {
@ -1090,6 +1190,7 @@ public final class MediaEditorScreen: ViewController {
MediaEditorScreenComponent( MediaEditorScreenComponent(
context: self.context, context: self.context,
mediaEditor: self.mediaEditor, mediaEditor: self.mediaEditor,
privacy: self.storyPrivacy,
openDrawing: { [weak self] mode in openDrawing: { [weak self] mode in
if let self { 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) 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)
@ -1291,6 +1392,81 @@ public final class MediaEditorScreen: ViewController {
super.displayNodeDidLoad() super.displayNodeDidLoad()
} }
func presentPrivacySettings() {
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) { func requestDismiss(animated: Bool) {
self.cancelled() self.cancelled()
@ -1298,7 +1474,7 @@ public final class MediaEditorScreen: ViewController {
self?.dismiss() self?.dismiss()
}) })
} }
func requestCompletion(caption: NSAttributedString, animated: Bool) { func requestCompletion(caption: NSAttributedString, animated: Bool) {
guard let mediaEditor = self.node.mediaEditor, let subject = self.node.subject else { guard let mediaEditor = self.node.mediaEditor, let subject = self.node.subject else {
return return
@ -1383,32 +1559,56 @@ public final class MediaEditorScreen: ViewController {
} }
if mediaEditor.resultIsVideo { if mediaEditor.resultIsVideo {
let exportSubject: MediaEditorVideoExport.Subject let exportSubject: Signal<MediaEditorVideoExport.Subject, NoError>
switch subject { switch subject {
case let .video(path, _): case let .video(path, _):
let asset = AVURLAsset(url: NSURL(fileURLWithPath: path) as URL) let asset = AVURLAsset(url: NSURL(fileURLWithPath: path) as URL)
exportSubject = .video(asset) exportSubject = .single(.video(asset))
case let .image(image, _): case let .image(image, _):
exportSubject = .image(image) exportSubject = .single(.image(image))
default: case let .asset(asset):
fatalError() 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 configuration = recommendedVideoExportConfiguration(values: mediaEditor.values) let _ = exportSubject.start(next: { [weak self] exportSubject in
let outputPath = NSTemporaryDirectory() + "\(Int64.random(in: 0 ..< .max)).mp4" guard let self else {
let videoExport = MediaEditorVideoExport(account: self.context.account, subject: exportSubject, configuration: configuration, outputPath: outputPath) return
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)
}
} }
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 { } else {
if let image = mediaEditor.resultImage { if let image = mediaEditor.resultImage {
@ -1429,3 +1629,70 @@ public final class MediaEditorScreen: ViewController {
(self.displayNode as! Node).containerLayoutUpdated(layout: layout, transition: 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
}
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "close.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,90 @@
%PDF-1.7
1 0 obj
<< /ExtGState << /E1 << /ca 0.500000 >> >> >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
/E1 gs
1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm
1.000000 1.000000 1.000000 scn
8.000000 0.000000 m
12.418278 0.000000 16.000000 3.581722 16.000000 8.000000 c
16.000000 12.418278 12.418278 16.000000 8.000000 16.000000 c
3.581722 16.000000 0.000000 12.418278 0.000000 8.000000 c
0.000000 3.581722 3.581722 0.000000 8.000000 0.000000 c
h
4.469631 11.530369 m
4.762524 11.823262 5.237398 11.823262 5.530291 11.530369 c
7.999961 9.060699 l
10.469630 11.530369 l
10.762524 11.823262 11.237397 11.823262 11.530291 11.530369 c
11.823184 11.237476 11.823184 10.762602 11.530291 10.469709 c
9.060621 8.000039 l
11.530291 5.530370 l
11.823184 5.237476 11.823184 4.762603 11.530291 4.469709 c
11.237397 4.176816 10.762524 4.176816 10.469630 4.469709 c
7.999961 6.939379 l
5.530291 4.469709 l
5.237398 4.176816 4.762524 4.176816 4.469631 4.469709 c
4.176738 4.762603 4.176738 5.237476 4.469631 5.530370 c
6.939301 8.000039 l
4.469631 10.469709 l
4.176738 10.762602 4.176738 11.237476 4.469631 11.530369 c
h
f*
n
Q
endstream
endobj
3 0 obj
1048
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 16.000000 16.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000074 00000 n
0000001178 00000 n
0000001201 00000 n
0000001374 00000 n
0000001448 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1507
%%EOF

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "Recipient.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,114 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 4.330078 -0.830017 cm
1.000000 1.000000 1.000000 scn
-0.830000 1.660004 m
-0.830000 1.201607 -0.458396 0.830004 0.000000 0.830004 c
0.458396 0.830004 0.830000 1.201607 0.830000 1.660004 c
-0.830000 1.660004 l
h
0.830000 9.160004 m
0.830000 9.618400 0.458396 9.990004 0.000000 9.990004 c
-0.458396 9.990004 -0.830000 9.618400 -0.830000 9.160004 c
0.830000 9.160004 l
h
0.830000 1.660004 m
0.830000 9.160004 l
-0.830000 9.160004 l
-0.830000 1.660004 l
0.830000 1.660004 l
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 0.830078 4.507935 cm
1.000000 1.000000 1.000000 scn
-0.586899 2.408951 m
-0.911034 2.084816 -0.911034 1.559289 -0.586899 1.235153 c
-0.262763 0.911018 0.262763 0.911018 0.586899 1.235153 c
-0.586899 2.408951 l
h
3.500000 5.322052 m
4.086899 5.908951 l
3.762764 6.233086 3.237236 6.233086 2.913101 5.908951 c
3.500000 5.322052 l
h
6.413101 1.235153 m
6.737236 0.911018 7.262764 0.911018 7.586899 1.235153 c
7.911034 1.559289 7.911034 2.084816 7.586899 2.408951 c
6.413101 1.235153 l
h
0.586899 1.235153 m
4.086899 4.735153 l
2.913101 5.908951 l
-0.586899 2.408951 l
0.586899 1.235153 l
h
2.913101 4.735153 m
6.413101 1.235153 l
7.586899 2.408951 l
4.086899 5.908951 l
2.913101 4.735153 l
h
f
n
Q
endstream
endobj
3 0 obj
1279
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 8.660156 10.660004 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000001369 00000 n
0000001392 00000 n
0000001564 00000 n
0000001638 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1697
%%EOF

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -135,6 +135,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
private let animatedStickerNode: AnimatedStickerNode private let animatedStickerNode: AnimatedStickerNode
private var downArrowsNode: DownArrowsIconNode? private var downArrowsNode: DownArrowsIconNode?
private let textNode: ImmediateTextNode private let textNode: ImmediateTextNode
private let closeButtonNode: HighlightableButtonNode
private var isArrowInverted: Bool = false private var isArrowInverted: Bool = false
@ -142,7 +143,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
private var validLayout: ContainerViewLayout? private var validLayout: ContainerViewLayout?
init(account: Account, sharedContext: SharedAccountContext, text: String, textEntities: [MessageTextEntity], style: TooltipScreen.Style, icon: TooltipScreen.Icon?, customContentNode: TooltipCustomContentNode? = nil, location: TooltipScreen.Location, displayDuration: TooltipScreen.DisplayDuration, inset: CGFloat = 13.0, shouldDismissOnTouch: @escaping (CGPoint) -> TooltipScreen.DismissOnTouch, requestDismiss: @escaping () -> Void, openActiveTextItem: ((TooltipActiveTextItem, TooltipActiveTextAction) -> Void)?) { init(account: Account, sharedContext: SharedAccountContext, text: String, textEntities: [MessageTextEntity], style: TooltipScreen.Style, icon: TooltipScreen.Icon? = nil, customContentNode: TooltipCustomContentNode? = nil, location: TooltipScreen.Location, displayDuration: TooltipScreen.DisplayDuration, inset: CGFloat = 13.0, shouldDismissOnTouch: @escaping (CGPoint) -> TooltipScreen.DismissOnTouch, requestDismiss: @escaping () -> Void, openActiveTextItem: ((TooltipActiveTextItem, TooltipActiveTextAction) -> Void)?) {
self.tooltipStyle = style self.tooltipStyle = style
self.icon = icon self.icon = icon
self.customContentNode = customContentNode self.customContentNode = customContentNode
@ -326,6 +327,9 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
self.downArrowsNode = DownArrowsIconNode() self.downArrowsNode = DownArrowsIconNode()
} }
self.closeButtonNode = HighlightableButtonNode()
self.closeButtonNode.setImage(UIImage(bundleImageName: "Components/Close"), for: .normal)
super.init() super.init()
self.containerNode.addSubnode(self.backgroundContainerNode) self.containerNode.addSubnode(self.backgroundContainerNode)
@ -338,6 +342,11 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
} }
self.containerNode.addSubnode(self.textNode) self.containerNode.addSubnode(self.textNode)
self.containerNode.addSubnode(self.animatedStickerNode) self.containerNode.addSubnode(self.animatedStickerNode)
if case .manual = displayDuration {
self.containerNode.addSubnode(self.closeButtonNode)
}
if let downArrowsNode = self.downArrowsNode { if let downArrowsNode = self.downArrowsNode {
self.containerNode.addSubnode(downArrowsNode) self.containerNode.addSubnode(downArrowsNode)
} }
@ -402,6 +411,12 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
openActiveTextItem?(.hashtag(hashtag.hashtag), .longTap) openActiveTextItem?(.hashtag(hashtag.hashtag), .longTap)
} }
} }
self.closeButtonNode.addTarget(self, action: #selector(self.closePressed), forControlEvents: .touchUpInside)
}
@objc private func closePressed() {
self.requestDismiss()
} }
func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
@ -453,7 +468,10 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
var invertArrow = false var invertArrow = false
switch self.location { switch self.location {
case let .point(rect, arrowPosition): case let .point(rect, arrowPosition):
let backgroundWidth = textSize.width + contentInset * 2.0 + animationSize.width + animationSpacing var backgroundWidth = textSize.width + contentInset * 2.0 + animationSize.width + animationSpacing
if self.closeButtonNode.supernode != nil {
backgroundWidth += 24.0
}
switch arrowPosition { switch arrowPosition {
case .bottom, .top: case .bottom, .top:
backgroundFrame = CGRect(origin: CGPoint(x: rect.midX - backgroundWidth / 2.0, y: rect.minY - bottomInset - backgroundHeight), size: CGSize(width: backgroundWidth, height: backgroundHeight)) backgroundFrame = CGRect(origin: CGPoint(x: rect.midX - backgroundWidth / 2.0, y: rect.minY - bottomInset - backgroundHeight), size: CGSize(width: backgroundWidth, height: backgroundHeight))
@ -533,7 +551,11 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
self.arrowNode.isHidden = true self.arrowNode.isHidden = true
} }
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: contentInset + animationSize.width + animationSpacing, y: floor((backgroundHeight - textSize.height) / 2.0)), size: textSize)) let textFrame = CGRect(origin: CGPoint(x: contentInset + animationSize.width + animationSpacing, y: floor((backgroundHeight - textSize.height) / 2.0)), size: textSize)
transition.updateFrame(node: self.textNode, frame: textFrame)
let closeSize = CGSize(width: 44.0, height: 44.0)
transition.updateFrame(node: self.closeButtonNode, frame: CGRect(origin: CGPoint(x: textFrame.maxX - 6.0, y: floor((backgroundHeight - closeSize.height) / 2.0)), size: closeSize))
let animationFrame = CGRect(origin: CGPoint(x: contentInset - animationInset, y: contentVerticalInset - animationInset), size: CGSize(width: animationSize.width + animationInset * 2.0, height: animationSize.height + animationInset * 2.0)) let animationFrame = CGRect(origin: CGPoint(x: contentInset - animationInset, y: contentVerticalInset - animationInset), size: CGSize(width: animationSize.width + animationInset * 2.0, height: animationSize.height + animationInset * 2.0))
transition.updateFrame(node: self.animatedStickerNode, frame: animationFrame) transition.updateFrame(node: self.animatedStickerNode, frame: animationFrame)
@ -557,6 +579,10 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
eventIsPresses = event.type == .presses eventIsPresses = event.type == .presses
} }
if event.type == .touches || eventIsPresses { if event.type == .touches || eventIsPresses {
if case .manual = self.displayDuration {
self.requestDismiss()
return self.view
}
switch self.shouldDismissOnTouch(point) { switch self.shouldDismissOnTouch(point) {
case .ignore: case .ignore:
break break
@ -680,6 +706,7 @@ public final class TooltipScreen: ViewController {
case `default` case `default`
case custom(Double) case custom(Double)
case infinite case infinite
case manual
} }
public enum Style { public enum Style {
@ -722,7 +749,20 @@ public final class TooltipScreen: ViewController {
public var alwaysVisible = false public var alwaysVisible = false
public init(account: Account, sharedContext: SharedAccountContext, text: String, textEntities: [MessageTextEntity] = [], style: TooltipScreen.Style = .default, icon: TooltipScreen.Icon?, customContentNode: TooltipCustomContentNode? = nil, location: TooltipScreen.Location, displayDuration: DisplayDuration = .default, inset: CGFloat = 13.0, shouldDismissOnTouch: @escaping (CGPoint) -> TooltipScreen.DismissOnTouch, openActiveTextItem: ((TooltipActiveTextItem, TooltipActiveTextAction) -> Void)? = nil) { public init(
account: Account,
sharedContext: SharedAccountContext,
text: String,
textEntities: [MessageTextEntity] = [],
style: TooltipScreen.Style = .default,
icon: TooltipScreen.Icon? = nil,
customContentNode: TooltipCustomContentNode? = nil,
location: TooltipScreen.Location,
displayDuration: DisplayDuration = .default,
inset: CGFloat = 13.0,
shouldDismissOnTouch: @escaping (CGPoint) -> TooltipScreen.DismissOnTouch,
openActiveTextItem: ((TooltipActiveTextItem, TooltipActiveTextAction) -> Void)? = nil
) {
self.account = account self.account = account
self.sharedContext = sharedContext self.sharedContext = sharedContext
self.text = text self.text = text
@ -766,7 +806,7 @@ public final class TooltipScreen: ViewController {
timeout = 5.0 timeout = 5.0
case let .custom(value): case let .custom(value):
timeout = value timeout = value
case .infinite: case .infinite, .manual:
return return
} }