Add bot preview mode for media editor

This commit is contained in:
Ilya Laktyushin 2024-07-19 04:13:05 +04:00
parent da25e4292c
commit d8d68722ae
8 changed files with 264 additions and 34 deletions

View File

@ -12535,3 +12535,5 @@ Sorry for the inconvenience.";
"Stars.Transaction.FragmentUnknown_URL" = "https://fragment.com/stars";
"Conversation.StatusBotSubscribers_1" = "1 user";
"Conversation.StatusBotSubscribers_any" = "%d users";
"Story.Editor.Add" = "Add";

View File

@ -1051,10 +1051,12 @@ public protocol SharedAccountContext: AnyObject {
func makeMediaPickerScreen(context: AccountContext, hasSearch: Bool, completion: @escaping (Any) -> Void) -> ViewController
func makeBotPreviewEditorScreen(context: AccountContext, source: Any?, target: Stories.PendingTarget, transitionArguments: (UIView, CGRect, UIImage?)?, externalState: MediaEditorTransitionOutExternalState, completion: @escaping (MediaEditorScreenResult, @escaping (@escaping () -> Void) -> Void) -> Void, cancelled: @escaping () -> Void) -> ViewController
func makeStickerEditorScreen(context: AccountContext, source: Any?, intro: Bool, transitionArguments: (UIView, CGRect, UIImage?)?, completion: @escaping (TelegramMediaFile, [String], @escaping () -> Void) -> Void, cancelled: @escaping () -> Void) -> ViewController
func makeStickerMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect?, completion: @escaping (Any?, UIView?, CGRect, UIImage?, Bool, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> ViewController
func makeStoryMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void, groupsPresented: @escaping () -> Void) -> ViewController
func makeStoryMediaPickerScreen(context: AccountContext, isDark: Bool, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void, groupsPresented: @escaping () -> Void) -> ViewController
func makeStickerPickerScreen(context: AccountContext, inputData: Promise<StickerPickerInput>, completion: @escaping (FileMediaReference) -> Void) -> ViewController

View File

@ -2982,12 +2982,16 @@ public func mediaPickerController(
public func storyMediaPickerController(
context: AccountContext,
isDark: Bool,
getSourceRect: @escaping () -> CGRect,
completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void,
dismissed: @escaping () -> Void,
groupsPresented: @escaping () -> Void
) -> ViewController {
let presentationData = context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkColorPresentationTheme)
var presentationData = context.sharedContext.currentPresentationData.with({ $0 })
if isDark {
presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)
}
let updatedPresentationData: (PresentationData, Signal<PresentationData, NoError>) = (presentationData, .single(presentationData))
let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, chatLocation: nil, buttons: [.standalone], initialButton: .standalone, fromMenu: false, hasTextInput: false, makeEntityInputView: {
return nil

View File

@ -2979,7 +2979,7 @@ public class CameraScreen: ViewController {
if let current = self.galleryController {
controller = current
} else {
controller = self.context.sharedContext.makeStoryMediaPickerScreen(context: self.context, getSourceRect: { [weak self] in
controller = self.context.sharedContext.makeStoryMediaPickerScreen(context: self.context, isDark: true, getSourceRect: { [weak self] in
if let self {
if let galleryButton = self.node.componentHost.findTaggedView(tag: galleryButtonTag) {
return galleryButton.convert(galleryButton.bounds, to: self.view).offsetBy(dx: 0.0, dy: -15.0)

View File

@ -818,7 +818,7 @@ final class MediaEditorScreenComponent: Component {
}
var doneButtonTitle: String?
var doneButtonIcon: UIImage
var doneButtonIcon: UIImage?
switch controller.mode {
case .storyEditor:
doneButtonTitle = isEditingStory ? environment.strings.Story_Editor_Done.uppercased() : environment.strings.Story_Editor_Next.uppercased()
@ -826,6 +826,10 @@ final class MediaEditorScreenComponent: Component {
case .stickerEditor:
doneButtonTitle = nil
doneButtonIcon = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Apply"), color: .white)!
case .botPreview:
//TODO:localize
doneButtonTitle = environment.strings.Story_Editor_Add
doneButtonIcon = nil
}
let doneButtonSize = self.doneButton.update(
@ -859,6 +863,8 @@ final class MediaEditorScreenComponent: Component {
}
case .stickerEditor:
controller.requestStickerCompletion(animated: true)
case .botPreview:
controller.requestStoryCompletion(animated: true)
}
}
)),
@ -2415,6 +2421,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
case storyEditor
case stickerEditor(mode: StickerEditorMode)
case botPreview
}
public enum TransitionIn {
@ -2870,7 +2877,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
let mediaEntity = DrawingMediaEntity(size: fittedSize)
mediaEntity.position = CGPoint(x: storyDimensions.width / 2.0, y: storyDimensions.height / 2.0)
switch controller.mode {
case .storyEditor:
case .storyEditor, .botPreview:
if fittedSize.height > fittedSize.width {
mediaEntity.scale = max(storyDimensions.width / fittedSize.width, storyDimensions.height / fittedSize.height)
} else {
@ -3623,7 +3630,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
transitionInView.contentMode = .scaleAspectFill
var initialScale: CGFloat
switch controller.mode {
case .storyEditor:
case .storyEditor, .botPreview:
if image.size.height > image.size.width {
initialScale = max(self.previewContainerView.bounds.width / image.size.width, self.previewContainerView.bounds.height / image.size.height)
} else {
@ -4779,12 +4786,17 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
mediaEditor.maybePauseVideo()
var hasInteractiveStickers = true
if let controller = self.controller, case .stickerEditor = controller.mode {
hasInteractiveStickers = false
if let controller = self.controller {
switch controller.mode {
case .stickerEditor, .botPreview:
hasInteractiveStickers = false
default:
break
}
}
var weatherSignal: Signal<StickerPickerScreen.Weather, NoError>
if "".isEmpty {
if hasInteractiveStickers {
let weatherPromise: Promise<StickerPickerScreen.Weather>
if let current = self.weatherPromise {
weatherPromise = current
@ -6096,7 +6108,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
save = presentationData.strings.Story_Editor_DraftKeepMedia
}
text = presentationData.strings.Story_Editor_DraftDiscaedText
case .stickerEditor:
case .stickerEditor, .botPreview:
title = presentationData.strings.Story_Editor_DraftDiscardMedia
text = presentationData.strings.Story_Editor_DiscardText
}
@ -7435,12 +7447,12 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
private final class DoneButtonContentComponent: CombinedComponent {
let backgroundColor: UIColor
let icon: UIImage
let icon: UIImage?
let title: String?
init(
backgroundColor: UIColor,
icon: UIImage,
icon: UIImage?,
title: String?
) {
self.backgroundColor = backgroundColor
@ -7464,12 +7476,14 @@ private final class DoneButtonContentComponent: CombinedComponent {
let text = Child(Text.self)
return { context in
let iconSize = context.component.icon.size
let icon = icon.update(
component: Image(image: context.component.icon, tintColor: .white, size: iconSize),
availableSize: CGSize(width: 180.0, height: 100.0),
transition: .immediate
)
var iconChild: _UpdatedChildComponent?
if let iconImage = context.component.icon {
iconChild = icon.update(
component: Image(image: iconImage, tintColor: .white, size: iconImage.size),
availableSize: CGSize(width: 180.0, height: 100.0),
transition: .immediate
)
}
let backgroundHeight: CGFloat = 33.0
var backgroundSize = CGSize(width: backgroundHeight, height: backgroundHeight)
@ -7489,7 +7503,10 @@ private final class DoneButtonContentComponent: CombinedComponent {
transition: .immediate
)
let updatedBackgroundWidth = backgroundSize.width + textSpacing + title!.size.width
var updatedBackgroundWidth = backgroundSize.width + title!.size.width
if let _ = iconChild {
updatedBackgroundWidth += textSpacing
}
if updatedBackgroundWidth < 126.0 {
backgroundSize.width = updatedBackgroundWidth
} else {
@ -7509,16 +7526,22 @@ private final class DoneButtonContentComponent: CombinedComponent {
)
if let title {
var titlePosition = backgroundSize.width / 2.0
if let _ = iconChild {
titlePosition = title.size.width / 2.0 + 15.0
}
context.add(title
.position(CGPoint(x: title.size.width / 2.0 + 15.0, y: backgroundHeight / 2.0))
.position(CGPoint(x: titlePosition, y: backgroundHeight / 2.0))
.opacity(hideTitle ? 0.0 : 1.0)
)
}
context.add(icon
.position(CGPoint(x: background.size.width - 16.0, y: backgroundSize.height / 2.0))
)
if let iconChild {
context.add(iconChild
.position(CGPoint(x: background.size.width - 16.0, y: backgroundSize.height / 2.0))
)
}
return backgroundSize
}
}

View File

@ -58,9 +58,10 @@ func getWeather(context: AccountContext) -> Signal<StickerPickerScreen.Weather,
return getWeatherData(context: context, location: location)
|> mapToSignal { weather in
if let weather {
if let match = context.animatedEmojiStickersValue[weather.emoji.strippedEmoji]?.first {
let effectiveEmoji = emojiFor(for: weather.emoji.strippedEmoji, date: Date(), location: location)
if let match = context.animatedEmojiStickersValue[effectiveEmoji]?.first {
return .single(.loaded(StickerPickerScreen.Weather.LoadedWeather(
emoji: weather.emoji.strippedEmoji,
emoji: effectiveEmoji,
emojiFile: match.file,
temperature: weather.temperature
)))
@ -97,3 +98,123 @@ private struct WeatherBotConfiguration {
}
}
}
private let J1970: Double = 2440588.0
private let moonEmojis = ["🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘", "🌑"]
private func emojiFor(for emoji: String, date: Date, location: CLLocationCoordinate2D) -> String {
var emoji = emoji
if !"".isEmpty, ["☀️", "🌤️"].contains(emoji) && !isDay(latitude: location.latitude, longitude: location.longitude, dateTime: date) {
emoji = moonPhaseEmoji(for: date)
}
return emoji
}
private func moonPhaseEmoji(for date: Date) -> String {
let julianDate = toJulianDate(date: date)
let referenceNewMoon: Double = 2451550.1
let synodicMonth: Double = 29.53058867
let daysSinceNewMoon = julianDate - referenceNewMoon
let newMoons = daysSinceNewMoon / synodicMonth
let currentMoonPhase = (newMoons - floor(newMoons)) * synodicMonth
switch currentMoonPhase {
case 0..<1.84566:
return moonEmojis[0]
case 1.84566..<5.53699:
return moonEmojis[1]
case 5.53699..<9.22831:
return moonEmojis[2]
case 9.22831..<12.91963:
return moonEmojis[3]
case 12.91963..<16.61096:
return moonEmojis[4]
case 16.61096..<20.30228:
return moonEmojis[5]
case 20.30228..<23.99361:
return moonEmojis[6]
case 23.99361..<27.68493:
return moonEmojis[7]
default:
return moonEmojis[8]
}
}
private func isDay(latitude: Double, longitude: Double, dateTime: Date) -> Bool {
let calendar = Calendar.current
let date = calendar.startOfDay(for: dateTime)
let time = dateTime.timeIntervalSince(date)
let sunrise = calculateSunrise(latitude: latitude, longitude: longitude, date: date)
let sunset = calculateSunset(latitude: latitude, longitude: longitude, date: date)
return time >= sunrise * 3600 && time <= sunset * 3600
}
private func calculateSunrise(latitude: Double, longitude: Double, date: Date) -> Double {
return calculateSunTime(latitude: latitude, longitude: longitude, date: date, isSunrise: true)
}
private func calculateSunset(latitude: Double, longitude: Double, date: Date) -> Double {
return calculateSunTime(latitude: latitude, longitude: longitude, date: date, isSunrise: false)
}
private func calculateSunTime(latitude: Double, longitude: Double, date: Date, isSunrise: Bool) -> Double {
let calendar = Calendar.current
let dayOfYear = calendar.ordinality(of: .day, in: .year, for: date)!
let zenith = 90.833
let D2R = Double.pi / 180.0
let R2D = 180.0 / Double.pi
let lngHour = longitude / 15.0
let t = Double(dayOfYear) + ((isSunrise ? 6.0 : 18.0) - lngHour) / 24.0
let M = (0.9856 * t) - 3.289
var L = M + (1.916 * sin(M * D2R)) + (0.020 * sin(2 * M * D2R)) + 282.634
if L > 360.0 {
L -= 360.0
} else if L < 0.0 {
L += 360.0
}
var RA = R2D * atan(0.91764 * tan(L * D2R))
if RA > 360.0 {
RA -= 360.0
} else if RA < 0.0 {
RA += 360.0
}
let Lquadrant = (floor(L / 90.0)) * 90.0
let RAquadrant = (floor(RA / 90.0)) * 90.0
RA += (Lquadrant - RAquadrant)
RA /= 15.0
let sinDec = 0.39782 * sin(L * D2R)
let cosDec = cos(asin(sinDec))
let cosH = (cos(zenith * D2R) - (sinDec * sin(latitude * D2R))) / (cosDec * cos(latitude * D2R))
if cosH > 1.0 || cosH < -1.0 {
return -1
}
var H = isSunrise ? (360.0 - R2D * acos(cosH)) : R2D * acos(cosH)
H /= 15.0
let T = H + RA - (0.06571 * t) - 6.622
var UT = T - lngHour
if UT > 24.0 {
UT -= 24.0
} else if UT < 0.0 {
UT += 24.0
}
return UT
}
private func toJulianDate(date: Date) -> Double {
return date.timeIntervalSince1970 / 86400.0 + J1970 - 0.5
}

View File

@ -9781,6 +9781,36 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
self.controller?.push(controller)
}
private func openBotPreviewEditor(target: Stories.PendingTarget, source: Any, transitionIn: (UIView, CGRect, UIImage?)?) {
let context = self.context
let externalState = MediaEditorTransitionOutExternalState(
storyTarget: target,
isForcedTarget: false,
isPeerArchived: false,
transitionOut: nil
)
let controller = context.sharedContext.makeBotPreviewEditorScreen(
context: context,
source: source,
target: target,
transitionArguments: transitionIn,
externalState: externalState,
completion: { result, commit in
if let rootController = context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface {
var viewControllers = rootController.viewControllers
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)
}
},
cancelled: {}
)
self.controller?.push(controller)
}
private func openPostStory(sourceFrame: CGRect?) {
self.postingAvailabilityDisposable?.dispose()
@ -9788,12 +9818,20 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
if !botInfo.flags.contains(.canEdit) {
return
}
let cameraTransitionIn: StoryCameraTransitionIn? = nil
if let rootController = self.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface {
let coordinator = rootController.openStoryCamera(customTarget: .botPreview(self.peerId), transitionIn: cameraTransitionIn, transitionedIn: {}, transitionOut: self.storyCameraTransitionOut())
coordinator?.animateIn()
}
let controller = self.context.sharedContext.makeStoryMediaPickerScreen(
context: self.context,
isDark: false,
getSourceRect: { return .zero },
completion: { [weak self] result, transitionView, transitionRect, transitionImage, transitionOut, dismissed in
guard let self else {
return
}
self.openBotPreviewEditor(target: .botPreview(self.peerId), source: result, transitionIn: (transitionView, transitionRect, transitionImage))
},
dismissed: {},
groupsPresented: {}
)
self.controller?.push(controller)
} else {
let canPostStatus: Signal<StoriesUploadAvailability, NoError>
canPostStatus = self.context.engine.messages.checkStoriesUploadAvailability(target: .peer(self.peerId))

View File

@ -2533,6 +2533,46 @@ public final class SharedAccountContextImpl: SharedAccountContext {
})
}
public func makeBotPreviewEditorScreen(context: AccountContext, source: Any?, target: Stories.PendingTarget, transitionArguments: (UIView, CGRect, UIImage?)?, externalState: MediaEditorTransitionOutExternalState, completion: @escaping (MediaEditorScreenResult, @escaping (@escaping () -> Void) -> Void) -> Void, cancelled: @escaping () -> Void) -> ViewController {
let subject: Signal<MediaEditorScreen.Subject?, NoError>
if let asset = source as? PHAsset {
subject = .single(.asset(asset))
} else if let image = source as? UIImage {
subject = .single(.image(image, PixelDimensions(image.size), nil, .bottomRight))
} else {
subject = .single(.empty(PixelDimensions(width: 1080, height: 1920)))
}
let editorController = MediaEditorScreen(
context: context,
mode: .botPreview,
subject: subject,
customTarget: nil,
transitionIn: transitionArguments.flatMap { .gallery(
MediaEditorScreen.TransitionIn.GalleryTransitionIn(
sourceView: $0.0,
sourceRect: $0.1,
sourceImage: $0.2
)
) },
transitionOut: { finished, isNew in
if !finished, let transitionArguments {
return MediaEditorScreen.TransitionOut(
destinationView: transitionArguments.0,
destinationRect: transitionArguments.0.bounds,
destinationCornerRadius: 0.0
)
}
return nil
}, completion: { result, commit in
completion(result, commit)
} as (MediaEditorScreen.Result, @escaping (@escaping () -> Void) -> Void) -> Void
)
editorController.cancelled = { _ in
cancelled()
}
return editorController
}
public func makeStickerEditorScreen(context: AccountContext, source: Any?, intro: Bool, transitionArguments: (UIView, CGRect, UIImage?)?, completion: @escaping (TelegramMediaFile, [String], @escaping () -> Void) -> Void, cancelled: @escaping () -> Void) -> ViewController {
let subject: Signal<MediaEditorScreen.Subject?, NoError>
var mode: MediaEditorScreen.Mode.StickerEditorMode
@ -2605,8 +2645,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return mediaPickerController(context: context, hasSearch: hasSearch, completion: completion)
}
public func makeStoryMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void, groupsPresented: @escaping () -> Void) -> ViewController {
return storyMediaPickerController(context: context, getSourceRect: getSourceRect, completion: completion, dismissed: dismissed, groupsPresented: groupsPresented)
public func makeStoryMediaPickerScreen(context: AccountContext, isDark: Bool, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void, groupsPresented: @escaping () -> Void) -> ViewController {
return storyMediaPickerController(context: context, isDark: isDark, getSourceRect: getSourceRect, completion: completion, dismissed: dismissed, groupsPresented: groupsPresented)
}
public func makeStickerMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect?, completion: @escaping (Any?, UIView?, CGRect, UIImage?, Bool, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> ViewController {