[WIP] Multiple story upload

This commit is contained in:
Ilya Laktyushin 2025-04-18 13:47:15 +04:00
parent 746239cf69
commit d8dd96e39e
21 changed files with 1351 additions and 348 deletions

View File

@ -14188,3 +14188,12 @@ Sorry for the inconvenience.";
"SendInviteLink.TextCallsRestrictedMultipleUsers_1" = "{user_list}, and **%d** more person do not accept calls."; "SendInviteLink.TextCallsRestrictedMultipleUsers_1" = "{user_list}, and **%d** more person do not accept calls.";
"SendInviteLink.TextCallsRestrictedMultipleUsers_any" = "{user_list}, and **%d** more people do not accept calls."; "SendInviteLink.TextCallsRestrictedMultipleUsers_any" = "{user_list}, and **%d** more people do not accept calls.";
"SendInviteLink.TextCallsRestrictedSendInviteLink" = "You can try to send an invite link instead."; "SendInviteLink.TextCallsRestrictedSendInviteLink" = "You can try to send an invite link instead.";
"Story.Privacy.ShareStories" = "Share Stories";
"Story.Privacy.PostStoriesAsHeader" = "POST STORIES AS";
"Story.Privacy.WhoCanViewStoriesHeader" = "WHO CAN VIEW THIS STORIES";
"Story.Privacy.PostStories_1" = "Post %@ Story";
"Story.Privacy.PostStories_any" = "Post %@ Stories";
"Story.Privacy.KeepOnMyPageManyInfo" = "Keep these stories on your profile even after they expire in %@. Privacy settings will apply.";
"Story.Privacy.KeepOnChannelPageManyInfo" = "Keep these stories on the channel profile even after they expire in %@.";
"Story.Privacy.KeepOnGroupPageManyInfo" = "Keep these stories on the group page even after they expire in %@.";

View File

