Add story editing from stories grid

This commit is contained in:
Ilya Laktyushin 2024-05-07 00:33:45 +04:00
parent bd4de97bd8
commit bb543f49ea
7 changed files with 104 additions and 288 deletions

View File

@ -3109,6 +3109,13 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
if controller.isEmbeddedEditor == true {
mediaEditor.onFirstDisplay = { [weak self] in
if let self {
if let transitionInView = self.transitionInView {
self.transitionInView = nil
transitionInView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak transitionInView] _ in
transitionInView?.removeFromSuperview()
})
}
if effectiveSubject.isPhoto {
self.previewContainerView.layer.allowsGroupOpacity = true
self.previewContainerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, completion: { _ in
@ -3765,6 +3772,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
if let transitionOut = controller.transitionOut(finished, isNew), let destinationView = transitionOut.destinationView {
var destinationTransitionView: UIView?
var destinationTransitionRect: CGRect = .zero
if !finished {
if let transitionIn = controller.transitionIn, case let .gallery(galleryTransitionIn) = transitionIn, let sourceImage = galleryTransitionIn.sourceImage, isNew != true {
let sourceSuperView = galleryTransitionIn.sourceView?.superview?.superview
@ -3774,6 +3782,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
destinationTransitionOutView.frame = self.previewContainerView.convert(self.previewContainerView.bounds, to: sourceSuperView)
sourceSuperView?.addSubview(destinationTransitionOutView)
destinationTransitionView = destinationTransitionOutView
destinationTransitionRect = galleryTransitionIn.sourceRect
}
if let view = self.componentHost.view as? MediaEditorScreenComponent.View {
view.animateOut(to: .gallery)
@ -3853,7 +3862,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
if let destinationTransitionView {
self.previewContainerView.layer.allowsGroupOpacity = true
self.previewContainerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false)
destinationTransitionView.layer.animateFrame(from: destinationTransitionView.frame, to: destinationView.convert(destinationView.bounds, to: destinationTransitionView.superview), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { [weak destinationTransitionView] _ in
destinationTransitionView.layer.animateFrame(from: destinationTransitionView.frame, to: destinationView.convert(destinationTransitionRect, to: destinationTransitionView.superview), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { [weak destinationTransitionView] _ in
destinationTransitionView?.removeFromSuperview()
})
}

View File

@ -43,6 +43,7 @@ swift_library(
"//submodules/UndoUI",
"//submodules/TelegramUI/Components/PlainButtonComponent",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/TelegramUI/Components/MediaEditorScreen",
],
visibility = [
"//visibility:public",

View File

@ -33,6 +33,7 @@ import ShareController
import UndoUI
import PlainButtonComponent
import ComponentDisplayAdapters
import MediaEditorScreen
private let mediaBadgeBackgroundColor = UIColor(white: 0.0, alpha: 0.6)
private let mediaBadgeTextColor = UIColor.white
@ -1266,6 +1267,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
private let listDisposable = MetaDisposable()
private var hiddenMediaDisposable: Disposable?
private let updateDisposable = MetaDisposable()
private var numberOfItemsToRequest: Int = 50
private var isRequestingView: Bool = false
@ -1765,6 +1767,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
self.hiddenMediaDisposable?.dispose()
self.animationTimer?.invalidate()
self.presentationDataDisposable?.dispose()
self.updateDisposable.dispose()
}
public func loadHole(anchor: SparseItemGrid.HoleAnchor, at location: SparseItemGrid.HoleLocation) -> Signal<Never, NoError> {
@ -1858,16 +1861,54 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
})))
}
/*items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.StoryList_ItemAction_Edit, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in
c.dismiss(completion: {
guard let self else {
return
}
let _ = self
})
})))*/
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.StoryList_ItemAction_Edit, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in
c.dismiss(completion: {
guard let self else {
return
}
let _ = (self.context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: self.peerId)
)
|> deliverOnMainQueue).startStandalone(next: { [weak self] peer in
guard let self, let peer else {
return
}
var foundItemLayer: SparseItemGridLayer?
var sourceImage: UIImage?
self.itemGrid.forEachVisibleItem { gridItem in
guard let itemLayer = gridItem.layer as? ItemLayer else {
return
}
if let listItem = itemLayer.item, listItem.story.id == item.id {
foundItemLayer = itemLayer
if let contents = itemLayer.contents, CFGetTypeID(contents as CFTypeRef) == CGImage.typeID {
sourceImage = UIImage(cgImage: contents as! CGImage)
}
}
}
guard let controller = MediaEditorScreen.makeEditStoryController(
context: self.context,
peer: peer,
storyItem: item,
videoPlaybackPosition: nil,
repost: false,
transitionIn: .gallery(MediaEditorScreen.TransitionIn.GalleryTransitionIn(sourceView: self.itemGrid.view, sourceRect: foundItemLayer?.frame ?? .zero, sourceImage: sourceImage)),
transitionOut: MediaEditorScreen.TransitionOut(destinationView: self.itemGrid.view, destinationRect: foundItemLayer?.frame ?? .zero, destinationCornerRadius: 0.0),
update: { [weak self] disposable in
guard let self else {
return
}
self.updateDisposable.set(disposable)
}
) else {
return
}
self.parentController?.push(controller)
})
})
})))
}
if !item.isForwardingDisabled, case .everyone = item.privacy?.base {
@ -1880,7 +1921,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
let _ = (self.context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: self.peerId)
)
|> deliverOnMainQueue).startStandalone(next: { [weak self] peer in
|> deliverOnMainQueue).startStandalone(next: { [weak self] peer in
guard let self else {
return
}

View File

@ -4308,7 +4308,7 @@ public final class StoryItemSetContainerComponent: Component {
self.sendMessageContext.currentSpeechHolder = speechHolder
}
case .translate:
self.sendMessageContext.performTranslateTextAction(view: self, text: text.string)
self.sendMessageContext.performTranslateTextAction(view: self, text: text.string, entities: [])
case .quote:
break
}
@ -5359,13 +5359,9 @@ public final class StoryItemSetContainerComponent: Component {
private let updateDisposable = MetaDisposable()
func openStoryEditing(repost: Bool = false) {
guard let component = self.component, let peerReference = PeerReference(component.slice.peer._asPeer()) else {
guard let component = self.component else {
return
}
let context = component.context
let peerId = component.slice.peer.id
let item = component.slice.item.storyItem
let id = item.id
self.isEditingStory = true
self.updateIsProgressPaused()
@ -5376,277 +5372,39 @@ public final class StoryItemSetContainerComponent: Component {
videoPlaybackPosition = view.videoPlaybackPosition
}
let subject: Signal<MediaEditorScreen.Subject?, NoError>
subject = getStorySource(engine: component.context.engine, peerId: component.context.account.peerId, id: Int64(item.id))
|> mapToSignal { source in
if !repost, let source {
return .single(.draft(source, Int64(item.id)))
} else {
let media = item.media._asMedia()
return fetchMediaData(context: context, postbox: context.account.postbox, userLocation: .peer(peerReference.id), customUserContentType: .story, mediaReference: .story(peer: peerReference, id: item.id, media: media))
|> mapToSignal { (value, isImage) -> Signal<MediaEditorScreen.Subject?, NoError> in
guard case let .data(data) = value, data.complete else {
return .complete()
}
if let image = UIImage(contentsOfFile: data.path) {
return .single(nil)
|> then(
.single(.image(image, PixelDimensions(image.size), nil, .bottomRight))
|> delay(0.1, queue: Queue.mainQueue())
)
} else {
var duration: Double?
if let file = media as? TelegramMediaFile {
duration = file.duration
}
let symlinkPath = data.path + ".mp4"
if fileSize(symlinkPath) == nil {
let _ = try? FileManager.default.linkItem(atPath: data.path, toPath: symlinkPath)
}
return .single(nil)
|> then(
.single(.video(symlinkPath, nil, false, nil, nil, PixelDimensions(width: 720, height: 1280), duration ?? 0.0, [], .bottomRight))
)
}
}
}
}
let initialCaption: NSAttributedString?
let initialPrivacy: EngineStoryPrivacy?
let initialMediaAreas: [MediaArea]
if repost {
initialCaption = nil
initialPrivacy = nil
initialMediaAreas = []
} else {
initialCaption = chatInputStateStringWithAppliedEntities(item.text, entities: item.entities)
initialPrivacy = item.privacy
initialMediaAreas = item.mediaAreas
}
let externalState = MediaEditorTransitionOutExternalState(
storyTarget: nil,
isForcedTarget: false,
isPeerArchived: false,
transitionOut: nil
)
var updateProgressImpl: ((Float) -> Void)?
let controller = MediaEditorScreen(
context: context,
mode: .storyEditor,
subject: subject,
isEditing: !repost,
forwardSource: repost ? (component.slice.peer, item) : nil,
initialCaption: initialCaption,
initialPrivacy: initialPrivacy,
initialMediaAreas: initialMediaAreas,
initialVideoPosition: videoPlaybackPosition,
guard let controller = MediaEditorScreen.makeEditStoryController(
context: component.context,
peer: component.slice.peer,
storyItem: component.slice.item.storyItem,
videoPlaybackPosition: videoPlaybackPosition,
repost: repost,
transitionIn: .noAnimation,
transitionOut: { finished, isNew in
if repost && finished {
if let transitionOut = externalState.transitionOut?(externalState.storyTarget, externalState.isPeerArchived), let destinationView = transitionOut.destinationView {
return MediaEditorScreen.TransitionOut(
destinationView: destinationView,
destinationRect: transitionOut.destinationRect,
destinationCornerRadius: transitionOut.destinationCornerRadius
)
} else {
return nil
}
} else {
return nil
}
},
completion: { [weak self] result, commit in
transitionOut: nil,
completed: { [weak self] in
guard let self else {
return
}
let entities = generateChatInputTextEntities(result.caption)
if repost {
let target: Stories.PendingTarget
let targetPeerId: EnginePeer.Id
if let sendAsPeerId = result.options.sendAsPeerId {
target = .peer(sendAsPeerId)
targetPeerId = sendAsPeerId
} else {
target = .myStories
targetPeerId = context.account.peerId
}
externalState.storyTarget = target
self.component?.controller()?.dismiss(animated: false)
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: targetPeerId))
|> deliverOnMainQueue).startStandalone(next: { peer in
guard let peer else {
return
}
if case let .user(user) = peer {
externalState.isPeerArchived = user.storiesHidden ?? false
} else if case let .channel(channel) = peer {
externalState.isPeerArchived = channel.storiesHidden ?? false
}
let forwardInfo = Stories.PendingForwardInfo(peerId: component.slice.peer.id, storyId: item.id, isModified: result.media != nil)
if let rootController = context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface {
var existingMedia: EngineMedia?
if let _ = result.media {
} else {
existingMedia = item.media
}
rootController.proceedWithStoryUpload(target: target, result: result as! MediaEditorScreenResult, existingMedia: existingMedia, forwardInfo: forwardInfo, externalState: externalState, commit: commit)
}
})
} else {
var updatedText: String?
var updatedEntities: [MessageTextEntity]?
if result.caption.string != item.text || entities != item.entities {
updatedText = result.caption.string
updatedEntities = entities
}
if let mediaResult = result.media {
switch mediaResult {
case let .image(image, dimensions):
updateProgressImpl?(0.0)
let tempFile = TempBox.shared.tempFile(fileName: "file")
defer {
TempBox.shared.dispose(tempFile)
}
if let imageData = compressImageToJPEG(image, quality: 0.7, tempFilePath: tempFile.path) {
self.updateDisposable.set((context.engine.messages.editStory(peerId: peerId, id: id, media: .image(dimensions: dimensions, data: imageData, stickers: result.stickers), mediaAreas: result.mediaAreas, text: updatedText, entities: updatedEntities, privacy: nil)
|> deliverOnMainQueue).startStrict(next: { [weak self] result in
guard let self else {
return
}
switch result {
case let .progress(progress):
updateProgressImpl?(progress)
case .completed:
Queue.mainQueue().after(0.1) {
self.isEditingStory = false
self.rewindCurrentItem()
self.updateIsProgressPaused()
self.state?.updated(transition: .easeInOut(duration: 0.2))
HapticFeedback().success()
commit({})
}
}
}))
}
case let .video(content, firstFrameImage, values, duration, dimensions):
updateProgressImpl?(0.0)
if let valuesData = try? JSONEncoder().encode(values) {
let data = MemoryBuffer(data: valuesData)
let digest = MemoryBuffer(data: data.md5Digest())
let 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 firstFrameImageData = firstFrameImage.flatMap { compressImageToJPEG($0, quality: 0.6, tempFilePath: tempFile.path) }
let firstFrameFile = firstFrameImageData.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
}
}
self.updateDisposable.set((context.engine.messages.editStory(peerId: peerId, id: id, media: .video(dimensions: dimensions, duration: duration, resource: resource, firstFrameFile: firstFrameFile, stickers: result.stickers), mediaAreas: result.mediaAreas, text: updatedText, entities: updatedEntities, privacy: nil)
|> deliverOnMainQueue).startStrict(next: { [weak self] result in
guard let self else {
return
}
switch result {
case let .progress(progress):
updateProgressImpl?(progress)
case .completed:
Queue.mainQueue().after(0.1) {
self.isEditingStory = false
self.rewindCurrentItem()
self.updateIsProgressPaused()
self.state?.updated(transition: .easeInOut(duration: 0.2))
HapticFeedback().success()
commit({})
}
}
}))
}
default:
break
}
} else if updatedText != nil {
let _ = (context.engine.messages.editStory(peerId: peerId, id: id, media: nil, mediaAreas: nil, text: updatedText, entities: updatedEntities, privacy: nil)
|> deliverOnMainQueue).startStandalone(next: { [weak self] result in
switch result {
case .completed:
Queue.mainQueue().after(0.1) {
if let self {
self.isEditingStory = false
self.rewindCurrentItem()
self.updateIsProgressPaused()
self.state?.updated(transition: .easeInOut(duration: 0.2))
HapticFeedback().success()
}
commit({})
}
default:
break
}
})
} else {
self.isEditingStory = false
self.rewindCurrentItem()
self.updateIsProgressPaused()
self.state?.updated(transition: .easeInOut(duration: 0.2))
HapticFeedback().success()
commit({})
}
self.component?.controller()?.dismiss(animated: false)
},
willDismiss: { [weak self] in
guard let self else {
return
}
self.isEditingStory = false
self.rewindCurrentItem()
self.updateIsProgressPaused()
self.state?.updated(transition: .easeInOut(duration: 0.2))
},
update: { [weak self] disposable in
guard let self else {
return
}
self.updateDisposable.set(disposable)
}
)
controller.willDismiss = { [weak self] in
self?.isEditingStory = false
self?.rewindCurrentItem()
self?.updateIsProgressPaused()
self?.state?.updated(transition: .easeInOut(duration: 0.2))
) else {
return
}
controller.navigationPresentation = .flatModal
self.component?.controller()?.push(controller)
updateProgressImpl = { [weak controller, weak self] progress in
controller?.updateEditProgress(progress, cancel: { [weak self] in
self?.updateDisposable.set(nil)
})
}
}
private func presentSaveUpgradeScreen() {
@ -7059,7 +6817,7 @@ public final class StoryItemSetContainerComponent: Component {
guard let self, let component = self.component else {
return
}
self.sendMessageContext.performTranslateTextAction(view: self, text: component.slice.item.storyItem.text)
self.sendMessageContext.performTranslateTextAction(view: self, text: component.slice.item.storyItem.text, entities: component.slice.item.storyItem.entities)
})))
}
}

