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)
}
if !initialContent.contains(where: { slice in
return !slice.items.isEmpty
}) {
if initialFocusedId == AnyHashable(self.context.account.peerId), let firstItem = initialContent.first, firstItem.id == initialFocusedId && firstItem.items.isEmpty {
if let rootController = self.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface {
rootController.openStoryCamera(transitionIn: cameraTransitionIn, transitionOut: { [weak self] _ in
guard let self else {

View File

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

View File

@ -14,6 +14,7 @@ import Camera
import MultilineTextComponent
import BlurredBackgroundComponent
import Photos
import LottieAnimationComponent
let videoRedColor = UIColor(rgb: 0xff3b30)
@ -284,17 +285,39 @@ private final class CameraScreenComponent: CombinedComponent {
.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(
component: CameraButton(
content: AnyComponent(Image(
image: state.image(.flash)
)),
content: AnyComponent(
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
guard let state else {
return
}
if state.cameraState.flashMode == .off {
state.camera.setFlashMode(.on)
} else if state.cameraState.flashMode == .on {
state.camera.setFlashMode(.auto)
} else {
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)
}
}
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) {
@ -928,11 +958,11 @@ public class CameraScreen: ViewController {
func animateOutToEditor() {
let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut))
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)
}
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)
}
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))
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)
}
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)
}
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)
}
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
dismissGalleryControllerImpl?()
if let self {
self.node.animateOutToEditor()
self.completion(.single(.asset(asset)))
}
})

View File

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

View File

@ -113,7 +113,7 @@ final class MediaEditorComposer {
}
if self.filteredImage == nil, let device = self.device, let cgImage = inputImage.cgImage {
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.renderFrame()

View File

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

View File

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

View File

@ -16,6 +16,9 @@ import Photos
import LottieAnimationComponent
import MessageInputPanelComponent
import EntityKeyboard
import TooltipUI
import BlurredBackgroundComponent
import AvatarNode
enum DrawingScreenType {
case drawing
@ -23,6 +26,7 @@ enum DrawingScreenType {
case sticker
}
private let privacyButtonTag = GenericComponentViewTag()
private let muteButtonTag = GenericComponentViewTag()
private let saveButtonTag = GenericComponentViewTag()
@ -31,17 +35,20 @@ final class MediaEditorScreenComponent: Component {
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
}
@ -50,6 +57,9 @@ final class MediaEditorScreenComponent: Component {
if lhs.context !== rhs.context {
return false
}
if lhs.privacy != rhs.privacy {
return false
}
return true
}
@ -139,8 +149,9 @@ final class MediaEditorScreenComponent: Component {
private let scrubber = ComponentView<Empty>()
private let saveButton = 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?
@ -199,6 +210,11 @@ final class MediaEditorScreenComponent: Component {
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() {
@ -243,6 +259,11 @@ final class MediaEditorScreenComponent: Component {
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() {
@ -285,6 +306,11 @@ final class MediaEditorScreenComponent: Component {
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() {
@ -327,6 +353,11 @@ final class MediaEditorScreenComponent: Component {
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 {
@ -592,6 +623,49 @@ final class MediaEditorScreenComponent: Component {
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(
transition: transition,
component: AnyComponent(Button(
@ -625,9 +699,9 @@ final class MediaEditorScreenComponent: Component {
if let saveButtonView = self.saveButton.view {
if saveButtonView.superview == nil {
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.shadowOpacity = 0.25
saveButtonView.layer.shadowOpacity = 0.2
self.addSubview(saveButtonView)
}
transition.setPosition(view: saveButtonView, position: saveButtonFrame.center)
@ -669,9 +743,9 @@ final class MediaEditorScreenComponent: Component {
if let muteButtonView = self.muteButton.view {
if muteButtonView.superview == nil {
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.shadowOpacity = 0.25
muteButtonView.layer.shadowOpacity = 0.2
//self.addSubview(muteButtonView)
}
transition.setPosition(view: muteButtonView, position: muteButtonFrame.center)
@ -735,6 +809,7 @@ public final class MediaEditorScreen: ViewController {
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>
@ -974,6 +1049,10 @@ public final class MediaEditorScreen: ViewController {
}
}
}
Queue.mainQueue().after(0.5) {
self.presentPrivacyTooltip()
}
}
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? {
let result = super.hitTest(point, with: event)
if result == self.componentHost.view {
@ -1051,6 +1145,12 @@ public final class MediaEditorScreen: ViewController {
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 {
@ -1090,6 +1190,7 @@ public final class MediaEditorScreen: ViewController {
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)
@ -1291,6 +1392,81 @@ public final class MediaEditorScreen: ViewController {
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) {
self.cancelled()
@ -1383,17 +1559,40 @@ public final class MediaEditorScreen: ViewController {
}
if mediaEditor.resultIsVideo {
let exportSubject: MediaEditorVideoExport.Subject
let exportSubject: Signal<MediaEditorVideoExport.Subject, NoError>
switch subject {
case let .video(path, _):
let asset = AVURLAsset(url: NSURL(fileURLWithPath: path) as URL)
exportSubject = .video(asset)
exportSubject = .single(.video(asset))
case let .image(image, _):
exportSubject = .image(image)
default:
fatalError()
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)
@ -1410,6 +1609,7 @@ public final class MediaEditorScreen: ViewController {
}
}
})
})
} else {
if let image = mediaEditor.resultImage {
makeEditorImageComposition(account: self.context.account, inputImage: image, dimensions: storyDimensions, values: mediaEditor.values, time: .zero, completion: { resultImage in
@ -1429,3 +1629,70 @@ public final class MediaEditorScreen: ViewController {
(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 var downArrowsNode: DownArrowsIconNode?
private let textNode: ImmediateTextNode
private let closeButtonNode: HighlightableButtonNode
private var isArrowInverted: Bool = false
@ -142,7 +143,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
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.icon = icon
self.customContentNode = customContentNode
@ -326,6 +327,9 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
self.downArrowsNode = DownArrowsIconNode()
}
self.closeButtonNode = HighlightableButtonNode()
self.closeButtonNode.setImage(UIImage(bundleImageName: "Components/Close"), for: .normal)
super.init()
self.containerNode.addSubnode(self.backgroundContainerNode)
@ -338,6 +342,11 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
}
self.containerNode.addSubnode(self.textNode)
self.containerNode.addSubnode(self.animatedStickerNode)
if case .manual = displayDuration {
self.containerNode.addSubnode(self.closeButtonNode)
}
if let downArrowsNode = self.downArrowsNode {
self.containerNode.addSubnode(downArrowsNode)
}
@ -402,6 +411,12 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
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) {
@ -453,7 +468,10 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
var invertArrow = false
switch self.location {
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 {
case .bottom, .top:
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
}
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))
transition.updateFrame(node: self.animatedStickerNode, frame: animationFrame)
@ -557,6 +579,10 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
eventIsPresses = event.type == .presses
}
if event.type == .touches || eventIsPresses {
if case .manual = self.displayDuration {
self.requestDismiss()
return self.view
}
switch self.shouldDismissOnTouch(point) {
case .ignore:
break
@ -680,6 +706,7 @@ public final class TooltipScreen: ViewController {
case `default`
case custom(Double)
case infinite
case manual
}
public enum Style {
@ -722,7 +749,20 @@ public final class TooltipScreen: ViewController {
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.sharedContext = sharedContext
self.text = text
@ -766,7 +806,7 @@ public final class TooltipScreen: ViewController {
timeout = 5.0
case let .custom(value):
timeout = value
case .infinite:
case .infinite, .manual:
return
}