@ -809,7 +809,7 @@ public protocol MediaEditorScreenResult {
public protocol TelegramRootControllerInterface: NavigationController { public protocol TelegramRootControllerInterface: NavigationController {
@discardableResult @discardableResult
func openStoryCamera(customTarget: Stories.PendingTarget?, transitionIn: StoryCameraTransitionIn?, transitionedIn: @escaping () -> Void, transitionOut: @escaping (Stories.PendingTarget?, Bool) -> StoryCameraTransitionOut?) -> StoryCameraTransitionInCoordinator? func openStoryCamera(customTarget: Stories.PendingTarget?, transitionIn: StoryCameraTransitionIn?, transitionedIn: @escaping () -> Void, transitionOut: @escaping (Stories.PendingTarget?, Bool) -> StoryCameraTransitionOut?) -> StoryCameraTransitionInCoordinator?
func proceedWithStoryUpload(target: Stories.PendingTarget, result: MediaEditorScreenResult, existingMedia: EngineMedia?, forwardInfo: Stories.PendingForwardInfo?, externalState: MediaEditorTransitionOutExternalState, commit: @escaping (@escaping () -> Void) -> Void) func proceedWithStoryUpload(target: Stories.PendingTarget, results: [MediaEditorScreenResult], existingMedia: EngineMedia?, forwardInfo: Stories.PendingForwardInfo?, externalState: MediaEditorTransitionOutExternalState, commit: @escaping (@escaping () -> Void) -> Void)
func getContactsController() -> ViewController? func getContactsController() -> ViewController?
func getChatsController() -> ViewController? func getChatsController() -> ViewController?
@ -1152,7 +1152,7 @@ public protocol SharedAccountContext: AnyObject {
func makeAvatarMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect?, canDelete: Bool, performDelete: @escaping () -> Void, completion: @escaping (Any?, UIView?, CGRect, UIImage?, Bool, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> ViewController func makeAvatarMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect?, canDelete: Bool, performDelete: @escaping () -> Void, completion: @escaping (Any?, UIView?, CGRect, UIImage?, Bool, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> ViewController
func makeStoryMediaPickerScreen(context: AccountContext, isDark: Bool, forCollage: Bool, selectionLimit: Int?, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, multipleCompletion: @escaping ([Any]) -> Void, dismissed: @escaping () -> Void, groupsPresented: @escaping () -> Void) -> ViewController func makeStoryMediaPickerScreen(context: AccountContext, isDark: Bool, forCollage: Bool, selectionLimit: Int?, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, multipleCompletion: @escaping ([Any], Bool) -> Void, dismissed: @escaping () -> Void, groupsPresented: @escaping () -> Void) -> ViewController
func makeStickerPickerScreen(context: AccountContext, inputData: Promise<StickerPickerInput>, completion: @escaping (FileMediaReference) -> Void) -> ViewController func makeStickerPickerScreen(context: AccountContext, inputData: Promise<StickerPickerInput>, completion: @escaping (FileMediaReference) -> Void) -> ViewController

View File

@ -42,6 +42,7 @@ public struct AttachmentMainButtonState {
public let isEnabled: Bool public let isEnabled: Bool
public let hasShimmer: Bool public let hasShimmer: Bool
public let iconName: String? public let iconName: String?
public let smallSpacing: Bool
public let position: Position? public let position: Position?
public init( public init(
@ -55,6 +56,7 @@ public struct AttachmentMainButtonState {
isEnabled: Bool, isEnabled: Bool,
hasShimmer: Bool, hasShimmer: Bool,
iconName: String? = nil, iconName: String? = nil,
smallSpacing: Bool = false,
position: Position? = nil position: Position? = nil
) { ) {
self.text = text self.text = text
@ -67,6 +69,7 @@ public struct AttachmentMainButtonState {
self.isEnabled = isEnabled self.isEnabled = isEnabled
self.hasShimmer = hasShimmer self.hasShimmer = hasShimmer
self.iconName = iconName self.iconName = iconName
self.smallSpacing = smallSpacing
self.position = position self.position = position
} }

View File

@ -790,6 +790,7 @@ private final class MainButtonNode: HighlightTrackingButtonNode {
iconNode = ASImageNode() iconNode = ASImageNode()
iconNode.displaysAsynchronously = false iconNode.displaysAsynchronously = false
iconNode.image = generateTintedImage(image: UIImage(bundleImageName: iconName), color: state.textColor) iconNode.image = generateTintedImage(image: UIImage(bundleImageName: iconName), color: state.textColor)
self.iconNode = iconNode
self.addSubnode(iconNode) self.addSubnode(iconNode)
} }
if let iconSize = iconNode.image?.size { if let iconSize = iconNode.image?.size {
@ -1806,7 +1807,9 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate {
} else { } else {
height = bounds.height + 8.0 height = bounds.height + 8.0
} }
if !isNarrowButton { if isTwoVerticalButtons && self.secondaryButtonState.smallSpacing {
} else if !isNarrowButton {
height += 9.0 height += 9.0
} }
if isTwoVerticalButtons { if isTwoVerticalButtons {
@ -1896,7 +1899,8 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate {
mainButtonFrame = CGRect(origin: CGPoint(x: buttonOriginX, y: buttonOriginY + sideInset + buttonSize.height), size: buttonSize) mainButtonFrame = CGRect(origin: CGPoint(x: buttonOriginX, y: buttonOriginY + sideInset + buttonSize.height), size: buttonSize)
case .bottom: case .bottom:
mainButtonFrame = CGRect(origin: CGPoint(x: buttonOriginX, y: buttonOriginY), size: buttonSize) mainButtonFrame = CGRect(origin: CGPoint(x: buttonOriginX, y: buttonOriginY), size: buttonSize)
secondaryButtonFrame = CGRect(origin: CGPoint(x: buttonOriginX, y: buttonOriginY + sideInset + buttonSize.height), size: buttonSize) let buttonSpacing = self.secondaryButtonState.smallSpacing ? 8.0 : sideInset
secondaryButtonFrame = CGRect(origin: CGPoint(x: buttonOriginX, y: buttonOriginY + buttonSpacing + buttonSize.height), size: buttonSize)
case .left: case .left:
secondaryButtonFrame = CGRect(origin: CGPoint(x: buttonOriginX, y: buttonOriginY), size: buttonSize) secondaryButtonFrame = CGRect(origin: CGPoint(x: buttonOriginX, y: buttonOriginY), size: buttonSize)
mainButtonFrame = CGRect(origin: CGPoint(x: buttonOriginX + buttonSize.width + sideInset, y: buttonOriginY), size: buttonSize) mainButtonFrame = CGRect(origin: CGPoint(x: buttonOriginX + buttonSize.width + sideInset, y: buttonOriginY), size: buttonSize)

View File

@ -566,6 +566,14 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
self.hasSelectionChanged(false) self.hasSelectionChanged(false)
} }
public func clearAll() {
for case let view as DrawingEntityView in self.subviews {
view.reset()
view.selectionView?.removeFromSuperview()
view.removeFromSuperview()
}
}
private func clear(animated: Bool = false) { private func clear(animated: Bool = false) {
if animated { if animated {
for case let view as DrawingEntityView in self.subviews { for case let view as DrawingEntityView in self.subviews {

View File

@ -184,7 +184,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
private let bannedSendVideos: (Int32, Bool)? private let bannedSendVideos: (Int32, Bool)?
private let canBoostToUnrestrict: Bool private let canBoostToUnrestrict: Bool
fileprivate let paidMediaAllowed: Bool fileprivate let paidMediaAllowed: Bool
private let subject: Subject fileprivate let subject: Subject
fileprivate let forCollage: Bool fileprivate let forCollage: Bool
private let saveEditedPhotos: Bool private let saveEditedPhotos: Bool
@ -1826,6 +1826,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
fileprivate let secondaryButtonStatePromise = Promise<AttachmentMainButtonState?>(nil) fileprivate let secondaryButtonStatePromise = Promise<AttachmentMainButtonState?>(nil)
private let mainButtonAction: (() -> Void)? private let mainButtonAction: (() -> Void)?
private let secondaryButtonAction: (() -> Void)?
public init( public init(
context: AccountContext, context: AccountContext,
@ -1845,7 +1846,8 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
selectionContext: TGMediaSelectionContext? = nil, selectionContext: TGMediaSelectionContext? = nil,
saveEditedPhotos: Bool = false, saveEditedPhotos: Bool = false,
mainButtonState: AttachmentMainButtonState? = nil, mainButtonState: AttachmentMainButtonState? = nil,
mainButtonAction: (() -> Void)? = nil mainButtonAction: (() -> Void)? = nil,
secondaryButtonAction: (() -> Void)? = nil
) { ) {
self.context = context self.context = context
@ -1865,6 +1867,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
self.saveEditedPhotos = saveEditedPhotos self.saveEditedPhotos = saveEditedPhotos
self.mainButtonStatePromise.set(.single(mainButtonState)) self.mainButtonStatePromise.set(.single(mainButtonState))
self.mainButtonAction = mainButtonAction self.mainButtonAction = mainButtonAction
self.secondaryButtonAction = secondaryButtonAction
let selectionContext = selectionContext ?? TGMediaSelectionContext() let selectionContext = selectionContext ?? TGMediaSelectionContext()
@ -1998,7 +2001,14 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
} else if collection == nil { } else if collection == nil {
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed))
var hasSelect = false
if forCollage { if forCollage {
hasSelect = true
} else if case .story = mode {
hasSelect = true
}
if hasSelect {
self.navigationItem.rightBarButtonItem = UIBarButtonItem(backButtonAppearanceWithTitle: self.presentationData.strings.Common_Select, target: self, action: #selector(self.selectPressed)) self.navigationItem.rightBarButtonItem = UIBarButtonItem(backButtonAppearanceWithTitle: self.presentationData.strings.Common_Select, target: self, action: #selector(self.selectPressed))
} else { } else {
if [.createSticker].contains(mode) { if [.createSticker].contains(mode) {
@ -2338,6 +2348,9 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
let transition = ContainedViewLayoutTransition.animated(duration: 0.25, curve: .easeInOut) let transition = ContainedViewLayoutTransition.animated(duration: 0.25, curve: .easeInOut)
var moreIsVisible = false var moreIsVisible = false
if case let .assets(_, mode) = self.subject, [.story, .createSticker].contains(mode) { if case let .assets(_, mode) = self.subject, [.story, .createSticker].contains(mode) {
if count == 1 {
self.requestAttachmentMenuExpansion()
}
moreIsVisible = true moreIsVisible = true
} else if case let .media(media) = self.subject { } else if case let .media(media) = self.subject {
self.titleView.title = media.count == 1 ? self.presentationData.strings.Attachment_Pasteboard : self.presentationData.strings.Attachment_SelectedMedia(count) self.titleView.title = media.count == 1 ? self.presentationData.strings.Attachment_Pasteboard : self.presentationData.strings.Attachment_SelectedMedia(count)
@ -2381,7 +2394,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
transition.updateAlpha(node: self.moreButtonNode.iconNode, alpha: moreIsVisible ? 1.0 : 0.0) transition.updateAlpha(node: self.moreButtonNode.iconNode, alpha: moreIsVisible ? 1.0 : 0.0)
transition.updateTransformScale(node: self.moreButtonNode.iconNode, scale: moreIsVisible ? 1.0 : 0.1) transition.updateTransformScale(node: self.moreButtonNode.iconNode, scale: moreIsVisible ? 1.0 : 0.1)
if self.selectionCount > 0 { if case .assets(_, .story) = self.subject, self.selectionCount > 0 {
//TODO:localize //TODO:localize
var text = "Create 1 Story" var text = "Create 1 Story"
if self.selectionCount > 1 { if self.selectionCount > 1 {
@ -2390,7 +2403,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
self.mainButtonStatePromise.set(.single(AttachmentMainButtonState(text: text, badge: nil, font: .bold, background: .color(self.presentationData.theme.actionSheet.controlAccentColor), textColor: self.presentationData.theme.list.itemCheckColors.foregroundColor, isVisible: true, progress: .none, isEnabled: true, hasShimmer: false, position: .top))) self.mainButtonStatePromise.set(.single(AttachmentMainButtonState(text: text, badge: nil, font: .bold, background: .color(self.presentationData.theme.actionSheet.controlAccentColor), textColor: self.presentationData.theme.list.itemCheckColors.foregroundColor, isVisible: true, progress: .none, isEnabled: true, hasShimmer: false, position: .top)))
if self.selectionCount > 1 && self.selectionCount <= 6 { if self.selectionCount > 1 && self.selectionCount <= 6 {
self.secondaryButtonStatePromise.set(.single(AttachmentMainButtonState(text: "Combine into Collage", badge: nil, font: .regular, background: .color(.clear), textColor: self.presentationData.theme.actionSheet.controlAccentColor, isVisible: true, progress: .none, isEnabled: true, hasShimmer: false, iconName: "Media Editor/Collage", position: .bottom))) self.secondaryButtonStatePromise.set(.single(AttachmentMainButtonState(text: "Combine into Collage", badge: nil, font: .regular, background: .color(.clear), textColor: self.presentationData.theme.actionSheet.controlAccentColor, isVisible: true, progress: .none, isEnabled: true, hasShimmer: false, iconName: "Media Editor/Collage", smallSpacing: true, position: .bottom)))
} else { } else {
self.secondaryButtonStatePromise.set(.single(nil)) self.secondaryButtonStatePromise.set(.single(nil))
} }
@ -2427,6 +2440,10 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
self.mainButtonAction?() self.mainButtonAction?()
} }
func secondaryButtonPressed() {
self.secondaryButtonAction?()
}
func dismissAllTooltips() { func dismissAllTooltips() {
self.undoOverlayController?.dismissWithCommitAction() self.undoOverlayController?.dismissWithCommitAction()
} }
@ -2810,7 +2827,7 @@ final class MediaPickerContext: AttachmentMediaPickerContext {
private weak var controller: MediaPickerScreenImpl? private weak var controller: MediaPickerScreenImpl?
var selectionCount: Signal<Int, NoError> { var selectionCount: Signal<Int, NoError> {
if self.controller?.forCollage == true { if let controller = self.controller, case .assets(_, .story) = controller.subject {
return .single(0) return .single(0)
} else { } else {
return Signal { [weak self] subscriber in return Signal { [weak self] subscriber in
@ -2973,7 +2990,7 @@ final class MediaPickerContext: AttachmentMediaPickerContext {
} }
func secondaryButtonAction() { func secondaryButtonAction() {
self.controller?.mainButtonPressed() self.controller?.secondaryButtonPressed()
} }
} }
@ -3162,7 +3179,7 @@ public func storyMediaPickerController(
selectionLimit: Int?, selectionLimit: Int?,
getSourceRect: @escaping () -> CGRect, getSourceRect: @escaping () -> CGRect,
completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void,
multipleCompletion: @escaping ([Any]) -> Void, multipleCompletion: @escaping ([Any], Bool) -> Void,
dismissed: @escaping () -> Void, dismissed: @escaping () -> Void,
groupsPresented: @escaping () -> Void groupsPresented: @escaping () -> Void
) -> ViewController { ) -> ViewController {
@ -3181,9 +3198,18 @@ public func storyMediaPickerController(
} }
} }
let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, chatLocation: nil, buttons: [.standalone], initialButton: .standalone, fromMenu: false, hasTextInput: false, makeEntityInputView: { let controller = AttachmentController(
return nil context: context,
}) updatedPresentationData: updatedPresentationData,
chatLocation: nil,
buttons: [.standalone],
initialButton: .standalone,
fromMenu: false,
hasTextInput: false,
makeEntityInputView: {
return nil
}
)
controller.forceSourceRect = true controller.forceSourceRect = true
controller.getSourceRect = getSourceRect controller.getSourceRect = getSourceRect
controller.requestController = { _, present in controller.requestController = { _, present in
@ -3207,7 +3233,18 @@ public func storyMediaPickerController(
results.append(asset) results.append(asset)
} }
} }
multipleCompletion(results) multipleCompletion(results, false)
}
},
secondaryButtonAction: { [weak selectionContext] in
if let selectionContext, let selectedItems = selectionContext.selectedItems() {
var results: [Any] = []
for item in selectedItems {
if let item = item as? TGMediaAsset, let asset = item.backingAsset {
results.append(asset)
}
}
multipleCompletion(results, true)
} }
} }
) )

View File

@ -1651,6 +1651,7 @@ public class CameraScreenImpl: ViewController, CameraScreen {
case videoCollage(VideoCollage) case videoCollage(VideoCollage)
case asset(PHAsset) case asset(PHAsset)
case draft(MediaEditorDraft) case draft(MediaEditorDraft)
case assets([PHAsset])
func withPIPPosition(_ position: CameraScreenImpl.PIPPosition) -> Result { func withPIPPosition(_ position: CameraScreenImpl.PIPPosition) -> Result {
switch self { switch self {
@ -3637,11 +3638,10 @@ public class CameraScreenImpl: ViewController, CameraScreen {
selectionLimit = 10 selectionLimit = 10
} }
} }
//TODO:unmock
controller = self.context.sharedContext.makeStoryMediaPickerScreen( controller = self.context.sharedContext.makeStoryMediaPickerScreen(
context: self.context, context: self.context,
isDark: true, isDark: true,
forCollage: self.cameraState.isCollageEnabled || "".isEmpty, forCollage: self.cameraState.isCollageEnabled,
selectionLimit: selectionLimit, selectionLimit: selectionLimit,
getSourceRect: { [weak self] in getSourceRect: { [weak self] in
if let self { if let self {
@ -3707,44 +3707,52 @@ public class CameraScreenImpl: ViewController, CameraScreen {
} }
} }
} }
}, multipleCompletion: { [weak self] results in }, multipleCompletion: { [weak self] results, collage in
guard let self else { guard let self else {
return return
} }
if !self.cameraState.isCollageEnabled { if collage {
var selectedGrid: Camera.CollageGrid = collageGrids.first! if !self.cameraState.isCollageEnabled {
for grid in collageGrids { var selectedGrid: Camera.CollageGrid = collageGrids.first!
if grid.count == results.count { for grid in collageGrids {
selectedGrid = grid if grid.count == results.count {
break selectedGrid = grid
break
}
} }
self.updateCameraState({
$0.updatedIsCollageEnabled(true).updatedCollageProgress(0.0).updatedIsDualCameraEnabled(false).updatedCollageGrid(selectedGrid)
}, transition: .spring(duration: 0.3))
} }
self.updateCameraState({
$0.updatedIsCollageEnabled(true).updatedCollageProgress(0.0).updatedIsDualCameraEnabled(false).updatedCollageGrid(selectedGrid) if let assets = results as? [PHAsset] {
}, transition: .spring(duration: 0.3)) var results: [Signal<CameraScreenImpl.Result, NoError>] = []
} for asset in assets {
if asset.mediaType == .video && asset.duration > 1.0 {
if let assets = results as? [PHAsset] { results.append(.single(.asset(asset)))
var results: [Signal<CameraScreenImpl.Result, NoError>] = [] } else {
for asset in assets { results.append(
if asset.mediaType == .video && asset.duration > 1.0 { assetImage(asset: asset, targetSize: CGSize(width: 1080, height: 1080), exact: false, deliveryMode: .highQualityFormat)
results.append(.single(.asset(asset))) |> runOn(Queue.concurrentDefaultQueue())
} else { |> mapToSignal { image -> Signal<CameraScreenImpl.Result, NoError> in
results.append( if let image {
assetImage(asset: asset, targetSize: CGSize(width: 1080, height: 1080), exact: false, deliveryMode: .highQualityFormat) return .single(.image(Result.Image(image: image, additionalImage: nil, additionalImagePosition: .topLeft)))
|> runOn(Queue.concurrentDefaultQueue()) } else {
|> mapToSignal { image -> Signal<CameraScreenImpl.Result, NoError> in return .complete()
if let image { }
return .single(.image(Result.Image(image: image, additionalImage: nil, additionalImagePosition: .topLeft)))
} else {
return .complete()
} }
} )
) }
} }
self.node.collage?.addResults(signals: results)
}
} else {
if let assets = results as? [PHAsset] {
self.completion(.single(.assets(assets)), nil, {
})
} }
self.node.collage?.addResults(signals: results)
} }
self.galleryController = nil self.galleryController = nil

View File

@ -65,6 +65,8 @@ swift_library(
"//submodules/UrlEscaping", "//submodules/UrlEscaping",
"//submodules/DeviceLocationManager", "//submodules/DeviceLocationManager",
"//submodules/TelegramUI/Components/SaveProgressScreen", "//submodules/TelegramUI/Components/SaveProgressScreen",
"//submodules/TelegramUI/Components/MediaAssetsContext",
"//submodules/CheckNode",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -160,7 +160,7 @@ public extension MediaEditorScreenImpl {
} else { } else {
existingMedia = storyItem.media existingMedia = storyItem.media
} }
rootController.proceedWithStoryUpload(target: target, result: result as! MediaEditorScreenResult, existingMedia: existingMedia, forwardInfo: forwardInfo, externalState: externalState, commit: commit) rootController.proceedWithStoryUpload(target: target, results: [result as! MediaEditorScreenResult], existingMedia: existingMedia, forwardInfo: forwardInfo, externalState: externalState, commit: commit)
} }
}) })
} else { } else {

View File

@ -327,6 +327,7 @@ final class MediaEditorScreenComponent: Component {
private let switchCameraButton = ComponentView<Empty>() private let switchCameraButton = ComponentView<Empty>()
private let selectionButton = ComponentView<Empty>() private let selectionButton = ComponentView<Empty>()
private let selectionPanel = ComponentView<Empty>()
private let textCancelButton = ComponentView<Empty>() private let textCancelButton = ComponentView<Empty>()
private let textDoneButton = ComponentView<Empty>() private let textDoneButton = ComponentView<Empty>()
@ -741,6 +742,13 @@ final class MediaEditorScreenComponent: Component {
return inputText return inputText
} }
func setInputText(_ text: NSAttributedString) {
guard let inputPanelView = self.inputPanel.view as? MessageInputPanelComponent.View else {
return
}
inputPanelView.setSendMessageInput(value: .text(text), updateState: true)
}
private func updateCoverPosition() { private func updateCoverPosition() {
guard let controller = self.environment?.controller() as? MediaEditorScreenImpl, let mediaEditor = controller.node.mediaEditor else { guard let controller = self.environment?.controller() as? MediaEditorScreenImpl, let mediaEditor = controller.node.mediaEditor else {
return return
@ -1993,39 +2001,118 @@ final class MediaEditorScreenComponent: Component {
transition.setAlpha(view: switchCameraButtonView, alpha: isRecordingAdditionalVideo ? 1.0 : 0.0) transition.setAlpha(view: switchCameraButtonView, alpha: isRecordingAdditionalVideo ? 1.0 : 0.0)
} }
if controller.node.items.count > 1 {
let selectionButtonSize = self.selectionButton.update( let selectionButtonSize = self.selectionButton.update(
transition: transition, transition: transition,
component: AnyComponent(PlainButtonComponent( component: AnyComponent(PlainButtonComponent(
content: AnyComponent( content: AnyComponent(
SelectionPanelButtonContentComponent( SelectionPanelButtonContentComponent(
count: 1, count: Int32(controller.node.items.count(where: { $0.isEnabled })),
isSelected: self.isSelectionPanelOpen, isSelected: self.isSelectionPanelOpen,
tag: nil tag: nil
)
),
effectAlignment: .center,
action: { [weak self] in
if let self {
self.isSelectionPanelOpen = !self.isSelectionPanelOpen
self.state?.updated()
}
},
animateAlpha: false
)),
environment: {},
containerSize: CGSize(width: 33.0, height: 33.0)
)
let selectionButtonFrame = CGRect(
origin: CGPoint(x: availableSize.width - selectionButtonSize.width - 12.0, y: inputPanelFrame.minY - selectionButtonSize.height - 3.0),
size: selectionButtonSize
)
if let selectionButtonView = self.selectionButton.view as? PlainButtonComponent.View {
if selectionButtonView.superview == nil {
self.addSubview(selectionButtonView)
}
transition.setPosition(view: selectionButtonView, position: selectionButtonFrame.center)
transition.setBounds(view: selectionButtonView, bounds: CGRect(origin: .zero, size: selectionButtonFrame.size))
transition.setScale(view: selectionButtonView, scale: displayTopButtons ? 1.0 : 0.01)
transition.setAlpha(view: selectionButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities ? 1.0 : 0.0)
if self.isSelectionPanelOpen {
let selectionPanelFrame = CGRect(
origin: CGPoint(x: 12.0, y: inputPanelFrame.minY - selectionButtonSize.height - 3.0 - 130.0),
size: CGSize(width: availableSize.width - 24.0, height: 120.0)
) )
),
effectAlignment: .center, var selectedItemId = ""
action: { [weak self] in if case let .asset(asset) = controller.node.subject {
if let self { selectedItemId = asset.localIdentifier
self.isSelectionPanelOpen = !self.isSelectionPanelOpen
self.state?.updated()
} }
},
animateAlpha: false let _ = self.selectionPanel.update(
)), transition: transition,
environment: {}, component: AnyComponent(
containerSize: CGSize(width: 33.0, height: 33.0) SelectionPanelComponent(
) previewContainerView: controller.node.previewContentContainerView,
let selectionButtonFrame = CGRect( frame: selectionPanelFrame,
origin: CGPoint(x: availableSize.width - selectionButtonSize.width - 12.0, y: max(environment.statusBarHeight + 10.0, inputPanelFrame.minY - selectionButtonSize.height - 3.0)), items: controller.node.items,
size: selectionButtonSize selectedItemId: selectedItemId,
) itemTapped: { [weak self, weak controller] id in
if let selectionButtonView = self.selectionButton.view { guard let self, let controller else {
if selectionButtonView.superview == nil { return
self.addSubview(selectionButtonView) }
self.isSelectionPanelOpen = false
self.state?.updated()
if let id {
controller.node.switchToItem(id)
}
},
itemSelectionToggled: { [weak self, weak controller] id in
guard let self, let controller else {
return
}
if let itemIndex = controller.node.items.firstIndex(where: { $0.asset.localIdentifier == id }) {
controller.node.items[itemIndex].isEnabled = !controller.node.items[itemIndex].isEnabled
}
self.state?.updated(transition: .spring(duration: 0.3))
},
itemReordered: { [weak self, weak controller] fromId, toId in
guard let self, let controller else {
return
}
guard let fromIndex = controller.node.items.firstIndex(where: { $0.asset.localIdentifier == fromId }), let toIndex = controller.node.items.firstIndex(where: { $0.asset.localIdentifier == toId }), toIndex < controller.node.items.count else {
return
}
let fromItem = controller.node.items[fromIndex]
let toItem = controller.node.items[toIndex]
controller.node.items[fromIndex] = toItem
controller.node.items[toIndex] = fromItem
self.state?.updated(transition: .spring(duration: 0.3))
}
)
),
environment: {},
containerSize: availableSize
)
if let selectionPanelView = self.selectionPanel.view as? SelectionPanelComponent.View {
if selectionPanelView.superview == nil {
self.insertSubview(selectionPanelView, belowSubview: selectionButtonView)
if let buttonView = selectionButtonView.contentView as? SelectionPanelButtonContentComponent.View {
selectionPanelView.animateIn(from: buttonView)
}
}
selectionPanelView.frame = CGRect(origin: .zero, size: availableSize)
}
} else if let selectionPanelView = self.selectionPanel.view as? SelectionPanelComponent.View {
if let buttonView = selectionButtonView.contentView as? SelectionPanelButtonContentComponent.View {
selectionPanelView.animateOut(to: buttonView, completion: { [weak selectionPanelView] in
selectionPanelView?.removeFromSuperview()
})
} else {
selectionPanelView.removeFromSuperview()
}
}
} }
transition.setPosition(view: selectionButtonView, position: selectionButtonFrame.center)
transition.setBounds(view: selectionButtonView, bounds: CGRect(origin: .zero, size: selectionButtonFrame.size))
} }
} else { } else {
inputPanelSize = CGSize(width: 0.0, height: 12.0) inputPanelSize = CGSize(width: 0.0, height: 12.0)
@ -2795,6 +2882,38 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
} }
} }
struct EditingItem: Equatable {
let asset: PHAsset
var values: MediaEditorValues?
var caption = NSAttributedString()
var thumbnail: UIImage?
var isEnabled = true
var version: Int = 0
init(asset: PHAsset) {
self.asset = asset
}
public static func ==(lhs: EditingItem, rhs: EditingItem) -> Bool {
if lhs.asset.localIdentifier != rhs.asset.localIdentifier {
return false
}
if lhs.values != rhs.values {
return false
}
if lhs.caption != rhs.caption {
return false
}
if lhs.thumbnail != rhs.thumbnail {
return false
}
if lhs.version != rhs.version {
return false
}
return true
}
}
final class Node: ViewControllerTracingNode, ASGestureRecognizerDelegate, UIScrollViewDelegate { final class Node: ViewControllerTracingNode, ASGestureRecognizerDelegate, UIScrollViewDelegate {
private weak var controller: MediaEditorScreenImpl? private weak var controller: MediaEditorScreenImpl?
private let context: AccountContext private let context: AccountContext
@ -2803,6 +2922,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
var subject: MediaEditorScreenImpl.Subject? var subject: MediaEditorScreenImpl.Subject?
var actualSubject: MediaEditorScreenImpl.Subject? var actualSubject: MediaEditorScreenImpl.Subject?
var items: [EditingItem] = []
private var subjectDisposable: Disposable? private var subjectDisposable: Disposable?
private var appInForegroundDisposable: Disposable? private var appInForegroundDisposable: Disposable?
@ -2891,6 +3011,10 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
private let readyValue = Promise<Bool>() private let readyValue = Promise<Bool>()
var componentHostView: MediaEditorScreenComponent.View? {
return self.componentHost.view as? MediaEditorScreenComponent.View
}
init(controller: MediaEditorScreenImpl) { init(controller: MediaEditorScreenImpl) {
self.controller = controller self.controller = controller
self.context = controller.context self.context = controller.context
@ -3062,7 +3186,46 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|> deliverOnMainQueue |> deliverOnMainQueue
).start(next: { [weak self] subject in ).start(next: { [weak self] subject in
if let self, let subject { if let self, let subject {
self.setup(with: subject) self.actualSubject = subject
var effectiveSubject = subject
switch subject {
case let .assets(assets):
effectiveSubject = .asset(assets.first!)
self.items = assets.map { EditingItem(asset: $0) }
case let .draft(draft, _):
for entity in draft.values.entities {
if case let .sticker(sticker) = entity {
switch sticker.content {
case let .message(ids, _, _, _, _):
effectiveSubject = .message(ids)
case let .gift(gift, _):
effectiveSubject = .gift(gift)
default:
break
}
}
}
default:
break
}
var privacy: MediaEditorResultPrivacy?
var values: MediaEditorValues?
var isDraft = false
if case let .draft(draft, _) = subject {
privacy = draft.privacy
values = draft.values
isDraft = true
}
self.setup(
subject: effectiveSubject,
privacy: privacy,
values: values,
caption: nil,
isDraft: isDraft
)
} }
}) })
@ -3183,35 +3346,24 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
self.stickerCutoutStatusDisposable?.dispose() self.stickerCutoutStatusDisposable?.dispose()
} }
private func setup(with subject: MediaEditorScreenImpl.Subject) { func setup(
subject: MediaEditorScreenImpl.Subject,
privacy: MediaEditorResultPrivacy? = nil,
values: MediaEditorValues?,
caption: NSAttributedString?,
isDraft: Bool = false
) {
guard let controller = self.controller else { guard let controller = self.controller else {
return return
} }
self.actualSubject = subject self.subject = subject
var effectiveSubject = subject
if case let .draft(draft, _ ) = subject {
for entity in draft.values.entities {
if case let .sticker(sticker) = entity {
switch sticker.content {
case let .message(ids, _, _, _, _):
effectiveSubject = .message(ids)
case let .gift(gift, _):
effectiveSubject = .gift(gift)
default:
break
}
}
}
}
self.subject = effectiveSubject
Queue.mainQueue().justDispatch { Queue.mainQueue().justDispatch {
controller.setupAudioSessionIfNeeded() controller.setupAudioSessionIfNeeded()
} }
if case let .draft(draft, _) = subject, let privacy = draft.privacy { if let privacy {
controller.state.privacy = privacy controller.state.privacy = privacy
} }
@ -3229,7 +3381,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
controller.isSavingAvailable = isSavingAvailable controller.isSavingAvailable = isSavingAvailable
controller.requestLayout(transition: .immediate) controller.requestLayout(transition: .immediate)
let mediaDimensions = effectiveSubject.dimensions let mediaDimensions = subject.dimensions
let maxSide: CGFloat = 1920.0 / UIScreen.main.scale let maxSide: CGFloat = 1920.0 / UIScreen.main.scale
let fittedSize = mediaDimensions.cgSize.fitted(CGSize(width: maxSide, height: maxSide)) let fittedSize = mediaDimensions.cgSize.fitted(CGSize(width: maxSide, height: maxSide))
let mediaEntity = DrawingMediaEntity(size: fittedSize) let mediaEntity = DrawingMediaEntity(size: fittedSize)
@ -3268,27 +3420,28 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
} }
let initialValues: MediaEditorValues? let initialValues: MediaEditorValues?
if case let .draft(draft, _) = subject { if let values {
initialValues = draft.values initialValues = values
for entity in draft.values.entities { for entity in values.entities {
self.entitiesView.add(entity.entity.duplicate(copy: true), announce: false) self.entitiesView.add(entity.entity.duplicate(copy: true), announce: false)
} }
if let drawingData = initialValues?.drawing?.pngData() { if let drawingData = values.drawing?.pngData() {
self.drawingView.setup(withDrawing: drawingData) self.drawingView.setup(withDrawing: drawingData)
} }
} else { } else {
initialValues = nil initialValues = nil
} }
var mediaEditorMode: MediaEditor.Mode = .default let mediaEditorMode: MediaEditor.Mode
if case .stickerEditor = controller.mode { switch controller.mode {
case .stickerEditor:
mediaEditorMode = .sticker mediaEditorMode = .sticker
} else if case .avatarEditor = controller.mode { case .avatarEditor, .coverEditor:
mediaEditorMode = .avatar
} else if case .coverEditor = controller.mode {
mediaEditorMode = .avatar mediaEditorMode = .avatar
default:
mediaEditorMode = .default
} }
if let mediaEntityView = self.entitiesView.add(mediaEntity, announce: false) as? DrawingMediaEntityView { if let mediaEntityView = self.entitiesView.add(mediaEntity, announce: false) as? DrawingMediaEntityView {
@ -3314,7 +3467,13 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
} }
} }
let mediaEditor = MediaEditor(context: self.context, mode: mediaEditorMode, subject: effectiveSubject.editorSubject, values: initialValues, hasHistogram: true) let mediaEditor = MediaEditor(
context: self.context,
mode: mediaEditorMode,
subject: subject.editorSubject,
values: initialValues,
hasHistogram: true
)
if case .avatarEditor = controller.mode { if case .avatarEditor = controller.mode {
mediaEditor.setVideoIsMuted(true) mediaEditor.setVideoIsMuted(true)
} else if case let .coverEditor(dimensions) = controller.mode { } else if case let .coverEditor(dimensions) = controller.mode {
@ -3327,7 +3486,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
mediaEditor.seek(initialVideoPosition, andPlay: true) mediaEditor.seek(initialVideoPosition, andPlay: true)
} }
} }
if self.context.sharedContext.currentPresentationData.with({$0}).autoNightModeTriggered { if !isDraft, self.context.sharedContext.currentPresentationData.with({$0}).autoNightModeTriggered {
switch subject { switch subject {
case .message, .gift: case .message, .gift:
mediaEditor.setNightTheme(true) mediaEditor.setNightTheme(true)
@ -3347,46 +3506,48 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
controller.requestLayout(transition: .animated(duration: 0.25, curve: .easeInOut)) controller.requestLayout(transition: .animated(duration: 0.25, curve: .easeInOut))
} }
} }
self.stickerCutoutStatusDisposable = (mediaEditor.cutoutStatus if case .stickerEditor = controller.mode {
|> deliverOnMainQueue).start(next: { [weak self] cutoutStatus in self.stickerCutoutStatusDisposable = (mediaEditor.cutoutStatus
guard let self else { |> deliverOnMainQueue).start(next: { [weak self] cutoutStatus in
return guard let self else {
return
}
self.stickerCutoutStatus = cutoutStatus
self.requestLayout(forceUpdate: true, transition: .easeInOut(duration: 0.25))
})
mediaEditor.maskUpdated = { [weak self] mask, apply in
guard let self else {
return
}
if self.stickerMaskDrawingView == nil {
self.setupMaskDrawingView(size: mask.size)
}
if apply, let maskData = mask.pngData() {
self.stickerMaskDrawingView?.setup(withDrawing: maskData, storeAsClear: true)
}
} }
self.stickerCutoutStatus = cutoutStatus mediaEditor.classificationUpdated = { [weak self] classes in
self.requestLayout(forceUpdate: true, transition: .easeInOut(duration: 0.25)) guard let self else {
}) return
mediaEditor.maskUpdated = { [weak self] mask, apply in }
guard let self else { self.controller?.stickerRecommendedEmoji = emojiForClasses(classes.map { $0.0 })
return
} }
if self.stickerMaskDrawingView == nil {
self.setupMaskDrawingView(size: mask.size)
}
if apply, let maskData = mask.pngData() {
self.stickerMaskDrawingView?.setup(withDrawing: maskData, storeAsClear: true)
}
}
mediaEditor.classificationUpdated = { [weak self] classes in
guard let self else {
return
}
self.controller?.stickerRecommendedEmoji = emojiForClasses(classes.map { $0.0 })
} }
mediaEditor.attachPreviewView(self.previewView, andPlay: !(self.controller?.isEditingStoryCover ?? false)) mediaEditor.attachPreviewView(self.previewView, andPlay: !(self.controller?.isEditingStoryCover ?? false))
if case .empty = effectiveSubject { if case .empty = subject {
self.stickerMaskDrawingView?.emptyColor = .black self.stickerMaskDrawingView?.emptyColor = .black
self.stickerMaskDrawingView?.clearWithEmptyColor() self.stickerMaskDrawingView?.clearWithEmptyColor()
} }
switch effectiveSubject { switch subject {
case .message, .gift: case .message, .gift:
break break
default: default:
self.readyValue.set(.single(true)) self.readyValue.set(.single(true))
} }
switch effectiveSubject { switch subject {
case let .image(_, _, additionalImage, position): case let .image(_, _, additionalImage, position):
if let additionalImage { if let additionalImage {
let image = generateImage(CGSize(width: additionalImage.size.width, height: additionalImage.size.width), contextGenerator: { size, context in let image = generateImage(CGSize(width: additionalImage.size.width, height: additionalImage.size.width), contextGenerator: { size, context in
@ -3431,7 +3592,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
case .message, .gift: case .message, .gift:
var isGift = false var isGift = false
let messages: Signal<[Message], NoError> let messages: Signal<[Message], NoError>
if case let .message(messageIds) = effectiveSubject { if case let .message(messageIds) = subject {
messages = self.context.engine.data.get( messages = self.context.engine.data.get(
EngineDataMap(messageIds.map(TelegramEngine.EngineData.Item.Messages.Message.init(id:))) EngineDataMap(messageIds.map(TelegramEngine.EngineData.Item.Messages.Message.init(id:)))
) )
@ -3444,7 +3605,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
} }
return messages return messages
} }
} else if case let .gift(gift) = effectiveSubject { } else if case let .gift(gift) = subject {
isGift = true isGift = true
let media: [Media] = [TelegramMediaAction(action: .starGiftUnique(gift: .unique(gift), isUpgrade: false, isTransferred: false, savedToProfile: false, canExportDate: nil, transferStars: nil, isRefunded: false, peerId: nil, senderId: nil, savedId: nil, resaleStars: nil))] let media: [Media] = [TelegramMediaAction(action: .starGiftUnique(gift: .unique(gift), isUpgrade: false, isTransferred: false, savedToProfile: false, canExportDate: nil, transferStars: nil, isRefunded: false, peerId: nil, senderId: nil, savedId: nil, resaleStars: nil))]
let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: self.context.account.peerId, namespace: Namespaces.Message.Cloud, id: -1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: media, peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: self.context.account.peerId, namespace: Namespaces.Message.Cloud, id: -1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: media, peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
@ -3468,7 +3629,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
} }
let wallpaperColors: Signal<(UIColor?, UIColor?), NoError> let wallpaperColors: Signal<(UIColor?, UIColor?), NoError>
if let subject = self.subject, case .gift = subject { if case .gift = subject {
wallpaperColors = self.mediaEditorPromise.get() wallpaperColors = self.mediaEditorPromise.get()
|> mapToSignal { mediaEditor in |> mapToSignal { mediaEditor in
if let mediaEditor { if let mediaEditor {
@ -3498,7 +3659,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
} }
let renderer = DrawingMessageRenderer(context: self.context, messages: messages, parentView: self.view, isGift: isGift, wallpaperDayColor: wallpaperColors.0, wallpaperNightColor: wallpaperColors.1) let renderer = DrawingMessageRenderer(context: self.context, messages: messages, parentView: self.view, isGift: isGift, wallpaperDayColor: wallpaperColors.0, wallpaperNightColor: wallpaperColors.1)
renderer.render(completion: { result in renderer.render(completion: { result in
if case .draft = subject, let existingEntityView = self.entitiesView.getView(where: { entityView in if isDraft, let existingEntityView = self.entitiesView.getView(where: { entityView in
if let stickerEntityView = entityView as? DrawingStickerEntityView { if let stickerEntityView = entityView as? DrawingStickerEntityView {
if case .message = (stickerEntityView.entity as! DrawingStickerEntity).content { if case .message = (stickerEntityView.entity as! DrawingStickerEntity).content {
return true return true
@ -3508,13 +3669,6 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
} }
return false return false
}) as? DrawingStickerEntityView { }) as? DrawingStickerEntityView {
#if DEBUG
if let data = result.dayImage.pngData() {
let path = NSTemporaryDirectory() + "\(Int(Date().timeIntervalSince1970)).png"
try? data.write(to: URL(fileURLWithPath: path))
}
#endif
existingEntityView.isNightTheme = isNightTheme existingEntityView.isNightTheme = isNightTheme
let messageEntity = existingEntityView.entity as! DrawingStickerEntity let messageEntity = existingEntityView.entity as! DrawingStickerEntity
messageEntity.renderImage = result.dayImage messageEntity.renderImage = result.dayImage
@ -3524,7 +3678,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
} else { } else {
var content: DrawingStickerEntity.Content var content: DrawingStickerEntity.Content
var position: CGPoint var position: CGPoint
switch effectiveSubject { switch subject {
case let .message(messageIds): case let .message(messageIds):
content = .message(messageIds, result.size, messageFile, result.mediaFrame?.rect, result.mediaFrame?.cornerRadius) content = .message(messageIds, result.size, messageFile, result.mediaFrame?.rect, result.mediaFrame?.cornerRadius)
position = CGPoint(x: storyDimensions.width / 2.0 - 54.0, y: storyDimensions.height / 2.0) position = CGPoint(x: storyDimensions.width / 2.0 - 54.0, y: storyDimensions.height / 2.0)
@ -3579,7 +3733,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
self.backgroundDimView.isHidden = false self.backgroundDimView.isHidden = false
}) })
} }
} else if CACurrentMediaTime() - self.initializationTimestamp > 0.2, case .image = subject { } else if CACurrentMediaTime() - self.initializationTimestamp > 0.2, case .image = subject {
self.previewContainerView.alpha = 1.0 self.previewContainerView.alpha = 1.0
self.previewContainerView.layer.allowsGroupOpacity = true self.previewContainerView.layer.allowsGroupOpacity = true
self.previewContainerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, completion: { _ in self.previewContainerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, completion: { _ in
@ -3608,7 +3762,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
}) })
} }
if effectiveSubject.isPhoto { if subject.isPhoto {
self.previewContainerView.layer.allowsGroupOpacity = true self.previewContainerView.layer.allowsGroupOpacity = true
self.previewContainerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, completion: { _ in self.previewContainerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, completion: { _ in
self.previewContainerView.layer.allowsGroupOpacity = false self.previewContainerView.layer.allowsGroupOpacity = false
@ -3621,6 +3775,12 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
} }
} }
} }
} else {
if let caption {
mediaEditor.onFirstDisplay = { [weak self] in
self?.componentHostView?.setInputText(caption)
}
}
} }
mediaEditor.onPlaybackAction = { [weak self] action in mediaEditor.onPlaybackAction = { [weak self] action in
@ -3815,7 +3975,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
return nil return nil
} }
let colorSpace = CGColorSpaceCreateDeviceRGB() let colorSpace = CGColorSpaceCreateDeviceRGB()
let imageSize = CGSize(width: 1080, height: 1920) let imageSize = storyDimensions
if let context = DrawingContext(size: imageSize, scale: 1.0, opaque: true, colorSpace: colorSpace) { if let context = DrawingContext(size: imageSize, scale: 1.0, opaque: true, colorSpace: colorSpace) {
context.withFlippedContext { context in context.withFlippedContext { context in
if let image = mediaEditor.resultImage?.cgImage { if let image = mediaEditor.resultImage?.cgImage {
@ -4214,9 +4374,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
completion() completion()
case .camera: case .camera:
if let view = self.componentHost.view as? MediaEditorScreenComponent.View { self.componentHostView?.animateIn(from: .camera, completion: completion)
view.animateIn(from: .camera, completion: completion)
}
if let subject = self.subject, case let .video(_, mainTransitionImage, _, _, additionalTransitionImage, _, _, positionChangeTimestamps, pipPosition) = subject, let mainTransitionImage { if let subject = self.subject, case let .video(_, mainTransitionImage, _, _, additionalTransitionImage, _, _, positionChangeTimestamps, pipPosition) = subject, let mainTransitionImage {
var transitionImage = mainTransitionImage var transitionImage = mainTransitionImage
@ -4227,7 +4385,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
backgroundImage = additionalTransitionImage backgroundImage = additionalTransitionImage
foregroundImage = mainTransitionImage foregroundImage = mainTransitionImage
} }
if let combinedTransitionImage = generateImage(CGSize(width: 1080, height: 1920), scale: 1.0, rotatedContext: { size, context in if let combinedTransitionImage = generateImage(storyDimensions, scale: 1.0, rotatedContext: { size, context in
UIGraphicsPushContext(context) UIGraphicsPushContext(context)
backgroundImage.draw(in: CGRect(origin: CGPoint(x: (size.width - backgroundImage.size.width) / 2.0, y: (size.height - backgroundImage.size.height) / 2.0), size: backgroundImage.size)) backgroundImage.draw(in: CGRect(origin: CGPoint(x: (size.width - backgroundImage.size.width) / 2.0, y: (size.height - backgroundImage.size.height) / 2.0), size: backgroundImage.size))
@ -4253,9 +4411,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
self.setupTransitionImage(sourceImage) self.setupTransitionImage(sourceImage)
} }
if let sourceView = transitionIn.sourceView { if let sourceView = transitionIn.sourceView {
if let view = self.componentHost.view as? MediaEditorScreenComponent.View { self.componentHostView?.animateIn(from: .gallery)
view.animateIn(from: .gallery)
}
let sourceLocalFrame = sourceView.convert(transitionIn.sourceRect, to: self.view) let sourceLocalFrame = sourceView.convert(transitionIn.sourceRect, to: self.view)
let sourceScale = sourceLocalFrame.width / self.previewContainerView.frame.width let sourceScale = sourceLocalFrame.width / self.previewContainerView.frame.width
@ -4273,7 +4429,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
self.backgroundDimView.isHidden = false self.backgroundDimView.isHidden = false
self.backgroundDimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.35) self.backgroundDimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.35)
if let componentView = self.componentHost.view { if let componentView = self.componentHostView {
componentView.layer.animatePosition(from: sourceLocalFrame.center, to: componentView.center, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) componentView.layer.animatePosition(from: sourceLocalFrame.center, to: componentView.center, duration: duration, timingFunction: kCAMediaTimingFunctionSpring)
componentView.layer.animateScale(from: sourceScale, to: 1.0, duration: duration, timingFunction: timingFunction) componentView.layer.animateScale(from: sourceScale, to: 1.0, duration: duration, timingFunction: timingFunction)
componentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) componentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
@ -4293,8 +4449,8 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
if animateIn, let layout = self.validLayout { if animateIn, let layout = self.validLayout {
self.layer.animatePosition(from: CGPoint(x: 0.0, y: layout.size.height), to: .zero, duration: 0.35, timingFunction: kCAMediaTimingFunctionSpring, additive: true) self.layer.animatePosition(from: CGPoint(x: 0.0, y: layout.size.height), to: .zero, duration: 0.35, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
completion() completion()
} else if let view = self.componentHost.view as? MediaEditorScreenComponent.View { } else {
view.animateIn(from: .camera, completion: completion) self.componentHostView?.animateIn(from: .camera, completion: completion)
} }
} }
} }
@ -4346,9 +4502,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
destinationTransitionView = destinationTransitionOutView destinationTransitionView = destinationTransitionOutView
destinationTransitionRect = galleryTransitionIn.sourceRect destinationTransitionRect = galleryTransitionIn.sourceRect
} }
if let view = self.componentHost.view as? MediaEditorScreenComponent.View { self.componentHostView?.animateOut(to: .gallery)
view.animateOut(to: .gallery)
}
} }
let destinationLocalFrame = destinationView.convert(transitionOut.destinationRect, to: self.view) let destinationLocalFrame = destinationView.convert(transitionOut.destinationRect, to: self.view)
let destinationScale = destinationLocalFrame.width / self.previewContainerView.frame.width let destinationScale = destinationLocalFrame.width / self.previewContainerView.frame.width
@ -4446,7 +4600,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
removeOnCompletion: false removeOnCompletion: false
) )
if let componentView = self.componentHost.view { if let componentView = self.componentHostView {
componentView.clipsToBounds = true componentView.clipsToBounds = true
componentView.layer.animatePosition(from: componentView.center, to: destinationLocalFrame.center, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) componentView.layer.animatePosition(from: componentView.center, to: destinationLocalFrame.center, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
componentView.layer.animateScale(from: 1.0, to: destinationScale, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) componentView.layer.animateScale(from: 1.0, to: destinationScale, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
@ -4464,18 +4618,14 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
} }
} }
} else if let transitionIn = controller.transitionIn, case .camera = transitionIn { } else if let transitionIn = controller.transitionIn, case .camera = transitionIn {
if let view = self.componentHost.view as? MediaEditorScreenComponent.View { self.componentHostView?.animateOut(to: .camera)
view.animateOut(to: .camera)
}
let transition = ComponentTransition(animation: .curve(duration: 0.25, curve: .easeInOut)) let transition = ComponentTransition(animation: .curve(duration: 0.25, curve: .easeInOut))
transition.setAlpha(view: self.previewContainerView, alpha: 0.0, completion: { _ in transition.setAlpha(view: self.previewContainerView, alpha: 0.0, completion: { _ in
completion() completion()
}) })
} else { } else {
if controller.isEmbeddedEditor { if controller.isEmbeddedEditor {
if let view = self.componentHost.view as? MediaEditorScreenComponent.View { self.componentHostView?.animateOut(to: .gallery)
view.animateOut(to: .gallery)
}
self.layer.allowsGroupOpacity = true self.layer.allowsGroupOpacity = true
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.4, removeOnCompletion: false, completion: { _ in self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.4, removeOnCompletion: false, completion: { _ in
@ -4495,9 +4645,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
self.isDisplayingTool = tool self.isDisplayingTool = tool
let transition: ComponentTransition = .easeInOut(duration: 0.2) let transition: ComponentTransition = .easeInOut(duration: 0.2)
if let view = self.componentHost.view as? MediaEditorScreenComponent.View { self.componentHostView?.animateOutToTool(inPlace: inPlace, transition: transition)
view.animateOutToTool(inPlace: inPlace, transition: transition)
}
self.requestUpdate(transition: transition) self.requestUpdate(transition: transition)
} }
@ -4505,9 +4653,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
self.isDisplayingTool = nil self.isDisplayingTool = nil
let transition: ComponentTransition = .easeInOut(duration: 0.2) let transition: ComponentTransition = .easeInOut(duration: 0.2)
if let view = self.componentHost.view as? MediaEditorScreenComponent.View { self.componentHostView?.animateInFromTool(inPlace: inPlace, transition: transition)
view.animateInFromTool(inPlace: inPlace, transition: transition)
}
self.requestUpdate(transition: transition) self.requestUpdate(transition: transition)
} }
@ -4721,12 +4867,10 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
} }
var location: CLLocationCoordinate2D? var location: CLLocationCoordinate2D?
if let subject = self.actualSubject { if case let .draft(draft, _) = self.actualSubject {
if case let .asset(asset) = subject { location = draft.location
location = asset.location?.coordinate } else if case let .asset(asset) = self.subject {
} else if case let .draft(draft, _) = subject { location = asset.location?.coordinate
location = draft.location
}
} }
let locationController = storyLocationPickerController( let locationController = storyLocationPickerController(
context: self.context, context: self.context,
@ -5195,7 +5339,6 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
func addWeather(_ weather: StickerPickerScreen.Weather.LoadedWeather?) { func addWeather(_ weather: StickerPickerScreen.Weather.LoadedWeather?) {
guard let weather else { guard let weather else {
return return
} }
let maxWeatherCount = 1 let maxWeatherCount = 1
@ -5222,6 +5365,58 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
) )
} }
func getCaption() -> NSAttributedString {
return self.componentHostView?.getInputText() ?? NSAttributedString()
}
func switchToItem(_ identifier: String) {
guard let controller = self.controller, let mediaEditor = self.mediaEditor, let itemIndex = self.items.firstIndex(where: { $0.asset.localIdentifier == identifier }), case let .asset(asset) = self.subject, let currentItemIndex = self.items.firstIndex(where: { $0.asset.localIdentifier == asset.localIdentifier }) else {
return
}
let entities = self.entitiesView.entities.filter { !($0 is DrawingMediaEntity) }
let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.entitiesView)
mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities)
var updatedCurrentItem = self.items[currentItemIndex]
updatedCurrentItem.caption = self.getCaption()
if mediaEditor.values.hasChanges && updatedCurrentItem.values != mediaEditor.values {
updatedCurrentItem.values = mediaEditor.values
updatedCurrentItem.version += 1
if let resultImage = mediaEditor.resultImage {
mediaEditor.seek(0.0, andPlay: false)
makeEditorImageComposition(
context: self.ciContext,
postbox: self.context.account.postbox,
inputImage: resultImage,
dimensions: storyDimensions,
values: mediaEditor.values,
time: .zero,
textScale: 2.0,
completion: { [weak self] resultImage in
updatedCurrentItem.version += 1
updatedCurrentItem.thumbnail = resultImage
self?.items[currentItemIndex] = updatedCurrentItem
}
)
}
} else {
updatedCurrentItem.version += 1
self.items[currentItemIndex] = updatedCurrentItem
}
self.entitiesView.clearAll()
let targetItem = self.items[itemIndex]
controller.node.setup(
subject: .asset(targetItem.asset),
values: targetItem.values,
caption: targetItem.caption
)
}
func requestCompletion(playHaptic: Bool = true) { func requestCompletion(playHaptic: Bool = true) {
guard let controller = self.controller else { guard let controller = self.controller else {
return return
@ -5323,7 +5518,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
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.componentHostView {
let point = self.view.convert(point, to: self.previewContainerView) let point = self.view.convert(point, to: self.previewContainerView)
if let previewResult = self.previewContainerView.hitTest(point, with: event) { if let previewResult = self.previewContainerView.hitTest(point, with: event) {
return previewResult return previewResult
@ -6181,6 +6376,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
case message([MessageId]) case message([MessageId])
case gift(StarGift.UniqueGift) case gift(StarGift.UniqueGift)
case sticker(TelegramMediaFile, [String]) case sticker(TelegramMediaFile, [String])
case assets([PHAsset])
var dimensions: PixelDimensions { var dimensions: PixelDimensions {
switch self { switch self {
@ -6192,8 +6388,8 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
return PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight)) return PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight))
case let .draft(draft, _): case let .draft(draft, _):
return draft.dimensions return draft.dimensions
case .message, .gift, .sticker, .videoCollage: case .message, .gift, .sticker, .videoCollage, .assets:
return PixelDimensions(width: 1080, height: 1920) return PixelDimensions(storyDimensions)
} }
} }
@ -6220,6 +6416,8 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
return .gift(gift) return .gift(gift)
case let .sticker(sticker, _): case let .sticker(sticker, _):
return .sticker(sticker) return .sticker(sticker)
case let .assets(assets):
return .asset(assets.first!)
} }
} }
@ -6247,6 +6445,8 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
return false return false
case .sticker: case .sticker:
return false return false
case .assets:
return false
} }
} }
} }
@ -6555,7 +6755,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
let privacy = privacy ?? self.state.privacy let privacy = privacy ?? self.state.privacy
let text = self.getCaption().string let text = self.node.getCaption().string
let mentions = generateTextEntities(text, enabledTypes: [.mention], currentEntities: []).map { (text as NSString).substring(with: NSRange(location: $0.range.lowerBound + 1, length: $0.range.upperBound - $0.range.lowerBound - 1)) } let mentions = generateTextEntities(text, enabledTypes: [.mention], currentEntities: []).map { (text as NSString).substring(with: NSRange(location: $0.range.lowerBound + 1, length: $0.range.upperBound - $0.range.lowerBound - 1)) }
let coverImage: UIImage? let coverImage: UIImage?
@ -6567,7 +6767,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
let stateContext = ShareWithPeersScreen.StateContext( let stateContext = ShareWithPeersScreen.StateContext(
context: self.context, context: self.context,
subject: .stories(editing: false), subject: .stories(editing: false, count: Int32(self.node.items.count(where: { $0.isEnabled }))),
editing: false, editing: false,
initialPeerIds: Set(privacy.privacy.additionallyIncludePeers), initialPeerIds: Set(privacy.privacy.additionallyIncludePeers),
closeFriends: self.closeFriends.get(), closeFriends: self.closeFriends.get(),
@ -7105,13 +7305,9 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
self?.dismissed() self?.dismissed()
}) })
} }
func getCaption() -> NSAttributedString {
return (self.node.componentHost.view as? MediaEditorScreenComponent.View)?.getInputText() ?? NSAttributedString()
}
fileprivate func checkCaptionLimit() -> Bool { fileprivate func checkCaptionLimit() -> Bool {
let caption = self.getCaption() let caption = self.node.getCaption()
if caption.length > self.context.userLimits.maxStoryCaptionLength { if caption.length > self.context.userLimits.maxStoryCaptionLength {
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId))
|> deliverOnMainQueue).start(next: { [weak self] peer in |> deliverOnMainQueue).start(next: { [weak self] peer in
@ -7160,7 +7356,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.node.entitiesView) let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.node.entitiesView)
mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities) mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities)
var caption = self.getCaption() var caption = self.node.getCaption()
caption = convertMarkdownToAttributes(caption) caption = convertMarkdownToAttributes(caption)
var hasEntityChanges = false var hasEntityChanges = false
@ -7209,7 +7405,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
} }
if self.isEmbeddedEditor && !(hasAnyChanges || hasEntityChanges) { if self.isEmbeddedEditor && !(hasAnyChanges || hasEntityChanges) {
self.saveDraft(id: randomId, edit: true) self.saveDraft(id: randomId, isEdit: true)
self.completion(MediaEditorScreenImpl.Result(media: nil, mediaAreas: [], caption: caption, coverTimestamp: mediaEditor.values.coverImageTimestamp, options: self.state.privacy, stickers: stickers, randomId: randomId), { [weak self] finished in self.completion(MediaEditorScreenImpl.Result(media: nil, mediaAreas: [], caption: caption, coverTimestamp: mediaEditor.values.coverImageTimestamp, options: self.state.privacy, stickers: stickers, randomId: randomId), { [weak self] finished in
self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in
@ -7498,7 +7694,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
} }
duration = 5.0 duration = 5.0
case .sticker: case .sticker:
let image = generateImage(CGSize(width: 1080, height: 1920), contextGenerator: { size, context in let image = generateImage(storyDimensions, contextGenerator: { size, context in
context.clear(CGRect(origin: .zero, size: size)) context.clear(CGRect(origin: .zero, size: size))
}, opaque: false, scale: 1.0) }, opaque: false, scale: 1.0)
let tempImagePath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).png" let tempImagePath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).png"
@ -7509,6 +7705,8 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
duration = 3.0 duration = 3.0
firstFrame = .single((image, nil)) firstFrame = .single((image, nil))
case .assets:
fatalError()
} }
let _ = combineLatest(queue: Queue.mainQueue(), firstFrame, videoResult) let _ = combineLatest(queue: Queue.mainQueue(), firstFrame, videoResult)
@ -8399,6 +8597,8 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
} }
case let .sticker(file, _): case let .sticker(file, _):
exportSubject = .single(.sticker(file: file)) exportSubject = .single(.sticker(file: file))
case .assets:
fatalError()
} }
let _ = (exportSubject let _ = (exportSubject

View File

@ -11,7 +11,16 @@ import DrawingUI
extension MediaEditorScreenImpl { extension MediaEditorScreenImpl {
func isEligibleForDraft() -> Bool { func isEligibleForDraft() -> Bool {
if self.isEditingStory { guard !self.isEditingStory else {
return false
}
if case .avatarEditor = self.mode {
return false
}
if case .coverEditor = self.mode {
return false
}
if case .assets = self.node.actualSubject {
return false return false
} }
guard let mediaEditor = self.node.mediaEditor else { guard let mediaEditor = self.node.mediaEditor else {
@ -21,13 +30,6 @@ extension MediaEditorScreenImpl {
let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.node.entitiesView) let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.node.entitiesView)
mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities) mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities)
if case .avatarEditor = self.mode {
return false
}
if case .coverEditor = self.mode {
return false
}
let filteredEntities = self.node.entitiesView.entities.filter { entity in let filteredEntities = self.node.entitiesView.entities.filter { entity in
if entity is DrawingMediaEntity { if entity is DrawingMediaEntity {
return false return false
@ -44,34 +46,42 @@ extension MediaEditorScreenImpl {
let values = mediaEditor.values let values = mediaEditor.values
let filteredValues = values.withUpdatedEntities([]) let filteredValues = values.withUpdatedEntities([])
let caption = self.node.getCaption()
let caption = self.getCaption()
if let subject = self.node.subject { if let subject = self.node.subject {
if case .asset = subject, !values.hasChanges && caption.string.isEmpty { switch subject {
return false case .asset:
} else if case .message = subject, !filteredValues.hasChanges && filteredEntities.isEmpty && caption.string.isEmpty { if !values.hasChanges && caption.string.isEmpty {
return false return false
} else if case .gift = subject, !filteredValues.hasChanges && filteredEntities.isEmpty && caption.string.isEmpty { }
return false case .message, .gift:
} else if case .empty = subject, !self.node.hasAnyChanges && !self.node.drawingView.internalState.canUndo { if !filteredValues.hasChanges && filteredEntities.isEmpty && caption.string.isEmpty {
return false return false
} else if case .videoCollage = subject { }
case .empty:
if !self.node.hasAnyChanges && !self.node.drawingView.internalState.canUndo {
return false
}
case .videoCollage:
return false return false
default:
break
} }
} }
return true return true
} }
func saveDraft(id: Int64?, edit: Bool = false) { func saveDraft(id: Int64?, isEdit: Bool = false, completion: ((MediaEditorDraft) -> Void)? = nil) {
guard case .storyEditor = self.mode, let subject = self.node.subject, let actualSubject = self.node.actualSubject, let mediaEditor = self.node.mediaEditor else { guard case .storyEditor = self.mode, let subject = self.node.subject, let actualSubject = self.node.actualSubject, let mediaEditor = self.node.mediaEditor else {
return return
} }
try? FileManager.default.createDirectory(atPath: draftPath(engine: self.context.engine), withIntermediateDirectories: true) try? FileManager.default.createDirectory(atPath: draftPath(engine: self.context.engine), withIntermediateDirectories: true)
let values = mediaEditor.values let values = mediaEditor.values
let privacy = self.state.privacy let privacy = self.state.privacy
let forwardSource = self.forwardSource let forwardSource = self.forwardSource
let caption = self.getCaption() let caption = self.node.getCaption()
let duration = mediaEditor.duration ?? 0.0 let duration = mediaEditor.duration ?? 0.0
let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
@ -99,10 +109,19 @@ extension MediaEditorScreenImpl {
} }
if let resultImage = mediaEditor.resultImage { if let resultImage = mediaEditor.resultImage {
if !edit { if !isEdit {
mediaEditor.seek(0.0, andPlay: false) mediaEditor.seek(0.0, andPlay: false)
} }
makeEditorImageComposition(context: self.node.ciContext, postbox: self.context.account.postbox, inputImage: resultImage, dimensions: storyDimensions, values: values, time: .zero, textScale: 2.0, completion: { resultImage in
makeEditorImageComposition(
context: self.node.ciContext,
postbox: self.context.account.postbox,
inputImage: resultImage,
dimensions: storyDimensions,
values: values,
time: .zero,
textScale: 2.0,
completion: { resultImage in
guard let resultImage else { guard let resultImage else {
return return
} }
@ -148,54 +167,64 @@ extension MediaEditorScreenImpl {
} }
let context = self.context let context = self.context
func innerSaveDraft(media: MediaInput) { func innerSaveDraft(media: MediaInput, save: Bool = true) -> MediaEditorDraft? {
let fittedSize = resultImage.size.aspectFitted(CGSize(width: 128.0, height: 128.0)) let fittedSize = resultImage.size.aspectFitted(CGSize(width: 128.0, height: 128.0))
if let thumbnailImage = generateScaledImage(image: resultImage, size: fittedSize) { guard let thumbnailImage = generateScaledImage(image: resultImage, size: fittedSize) else {
let path = "\(Int64.random(in: .min ... .max)).\(media.fileExtension)" return nil
let draft = MediaEditorDraft( }
path: path, let path = "\(Int64.random(in: .min ... .max)).\(media.fileExtension)"
isVideo: media.isVideo, let draft = MediaEditorDraft(
thumbnail: thumbnailImage, path: path,
dimensions: media.dimensions, isVideo: media.isVideo,
duration: media.duration, thumbnail: thumbnailImage,
values: values, dimensions: media.dimensions,
caption: caption, duration: media.duration,
privacy: privacy, values: values,
forwardInfo: forwardSource.flatMap { StoryId(peerId: $0.0.id, id: $0.1.id) }, caption: caption,
timestamp: timestamp, privacy: privacy,
location: location, forwardInfo: forwardSource.flatMap { StoryId(peerId: $0.0.id, id: $0.1.id) },
expiresOn: expiresOn timestamp: timestamp,
) location: location,
switch media { expiresOn: expiresOn
case let .image(image, _): )
if let data = image.jpegData(compressionQuality: 0.87) { switch media {
try? data.write(to: URL(fileURLWithPath: draft.fullPath(engine: context.engine))) case let .image(image, _):
} if let data = image.jpegData(compressionQuality: 0.87) {
case let .video(path, _, _): try? data.write(to: URL(fileURLWithPath: draft.fullPath(engine: context.engine)))
try? FileManager.default.copyItem(atPath: path, toPath: draft.fullPath(engine: context.engine))
} }
case let .video(path, _, _):
try? FileManager.default.copyItem(atPath: path, toPath: draft.fullPath(engine: context.engine))
}
if save {
if let id { if let id {
saveStorySource(engine: context.engine, item: draft, peerId: context.account.peerId, id: id) saveStorySource(engine: context.engine, item: draft, peerId: context.account.peerId, id: id)
} else { } else {
addStoryDraft(engine: context.engine, item: draft) addStoryDraft(engine: context.engine, item: draft)
} }
} }
return draft
} }
switch subject { switch subject {
case .empty: case .empty:
break break
case let .image(image, dimensions, _, _): case let .image(image, dimensions, _, _):
innerSaveDraft(media: .image(image: image, dimensions: dimensions)) if let draft = innerSaveDraft(media: .image(image: image, dimensions: dimensions)) {
completion?(draft)
}
case let .video(path, _, _, _, _, dimensions, _, _, _): case let .video(path, _, _, _, _, dimensions, _, _, _):
innerSaveDraft(media: .video(path: path, dimensions: dimensions, duration: duration)) if let draft = innerSaveDraft(media: .video(path: path, dimensions: dimensions, duration: duration)) {
completion?(draft)
}
case let .videoCollage(items): case let .videoCollage(items):
let _ = items let _ = items
case let .asset(asset): case let .asset(asset):
if asset.mediaType == .video { if asset.mediaType == .video {
PHImageManager.default().requestAVAsset(forVideo: asset, options: nil) { avAsset, _, _ in PHImageManager.default().requestAVAsset(forVideo: asset, options: nil) { avAsset, _, _ in
if let urlAsset = avAsset as? AVURLAsset { if let urlAsset = avAsset as? AVURLAsset {
innerSaveDraft(media: .video(path: urlAsset.url.relativePath, dimensions: PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight)), duration: duration)) if let draft = innerSaveDraft(media: .video(path: urlAsset.url.relativePath, dimensions: PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight)), duration: duration)) {
completion?(draft)
}
} }
} }
} else { } else {
@ -203,22 +232,32 @@ extension MediaEditorScreenImpl {
options.deliveryMode = .highQualityFormat options.deliveryMode = .highQualityFormat
PHImageManager.default().requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .default, options: options) { image, _ in PHImageManager.default().requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .default, options: options) { image, _ in
if let image { if let image {
innerSaveDraft(media: .image(image: image, dimensions: PixelDimensions(image.size))) if let draft = innerSaveDraft(media: .image(image: image, dimensions: PixelDimensions(image.size))) {
completion?(draft)
}
} }
} }
} }
case let .draft(draft, _): case let .draft(draft, _):
if draft.isVideo { if draft.isVideo {
innerSaveDraft(media: .video(path: draft.fullPath(engine: context.engine), dimensions: draft.dimensions, duration: draft.duration ?? 0.0)) if let draft = innerSaveDraft(media: .video(path: draft.fullPath(engine: context.engine), dimensions: draft.dimensions, duration: draft.duration ?? 0.0)) {
completion?(draft)
}
} else if let image = UIImage(contentsOfFile: draft.fullPath(engine: context.engine)) { } else if let image = UIImage(contentsOfFile: draft.fullPath(engine: context.engine)) {
innerSaveDraft(media: .image(image: image, dimensions: draft.dimensions)) if let draft = innerSaveDraft(media: .image(image: image, dimensions: draft.dimensions)) {
completion?(draft)
}
} }
case .message, .gift: case .message, .gift:
if let pixel = generateSingleColorImage(size: CGSize(width: 1, height: 1), color: .black) { if let pixel = generateSingleColorImage(size: CGSize(width: 1, height: 1), color: .black) {
innerSaveDraft(media: .image(image: pixel, dimensions: PixelDimensions(width: 1080, height: 1920))) if let draft = innerSaveDraft(media: .image(image: pixel, dimensions: PixelDimensions(width: 1080, height: 1920))) {
completion?(draft)
}
} }
case .sticker: case .sticker:
break break
case .assets:
break
} }
if case let .draft(draft, _) = actualSubject { if case let .draft(draft, _) = actualSubject {

View File

@ -34,7 +34,7 @@ final class SelectionPanelButtonContentComponent: Component {
return false return false
} }
private let backgroundView: BlurredBackgroundView let backgroundView: BlurredBackgroundView
private let outline = SimpleLayer() private let outline = SimpleLayer()
private let icon = SimpleLayer() private let icon = SimpleLayer()
private let label = ComponentView<Empty>() private let label = ComponentView<Empty>()

View File

@ -1,7 +1,670 @@
import Foundation import Foundation
import UIKit import UIKit
import Display import Display
import ComponentFlow import ComponentFlow
import SwiftSignalKit
import AccountContext
import MediaEditor
import MediaAssetsContext
import CheckNode
import TelegramPresentationData
final class SelectionPanelComponent: Component {
let previewContainerView: PortalSourceView
let frame: CGRect
let items: [MediaEditorScreenImpl.EditingItem]
let selectedItemId: String
let itemTapped: (String?) -> Void
let itemSelectionToggled: (String) -> Void
let itemReordered: (String, String) -> Void
init(
previewContainerView: PortalSourceView,
frame: CGRect,
items: [MediaEditorScreenImpl.EditingItem],
selectedItemId: String,
itemTapped: @escaping (String?) -> Void,
itemSelectionToggled: @escaping (String) -> Void,
itemReordered: @escaping (String, String) -> Void
) {
self.previewContainerView = previewContainerView
self.frame = frame
self.items = items
self.selectedItemId = selectedItemId
self.itemTapped = itemTapped
self.itemSelectionToggled = itemSelectionToggled
self.itemReordered = itemReordered
}
static func ==(lhs: SelectionPanelComponent, rhs: SelectionPanelComponent) -> Bool {
return lhs.frame == rhs.frame && lhs.items == rhs.items && lhs.selectedItemId == rhs.selectedItemId
}
final class View: UIView, UIGestureRecognizerDelegate {
final class ItemView: UIView {
private let backgroundNode: ASImageNode
private let imageNode: ImageNode
private let checkNode: InteractiveCheckNode
private var selectionLayer: SimpleShapeLayer?
var toggleSelection: () -> Void = {}
override init(frame: CGRect) {
self.backgroundNode = ASImageNode()
self.backgroundNode.displaysAsynchronously = false
self.imageNode = ImageNode()
self.imageNode.contentMode = .scaleAspectFill
self.checkNode = InteractiveCheckNode(theme: CheckNodeTheme(theme: defaultDarkColorPresentationTheme, style: .overlay))
super.init(frame: frame)
self.clipsToBounds = true
self.layer.cornerRadius = 6.0
self.addSubview(self.backgroundNode.view)
self.addSubview(self.imageNode.view)
self.addSubview(self.checkNode.view)
self.checkNode.valueChanged = { [weak self] value in
guard let self else {
return
}
self.toggleSelection()
}
}
required init?(coder aDecoder: NSCoder) {
preconditionFailure()
}
fileprivate var item: MediaEditorScreenImpl.EditingItem?
func update(item: MediaEditorScreenImpl.EditingItem, number: Int, isSelected: Bool, isEnabled: Bool, size: CGSize, portalView: PortalView?, transition: ComponentTransition) {
let previousItem = self.item
self.item = item
if previousItem?.asset.localIdentifier != item.asset.localIdentifier || previousItem?.version != item.version {
let imageSignal: Signal<UIImage?, NoError>
if let thumbnail = item.thumbnail {
imageSignal = .single(thumbnail)
self.imageNode.contentMode = .scaleAspectFill
} else {
imageSignal = assetImage(asset: item.asset, targetSize:CGSize(width: 128.0 * UIScreenScale, height: 128.0 * UIScreenScale), exact: false, synchronous: true)
self.imageNode.contentUpdated = { [weak self] image in
if let self {
if self.backgroundNode.image == nil {
if let image, image.size.width > image.size.height {
self.imageNode.contentMode = .scaleAspectFit
Queue.concurrentDefaultQueue().async {
let colors = mediaEditorGetGradientColors(from: image)
let gradientImage = mediaEditorGenerateGradientImage(size: CGSize(width: 3.0, height: 128.0), colors: colors.array)
Queue.mainQueue().async {
self.backgroundNode.image = gradientImage
}
}
} else {
self.imageNode.contentMode = .scaleAspectFill
}
}
}
}
}
self.imageNode.setSignal(imageSignal)
}
let backgroundSize = CGSize(width: size.width, height: floorToScreenPixels(size.width / 9.0 * 16.0))
self.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((size.height - backgroundSize.height) / 2.0)), size: backgroundSize)
self.imageNode.frame = CGRect(origin: .zero, size: size)
//self.checkNode.content = .counter(number)
self.checkNode.setSelected(isEnabled, animated: previousItem != nil)
let checkSize = CGSize(width: 29.0, height: 29.0)
self.checkNode.frame = CGRect(origin: CGPoint(x: size.width - checkSize.width - 4.0, y: 4.0), size: checkSize)
if isSelected, let portalView {
portalView.view.frame = CGRect(origin: .zero, size: size)
self.insertSubview(portalView.view, aboveSubview: self.imageNode.view)
}
let lineWidth: CGFloat = 2.0 - UIScreenPixel
let selectionFrame = CGRect(origin: .zero, size: size)
if isSelected {
let selectionLayer: SimpleShapeLayer
if let current = self.selectionLayer {
selectionLayer = current
} else {
selectionLayer = SimpleShapeLayer()
self.selectionLayer = selectionLayer
self.layer.addSublayer(selectionLayer)
selectionLayer.fillColor = UIColor.clear.cgColor
selectionLayer.strokeColor = UIColor.white.cgColor
selectionLayer.lineWidth = lineWidth
selectionLayer.frame = selectionFrame
selectionLayer.path = CGPath(roundedRect: CGRect(origin: .zero, size: selectionFrame.size).insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0), cornerWidth: 6.0, cornerHeight: 6.0, transform: nil)
// if !transition.animation.isImmediate {
// let initialPath = CGPath(roundedRect: CGRect(origin: .zero, size: selectionFrame.size).insetBy(dx: 0.0, dy: 0.0), cornerWidth: 6.0, cornerHeight: 6.0, transform: nil)
// selectionLayer.animate(from: initialPath, to: selectionLayer.path as AnyObject, keyPath: "path", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2)
// selectionLayer.animateShapeLineWidth(from: 0.0, to: lineWidth, duration: 0.2)
// }
}
} else if let selectionLayer = self.selectionLayer {
self.selectionLayer = nil
selectionLayer.removeFromSuperlayer()
// let targetPath = CGPath(roundedRect: CGRect(origin: .zero, size: selectionFrame.size).insetBy(dx: 0.0, dy: 0.0), cornerWidth: 6.0, cornerHeight: 6.0, transform: nil)
// selectionLayer.animate(from: selectionLayer.path, to: targetPath, keyPath: "path", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2, removeOnCompletion: false)
// selectionLayer.animateShapeLineWidth(from: selectionLayer.lineWidth, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
// selectionLayer.removeFromSuperlayer()
// })
}
}
}
private let backgroundView: BlurredBackgroundView
private let backgroundMaskView: UIView
private let backgroundMaskPanelView: UIView
private let scrollView: UIScrollView
private var itemViews: [AnyHashable: ItemView] = [:]
private var portalView: PortalView?
private var reorderRecognizer: ReorderGestureRecognizer?
private var reorderingItem: (id: AnyHashable, initialPosition: CGPoint, position: CGPoint, snapshotView: UIView)?
private var tapRecognizer: UITapGestureRecognizer?
private var component: SelectionPanelComponent?
private var state: EmptyComponentState?
override init(frame: CGRect) {
self.backgroundView = BlurredBackgroundView(color: UIColor(white: 0.2, alpha: 0.45), enableBlur: true)
self.backgroundMaskView = UIView(frame: .zero)
self.backgroundMaskPanelView = UIView(frame: .zero)
self.backgroundMaskPanelView.backgroundColor = UIColor.white
self.backgroundMaskPanelView.clipsToBounds = true
self.backgroundMaskPanelView.layer.cornerRadius = 10.0
self.scrollView = UIScrollView(frame: .zero)
self.scrollView.contentInsetAdjustmentBehavior = .never
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.showsVerticalScrollIndicator = false
self.scrollView.layer.cornerRadius = 10.0
super.init(frame: frame)
self.backgroundView.mask = self.backgroundMaskView
let reorderRecognizer = ReorderGestureRecognizer(
shouldBegin: { [weak self] point in
guard let self, let item = self.item(at: point) else {
return (allowed: false, requiresLongPress: false, item: nil)
}
return (allowed: true, requiresLongPress: true, item: item)
},
willBegin: { point in
},
began: { [weak self] item in
guard let self else {
return
}
self.setReorderingItem(item: item)
},
ended: { [weak self] in
guard let self else {
return
}
self.setReorderingItem(item: nil)
},
moved: { [weak self] distance in
guard let self else {
return
}
self.moveReorderingItem(distance: distance)
},
isActiveUpdated: { _ in
}
)
reorderRecognizer.delegate = self
self.reorderRecognizer = reorderRecognizer
self.addGestureRecognizer(reorderRecognizer)
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap))
self.tapRecognizer = tapRecognizer
self.addGestureRecognizer(tapRecognizer)
self.addSubview(self.backgroundView)
self.addSubview(self.scrollView)
self.backgroundMaskView.addSubview(self.backgroundMaskPanelView)
}
required init?(coder: NSCoder) {
preconditionFailure()
}
deinit {
}
@objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) {
guard let component = self.component else {
return
}
self.reorderRecognizer?.isEnabled = false
self.reorderRecognizer?.isEnabled = true
let location = gestureRecognizer.location(in: self)
if let itemView = self.item(at: location), let item = itemView.item, item.asset.localIdentifier != component.selectedItemId {
component.itemTapped(item.asset.localIdentifier)
} else {
component.itemTapped(nil)
}
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if otherGestureRecognizer is UITapGestureRecognizer {
return true
}
if otherGestureRecognizer is UIPanGestureRecognizer {
if gestureRecognizer === self.reorderRecognizer, ![.began, .changed].contains(gestureRecognizer.state) {
gestureRecognizer.isEnabled = false
gestureRecognizer.isEnabled = true
return true
} else {
return false
}
}
return false
}
func item(at point: CGPoint) -> ItemView? {
let point = self.convert(point, to: self.scrollView)
for (_, itemView) in self.itemViews {
if itemView.frame.contains(point) {
return itemView
}
}
return nil
}
func setReorderingItem(item: ItemView?) {
self.tapRecognizer?.isEnabled = false
self.tapRecognizer?.isEnabled = true
var mappedItem: (AnyHashable, ItemView)?
if let item {
for (id, visibleItem) in self.itemViews {
if visibleItem === item {
mappedItem = (id, visibleItem)
break
}
}
}
if self.reorderingItem?.id != mappedItem?.0 {
let transition: ComponentTransition = .spring(duration: 0.4)
if let (id, itemView) = mappedItem, let snapshotView = itemView.snapshotView(afterScreenUpdates: false) {
itemView.isHidden = true
let position = self.scrollView.convert(itemView.center, to: self)
snapshotView.center = position
transition.setScale(view: snapshotView, scale: 0.9)
self.addSubview(snapshotView)
self.reorderingItem = (id, position, position, snapshotView)
} else {
if let (id, _, _, snapshotView) = self.reorderingItem {
if let itemView = self.itemViews[id] {
if let innerSnapshotView = snapshotView.snapshotView(afterScreenUpdates: false) {
innerSnapshotView.center = self.convert(snapshotView.center, to: self.scrollView)
innerSnapshotView.transform = CGAffineTransformMakeScale(0.9, 0.9)
self.scrollView.addSubview(innerSnapshotView)
transition.setPosition(view: innerSnapshotView, position: itemView.center, completion: { [weak innerSnapshotView] _ in
innerSnapshotView?.removeFromSuperview()
itemView.isHidden = false
})
transition.setScale(view: innerSnapshotView, scale: 1.0)
}
transition.setPosition(view: snapshotView, position: self.scrollView.convert(itemView.center, to: self), completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
transition.setScale(view: snapshotView, scale: 1.0)
transition.setAlpha(view: snapshotView, alpha: 0.0)
}
}
self.reorderingItem = nil
}
self.state?.updated(transition: transition)
}
}
func moveReorderingItem(distance: CGPoint) {
guard let component = self.component else {
return
}
if let (id, initialPosition, _, snapshotView) = self.reorderingItem {
let targetPosition = CGPoint(x: initialPosition.x + distance.x, y: initialPosition.y + distance.y)
self.reorderingItem = (id, initialPosition, targetPosition, snapshotView)
snapshotView.center = targetPosition
let mappedPosition = self.convert(targetPosition, to: self.scrollView)
if let visibleReorderingItem = self.itemViews[id], let fromId = self.itemViews[id]?.item?.asset.localIdentifier {
for (_, visibleItem) in self.itemViews {
if visibleItem === visibleReorderingItem {
continue
}
if visibleItem.frame.contains(mappedPosition), let toId = visibleItem.item?.asset.localIdentifier {
component.itemReordered(fromId, toId)
break
}
}
}
}
}
func animateIn(from buttonView: SelectionPanelButtonContentComponent.View) {
}
func animateOut(to buttonView: SelectionPanelButtonContentComponent.View, completion: @escaping () -> Void) {
completion()
}
func update(component: SelectionPanelComponent, availableSize: CGSize, state: EmptyComponentState, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
if self.portalView == nil {
if let portalView = PortalView(matchPosition: false) {
portalView.view.layer.rasterizationScale = UIScreenScale
let scale = 95.0 / component.previewContainerView.frame.width
portalView.view.transform = CGAffineTransformMakeScale(scale, scale)
component.previewContainerView.addPortal(view: portalView)
self.portalView = portalView
}
}
var validIds = Set<AnyHashable>()
let itemSize = CGSize(width: 95.0, height: 112.0)
let spacing: CGFloat = 4.0
var itemFrame: CGRect = CGRect(origin: CGPoint(x: spacing, y: spacing), size: itemSize)
var index = 1
for item in component.items {
let id = item.asset.localIdentifier
validIds.insert(id)
var itemTransition = transition
let itemView: ItemView
if let current = self.itemViews[id] {
itemView = current
} else {
itemView = ItemView(frame: itemFrame)
self.scrollView.addSubview(itemView)
self.itemViews[id] = itemView
itemTransition = .immediate
}
itemView.toggleSelection = { [weak self] in
guard let self, let component = self.component else {
return
}
component.itemSelectionToggled(id)
}
itemView.update(item: item, number: index, isSelected: item.asset.localIdentifier == component.selectedItemId, isEnabled: item.isEnabled, size: itemFrame.size, portalView: self.portalView, transition: itemTransition)
itemTransition.setBounds(view: itemView, bounds: CGRect(origin: .zero, size: itemFrame.size))
itemTransition.setPosition(view: itemView, position: itemFrame.center)
itemFrame.origin.x += itemSize.width + spacing
index += 1
}
var removeIds: [AnyHashable] = []
for (id, itemView) in self.itemViews {
if !validIds.contains(id) {
removeIds.append(id)
transition.setAlpha(view: itemView, alpha: 0.0, completion: { [weak itemView] _ in
itemView?.removeFromSuperview()
})
}
}
for id in removeIds {
self.itemViews.removeValue(forKey: id)
}
let contentSize = CGSize(width: itemFrame.minX, height: itemSize.height + spacing * 2.0)
if self.scrollView.contentSize != contentSize {
self.scrollView.contentSize = contentSize
}
let backgroundSize = CGSize(width: min(availableSize.width - 24.0, contentSize.width), height: contentSize.height)
self.backgroundView.frame = CGRect(origin: .zero, size: availableSize)
self.backgroundView.update(size: availableSize, transition: .immediate)
let contentFrame = CGRect(origin: CGPoint(x: availableSize.width - 12.0 - backgroundSize.width, y: component.frame.minY), size: backgroundSize)
self.backgroundMaskPanelView.frame = contentFrame
self.scrollView.frame = contentFrame
return availableSize
}
}
func makeView() -> View {
return View()
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, transition: transition)
}
}
private final class ReorderGestureRecognizer: UIGestureRecognizer {
private let shouldBegin: (CGPoint) -> (allowed: Bool, requiresLongPress: Bool, item: SelectionPanelComponent.View.ItemView?)
private let willBegin: (CGPoint) -> Void
private let began: (SelectionPanelComponent.View.ItemView) -> Void
private let ended: () -> Void
private let moved: (CGPoint) -> Void
private let isActiveUpdated: (Bool) -> Void
private var initialLocation: CGPoint?
private var longTapTimer: SwiftSignalKit.Timer?
private var longPressTimer: SwiftSignalKit.Timer?
private var itemView: SelectionPanelComponent.View.ItemView?
public init(shouldBegin: @escaping (CGPoint) -> (allowed: Bool, requiresLongPress: Bool, item: SelectionPanelComponent.View.ItemView?), willBegin: @escaping (CGPoint) -> Void, began: @escaping (SelectionPanelComponent.View.ItemView) -> Void, ended: @escaping () -> Void, moved: @escaping (CGPoint) -> Void, isActiveUpdated: @escaping (Bool) -> Void) {
self.shouldBegin = shouldBegin
self.willBegin = willBegin
self.began = began
self.ended = ended
self.moved = moved
self.isActiveUpdated = isActiveUpdated
super.init(target: nil, action: nil)
}
deinit {
self.longTapTimer?.invalidate()
self.longPressTimer?.invalidate()
}
private func startLongTapTimer() {
self.longTapTimer?.invalidate()
let longTapTimer = SwiftSignalKit.Timer(timeout: 0.25, repeat: false, completion: { [weak self] in
self?.longTapTimerFired()
}, queue: Queue.mainQueue())
self.longTapTimer = longTapTimer
longTapTimer.start()
}
private func stopLongTapTimer() {
self.itemView = nil
self.longTapTimer?.invalidate()
self.longTapTimer = nil
}
private func startLongPressTimer() {
self.longPressTimer?.invalidate()
let longPressTimer = SwiftSignalKit.Timer(timeout: 0.6, repeat: false, completion: { [weak self] in
self?.longPressTimerFired()
}, queue: Queue.mainQueue())
self.longPressTimer = longPressTimer
longPressTimer.start()
}
private func stopLongPressTimer() {
self.itemView = nil
self.longPressTimer?.invalidate()
self.longPressTimer = nil
}
override public func reset() {
super.reset()
self.itemView = nil
self.stopLongTapTimer()
self.stopLongPressTimer()
self.initialLocation = nil
self.isActiveUpdated(false)
}
private func longTapTimerFired() {
guard let location = self.initialLocation else {
return
}
self.longTapTimer?.invalidate()
self.longTapTimer = nil
self.willBegin(location)
}
private func longPressTimerFired() {
guard let _ = self.initialLocation else {
return
}
self.isActiveUpdated(true)
self.state = .began
self.longPressTimer?.invalidate()
self.longPressTimer = nil
self.longTapTimer?.invalidate()
self.longTapTimer = nil
if let itemView = self.itemView {
self.began(itemView)
}
self.isActiveUpdated(true)
}
override public func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
if self.numberOfTouches > 1 {
self.isActiveUpdated(false)
self.state = .failed
self.ended()
return
}
if self.state == .possible {
if let location = touches.first?.location(in: self.view) {
let (allowed, requiresLongPress, itemView) = self.shouldBegin(location)
if allowed {
self.isActiveUpdated(true)
self.itemView = itemView
self.initialLocation = location
if requiresLongPress {
self.startLongTapTimer()
self.startLongPressTimer()
} else {
self.state = .began
if let itemView = self.itemView {
self.began(itemView)
}
}
} else {
self.isActiveUpdated(false)
self.state = .failed
}
} else {
self.isActiveUpdated(false)
self.state = .failed
}
}
}
override public func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesEnded(touches, with: event)
self.initialLocation = nil
self.stopLongTapTimer()
if self.longPressTimer != nil {
self.stopLongPressTimer()
self.isActiveUpdated(false)
self.state = .failed
}
if self.state == .began || self.state == .changed {
self.isActiveUpdated(false)
self.ended()
self.state = .failed
}
}
override public func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesCancelled(touches, with: event)
self.initialLocation = nil
self.stopLongTapTimer()
if self.longPressTimer != nil {
self.isActiveUpdated(false)
self.stopLongPressTimer()
self.state = .failed
}
if self.state == .began || self.state == .changed {
self.isActiveUpdated(false)
self.ended()
self.state = .failed
}
}
override public func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesMoved(touches, with: event)
if (self.state == .began || self.state == .changed), let initialLocation = self.initialLocation, let location = touches.first?.location(in: self.view) {
self.state = .changed
let offset = CGPoint(x: location.x - initialLocation.x, y: location.y - initialLocation.y)
self.moved(offset)
} else if let touch = touches.first, let initialTapLocation = self.initialLocation, self.longPressTimer != nil {
let touchLocation = touch.location(in: self.view)
let dX = touchLocation.x - initialTapLocation.x
let dY = touchLocation.y - initialTapLocation.y
if dX * dX + dY * dY > 3.0 * 3.0 {
self.stopLongTapTimer()
self.stopLongPressTimer()
self.initialLocation = nil
self.isActiveUpdated(false)
self.state = .failed
}
}
}
}