View File

@ -1159,7 +1159,7 @@ final class StoryItemSetContainerSendMessage {
controller.present(shareController, in: .window(.root))
}
func performTranslateTextAction(view: StoryItemSetContainerComponent.View, text: String) {
func performTranslateTextAction(view: StoryItemSetContainerComponent.View, text: String, entities: [MessageTextEntity]) {
guard let component = view.component else {
return
}
@ -1190,7 +1190,7 @@ final class StoryItemSetContainerSendMessage {
let _ = ApplicationSpecificNotice.incrementTranslationSuggestion(accountManager: component.context.sharedContext.accountManager, timestamp: Int32(Date().timeIntervalSince1970)).start()
let translateController = TranslateScreen(context: component.context, forceTheme: defaultDarkPresentationTheme, text: text, canCopy: true, fromLanguage: language, ignoredLanguages: translationSettings.ignoredLanguages)
let translateController = TranslateScreen(context: component.context, forceTheme: defaultDarkPresentationTheme, text: text, entities: entities, canCopy: true, fromLanguage: language, ignoredLanguages: translationSettings.ignoredLanguages)
translateController.pushController = { [weak view] c in
guard let view, let component = view.component else {
return
@ -1762,7 +1762,7 @@ final class StoryItemSetContainerSendMessage {
self.sendMessages(view: view, peer: targetPeer, messages: enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime)
} else {
let contactController = component.context.sharedContext.makeDeviceContactInfoController(context: component.context, subject: .filter(peer: peerAndContactData.0?._asPeer(), contactId: nil, contactData: contactData, completion: { [weak self, weak view] peer, contactData in
let contactController = component.context.sharedContext.makeDeviceContactInfoController(context: ShareControllerAppAccountContext(context: component.context), environment: ShareControllerAppEnvironment(sharedContext: component.context.sharedContext), subject: .filter(peer: peerAndContactData.0?._asPeer(), contactId: nil, contactData: contactData, completion: { [weak self, weak view] peer, contactData in
guard let self, let view else {
return
}

View File

@ -27,6 +27,7 @@ swift_library(
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/Components/ViewControllerComponent:ViewControllerComponent",
"//submodules/Components/MultilineTextComponent:MultilineTextComponent",
"//submodules/Components/MultilineTextWithEntitiesComponent:MultilineTextWithEntitiesComponent",
"//submodules/Components/BundleIconComponent:BundleIconComponent",
"//submodules/UndoUI:UndoUI",
"//submodules/ActivityIndicator:ActivityIndicator",

View File

@ -11,6 +11,7 @@ import Speak
import ComponentFlow
import ViewControllerComponent
import MultilineTextComponent
import MultilineTextWithEntitiesComponent
import BundleIconComponent
import UndoUI
@ -35,15 +36,17 @@ private final class TranslateScreenComponent: CombinedComponent {
let context: AccountContext
let text: String
let entities: [MessageTextEntity]
let fromLanguage: String?
let toLanguage: String
let copyTranslation: ((String) -> Void)?
let changeLanguage: (String, String, @escaping (String, String) -> Void) -> Void
let expand: () -> Void
init(context: AccountContext, text: String, fromLanguage: String?, toLanguage: String, copyTranslation: ((String) -> Void)?, changeLanguage: @escaping (String, String, @escaping (String, String) -> Void) -> Void, expand: @escaping () -> Void) {
init(context: AccountContext, text: String, entities: [MessageTextEntity], fromLanguage: String?, toLanguage: String, copyTranslation: ((String) -> Void)?, changeLanguage: @escaping (String, String, @escaping (String, String) -> Void) -> Void, expand: @escaping () -> Void) {
self.context = context
self.text = text
self.entities = entities
self.fromLanguage = fromLanguage
self.toLanguage = toLanguage
self.copyTranslation = copyTranslation
@ -58,6 +61,9 @@ private final class TranslateScreenComponent: CombinedComponent {
if lhs.text != rhs.text {
return false
}
if lhs.entities != rhs.entities {
return false
}
if lhs.fromLanguage != rhs.fromLanguage {
return false
}
@ -995,7 +1001,7 @@ public class TranslateScreen: ViewController {
public var wasDismissed: (() -> Void)?
public convenience init(context: AccountContext, forceTheme: PresentationTheme? = nil, text: String, canCopy: Bool, fromLanguage: String?, toLanguage: String? = nil, isExpanded: Bool = false, ignoredLanguages: [String]? = nil) {
public convenience init(context: AccountContext, forceTheme: PresentationTheme? = nil, text: String, entities: [MessageTextEntity] = [], canCopy: Bool, fromLanguage: String?, toLanguage: String? = nil, isExpanded: Bool = false, ignoredLanguages: [String]? = nil) {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var baseLanguageCode = presentationData.strings.baseLanguageCode
@ -1024,7 +1030,7 @@ public class TranslateScreen: ViewController {
var copyTranslationImpl: ((String) -> Void)?
var changeLanguageImpl: ((String, String, @escaping (String, String) -> Void) -> Void)?
var expandImpl: (() -> Void)?
self.init(context: context, component: TranslateScreenComponent(context: context, text: text, fromLanguage: fromLanguage, toLanguage: toLanguage, copyTranslation: !canCopy ? nil : { text in
self.init(context: context, component: TranslateScreenComponent(context: context, text: text, entities: entities, fromLanguage: fromLanguage, toLanguage: toLanguage, copyTranslation: !canCopy ? nil : { text in
copyTranslationImpl?(text)
}, changeLanguage: { fromLang, toLang, completion in
changeLanguageImpl?(fromLang, toLang, completion)