diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 9b119f2058..757d16d92f 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -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"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 33d919d785..0861c5dff9 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -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, completion: @escaping (FileMediaReference) -> Void) -> ViewController diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index f3d483c191..ae44cd0bf1 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -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, .single(presentationData)) let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, chatLocation: nil, buttons: [.standalone], initialButton: .standalone, fromMenu: false, hasTextInput: false, makeEntityInputView: { return nil diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift index 6baabab4ba..8dd1c1a33d 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift @@ -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) diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index cfde1b0530..b4f1b8c945 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -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 - if "".isEmpty { + if hasInteractiveStickers { let weatherPromise: Promise 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 } } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/Weather.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/Weather.swift index 0a0f0bfed5..8236b696c2 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/Weather.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/Weather.swift @@ -58,9 +58,10 @@ func getWeather(context: AccountContext) -> Signal 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 +} diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 92698737c5..4103cd1e84 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -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 canPostStatus = self.context.engine.messages.checkStoriesUploadAvailability(target: .peer(self.peerId)) diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 54f2d73f79..17bace6e26 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -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 + 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 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 {