View File

@ -10018,7 +10018,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
viewControllers = viewControllers.filter { !($0 is AttachmentController)} viewControllers = viewControllers.filter { !($0 is AttachmentController)}
rootController.setViewControllers(viewControllers, animated: false) rootController.setViewControllers(viewControllers, animated: false)
rootController.proceedWithStoryUpload(target: target, result: result, existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit) rootController.proceedWithStoryUpload(target: target, results: [result], existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit)
} }
}, },
cancelled: {} cancelled: {}
@ -10053,7 +10053,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
self.openBotPreviewEditor(target: .botPreview(id: self.peerId, language: pane.currentBotPreviewLanguage?.id), source: result, transitionIn: (transitionView, transitionRect, transitionImage)) self.openBotPreviewEditor(target: .botPreview(id: self.peerId, language: pane.currentBotPreviewLanguage?.id), source: result, transitionIn: (transitionView, transitionRect, transitionImage))
}, },
multipleCompletion: { _ in }, multipleCompletion: { _, _ in },
dismissed: {}, dismissed: {},
groupsPresented: {} groupsPresented: {}
) )

View File

@ -991,10 +991,22 @@ final class ShareWithPeersScreenComponent: Component {
} }
let sectionTitle: String let sectionTitle: String
if section.id == 0, case .stories = component.stateContext.subject { if section.id == 0, case let .stories(_, count) = component.stateContext.subject {
sectionTitle = component.coverItem == nil ? environment.strings.Story_Privacy_PostStoryAsHeader : "" if component.coverItem == nil {
if count > 1 {
sectionTitle = environment.strings.Story_Privacy_PostStoriesAsHeader
} else {
sectionTitle = environment.strings.Story_Privacy_PostStoryAsHeader
}
} else {
sectionTitle = ""
}
} else if section.id == 2 { } else if section.id == 2 {
sectionTitle = environment.strings.Story_Privacy_WhoCanViewHeader if case let .stories(_, count) = component.stateContext.subject, count > 1 {
sectionTitle = environment.strings.Story_Privacy_WhoCanViewStoriesHeader
} else {
sectionTitle = environment.strings.Story_Privacy_WhoCanViewHeader
}
} else if section.id == 1 { } else if section.id == 1 {
if case let .members(isGroup, _, _) = component.stateContext.subject { if case let .members(isGroup, _, _) = component.stateContext.subject {
sectionTitle = isGroup ? environment.strings.BoostGift_Members_SectionTitle : environment.strings.BoostGift_Subscribers_SectionTitle sectionTitle = isGroup ? environment.strings.BoostGift_Members_SectionTitle : environment.strings.BoostGift_Subscribers_SectionTitle
@ -1637,12 +1649,21 @@ final class ShareWithPeersScreenComponent: Component {
} }
let footerValue = environment.strings.Story_Privacy_KeepOnMyPageHours(Int32(component.timeout / 3600)) let footerValue = environment.strings.Story_Privacy_KeepOnMyPageHours(Int32(component.timeout / 3600))
var footerText = environment.strings.Story_Privacy_KeepOnMyPageInfo(footerValue).string var footerText: String
if case let .stories(_, count) = component.stateContext.subject, count > 1 {
if let sendAsPeerId = self.sendAsPeerId, sendAsPeerId.isGroupOrChannel == true { if let sendAsPeerId = self.sendAsPeerId, sendAsPeerId.isGroupOrChannel == true {
footerText = isSendAsGroup ? environment.strings.Story_Privacy_KeepOnGroupPageInfo(footerValue).string : environment.strings.Story_Privacy_KeepOnChannelPageInfo(footerValue).string footerText = isSendAsGroup ? environment.strings.Story_Privacy_KeepOnGroupPageManyInfo(footerValue).string : environment.strings.Story_Privacy_KeepOnChannelPageManyInfo(footerValue).string
} else {
footerText = environment.strings.Story_Privacy_KeepOnMyPageManyInfo(footerValue).string
}
} else {
if let sendAsPeerId = self.sendAsPeerId, sendAsPeerId.isGroupOrChannel == true {
footerText = isSendAsGroup ? environment.strings.Story_Privacy_KeepOnGroupPageInfo(footerValue).string : environment.strings.Story_Privacy_KeepOnChannelPageInfo(footerValue).string
} else {
footerText = environment.strings.Story_Privacy_KeepOnMyPageInfo(footerValue).string
}
} }
let footerSize = sectionFooter.update( let footerSize = sectionFooter.update(
transition: sectionFooterTransition, transition: sectionFooterTransition,
component: AnyComponent(MultilineTextComponent( component: AnyComponent(MultilineTextComponent(
@ -2371,7 +2392,7 @@ final class ShareWithPeersScreenComponent: Component {
) )
var footersTotalHeight: CGFloat = 0.0 var footersTotalHeight: CGFloat = 0.0
if case let .stories(editing) = component.stateContext.subject { if case let .stories(editing, _) = component.stateContext.subject {
let body = MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.freeTextColor) let body = MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.freeTextColor)
let bold = MarkdownAttributeSet(font: Font.semibold(13.0), textColor: environment.theme.list.freeTextColor) let bold = MarkdownAttributeSet(font: Font.semibold(13.0), textColor: environment.theme.list.freeTextColor)
let link = MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemAccentColor) let link = MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemAccentColor)
@ -2451,7 +2472,7 @@ final class ShareWithPeersScreenComponent: Component {
itemHeight: peerItemSize.height, itemHeight: peerItemSize.height,
itemCount: peers.count itemCount: peers.count
)) ))
} else if case let .stories(editing) = component.stateContext.subject { } else if case let .stories(editing, _) = component.stateContext.subject {
if !editing && hasChannels { if !editing && hasChannels {
sections.append(ItemLayout.Section( sections.append(ItemLayout.Section(
id: 0, id: 0,
@ -2533,12 +2554,17 @@ final class ShareWithPeersScreenComponent: Component {
switch component.stateContext.subject { switch component.stateContext.subject {
case .peers: case .peers:
title = environment.strings.Story_Privacy_PostStoryAs title = environment.strings.Story_Privacy_PostStoryAs
case let .stories(editing): case let .stories(editing, count):
if editing { if editing {
title = environment.strings.Story_Privacy_EditStory title = environment.strings.Story_Privacy_EditStory
} else { } else {
title = environment.strings.Story_Privacy_ShareStory if count > 1 {
actionButtonTitle = environment.strings.Story_Privacy_PostStory title = environment.strings.Story_Privacy_ShareStories
actionButtonTitle = environment.strings.Story_Privacy_PostStories(count)
} else {
title = environment.strings.Story_Privacy_ShareStory
actionButtonTitle = environment.strings.Story_Privacy_PostStory
}
} }
case let .chats(grayList): case let .chats(grayList):
if grayList { if grayList {
@ -2627,7 +2653,7 @@ final class ShareWithPeersScreenComponent: Component {
inset = 1000.0 inset = 1000.0
} else if case .channels = component.stateContext.subject { } else if case .channels = component.stateContext.subject {
inset = 1000.0 inset = 1000.0
} else if case let .stories(editing) = component.stateContext.subject { } else if case let .stories(editing, _) = component.stateContext.subject {
if editing { if editing {
inset = 351.0 inset = 351.0
inset += 10.0 + environment.safeInsets.bottom + 50.0 + footersTotalHeight inset += 10.0 + environment.safeInsets.bottom + 50.0 + footersTotalHeight
@ -3026,7 +3052,7 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
var categoryItems: [ShareWithPeersScreenComponent.CategoryItem] = [] var categoryItems: [ShareWithPeersScreenComponent.CategoryItem] = []
var optionItems: [ShareWithPeersScreenComponent.OptionItem] = [] var optionItems: [ShareWithPeersScreenComponent.OptionItem] = []
var coverItem: ShareWithPeersScreenComponent.CoverItem? var coverItem: ShareWithPeersScreenComponent.CoverItem?
if case let .stories(editing) = stateContext.subject { if case let .stories(editing, _) = stateContext.subject {
var everyoneSubtitle = presentationData.strings.Story_Privacy_ExcludePeople var everyoneSubtitle = presentationData.strings.Story_Privacy_ExcludePeople
if (stateContext.stateValue?.savedSelectedPeers[.everyone]?.count ?? 0) > 0 { if (stateContext.stateValue?.savedSelectedPeers[.everyone]?.count ?? 0) > 0 {
var peerNamesArray: [String] = [] var peerNamesArray: [String] = []

View File

@ -44,7 +44,7 @@ public extension ShareWithPeersScreen {
final class StateContext { final class StateContext {
public enum Subject: Equatable { public enum Subject: Equatable {
case peers(peers: [EnginePeer], peerId: EnginePeer.Id?) case peers(peers: [EnginePeer], peerId: EnginePeer.Id?)
case stories(editing: Bool) case stories(editing: Bool, count: Int32)
case chats(blocked: Bool) case chats(blocked: Bool)
case contacts(base: EngineStoryPrivacy.Base) case contacts(base: EngineStoryPrivacy.Base)
case contactsSearch(query: String, onlyContacts: Bool) case contactsSearch(query: String, onlyContacts: Bool)

View File

@ -5068,7 +5068,7 @@ public final class StoryItemSetContainerComponent: Component {
let stateContext = ShareWithPeersScreen.StateContext( let stateContext = ShareWithPeersScreen.StateContext(
context: context, context: context,
subject: .stories(editing: true), subject: .stories(editing: true, count: 1),
editing: true, editing: true,
initialSelectedPeers: selectedPeers, initialSelectedPeers: selectedPeers,
closeFriends: component.closeFriends.get(), closeFriends: component.closeFriends.get(),

View File

@ -3572,7 +3572,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return mediaPickerController(context: context, hasSearch: hasSearch, completion: completion) return mediaPickerController(context: context, hasSearch: hasSearch, completion: completion)
} }
public func makeStoryMediaPickerScreen(context: AccountContext, isDark: Bool, forCollage: Bool, selectionLimit: Int?, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, multipleCompletion: @escaping ([Any]) -> Void, dismissed: @escaping () -> Void, groupsPresented: @escaping () -> Void) -> ViewController { public func makeStoryMediaPickerScreen(context: AccountContext, isDark: Bool, forCollage: Bool, selectionLimit: Int?, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, multipleCompletion: @escaping ([Any], Bool) -> Void, dismissed: @escaping () -> Void, groupsPresented: @escaping () -> Void) -> ViewController {
return storyMediaPickerController(context: context, isDark: isDark, forCollage: forCollage, selectionLimit: selectionLimit, getSourceRect: getSourceRect, completion: completion, multipleCompletion: multipleCompletion, dismissed: dismissed, groupsPresented: groupsPresented) return storyMediaPickerController(context: context, isDark: isDark, forCollage: forCollage, selectionLimit: selectionLimit, getSourceRect: getSourceRect, completion: completion, multipleCompletion: multipleCompletion, dismissed: dismissed, groupsPresented: groupsPresented)
} }
@ -3737,7 +3737,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
externalState.storyTarget = target externalState.storyTarget = target
if let rootController = context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface { if let rootController = context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface {
rootController.proceedWithStoryUpload(target: target, result: result, existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit) rootController.proceedWithStoryUpload(target: target, results: [result], existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit)
} }
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: targetPeerId)) let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: targetPeerId))

View File

@ -391,6 +391,8 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
return .asset(asset) return .asset(asset)
case let .draft(draft): case let .draft(draft):
return .draft(draft, nil) return .draft(draft, nil)
case let .assets(assets):
return .assets(assets)
} }
} }
@ -451,7 +453,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
if let customTarget, case .botPreview = customTarget { if let customTarget, case .botPreview = customTarget {
externalState.storyTarget = customTarget externalState.storyTarget = customTarget
self.proceedWithStoryUpload(target: customTarget, result: result, existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit) self.proceedWithStoryUpload(target: customTarget, results: [result], existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit)
dismissCameraImpl?() dismissCameraImpl?()
return return
@ -484,7 +486,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
externalState.isPeerArchived = channel.storiesHidden ?? false externalState.isPeerArchived = channel.storiesHidden ?? false
} }
self.proceedWithStoryUpload(target: target, result: result, existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit) self.proceedWithStoryUpload(target: target, results: [result], existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit)
dismissCameraImpl?() dismissCameraImpl?()
}) })
@ -548,8 +550,8 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
}) })
} }
public func proceedWithStoryUpload(target: Stories.PendingTarget, result: MediaEditorScreenResult, existingMedia: EngineMedia?, forwardInfo: Stories.PendingForwardInfo?, externalState: MediaEditorTransitionOutExternalState, commit: @escaping (@escaping () -> Void) -> Void) { public func proceedWithStoryUpload(target: Stories.PendingTarget, results: [MediaEditorScreenResult], existingMedia: EngineMedia?, forwardInfo: Stories.PendingForwardInfo?, externalState: MediaEditorTransitionOutExternalState, commit: @escaping (@escaping () -> Void) -> Void) {
guard let result = result as? MediaEditorScreenImpl.Result else { guard let results = results as? [MediaEditorScreenImpl.Result] else {
return return
} }
let context = self.context let context = self.context
@ -657,83 +659,85 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
} }
if let _ = self.chatListController as? ChatListControllerImpl { if let _ = self.chatListController as? ChatListControllerImpl {
var media: EngineStoryInputMedia? for result in results {
var media: EngineStoryInputMedia?
if let mediaResult = result.media {
switch mediaResult { if let mediaResult = result.media {
case let .image(image, dimensions): switch mediaResult {
let tempFile = TempBox.shared.tempFile(fileName: "file") case let .image(image, dimensions):
defer {
TempBox.shared.dispose(tempFile)
}
if let imageData = compressImageToJPEG(image, quality: 0.7, tempFilePath: tempFile.path) {
media = .image(dimensions: dimensions, data: imageData, stickers: result.stickers)
}
case let .video(content, firstFrameImage, values, duration, dimensions):
let adjustments: VideoMediaResourceAdjustments
if let valuesData = try? JSONEncoder().encode(values) {
let data = MemoryBuffer(data: valuesData)
let digest = MemoryBuffer(data: data.md5Digest())
adjustments = VideoMediaResourceAdjustments(data: data, digest: digest, isStory: true)
let resource: TelegramMediaResource
switch content {
case let .imageFile(path):
resource = LocalFileVideoMediaResource(randomId: Int64.random(in: .min ... .max), path: path, adjustments: adjustments)
case let .videoFile(path):
resource = LocalFileVideoMediaResource(randomId: Int64.random(in: .min ... .max), path: path, adjustments: adjustments)
case let .asset(localIdentifier):
resource = VideoLibraryMediaResource(localIdentifier: localIdentifier, conversion: .compress(adjustments))
}
let tempFile = TempBox.shared.tempFile(fileName: "file") let tempFile = TempBox.shared.tempFile(fileName: "file")
defer { defer {
TempBox.shared.dispose(tempFile) TempBox.shared.dispose(tempFile)
} }
let imageData = firstFrameImage.flatMap { compressImageToJPEG($0, quality: 0.6, tempFilePath: tempFile.path) } if let imageData = compressImageToJPEG(image, quality: 0.7, tempFilePath: tempFile.path) {
let firstFrameFile = imageData.flatMap { data -> TempBoxFile? in media = .image(dimensions: dimensions, data: imageData, stickers: result.stickers)
let file = TempBox.shared.tempFile(fileName: "image.jpg")
if let _ = try? data.write(to: URL(fileURLWithPath: file.path)) {
return file
} else {
return nil
}
} }
case let .video(content, firstFrameImage, values, duration, dimensions):
var coverTime: Double? let adjustments: VideoMediaResourceAdjustments
if let coverImageTimestamp = values.coverImageTimestamp { if let valuesData = try? JSONEncoder().encode(values) {
if let trimRange = values.videoTrimRange { let data = MemoryBuffer(data: valuesData)
coverTime = min(duration, coverImageTimestamp - trimRange.lowerBound) let digest = MemoryBuffer(data: data.md5Digest())
} else { adjustments = VideoMediaResourceAdjustments(data: data, digest: digest, isStory: true)
coverTime = min(duration, coverImageTimestamp)
let resource: TelegramMediaResource
switch content {
case let .imageFile(path):
resource = LocalFileVideoMediaResource(randomId: Int64.random(in: .min ... .max), path: path, adjustments: adjustments)
case let .videoFile(path):
resource = LocalFileVideoMediaResource(randomId: Int64.random(in: .min ... .max), path: path, adjustments: adjustments)
case let .asset(localIdentifier):
resource = VideoLibraryMediaResource(localIdentifier: localIdentifier, conversion: .compress(adjustments))
} }
let tempFile = TempBox.shared.tempFile(fileName: "file")
defer {
TempBox.shared.dispose(tempFile)
}
let imageData = firstFrameImage.flatMap { compressImageToJPEG($0, quality: 0.6, tempFilePath: tempFile.path) }
let firstFrameFile = imageData.flatMap { data -> TempBoxFile? in
let file = TempBox.shared.tempFile(fileName: "image.jpg")
if let _ = try? data.write(to: URL(fileURLWithPath: file.path)) {
return file
} else {
return nil
}
}
var coverTime: Double?
if let coverImageTimestamp = values.coverImageTimestamp {
if let trimRange = values.videoTrimRange {
coverTime = min(duration, coverImageTimestamp - trimRange.lowerBound)
} else {
coverTime = min(duration, coverImageTimestamp)
}
}
media = .video(dimensions: dimensions, duration: duration, resource: resource, firstFrameFile: firstFrameFile, stickers: result.stickers, coverTime: coverTime)
} }
default:
media = .video(dimensions: dimensions, duration: duration, resource: resource, firstFrameFile: firstFrameFile, stickers: result.stickers, coverTime: coverTime) break
} }
default: } else if let existingMedia {
break media = .existing(media: existingMedia._asMedia())
}
if let media {
let _ = (context.engine.messages.uploadStory(
target: target,
media: media,
mediaAreas: result.mediaAreas,
text: result.caption.string,
entities: generateChatInputTextEntities(result.caption),
pin: result.options.pin,
privacy: result.options.privacy,
isForwardingDisabled: result.options.isForwardingDisabled,
period: result.options.timeout,
randomId: result.randomId,
forwardInfo: forwardInfo
)
|> deliverOnMainQueue).startStandalone(next: { stableId in
moveStorySource(engine: context.engine, peerId: context.account.peerId, from: result.randomId, to: Int64(stableId))
})
} }
} else if let existingMedia {
media = .existing(media: existingMedia._asMedia())
}
if let media {
let _ = (context.engine.messages.uploadStory(
target: target,
media: media,
mediaAreas: result.mediaAreas,
text: result.caption.string,
entities: generateChatInputTextEntities(result.caption),
pin: result.options.pin,
privacy: result.options.privacy,
isForwardingDisabled: result.options.isForwardingDisabled,
period: result.options.timeout,
randomId: result.randomId,
forwardInfo: forwardInfo
)
|> deliverOnMainQueue).startStandalone(next: { stableId in
moveStorySource(engine: context.engine, peerId: context.account.peerId, from: result.randomId, to: Int64(stableId))
})
} }
completionImpl() completionImpl()
} }

View File

@ -1483,7 +1483,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
externalState.storyTarget = target externalState.storyTarget = target
if let rootController = self.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface { if let rootController = self.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface {
rootController.proceedWithStoryUpload(target: target, result: result, existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit) rootController.proceedWithStoryUpload(target: target, results: [result], existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit)
} }
}) })
if let navigationController = self.controller?.getNavigationController() { if let navigationController = self.controller?.getNavigationController() {