[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_any" = "{user_list}, and **%d** more people do not accept calls.";
"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 {
@discardableResult
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 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 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

View File

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

View File

@ -790,6 +790,7 @@ private final class MainButtonNode: HighlightTrackingButtonNode {
iconNode = ASImageNode()
iconNode.displaysAsynchronously = false
iconNode.image = generateTintedImage(image: UIImage(bundleImageName: iconName), color: state.textColor)
self.iconNode = iconNode
self.addSubnode(iconNode)
}
if let iconSize = iconNode.image?.size {
@ -1806,7 +1807,9 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate {
} else {
height = bounds.height + 8.0
}
if !isNarrowButton {
if isTwoVerticalButtons && self.secondaryButtonState.smallSpacing {
} else if !isNarrowButton {
height += 9.0
}
if isTwoVerticalButtons {
@ -1896,7 +1899,8 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate {
mainButtonFrame = CGRect(origin: CGPoint(x: buttonOriginX, y: buttonOriginY + sideInset + buttonSize.height), size: buttonSize)
case .bottom:
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:
secondaryButtonFrame = CGRect(origin: CGPoint(x: buttonOriginX, 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)
}
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) {
if animated {
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 canBoostToUnrestrict: Bool
fileprivate let paidMediaAllowed: Bool
private let subject: Subject
fileprivate let subject: Subject
fileprivate let forCollage: Bool
private let saveEditedPhotos: Bool
@ -1826,6 +1826,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
fileprivate let secondaryButtonStatePromise = Promise<AttachmentMainButtonState?>(nil)
private let mainButtonAction: (() -> Void)?
private let secondaryButtonAction: (() -> Void)?
public init(
context: AccountContext,
@ -1845,7 +1846,8 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
selectionContext: TGMediaSelectionContext? = nil,
saveEditedPhotos: Bool = false,
mainButtonState: AttachmentMainButtonState? = nil,
mainButtonAction: (() -> Void)? = nil
mainButtonAction: (() -> Void)? = nil,
secondaryButtonAction: (() -> Void)? = nil
) {
self.context = context
@ -1865,6 +1867,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
self.saveEditedPhotos = saveEditedPhotos
self.mainButtonStatePromise.set(.single(mainButtonState))
self.mainButtonAction = mainButtonAction
self.secondaryButtonAction = secondaryButtonAction
let selectionContext = selectionContext ?? TGMediaSelectionContext()
@ -1998,7 +2001,14 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
} else if collection == nil {
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed))
var hasSelect = false
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))
} else {
if [.createSticker].contains(mode) {
@ -2338,6 +2348,9 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
let transition = ContainedViewLayoutTransition.animated(duration: 0.25, curve: .easeInOut)
var moreIsVisible = false
if case let .assets(_, mode) = self.subject, [.story, .createSticker].contains(mode) {
if count == 1 {
self.requestAttachmentMenuExpansion()
}
moreIsVisible = true
} 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)
@ -2381,7 +2394,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
transition.updateAlpha(node: self.moreButtonNode.iconNode, alpha: moreIsVisible ? 1.0 : 0.0)
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
var text = "Create 1 Story"
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)))
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 {
self.secondaryButtonStatePromise.set(.single(nil))
}
@ -2427,6 +2440,10 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
self.mainButtonAction?()
}
func secondaryButtonPressed() {
self.secondaryButtonAction?()
}
func dismissAllTooltips() {
self.undoOverlayController?.dismissWithCommitAction()
}
@ -2810,7 +2827,7 @@ final class MediaPickerContext: AttachmentMediaPickerContext {
private weak var controller: MediaPickerScreenImpl?
var selectionCount: Signal<Int, NoError> {
if self.controller?.forCollage == true {
if let controller = self.controller, case .assets(_, .story) = controller.subject {
return .single(0)
} else {
return Signal { [weak self] subscriber in
@ -2973,7 +2990,7 @@ final class MediaPickerContext: AttachmentMediaPickerContext {
}
func secondaryButtonAction() {
self.controller?.mainButtonPressed()
self.controller?.secondaryButtonPressed()
}
}
@ -3162,7 +3179,7 @@ public func storyMediaPickerController(
selectionLimit: Int?,
getSourceRect: @escaping () -> CGRect,
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,
groupsPresented: @escaping () -> Void
) -> 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: {
return nil
})
let controller = AttachmentController(
context: context,
updatedPresentationData: updatedPresentationData,
chatLocation: nil,
buttons: [.standalone],
initialButton: .standalone,
fromMenu: false,
hasTextInput: false,
makeEntityInputView: {
return nil
}
)
controller.forceSourceRect = true
controller.getSourceRect = getSourceRect
controller.requestController = { _, present in
@ -3207,7 +3233,18 @@ public func storyMediaPickerController(
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 asset(PHAsset)
case draft(MediaEditorDraft)
case assets([PHAsset])
func withPIPPosition(_ position: CameraScreenImpl.PIPPosition) -> Result {
switch self {
@ -3637,11 +3638,10 @@ public class CameraScreenImpl: ViewController, CameraScreen {
selectionLimit = 10
}
}
//TODO:unmock
controller = self.context.sharedContext.makeStoryMediaPickerScreen(
context: self.context,
isDark: true,
forCollage: self.cameraState.isCollageEnabled || "".isEmpty,
forCollage: self.cameraState.isCollageEnabled,
selectionLimit: selectionLimit,
getSourceRect: { [weak self] in
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 {
return
}
if !self.cameraState.isCollageEnabled {
var selectedGrid: Camera.CollageGrid = collageGrids.first!
for grid in collageGrids {
if grid.count == results.count {
selectedGrid = grid
break
if collage {
if !self.cameraState.isCollageEnabled {
var selectedGrid: Camera.CollageGrid = collageGrids.first!
for grid in collageGrids {
if grid.count == results.count {
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)
}, transition: .spring(duration: 0.3))
}
if let assets = results as? [PHAsset] {
var results: [Signal<CameraScreenImpl.Result, NoError>] = []
for asset in assets {
if asset.mediaType == .video && asset.duration > 1.0 {
results.append(.single(.asset(asset)))
} else {
results.append(
assetImage(asset: asset, targetSize: CGSize(width: 1080, height: 1080), exact: false, deliveryMode: .highQualityFormat)
|> runOn(Queue.concurrentDefaultQueue())
|> mapToSignal { image -> Signal<CameraScreenImpl.Result, NoError> in
if let image {
return .single(.image(Result.Image(image: image, additionalImage: nil, additionalImagePosition: .topLeft)))
} else {
return .complete()
if let assets = results as? [PHAsset] {
var results: [Signal<CameraScreenImpl.Result, NoError>] = []
for asset in assets {
if asset.mediaType == .video && asset.duration > 1.0 {
results.append(.single(.asset(asset)))
} else {
results.append(
assetImage(asset: asset, targetSize: CGSize(width: 1080, height: 1080), exact: false, deliveryMode: .highQualityFormat)
|> runOn(Queue.concurrentDefaultQueue())
|> mapToSignal { image -> Signal<CameraScreenImpl.Result, NoError> in
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

View File

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

View File

@ -160,7 +160,7 @@ public extension MediaEditorScreenImpl {
} else {
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 {

View File

@ -327,6 +327,7 @@ final class MediaEditorScreenComponent: Component {
private let switchCameraButton = ComponentView<Empty>()
private let selectionButton = ComponentView<Empty>()
private let selectionPanel = ComponentView<Empty>()
private let textCancelButton = ComponentView<Empty>()
private let textDoneButton = ComponentView<Empty>()
@ -741,6 +742,13 @@ final class MediaEditorScreenComponent: Component {
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() {
guard let controller = self.environment?.controller() as? MediaEditorScreenImpl, let mediaEditor = controller.node.mediaEditor else {
return
@ -1993,39 +2001,118 @@ final class MediaEditorScreenComponent: Component {
transition.setAlpha(view: switchCameraButtonView, alpha: isRecordingAdditionalVideo ? 1.0 : 0.0)
}
let selectionButtonSize = self.selectionButton.update(
transition: transition,
component: AnyComponent(PlainButtonComponent(
content: AnyComponent(
SelectionPanelButtonContentComponent(
count: 1,
isSelected: self.isSelectionPanelOpen,
tag: nil
if controller.node.items.count > 1 {
let selectionButtonSize = self.selectionButton.update(
transition: transition,
component: AnyComponent(PlainButtonComponent(
content: AnyComponent(
SelectionPanelButtonContentComponent(
count: Int32(controller.node.items.count(where: { $0.isEnabled })),
isSelected: self.isSelectionPanelOpen,
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,
action: { [weak self] in
if let self {
self.isSelectionPanelOpen = !self.isSelectionPanelOpen
self.state?.updated()
var selectedItemId = ""
if case let .asset(asset) = controller.node.subject {
selectedItemId = asset.localIdentifier
}
},
animateAlpha: false
)),
environment: {},
containerSize: CGSize(width: 33.0, height: 33.0)
)
let selectionButtonFrame = CGRect(
origin: CGPoint(x: availableSize.width - selectionButtonSize.width - 12.0, y: max(environment.statusBarHeight + 10.0, inputPanelFrame.minY - selectionButtonSize.height - 3.0)),
size: selectionButtonSize
)
if let selectionButtonView = self.selectionButton.view {
if selectionButtonView.superview == nil {
self.addSubview(selectionButtonView)
let _ = self.selectionPanel.update(
transition: transition,
component: AnyComponent(
SelectionPanelComponent(
previewContainerView: controller.node.previewContentContainerView,
frame: selectionPanelFrame,
items: controller.node.items,
selectedItemId: selectedItemId,
itemTapped: { [weak self, weak controller] id in
guard let self, let controller else {
return
}
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 {
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 {
private weak var controller: MediaEditorScreenImpl?
private let context: AccountContext
@ -2803,6 +2922,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
var subject: MediaEditorScreenImpl.Subject?
var actualSubject: MediaEditorScreenImpl.Subject?
var items: [EditingItem] = []
private var subjectDisposable: Disposable?
private var appInForegroundDisposable: Disposable?
@ -2891,6 +3011,10 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
private let readyValue = Promise<Bool>()
var componentHostView: MediaEditorScreenComponent.View? {
return self.componentHost.view as? MediaEditorScreenComponent.View
}
init(controller: MediaEditorScreenImpl) {
self.controller = controller
self.context = controller.context
@ -3062,7 +3186,46 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|> deliverOnMainQueue
).start(next: { [weak self] subject in
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()
}
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 {
return
}
self.actualSubject = 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
self.subject = subject
Queue.mainQueue().justDispatch {
controller.setupAudioSessionIfNeeded()
}
if case let .draft(draft, _) = subject, let privacy = draft.privacy {
if let privacy {
controller.state.privacy = privacy
}
@ -3229,7 +3381,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
controller.isSavingAvailable = isSavingAvailable
controller.requestLayout(transition: .immediate)
let mediaDimensions = effectiveSubject.dimensions
let mediaDimensions = subject.dimensions
let maxSide: CGFloat = 1920.0 / UIScreen.main.scale
let fittedSize = mediaDimensions.cgSize.fitted(CGSize(width: maxSide, height: maxSide))
let mediaEntity = DrawingMediaEntity(size: fittedSize)
@ -3268,27 +3420,28 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
}
let initialValues: MediaEditorValues?
if case let .draft(draft, _) = subject {
initialValues = draft.values
if let values {
initialValues = values
for entity in draft.values.entities {
for entity in values.entities {
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)
}
} else {
initialValues = nil
}
var mediaEditorMode: MediaEditor.Mode = .default
if case .stickerEditor = controller.mode {
let mediaEditorMode: MediaEditor.Mode
switch controller.mode {
case .stickerEditor:
mediaEditorMode = .sticker
} else if case .avatarEditor = controller.mode {
mediaEditorMode = .avatar
} else if case .coverEditor = controller.mode {
case .avatarEditor, .coverEditor:
mediaEditorMode = .avatar
default:
mediaEditorMode = .default
}
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 {
mediaEditor.setVideoIsMuted(true)
} else if case let .coverEditor(dimensions) = controller.mode {
@ -3327,7 +3486,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
mediaEditor.seek(initialVideoPosition, andPlay: true)
}
}
if self.context.sharedContext.currentPresentationData.with({$0}).autoNightModeTriggered {
if !isDraft, self.context.sharedContext.currentPresentationData.with({$0}).autoNightModeTriggered {
switch subject {
case .message, .gift:
mediaEditor.setNightTheme(true)
@ -3347,46 +3506,48 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
controller.requestLayout(transition: .animated(duration: 0.25, curve: .easeInOut))
}
}
self.stickerCutoutStatusDisposable = (mediaEditor.cutoutStatus
|> deliverOnMainQueue).start(next: { [weak self] cutoutStatus in
guard let self else {
return
if case .stickerEditor = controller.mode {
self.stickerCutoutStatusDisposable = (mediaEditor.cutoutStatus
|> deliverOnMainQueue).start(next: { [weak self] cutoutStatus in
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
self.requestLayout(forceUpdate: true, transition: .easeInOut(duration: 0.25))
})
mediaEditor.maskUpdated = { [weak self] mask, apply in
guard let self else {
return
mediaEditor.classificationUpdated = { [weak self] classes in
guard let self else {
return
}
self.controller?.stickerRecommendedEmoji = emojiForClasses(classes.map { $0.0 })
}
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))
if case .empty = effectiveSubject {
if case .empty = subject {
self.stickerMaskDrawingView?.emptyColor = .black
self.stickerMaskDrawingView?.clearWithEmptyColor()
}
switch effectiveSubject {
switch subject {
case .message, .gift:
break
default:
self.readyValue.set(.single(true))
}
switch effectiveSubject {
switch subject {
case let .image(_, _, additionalImage, position):
if let additionalImage {
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:
var isGift = false
let messages: Signal<[Message], NoError>
if case let .message(messageIds) = effectiveSubject {
if case let .message(messageIds) = subject {
messages = self.context.engine.data.get(
EngineDataMap(messageIds.map(TelegramEngine.EngineData.Item.Messages.Message.init(id:)))
)
@ -3444,7 +3605,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
}
return messages
}
} else if case let .gift(gift) = effectiveSubject {
} else if case let .gift(gift) = subject {
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 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>
if let subject = self.subject, case .gift = subject {
if case .gift = subject {
wallpaperColors = self.mediaEditorPromise.get()
|> mapToSignal { mediaEditor in
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)
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 case .message = (stickerEntityView.entity as! DrawingStickerEntity).content {
return true
@ -3508,13 +3669,6 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
}
return false
}) 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
let messageEntity = existingEntityView.entity as! DrawingStickerEntity
messageEntity.renderImage = result.dayImage
@ -3524,7 +3678,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
} else {
var content: DrawingStickerEntity.Content
var position: CGPoint
switch effectiveSubject {
switch subject {
case let .message(messageIds):
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)
@ -3579,7 +3733,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
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.layer.allowsGroupOpacity = true
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.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, completion: { _ in
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
@ -3815,7 +3975,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
return nil
}
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) {
context.withFlippedContext { context in
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)
completion()
case .camera:
if let view = self.componentHost.view as? MediaEditorScreenComponent.View {
view.animateIn(from: .camera, completion: completion)
}
self.componentHostView?.animateIn(from: .camera, completion: completion)
if let subject = self.subject, case let .video(_, mainTransitionImage, _, _, additionalTransitionImage, _, _, positionChangeTimestamps, pipPosition) = subject, let mainTransitionImage {
var transitionImage = mainTransitionImage
@ -4227,7 +4385,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
backgroundImage = additionalTransitionImage
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)
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)
}
if let sourceView = transitionIn.sourceView {
if let view = self.componentHost.view as? MediaEditorScreenComponent.View {
view.animateIn(from: .gallery)
}
self.componentHostView?.animateIn(from: .gallery)
let sourceLocalFrame = sourceView.convert(transitionIn.sourceRect, to: self.view)
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.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.animateScale(from: sourceScale, to: 1.0, duration: duration, timingFunction: timingFunction)
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 {
self.layer.animatePosition(from: CGPoint(x: 0.0, y: layout.size.height), to: .zero, duration: 0.35, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
completion()
} else if let view = self.componentHost.view as? MediaEditorScreenComponent.View {
view.animateIn(from: .camera, completion: completion)
} else {
self.componentHostView?.animateIn(from: .camera, completion: completion)
}
}
}
@ -4346,9 +4502,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
destinationTransitionView = destinationTransitionOutView
destinationTransitionRect = galleryTransitionIn.sourceRect
}
if let view = self.componentHost.view as? MediaEditorScreenComponent.View {
view.animateOut(to: .gallery)
}
self.componentHostView?.animateOut(to: .gallery)
}
let destinationLocalFrame = destinationView.convert(transitionOut.destinationRect, to: self.view)
let destinationScale = destinationLocalFrame.width / self.previewContainerView.frame.width
@ -4446,7 +4600,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
removeOnCompletion: false
)
if let componentView = self.componentHost.view {
if let componentView = self.componentHostView {
componentView.clipsToBounds = true
componentView.layer.animatePosition(from: componentView.center, to: destinationLocalFrame.center, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
componentView.layer.animateScale(from: 1.0, to: 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 {
if let view = self.componentHost.view as? MediaEditorScreenComponent.View {
view.animateOut(to: .camera)
}
self.componentHostView?.animateOut(to: .camera)
let transition = ComponentTransition(animation: .curve(duration: 0.25, curve: .easeInOut))
transition.setAlpha(view: self.previewContainerView, alpha: 0.0, completion: { _ in
completion()
})
} else {
if controller.isEmbeddedEditor {
if let view = self.componentHost.view as? MediaEditorScreenComponent.View {
view.animateOut(to: .gallery)
}
self.componentHostView?.animateOut(to: .gallery)
self.layer.allowsGroupOpacity = true
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
let transition: ComponentTransition = .easeInOut(duration: 0.2)
if let view = self.componentHost.view as? MediaEditorScreenComponent.View {
view.animateOutToTool(inPlace: inPlace, transition: transition)
}
self.componentHostView?.animateOutToTool(inPlace: inPlace, transition: transition)
self.requestUpdate(transition: transition)
}
@ -4505,9 +4653,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
self.isDisplayingTool = nil
let transition: ComponentTransition = .easeInOut(duration: 0.2)
if let view = self.componentHost.view as? MediaEditorScreenComponent.View {
view.animateInFromTool(inPlace: inPlace, transition: transition)
}
self.componentHostView?.animateInFromTool(inPlace: inPlace, transition: transition)
self.requestUpdate(transition: transition)
}
@ -4721,12 +4867,10 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
}
var location: CLLocationCoordinate2D?
if let subject = self.actualSubject {
if case let .asset(asset) = subject {
location = asset.location?.coordinate
} else if case let .draft(draft, _) = subject {
location = draft.location
}
if case let .draft(draft, _) = self.actualSubject {
location = draft.location
} else if case let .asset(asset) = self.subject {
location = asset.location?.coordinate
}
let locationController = storyLocationPickerController(
context: self.context,
@ -5195,7 +5339,6 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
func addWeather(_ weather: StickerPickerScreen.Weather.LoadedWeather?) {
guard let weather else {
return
}
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) {
guard let controller = self.controller else {
return
@ -5323,7 +5518,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
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)
if let previewResult = self.previewContainerView.hitTest(point, with: event) {
return previewResult
@ -6181,6 +6376,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
case message([MessageId])
case gift(StarGift.UniqueGift)
case sticker(TelegramMediaFile, [String])
case assets([PHAsset])
var dimensions: PixelDimensions {
switch self {
@ -6192,8 +6388,8 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
return PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight))
case let .draft(draft, _):
return draft.dimensions
case .message, .gift, .sticker, .videoCollage:
return PixelDimensions(width: 1080, height: 1920)
case .message, .gift, .sticker, .videoCollage, .assets:
return PixelDimensions(storyDimensions)
}
}
@ -6220,6 +6416,8 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
return .gift(gift)
case let .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
case .sticker:
return false
case .assets:
return false
}
}
}
@ -6555,7 +6755,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
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 coverImage: UIImage?
@ -6567,7 +6767,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
let stateContext = ShareWithPeersScreen.StateContext(
context: self.context,
subject: .stories(editing: false),
subject: .stories(editing: false, count: Int32(self.node.items.count(where: { $0.isEnabled }))),
editing: false,
initialPeerIds: Set(privacy.privacy.additionallyIncludePeers),
closeFriends: self.closeFriends.get(),
@ -7105,13 +7305,9 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
self?.dismissed()
})
}
func getCaption() -> NSAttributedString {
return (self.node.componentHost.view as? MediaEditorScreenComponent.View)?.getInputText() ?? NSAttributedString()
}
fileprivate func checkCaptionLimit() -> Bool {
let caption = self.getCaption()
let caption = self.node.getCaption()
if caption.length > self.context.userLimits.maxStoryCaptionLength {
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId))
|> 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)
mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities)
var caption = self.getCaption()
var caption = self.node.getCaption()
caption = convertMarkdownToAttributes(caption)
var hasEntityChanges = false
@ -7209,7 +7405,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
}
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?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in
@ -7498,7 +7694,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
}
duration = 5.0
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))
}, opaque: false, scale: 1.0)
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
firstFrame = .single((image, nil))
case .assets:
fatalError()
}
let _ = combineLatest(queue: Queue.mainQueue(), firstFrame, videoResult)
@ -8399,6 +8597,8 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
}
case let .sticker(file, _):
exportSubject = .single(.sticker(file: file))
case .assets:
fatalError()
}
let _ = (exportSubject

View File

@ -11,7 +11,16 @@ import DrawingUI
extension MediaEditorScreenImpl {
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
}
guard let mediaEditor = self.node.mediaEditor else {
@ -21,13 +30,6 @@ extension MediaEditorScreenImpl {
let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.node.entitiesView)
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
if entity is DrawingMediaEntity {
return false
@ -44,34 +46,42 @@ extension MediaEditorScreenImpl {
let values = mediaEditor.values
let filteredValues = values.withUpdatedEntities([])
let caption = self.node.getCaption()
let caption = self.getCaption()
if let subject = self.node.subject {
if case .asset = subject, !values.hasChanges && caption.string.isEmpty {
return false
} else if case .message = subject, !filteredValues.hasChanges && filteredEntities.isEmpty && caption.string.isEmpty {
return false
} else if case .gift = subject, !filteredValues.hasChanges && filteredEntities.isEmpty && caption.string.isEmpty {
return false
} else if case .empty = subject, !self.node.hasAnyChanges && !self.node.drawingView.internalState.canUndo {
return false
} else if case .videoCollage = subject {
switch subject {
case .asset:
if !values.hasChanges && caption.string.isEmpty {
return false
}
case .message, .gift:
if !filteredValues.hasChanges && filteredEntities.isEmpty && caption.string.isEmpty {
return false
}
case .empty:
if !self.node.hasAnyChanges && !self.node.drawingView.internalState.canUndo {
return false
}
case .videoCollage:
return false
default:
break
}
}
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 {
return
}
try? FileManager.default.createDirectory(atPath: draftPath(engine: self.context.engine), withIntermediateDirectories: true)
let values = mediaEditor.values
let privacy = self.state.privacy
let forwardSource = self.forwardSource
let caption = self.getCaption()
let caption = self.node.getCaption()
let duration = mediaEditor.duration ?? 0.0
let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
@ -99,10 +109,19 @@ extension MediaEditorScreenImpl {
}
if let resultImage = mediaEditor.resultImage {
if !edit {
if !isEdit {
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 {
return
}
@ -148,54 +167,64 @@ extension MediaEditorScreenImpl {
}
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))
if let thumbnailImage = generateScaledImage(image: resultImage, size: fittedSize) {
let path = "\(Int64.random(in: .min ... .max)).\(media.fileExtension)"
let draft = MediaEditorDraft(
path: path,
isVideo: media.isVideo,
thumbnail: thumbnailImage,
dimensions: media.dimensions,
duration: media.duration,
values: values,
caption: caption,
privacy: privacy,
forwardInfo: forwardSource.flatMap { StoryId(peerId: $0.0.id, id: $0.1.id) },
timestamp: timestamp,
location: location,
expiresOn: expiresOn
)
switch media {
case let .image(image, _):
if let data = image.jpegData(compressionQuality: 0.87) {
try? data.write(to: URL(fileURLWithPath: draft.fullPath(engine: context.engine)))
}
case let .video(path, _, _):
try? FileManager.default.copyItem(atPath: path, toPath: draft.fullPath(engine: context.engine))
guard let thumbnailImage = generateScaledImage(image: resultImage, size: fittedSize) else {
return nil
}
let path = "\(Int64.random(in: .min ... .max)).\(media.fileExtension)"
let draft = MediaEditorDraft(
path: path,
isVideo: media.isVideo,
thumbnail: thumbnailImage,
dimensions: media.dimensions,
duration: media.duration,
values: values,
caption: caption,
privacy: privacy,
forwardInfo: forwardSource.flatMap { StoryId(peerId: $0.0.id, id: $0.1.id) },
timestamp: timestamp,
location: location,
expiresOn: expiresOn
)
switch media {
case let .image(image, _):
if let data = image.jpegData(compressionQuality: 0.87) {
try? data.write(to: URL(fileURLWithPath: 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 {
saveStorySource(engine: context.engine, item: draft, peerId: context.account.peerId, id: id)
} else {
addStoryDraft(engine: context.engine, item: draft)
}
}
return draft
}
switch subject {
case .empty:
break
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, _, _, _):
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):
let _ = items
case let .asset(asset):
if asset.mediaType == .video {
PHImageManager.default().requestAVAsset(forVideo: asset, options: nil) { avAsset, _, _ in
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 {
@ -203,22 +232,32 @@ extension MediaEditorScreenImpl {
options.deliveryMode = .highQualityFormat
PHImageManager.default().requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .default, options: options) { image, _ in
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, _):
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)) {
innerSaveDraft(media: .image(image: image, dimensions: draft.dimensions))
if let draft = innerSaveDraft(media: .image(image: image, dimensions: draft.dimensions)) {
completion?(draft)
}
}
case .message, .gift:
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:
break
case .assets:
break
}
if case let .draft(draft, _) = actualSubject {

View File

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

View File

@ -1,7 +1,670 @@
import Foundation
import UIKit
import Display
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)}
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: {}
@ -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))
},
multipleCompletion: { _ in },
multipleCompletion: { _, _ in },
dismissed: {},
groupsPresented: {}
)

View File

@ -991,10 +991,22 @@ final class ShareWithPeersScreenComponent: Component {
}
let sectionTitle: String
if section.id == 0, case .stories = component.stateContext.subject {
sectionTitle = component.coverItem == nil ? environment.strings.Story_Privacy_PostStoryAsHeader : ""
if section.id == 0, case let .stories(_, count) = component.stateContext.subject {
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 {
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 {
if case let .members(isGroup, _, _) = component.stateContext.subject {
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))
var footerText = environment.strings.Story_Privacy_KeepOnMyPageInfo(footerValue).string
if let sendAsPeerId = self.sendAsPeerId, sendAsPeerId.isGroupOrChannel == true {
footerText = isSendAsGroup ? environment.strings.Story_Privacy_KeepOnGroupPageInfo(footerValue).string : environment.strings.Story_Privacy_KeepOnChannelPageInfo(footerValue).string
var footerText: String
if case let .stories(_, count) = component.stateContext.subject, count > 1 {
if let sendAsPeerId = self.sendAsPeerId, sendAsPeerId.isGroupOrChannel == true {
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(
transition: sectionFooterTransition,
component: AnyComponent(MultilineTextComponent(
@ -2371,7 +2392,7 @@ final class ShareWithPeersScreenComponent: Component {
)
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 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)
@ -2451,7 +2472,7 @@ final class ShareWithPeersScreenComponent: Component {
itemHeight: peerItemSize.height,
itemCount: peers.count
))
} else if case let .stories(editing) = component.stateContext.subject {
} else if case let .stories(editing, _) = component.stateContext.subject {
if !editing && hasChannels {
sections.append(ItemLayout.Section(
id: 0,
@ -2533,12 +2554,17 @@ final class ShareWithPeersScreenComponent: Component {
switch component.stateContext.subject {
case .peers:
title = environment.strings.Story_Privacy_PostStoryAs
case let .stories(editing):
case let .stories(editing, count):
if editing {
title = environment.strings.Story_Privacy_EditStory
} else {
title = environment.strings.Story_Privacy_ShareStory
actionButtonTitle = environment.strings.Story_Privacy_PostStory
if count > 1 {
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):
if grayList {
@ -2627,7 +2653,7 @@ final class ShareWithPeersScreenComponent: Component {
inset = 1000.0
} else if case .channels = component.stateContext.subject {
inset = 1000.0
} else if case let .stories(editing) = component.stateContext.subject {
} else if case let .stories(editing, _) = component.stateContext.subject {
if editing {
inset = 351.0
inset += 10.0 + environment.safeInsets.bottom + 50.0 + footersTotalHeight
@ -3026,7 +3052,7 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
var categoryItems: [ShareWithPeersScreenComponent.CategoryItem] = []
var optionItems: [ShareWithPeersScreenComponent.OptionItem] = []
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
if (stateContext.stateValue?.savedSelectedPeers[.everyone]?.count ?? 0) > 0 {
var peerNamesArray: [String] = []

View File

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

View File

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

View File

@ -3572,7 +3572,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
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)
}
@ -3737,7 +3737,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
externalState.storyTarget = target
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))

View File

@ -391,6 +391,8 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
return .asset(asset)
case let .draft(draft):
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 {
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?()
return
@ -484,7 +486,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
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?()
})
@ -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) {
guard let result = result as? MediaEditorScreenImpl.Result else {
public func proceedWithStoryUpload(target: Stories.PendingTarget, results: [MediaEditorScreenResult], existingMedia: EngineMedia?, forwardInfo: Stories.PendingForwardInfo?, externalState: MediaEditorTransitionOutExternalState, commit: @escaping (@escaping () -> Void) -> Void) {
guard let results = results as? [MediaEditorScreenImpl.Result] else {
return
}
let context = self.context
@ -657,83 +659,85 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
}
if let _ = self.chatListController as? ChatListControllerImpl {
var media: EngineStoryInputMedia?
if let mediaResult = result.media {
switch mediaResult {
case let .image(image, dimensions):
let tempFile = TempBox.shared.tempFile(fileName: "file")
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))
}
for result in results {
var media: EngineStoryInputMedia?
if let mediaResult = result.media {
switch mediaResult {
case let .image(image, dimensions):
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
}
if let imageData = compressImageToJPEG(image, quality: 0.7, tempFilePath: tempFile.path) {
media = .image(dimensions: dimensions, data: imageData, stickers: result.stickers)
}
var coverTime: Double?
if let coverImageTimestamp = values.coverImageTimestamp {
if let trimRange = values.videoTrimRange {
coverTime = min(duration, coverImageTimestamp - trimRange.lowerBound)
} else {
coverTime = min(duration, coverImageTimestamp)
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")
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)
}
media = .video(dimensions: dimensions, duration: duration, resource: resource, firstFrameFile: firstFrameFile, stickers: result.stickers, coverTime: coverTime)
default:
break
}
default:
break
} 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))
})
}
} 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()
}

View File

@ -1483,7 +1483,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
externalState.storyTarget = target
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() {